@nwire/studio 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/App.vue +62 -56
- package/src/components/BcCard.stories.ts +47 -0
- package/src/components/BcCard.vue +152 -0
- package/src/components/DurationBar.stories.ts +55 -0
- package/src/components/DurationBar.vue +72 -0
- package/src/components/ErrorCard.stories.ts +133 -0
- package/src/components/ErrorCard.vue +153 -0
- package/src/components/GraphCanvas.stories.ts +48 -0
- package/src/components/GraphCanvas.vue +88 -0
- package/src/components/KpiTile.stories.ts +32 -0
- package/src/components/KpiTile.vue +39 -0
- package/src/components/LiveTable.stories.ts +78 -0
- package/src/components/LiveTable.vue +186 -0
- package/src/components/MetadataInspector.stories.ts +53 -0
- package/src/components/MetadataInspector.vue +105 -0
- package/src/components/NodeCard.stories.ts +44 -0
- package/src/components/NodeCard.vue +150 -0
- package/src/components/RcaPanel.stories.ts +95 -0
- package/src/components/RcaPanel.vue +223 -0
- package/src/components/ServiceNode.vue +134 -0
- package/src/components/SourceDrawer.vue +6 -4
- package/src/components/SourcePill.vue +10 -3
- package/src/components/StatusBadge.stories.ts +33 -0
- package/src/components/StatusBadge.vue +54 -0
- package/src/components/Waterfall.stories.ts +85 -0
- package/src/components/Waterfall.vue +53 -0
- package/src/components/WaterfallRow.vue +74 -0
- package/src/components/__tests__/BcCard.test.ts +53 -0
- package/src/components/__tests__/DurationBar.test.ts +31 -0
- package/src/components/__tests__/ErrorCard.test.ts +71 -0
- package/src/components/__tests__/KpiTile.test.ts +23 -0
- package/src/components/__tests__/LiveTable.test.ts +100 -0
- package/src/components/__tests__/MetadataInspector.test.ts +38 -0
- package/src/components/__tests__/NodeCard.test.ts +116 -0
- package/src/components/__tests__/RcaPanel.test.ts +81 -0
- package/src/components/__tests__/StatusBadge.test.ts +23 -0
- package/src/components/__tests__/Waterfall.test.ts +54 -0
- package/src/components/index.ts +13 -0
- package/src/composables/__tests__/composables-context.test.ts +107 -0
- package/src/composables/__tests__/useTelemetry.test.ts +104 -0
- package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
- package/src/composables/useDiscovery.ts +73 -0
- package/src/composables/useEndpoints.ts +94 -0
- package/src/composables/useLogTail.ts +51 -0
- package/src/composables/useManifest.ts +43 -0
- package/src/composables/useProcesses.ts +114 -0
- package/src/composables/useProject.ts +34 -0
- package/src/composables/useTelemetry.ts +270 -0
- package/src/lib/__tests__/bc-graph.test.ts +218 -0
- package/src/lib/__tests__/dispatch-form.test.ts +113 -0
- package/src/lib/__tests__/error-friendly.test.ts +198 -0
- package/src/lib/__tests__/home.test.ts +231 -0
- package/src/lib/__tests__/inspect.test.ts +160 -0
- package/src/lib/__tests__/kind-colors.test.ts +59 -0
- package/src/lib/__tests__/live-table.test.ts +194 -0
- package/src/lib/__tests__/manifest-health.test.ts +120 -0
- package/src/lib/__tests__/manifest.test.ts +87 -0
- package/src/lib/__tests__/metadata.test.ts +47 -0
- package/src/lib/__tests__/node-metrics.test.ts +144 -0
- package/src/lib/__tests__/operate.test.ts +97 -0
- package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
- package/src/lib/__tests__/rca.test.ts +124 -0
- package/src/lib/__tests__/telemetry.test.ts +91 -0
- package/src/lib/__tests__/topology-graph.test.ts +331 -0
- package/src/lib/__tests__/topology-view.test.ts +154 -0
- package/src/lib/__tests__/waterfall.test.ts +165 -0
- package/src/lib/bc-graph.ts +298 -0
- package/src/lib/dispatch-form.ts +160 -0
- package/src/lib/error-friendly.ts +288 -0
- package/src/lib/home.ts +191 -0
- package/src/lib/inspect.ts +226 -0
- package/src/lib/kind-colors.ts +132 -0
- package/src/lib/live-table.ts +204 -0
- package/src/lib/manifest-health.ts +71 -0
- package/src/lib/manifest.ts +139 -0
- package/src/lib/metadata.ts +52 -0
- package/src/lib/node-metrics.ts +242 -0
- package/src/lib/operate.ts +114 -0
- package/src/lib/pipeline-flow.ts +120 -0
- package/src/lib/rca.ts +193 -0
- package/src/lib/telemetry.ts +155 -0
- package/src/lib/topology-graph.ts +551 -0
- package/src/lib/topology-view.ts +185 -0
- package/src/lib/waterfall.ts +148 -0
- package/src/main.ts +63 -29
- package/src/pages/Errors.vue +272 -0
- package/src/pages/Home.stories.ts +7 -8
- package/src/pages/Home.vue +255 -540
- package/src/pages/Hooks.stories.ts +44 -0
- package/src/pages/Hooks.vue +165 -164
- package/src/pages/Inspect.vue +240 -0
- package/src/pages/Map.vue +187 -0
- package/src/pages/Operate.vue +74 -0
- package/src/pages/Plugins.stories.ts +1 -1
- package/src/pages/Plugins.vue +174 -238
- package/src/pages/Projects.vue +62 -60
- package/src/pages/Streams.vue +344 -0
- package/src/pages/Topology.vue +318 -136
- package/src/pages/Trace.vue +174 -412
- package/src/pages/__tests__/Home.test.ts +109 -54
- package/src/pages/__tests__/Hooks.test.ts +5 -5
- package/src/pages/__tests__/Inspect.test.ts +111 -0
- package/src/pages/__tests__/Plugins.test.ts +85 -35
- package/src/pages/__tests__/Trace.test.ts +117 -0
- package/src/pages/operate/CommandsPanel.vue +186 -0
- package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
- package/src/pages/operate/EndpointPicker.vue +56 -0
- package/src/pages/operate/RunPanel.vue +316 -0
- package/src/server/__tests__/nwire-read.test.ts +80 -0
- package/src/server/nwire-read.ts +63 -0
- package/vite.config.ts +220 -2
- package/src/lib/__tests__/normalize-cache.test.ts +0 -105
- package/src/lib/cache.ts +0 -312
- package/src/lib/normalize-cache.ts +0 -92
- package/src/pages/Actions.vue +0 -171
- package/src/pages/Apps.vue +0 -177
- package/src/pages/Commands.vue +0 -262
- package/src/pages/Events.vue +0 -210
- package/src/pages/Live.vue +0 -249
- package/src/pages/Overview.vue +0 -161
- package/src/pages/Projections.vue +0 -148
- package/src/pages/Queries.vue +0 -148
- package/src/pages/Run.vue +0 -618
- package/src/pages/Sinks.vue +0 -124
- package/src/pages/TraceNode.vue +0 -164
- package/src/pages/Workflows.vue +0 -184
- package/src/pages/__tests__/Actions.test.ts +0 -98
- package/src/pages/__tests__/Projections.test.ts +0 -90
- package/src/pages/__tests__/Queries.test.ts +0 -86
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cache normalizer — defends Studio against stale or partial
|
|
3
|
-
* `.nwire/manifest.json` payloads.
|
|
4
|
-
*
|
|
5
|
-
* Reality check: the manifest is generated by `@nwire/scan` and its schema
|
|
6
|
-
* evolves. A Studio served against a manifest built before a given field
|
|
7
|
-
* exists must not crash — pages that depend on that field should render
|
|
8
|
-
* an empty state, not a TypeError.
|
|
9
|
-
*
|
|
10
|
-
* The normalizer:
|
|
11
|
-
* 1. Returns `null` if the value isn't an object (Studio renders an error
|
|
12
|
-
* banner instead of throwing).
|
|
13
|
-
* 2. Fills every expected array field with `[]`.
|
|
14
|
-
* 3. Fills nested shapes (graph.events) with safe defaults.
|
|
15
|
-
* 4. Reports which fields were missing — surfaced as a warning so the
|
|
16
|
-
* operator knows the cache is stale and can rebuild.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { Cache } from "./cache";
|
|
20
|
-
|
|
21
|
-
export interface NormalizeResult {
|
|
22
|
-
/** The normalized cache, ready to render. `null` if the input is unusable. */
|
|
23
|
-
readonly cache: Cache | null;
|
|
24
|
-
/** Field paths that were missing in the input and filled with defaults. */
|
|
25
|
-
readonly missingFields: readonly string[];
|
|
26
|
-
/** Human-readable description of what made the input unusable. */
|
|
27
|
-
readonly fatalError?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const ARRAY_FIELDS = [
|
|
31
|
-
"apps",
|
|
32
|
-
"actions",
|
|
33
|
-
"events",
|
|
34
|
-
"actors",
|
|
35
|
-
"projections",
|
|
36
|
-
"queries",
|
|
37
|
-
"resolvers",
|
|
38
|
-
"routes",
|
|
39
|
-
"workflows",
|
|
40
|
-
"externalCalls",
|
|
41
|
-
"inboundWebhooks",
|
|
42
|
-
"outboxes",
|
|
43
|
-
"inboxes",
|
|
44
|
-
"crons",
|
|
45
|
-
"hooks",
|
|
46
|
-
"plugins",
|
|
47
|
-
"sinks",
|
|
48
|
-
"bindings",
|
|
49
|
-
] as const;
|
|
50
|
-
|
|
51
|
-
export function normalizeCache(raw: unknown): NormalizeResult {
|
|
52
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
53
|
-
return {
|
|
54
|
-
cache: null,
|
|
55
|
-
missingFields: [],
|
|
56
|
-
fatalError: "Manifest is not a JSON object.",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const input = raw as Record<string, unknown>;
|
|
61
|
-
const missing: string[] = [];
|
|
62
|
-
|
|
63
|
-
const out: Record<string, unknown> = {
|
|
64
|
-
generatedAt:
|
|
65
|
-
typeof input.generatedAt === "string" ? input.generatedAt : new Date(0).toISOString(),
|
|
66
|
-
};
|
|
67
|
-
if (typeof input.generatedAt !== "string") missing.push("generatedAt");
|
|
68
|
-
|
|
69
|
-
for (const field of ARRAY_FIELDS) {
|
|
70
|
-
if (Array.isArray(input[field])) {
|
|
71
|
-
out[field] = input[field];
|
|
72
|
-
} else {
|
|
73
|
-
out[field] = [];
|
|
74
|
-
missing.push(field);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// graph.events — nested arrays
|
|
79
|
-
const rawGraph = input.graph as { events?: unknown } | undefined;
|
|
80
|
-
if (rawGraph && typeof rawGraph === "object" && Array.isArray(rawGraph.events)) {
|
|
81
|
-
out.graph = { events: rawGraph.events };
|
|
82
|
-
} else {
|
|
83
|
-
out.graph = { events: [] };
|
|
84
|
-
if (!rawGraph) missing.push("graph");
|
|
85
|
-
else missing.push("graph.events");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
cache: out as unknown as Cache,
|
|
90
|
-
missingFields: missing,
|
|
91
|
-
};
|
|
92
|
-
}
|
package/src/pages/Actions.vue
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref, watch } from "vue";
|
|
3
|
-
import { useRoute, useRouter } from "vue-router";
|
|
4
|
-
import { useCache } from "@/lib/cache";
|
|
5
|
-
import { Search, Zap, Shield, Globe, Lock, Anchor, Activity } from "lucide-vue-next";
|
|
6
|
-
import { SchemaTree, SourcePill, SourceDrawer } from "@/components";
|
|
7
|
-
|
|
8
|
-
const route = useRoute();
|
|
9
|
-
const router = useRouter();
|
|
10
|
-
const { cache } = useCache();
|
|
11
|
-
const filter = ref("");
|
|
12
|
-
const selected = ref<string | null>(null);
|
|
13
|
-
const sourcePreview = ref<{ file: string; line: number; column?: number } | null>(null);
|
|
14
|
-
|
|
15
|
-
function applyQueryPreselect(): void {
|
|
16
|
-
const name = route.query.name;
|
|
17
|
-
if (typeof name === "string" && name.length > 0) {
|
|
18
|
-
selected.value = name;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
onMounted(applyQueryPreselect);
|
|
23
|
-
watch(() => route.query.name, applyQueryPreselect);
|
|
24
|
-
|
|
25
|
-
const filtered = computed(() => {
|
|
26
|
-
if (!cache.value) return [];
|
|
27
|
-
const q = filter.value.toLowerCase();
|
|
28
|
-
return cache.value.actions.filter(
|
|
29
|
-
(a) =>
|
|
30
|
-
!q ||
|
|
31
|
-
a.name.toLowerCase().includes(q) ||
|
|
32
|
-
(a.description ?? "").toLowerCase().includes(q) ||
|
|
33
|
-
a.app.toLowerCase().includes(q),
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const detail = computed(() => filtered.value.find((a) => a.name === selected.value) ?? null);
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
<template>
|
|
41
|
-
<div v-if="cache" class="h-full flex">
|
|
42
|
-
<div class="w-2/5 border-r border-zinc-800 flex flex-col">
|
|
43
|
-
<div class="border-b border-zinc-800 px-4 py-3">
|
|
44
|
-
<h1 class="text-lg font-semibold tracking-tight">Actions</h1>
|
|
45
|
-
<div class="relative mt-2">
|
|
46
|
-
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-500" />
|
|
47
|
-
<input
|
|
48
|
-
v-model="filter"
|
|
49
|
-
placeholder="filter by name, app, description…"
|
|
50
|
-
class="w-full bg-zinc-900 border border-zinc-800 rounded pl-7 pr-2 py-1.5 text-sm placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="text-[10px] text-zinc-500 mt-1">
|
|
54
|
-
{{ filtered.length }} / {{ cache.actions.length }}
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="flex-1 overflow-auto">
|
|
58
|
-
<button
|
|
59
|
-
v-for="a in filtered"
|
|
60
|
-
:key="`${a.app}::${a.name}`"
|
|
61
|
-
class="w-full text-left px-4 py-2.5 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
62
|
-
:class="{ 'bg-zinc-900': selected === a.name }"
|
|
63
|
-
@click="selected = a.name"
|
|
64
|
-
>
|
|
65
|
-
<div class="flex items-center justify-between">
|
|
66
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
67
|
-
<Zap class="w-3 h-3 text-amber-400 shrink-0" />
|
|
68
|
-
<span class="font-mono text-sm truncate">{{ a.name }}</span>
|
|
69
|
-
</div>
|
|
70
|
-
<div class="flex items-center gap-1 shrink-0">
|
|
71
|
-
<Shield v-if="a.policy" class="w-3 h-3 text-blue-400" />
|
|
72
|
-
<component
|
|
73
|
-
:is="a.public ? Globe : Lock"
|
|
74
|
-
class="w-3 h-3"
|
|
75
|
-
:class="a.public ? 'text-emerald-400' : 'text-zinc-500'"
|
|
76
|
-
:title="a.public ? 'public — other apps may dispatch' : 'private — app-internal'"
|
|
77
|
-
/>
|
|
78
|
-
<span class="text-[10px] text-zinc-500">{{ a.app }}</span>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
<div v-if="a.description" class="text-xs text-zinc-500 mt-1 line-clamp-2 ml-5">
|
|
82
|
-
{{ a.description }}
|
|
83
|
-
</div>
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div class="flex-1 overflow-auto">
|
|
89
|
-
<div v-if="!detail" class="p-6 text-zinc-500 text-sm">
|
|
90
|
-
Select an action to view its schema and metadata.
|
|
91
|
-
</div>
|
|
92
|
-
<div v-else class="p-6 space-y-5">
|
|
93
|
-
<div>
|
|
94
|
-
<div class="text-[10px] uppercase tracking-wide text-zinc-500">
|
|
95
|
-
{{ detail.app }}
|
|
96
|
-
</div>
|
|
97
|
-
<h2 class="font-mono text-xl mt-1">{{ detail.name }}</h2>
|
|
98
|
-
<p v-if="detail.description" class="text-sm text-zinc-400 mt-2 max-w-2xl">
|
|
99
|
-
{{ detail.description }}
|
|
100
|
-
</p>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div class="flex flex-wrap gap-2">
|
|
104
|
-
<span
|
|
105
|
-
v-if="detail.hasInlineHandler"
|
|
106
|
-
class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded bg-emerald-950/50 border border-emerald-900 text-emerald-300"
|
|
107
|
-
>
|
|
108
|
-
inline handler
|
|
109
|
-
</span>
|
|
110
|
-
<span
|
|
111
|
-
v-if="detail.retry"
|
|
112
|
-
class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded bg-amber-950/50 border border-amber-900 text-amber-300"
|
|
113
|
-
>
|
|
114
|
-
retry
|
|
115
|
-
</span>
|
|
116
|
-
<span
|
|
117
|
-
v-if="detail.policy"
|
|
118
|
-
class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded bg-blue-950/50 border border-blue-900 text-blue-300"
|
|
119
|
-
>
|
|
120
|
-
policy: {{ detail.policy }}
|
|
121
|
-
</span>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div v-if="detail.persona || detail.journeyStep" class="text-xs space-y-1 text-zinc-400">
|
|
125
|
-
<div v-if="detail.persona" class="flex items-center gap-2">
|
|
126
|
-
<span class="text-zinc-500 w-24">Persona</span>
|
|
127
|
-
<span class="font-mono">{{ detail.persona }}</span>
|
|
128
|
-
</div>
|
|
129
|
-
<div v-if="detail.journeyStep" class="flex items-center gap-2">
|
|
130
|
-
<span class="text-zinc-500 w-24">Journey step</span>
|
|
131
|
-
<span class="font-mono">{{ detail.journeyStep }}</span>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<div v-if="detail.source" class="flex items-center gap-2">
|
|
136
|
-
<SourcePill :source="detail.source" @click="sourcePreview = detail.source!" />
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<SchemaTree :schema="detail.inputSchema" label="Input schema" />
|
|
140
|
-
|
|
141
|
-
<div class="flex flex-wrap gap-2">
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
class="inline-flex items-center gap-1.5 text-xs px-2 py-1 rounded border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800/70 text-zinc-300"
|
|
145
|
-
:data-testid="`hooks-link-${detail.name}`"
|
|
146
|
-
@click="
|
|
147
|
-
router.push({ path: '/hooks', query: { name: `action.before:${detail.name}` } })
|
|
148
|
-
"
|
|
149
|
-
>
|
|
150
|
-
<Anchor class="w-3 h-3 text-zinc-500" />
|
|
151
|
-
View hooks
|
|
152
|
-
</button>
|
|
153
|
-
<button
|
|
154
|
-
type="button"
|
|
155
|
-
class="inline-flex items-center gap-1.5 text-xs px-2 py-1 rounded border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800/70 text-zinc-300"
|
|
156
|
-
:data-testid="`trace-link-${detail.name}`"
|
|
157
|
-
@click="router.push({ path: '/trace', query: { action: detail.name } })"
|
|
158
|
-
>
|
|
159
|
-
<Activity class="w-3 h-3 text-zinc-500" />
|
|
160
|
-
Recent traces of this
|
|
161
|
-
</button>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div class="text-xs text-zinc-500">
|
|
165
|
-
Use the Try page for form-from-schema dispatch against the live runtime.
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
<SourceDrawer :source="sourcePreview" @close="sourcePreview = null" />
|
|
170
|
-
</div>
|
|
171
|
-
</template>
|
package/src/pages/Apps.vue
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Apps — every App registered in this workspace. An App is the
|
|
4
|
-
* bounded-context unit; multi-app systems compose via
|
|
5
|
-
* `appCompose(a, b)` and run side-by-side under one endpoint.
|
|
6
|
-
*/
|
|
7
|
-
import { computed, ref } from "vue";
|
|
8
|
-
import { useCache } from "@/lib/cache";
|
|
9
|
-
import { Network, Puzzle } from "lucide-vue-next";
|
|
10
|
-
import {
|
|
11
|
-
PageHeader,
|
|
12
|
-
FilterInput,
|
|
13
|
-
EmptyState,
|
|
14
|
-
MasterDetail,
|
|
15
|
-
KindBadge,
|
|
16
|
-
ListRow,
|
|
17
|
-
} from "@/components";
|
|
18
|
-
|
|
19
|
-
const { cache } = useCache();
|
|
20
|
-
const filter = ref("");
|
|
21
|
-
const selected = ref<string | null>(null);
|
|
22
|
-
|
|
23
|
-
const filtered = computed(() => {
|
|
24
|
-
if (!cache.value) return [];
|
|
25
|
-
const q = filter.value.toLowerCase();
|
|
26
|
-
return cache.value.apps.filter(
|
|
27
|
-
(a) =>
|
|
28
|
-
!q || a.name.toLowerCase().includes(q) || (a.description ?? "").toLowerCase().includes(q),
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const detail = computed(() => filtered.value.find((a) => a.name === selected.value) ?? null);
|
|
33
|
-
|
|
34
|
-
function countFor(
|
|
35
|
-
appName: string,
|
|
36
|
-
kind: "actions" | "events" | "projections" | "queries" | "workflows",
|
|
37
|
-
): number {
|
|
38
|
-
if (!cache.value) return 0;
|
|
39
|
-
return cache.value[kind].filter((x: { app: string }) => x.app === appName).length;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function pluginsFor(appName: string) {
|
|
43
|
-
if (!cache.value) return [];
|
|
44
|
-
return cache.value.plugins.filter((p) => p.app === appName);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sinksFor(appName: string) {
|
|
48
|
-
if (!cache.value) return [];
|
|
49
|
-
return cache.value.sinks?.filter((s) => s.app === appName) ?? [];
|
|
50
|
-
}
|
|
51
|
-
</script>
|
|
52
|
-
|
|
53
|
-
<template>
|
|
54
|
-
<div v-if="cache" class="h-full flex flex-col" data-testid="apps-page">
|
|
55
|
-
<div class="p-6 pb-3 border-b border-zinc-800">
|
|
56
|
-
<PageHeader
|
|
57
|
-
title="Apps"
|
|
58
|
-
subtitle="Every App registered in this workspace — its plugins, surface, and outbound sinks."
|
|
59
|
-
:icon="Network"
|
|
60
|
-
icon-color="text-emerald-400"
|
|
61
|
-
:count="filtered.length"
|
|
62
|
-
:total="cache.apps.length"
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<EmptyState
|
|
67
|
-
v-if="cache.apps.length === 0"
|
|
68
|
-
title="No apps in cache"
|
|
69
|
-
hint="Apps are declared via createApp({appName, plugins}). Run `nwire cache` after adding one."
|
|
70
|
-
:icon="Network"
|
|
71
|
-
/>
|
|
72
|
-
|
|
73
|
-
<MasterDetail v-else class="flex-1">
|
|
74
|
-
<template #listHeader>
|
|
75
|
-
<FilterInput v-model="filter" placeholder="filter by name…" />
|
|
76
|
-
</template>
|
|
77
|
-
|
|
78
|
-
<template #list>
|
|
79
|
-
<ListRow
|
|
80
|
-
v-for="app in filtered"
|
|
81
|
-
:key="app.name"
|
|
82
|
-
:selected="selected === app.name"
|
|
83
|
-
@click="selected = app.name"
|
|
84
|
-
>
|
|
85
|
-
<template #title>
|
|
86
|
-
<Network class="w-3 h-3 text-emerald-400 shrink-0" />
|
|
87
|
-
<span class="font-mono text-sm truncate">{{ app.name }}</span>
|
|
88
|
-
</template>
|
|
89
|
-
<template #meta>
|
|
90
|
-
<span class="text-[10px] text-zinc-500">
|
|
91
|
-
{{ app.plugins.length }} plugin{{ app.plugins.length === 1 ? "" : "s" }}
|
|
92
|
-
</span>
|
|
93
|
-
</template>
|
|
94
|
-
<template v-if="app.description" #description>
|
|
95
|
-
{{ app.description }}
|
|
96
|
-
</template>
|
|
97
|
-
</ListRow>
|
|
98
|
-
</template>
|
|
99
|
-
|
|
100
|
-
<template #empty
|
|
101
|
-
>Select an app to see its plugin stack, primitive counts, and sinks.</template
|
|
102
|
-
>
|
|
103
|
-
|
|
104
|
-
<template v-if="detail" #detail>
|
|
105
|
-
<div class="p-6 space-y-6" data-testid="app-detail">
|
|
106
|
-
<div>
|
|
107
|
-
<h2 class="font-mono text-xl">{{ detail.name }}</h2>
|
|
108
|
-
<p v-if="detail.description" class="text-sm text-zinc-400 mt-2 max-w-2xl">
|
|
109
|
-
{{ detail.description }}
|
|
110
|
-
</p>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div class="space-y-3">
|
|
114
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-500">Plugins</h3>
|
|
115
|
-
<div v-if="pluginsFor(detail.name).length === 0" class="text-xs text-zinc-600">
|
|
116
|
-
No plugins installed.
|
|
117
|
-
</div>
|
|
118
|
-
<div v-else class="flex flex-wrap gap-2">
|
|
119
|
-
<div
|
|
120
|
-
v-for="p in pluginsFor(detail.name)"
|
|
121
|
-
:key="p.name"
|
|
122
|
-
class="inline-flex items-center gap-1.5 rounded border border-zinc-800 bg-zinc-900/50 px-2.5 py-1 text-xs font-mono"
|
|
123
|
-
>
|
|
124
|
-
<Puzzle class="w-3 h-3 text-fuchsia-400" />
|
|
125
|
-
{{ p.name }}
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div class="space-y-3">
|
|
131
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-500">Outbound sinks</h3>
|
|
132
|
-
<div v-if="sinksFor(detail.name).length === 0" class="text-xs text-zinc-600">
|
|
133
|
-
No outbound sinks installed. Events stay in-process.
|
|
134
|
-
</div>
|
|
135
|
-
<div v-else class="space-y-1">
|
|
136
|
-
<div
|
|
137
|
-
v-for="s in sinksFor(detail.name)"
|
|
138
|
-
:key="s.name"
|
|
139
|
-
class="font-mono text-xs flex items-center gap-2"
|
|
140
|
-
>
|
|
141
|
-
<KindBadge variant="public">{{ s.position }}</KindBadge>
|
|
142
|
-
<span class="text-amber-200">{{ s.name }}</span>
|
|
143
|
-
<span v-if="s.kind" class="text-zinc-500">· {{ s.kind }}</span>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div class="space-y-3">
|
|
149
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-500">Surface</h3>
|
|
150
|
-
<div class="grid grid-cols-5 gap-3 text-sm">
|
|
151
|
-
<div>
|
|
152
|
-
<div class="text-zinc-500 text-[10px] uppercase">actions</div>
|
|
153
|
-
<div class="tabular-nums">{{ countFor(detail.name, "actions") }}</div>
|
|
154
|
-
</div>
|
|
155
|
-
<div>
|
|
156
|
-
<div class="text-zinc-500 text-[10px] uppercase">events</div>
|
|
157
|
-
<div class="tabular-nums">{{ countFor(detail.name, "events") }}</div>
|
|
158
|
-
</div>
|
|
159
|
-
<div>
|
|
160
|
-
<div class="text-zinc-500 text-[10px] uppercase">projections</div>
|
|
161
|
-
<div class="tabular-nums">{{ countFor(detail.name, "projections") }}</div>
|
|
162
|
-
</div>
|
|
163
|
-
<div>
|
|
164
|
-
<div class="text-zinc-500 text-[10px] uppercase">queries</div>
|
|
165
|
-
<div class="tabular-nums">{{ countFor(detail.name, "queries") }}</div>
|
|
166
|
-
</div>
|
|
167
|
-
<div>
|
|
168
|
-
<div class="text-zinc-500 text-[10px] uppercase">workflows</div>
|
|
169
|
-
<div class="tabular-nums">{{ countFor(detail.name, "workflows") }}</div>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
</template>
|
|
175
|
-
</MasterDetail>
|
|
176
|
-
</div>
|
|
177
|
-
</template>
|