feat(spa): dashboard hero + branded categories/import/test pages
This commit is contained in:
@@ -43,7 +43,10 @@ async function remove(row: CategoryRow) {
|
||||
|
||||
<template>
|
||||
<div class="p-6 text-wp-text max-w-3xl">
|
||||
<h1 class="text-xl font-semibold mb-4">Categories</h1>
|
||||
<h1 class="text-xl font-semibold m-0">Categories</h1>
|
||||
<p class="text-sm text-wp-text2 m-0 mt-1 mb-6">
|
||||
Tag groups for filtering modules. Color appears on category chips in the DataTable.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2 mb-6 items-end">
|
||||
<div class="flex-1">
|
||||
@@ -54,12 +57,12 @@ async function remove(row: CategoryRow) {
|
||||
<label for="cat-color" class="block text-xs text-wp-text2 mb-1">Color</label>
|
||||
<ColorPicker id="cat-color" v-model="newColor" />
|
||||
</div>
|
||||
<Button label="+ Add" severity="primary" data-test="add-category-btn" @click="add" />
|
||||
<Button label="Add" icon="pi pi-plus" severity="primary" data-test="add-category-btn" @click="add" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="store.items" data-key="id" :loading="store.loading">
|
||||
<Column field="name" header="Name" />
|
||||
<Column field="color" header="Color" style="width:8rem">
|
||||
<Column field="color" header="Color" header-style="width:9rem">
|
||||
<template #body="{ data }">
|
||||
<span
|
||||
class="inline-block w-4 h-4 rounded mr-2 align-middle"
|
||||
@@ -69,14 +72,11 @@ async function remove(row: CategoryRow) {
|
||||
<span class="font-mono text-xs">{{ data.color || "—" }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="sort_order" header="Order" sortable style="width:6rem" />
|
||||
<Column header="Actions" style="width:6rem">
|
||||
<Column field="sort_order" header="Order" sortable header-style="width:6rem" />
|
||||
<Column header="Actions" header-style="width:6rem">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
icon="pi pi-trash" text rounded size="small" severity="danger"
|
||||
aria-label="Delete category"
|
||||
@click="remove(data)"
|
||||
/>
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger"
|
||||
aria-label="Delete category" @click="remove(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
<template #empty>
|
||||
|
||||
@@ -1,9 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import Button from "primevue/button";
|
||||
import { useModuleStore } from "../stores/moduleStore";
|
||||
import { useCategoryStore } from "../stores/categoryStore";
|
||||
import RelativeDate from "../components/RelativeDate.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const moduleStore = useModuleStore();
|
||||
const categoryStore = useCategoryStore();
|
||||
const faviconUrl = `${import.meta.env.BASE_URL}images/favicon.svg`;
|
||||
|
||||
onMounted(async () => {
|
||||
moduleStore.filter.type = undefined;
|
||||
await Promise.all([moduleStore.fetchAll(), categoryStore.fetchAll()]);
|
||||
});
|
||||
|
||||
const recent = computed(() => [...moduleStore.items].slice(0, 5));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 text-wp-text">
|
||||
<h1 class="text-xl font-semibold m-0">Dashboard</h1>
|
||||
<p class="text-sm text-wp-text2 m-0 mt-2">Stub — wired up in Task 9.</p>
|
||||
<section class="hero-card mb-6">
|
||||
<img :src="faviconUrl" alt="" class="hero-card__logo" />
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-semibold m-0">Welcome to Wildcard Pipeline</h1>
|
||||
<p class="text-sm m-0 mt-1 opacity-90">
|
||||
Manage modules — wildcards, fixed values — that drop into ComfyUI prompts as snapshots.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">Wildcards</div>
|
||||
<div class="stat-card__value">{{ moduleStore.wildcards.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">Fixed values</div>
|
||||
<div class="stat-card__value">{{ moduleStore.fixedValues.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">Categories</div>
|
||||
<div class="stat-card__value">{{ categoryStore.items.length }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">Total modules</div>
|
||||
<div class="stat-card__value">{{ moduleStore.items.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-sm uppercase tracking-wider text-wp-text2 mb-2">Recent edits</h2>
|
||||
<div v-if="recent.length" class="bg-wp-bg2 border border-wp-border rounded">
|
||||
<div
|
||||
v-for="m in recent" :key="m.id"
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-wp-border last:border-b-0 cursor-pointer hover:bg-wp-bg3"
|
||||
@click="router.push(m.type === 'wildcard' ? `/wildcards/${m.id}/edit` : `/fixed-values/${m.id}/edit`)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="m.type === 'wildcard' ? 'pi pi-th-large text-wp-violet' : 'pi pi-tag text-wp-rose'" />
|
||||
<span class="font-medium">{{ m.name }}</span>
|
||||
<span class="text-xs text-wp-text3 font-mono">{{ m.id }}</span>
|
||||
</div>
|
||||
<RelativeDate :value="m.updated_at" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-wp-text2">No modules yet.</p>
|
||||
|
||||
<div class="mt-6 flex gap-2 flex-wrap">
|
||||
<Button label="New wildcard" icon="pi pi-plus" severity="primary" @click="router.push('/wildcards/new')" />
|
||||
<Button label="New fixed values" icon="pi pi-plus" severity="primary" outlined @click="router.push('/fixed-values/new')" />
|
||||
<Button label="Open test runner" icon="pi pi-bolt" severity="secondary" outlined @click="router.push('/test')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-card {
|
||||
display: flex; gap: 16px; align-items: center;
|
||||
padding: 24px;
|
||||
background: var(--wp-brand-gradient);
|
||||
border-radius: var(--wp-radius);
|
||||
color: #fff;
|
||||
}
|
||||
.hero-card__logo { width: 64px; height: 64px; }
|
||||
.stat-card {
|
||||
background: var(--wp-bg2);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius);
|
||||
padding: 16px;
|
||||
}
|
||||
.stat-card__label {
|
||||
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
||||
color: var(--wp-text2);
|
||||
}
|
||||
.stat-card__value { font-size: 28px; font-weight: 600; margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,6 @@ async function onUpload(e: Event) {
|
||||
} catch (err) {
|
||||
toast.add({ severity: "error", summary: "Import failed", detail: String(err), life: 5000 });
|
||||
} finally {
|
||||
// reset so the same file can be selected again
|
||||
if (uploadInput.value) uploadInput.value.value = "";
|
||||
}
|
||||
}
|
||||
@@ -45,10 +44,9 @@ async function onUpload(e: Event) {
|
||||
|
||||
<template>
|
||||
<div class="p-6 text-wp-text max-w-2xl">
|
||||
<h1 class="text-xl font-semibold mb-4">Import / Export</h1>
|
||||
<p class="text-sm text-wp-text2 mb-6">
|
||||
Library bundles are JSON files containing all module declarations and categories.
|
||||
Workflow files are NOT handled here.
|
||||
<h1 class="text-xl font-semibold m-0">Import / Export</h1>
|
||||
<p class="text-sm text-wp-text2 m-0 mt-1 mb-6">
|
||||
Library bundles are JSON files containing all module declarations and categories. Workflow files are NOT handled here.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-3 items-center mb-6">
|
||||
@@ -56,13 +54,14 @@ async function onUpload(e: Event) {
|
||||
<Button icon="pi pi-upload" label="Import bundle" severity="secondary" outlined @click="uploadInput?.click()" />
|
||||
<input
|
||||
ref="uploadInput" type="file" accept="application/json"
|
||||
aria-label="Upload bundle JSON file"
|
||||
class="hidden" data-test="upload-input"
|
||||
aria-label="Upload bundle JSON file"
|
||||
@change="onUpload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="lastResult" class="text-sm text-wp-text2 p-3 rounded bg-wp-bg2 border border-wp-border">
|
||||
<i class="pi pi-info-circle mr-2" />
|
||||
Last import: {{ lastResult }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,10 @@ const variableBinding = ref("$x");
|
||||
const result = ref<TestResponse | null>(null);
|
||||
const running = ref(false);
|
||||
|
||||
onMounted(async () => { await store.fetchAll(); });
|
||||
onMounted(async () => {
|
||||
store.filter.type = undefined;
|
||||
await store.fetchAll();
|
||||
});
|
||||
|
||||
const selectedModule = computed(() => store.items.find((m) => m.id === moduleId.value));
|
||||
|
||||
@@ -32,14 +35,9 @@ async function run() {
|
||||
if (!mod) return;
|
||||
running.value = true;
|
||||
try {
|
||||
const instance = mod.type === "wildcard"
|
||||
? { variable_binding: variableBinding.value }
|
||||
: {};
|
||||
const instance = mod.type === "wildcard" ? { variable_binding: variableBinding.value } : {};
|
||||
result.value = await api.test({
|
||||
type: mod.type,
|
||||
payload: mod.payload,
|
||||
instance,
|
||||
samples: samples.value,
|
||||
type: mod.type, payload: mod.payload, instance, samples: samples.value,
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Test failed", detail: String(e), life: 4000 });
|
||||
@@ -59,20 +57,18 @@ const histogramEntries = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="p-6 text-wp-text max-w-3xl">
|
||||
<h1 class="text-xl font-semibold mb-4">Test runner</h1>
|
||||
<h1 class="text-xl font-semibold m-0">Test runner</h1>
|
||||
<p class="text-sm text-wp-text2 m-0 mt-1 mb-6">
|
||||
Run a module against the engine N times, view the resolved-value distribution.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3 mb-4">
|
||||
<div class="col-span-2">
|
||||
<label for="tr-module" class="block text-xs text-wp-text2 mb-1">Module</label>
|
||||
<Select
|
||||
id="tr-module"
|
||||
v-model="moduleId"
|
||||
:options="store.items"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
placeholder="Select a module…"
|
||||
filter
|
||||
class="w-full"
|
||||
id="tr-module" v-model="moduleId"
|
||||
:options="store.items" option-label="name" option-value="id"
|
||||
placeholder="Select a module…" filter class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -95,14 +91,10 @@ const histogramEntries = computed(() => {
|
||||
<div v-if="result" class="mt-6">
|
||||
<h2 class="text-sm uppercase tracking-wider text-wp-text2 mb-2">Histogram</h2>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="entry in histogramEntries"
|
||||
:key="entry.value"
|
||||
class="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<div v-for="entry in histogramEntries" :key="entry.value" class="flex items-center gap-2 text-sm">
|
||||
<div class="w-48 truncate" :title="entry.value">{{ entry.value }}</div>
|
||||
<div class="flex-1 h-3 bg-wp-bg2 rounded overflow-hidden">
|
||||
<div class="h-full bg-wp-accent" :style="{ width: entry.pct + '%' }" />
|
||||
<div class="h-full" :style="{ width: entry.pct + '%', background: 'var(--wp-accent)' }" />
|
||||
</div>
|
||||
<div class="w-12 text-right text-wp-text2">{{ entry.count }}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user