@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.
Files changed (130) hide show
  1. package/package.json +5 -3
  2. package/src/App.vue +62 -56
  3. package/src/components/BcCard.stories.ts +47 -0
  4. package/src/components/BcCard.vue +152 -0
  5. package/src/components/DurationBar.stories.ts +55 -0
  6. package/src/components/DurationBar.vue +72 -0
  7. package/src/components/ErrorCard.stories.ts +133 -0
  8. package/src/components/ErrorCard.vue +153 -0
  9. package/src/components/GraphCanvas.stories.ts +48 -0
  10. package/src/components/GraphCanvas.vue +88 -0
  11. package/src/components/KpiTile.stories.ts +32 -0
  12. package/src/components/KpiTile.vue +39 -0
  13. package/src/components/LiveTable.stories.ts +78 -0
  14. package/src/components/LiveTable.vue +186 -0
  15. package/src/components/MetadataInspector.stories.ts +53 -0
  16. package/src/components/MetadataInspector.vue +105 -0
  17. package/src/components/NodeCard.stories.ts +44 -0
  18. package/src/components/NodeCard.vue +150 -0
  19. package/src/components/RcaPanel.stories.ts +95 -0
  20. package/src/components/RcaPanel.vue +223 -0
  21. package/src/components/ServiceNode.vue +134 -0
  22. package/src/components/SourceDrawer.vue +6 -4
  23. package/src/components/SourcePill.vue +10 -3
  24. package/src/components/StatusBadge.stories.ts +33 -0
  25. package/src/components/StatusBadge.vue +54 -0
  26. package/src/components/Waterfall.stories.ts +85 -0
  27. package/src/components/Waterfall.vue +53 -0
  28. package/src/components/WaterfallRow.vue +74 -0
  29. package/src/components/__tests__/BcCard.test.ts +53 -0
  30. package/src/components/__tests__/DurationBar.test.ts +31 -0
  31. package/src/components/__tests__/ErrorCard.test.ts +71 -0
  32. package/src/components/__tests__/KpiTile.test.ts +23 -0
  33. package/src/components/__tests__/LiveTable.test.ts +100 -0
  34. package/src/components/__tests__/MetadataInspector.test.ts +38 -0
  35. package/src/components/__tests__/NodeCard.test.ts +116 -0
  36. package/src/components/__tests__/RcaPanel.test.ts +81 -0
  37. package/src/components/__tests__/StatusBadge.test.ts +23 -0
  38. package/src/components/__tests__/Waterfall.test.ts +54 -0
  39. package/src/components/index.ts +13 -0
  40. package/src/composables/__tests__/composables-context.test.ts +107 -0
  41. package/src/composables/__tests__/useTelemetry.test.ts +104 -0
  42. package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
  43. package/src/composables/useDiscovery.ts +73 -0
  44. package/src/composables/useEndpoints.ts +94 -0
  45. package/src/composables/useLogTail.ts +51 -0
  46. package/src/composables/useManifest.ts +43 -0
  47. package/src/composables/useProcesses.ts +114 -0
  48. package/src/composables/useProject.ts +34 -0
  49. package/src/composables/useTelemetry.ts +270 -0
  50. package/src/lib/__tests__/bc-graph.test.ts +218 -0
  51. package/src/lib/__tests__/dispatch-form.test.ts +113 -0
  52. package/src/lib/__tests__/error-friendly.test.ts +198 -0
  53. package/src/lib/__tests__/home.test.ts +231 -0
  54. package/src/lib/__tests__/inspect.test.ts +160 -0
  55. package/src/lib/__tests__/kind-colors.test.ts +59 -0
  56. package/src/lib/__tests__/live-table.test.ts +194 -0
  57. package/src/lib/__tests__/manifest-health.test.ts +120 -0
  58. package/src/lib/__tests__/manifest.test.ts +87 -0
  59. package/src/lib/__tests__/metadata.test.ts +47 -0
  60. package/src/lib/__tests__/node-metrics.test.ts +144 -0
  61. package/src/lib/__tests__/operate.test.ts +97 -0
  62. package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
  63. package/src/lib/__tests__/rca.test.ts +124 -0
  64. package/src/lib/__tests__/telemetry.test.ts +91 -0
  65. package/src/lib/__tests__/topology-graph.test.ts +331 -0
  66. package/src/lib/__tests__/topology-view.test.ts +154 -0
  67. package/src/lib/__tests__/waterfall.test.ts +165 -0
  68. package/src/lib/bc-graph.ts +298 -0
  69. package/src/lib/dispatch-form.ts +160 -0
  70. package/src/lib/error-friendly.ts +288 -0
  71. package/src/lib/home.ts +191 -0
  72. package/src/lib/inspect.ts +226 -0
  73. package/src/lib/kind-colors.ts +132 -0
  74. package/src/lib/live-table.ts +204 -0
  75. package/src/lib/manifest-health.ts +71 -0
  76. package/src/lib/manifest.ts +139 -0
  77. package/src/lib/metadata.ts +52 -0
  78. package/src/lib/node-metrics.ts +242 -0
  79. package/src/lib/operate.ts +114 -0
  80. package/src/lib/pipeline-flow.ts +120 -0
  81. package/src/lib/rca.ts +193 -0
  82. package/src/lib/telemetry.ts +155 -0
  83. package/src/lib/topology-graph.ts +551 -0
  84. package/src/lib/topology-view.ts +185 -0
  85. package/src/lib/waterfall.ts +148 -0
  86. package/src/main.ts +63 -29
  87. package/src/pages/Errors.vue +272 -0
  88. package/src/pages/Home.stories.ts +7 -8
  89. package/src/pages/Home.vue +255 -540
  90. package/src/pages/Hooks.stories.ts +44 -0
  91. package/src/pages/Hooks.vue +165 -164
  92. package/src/pages/Inspect.vue +240 -0
  93. package/src/pages/Map.vue +187 -0
  94. package/src/pages/Operate.vue +74 -0
  95. package/src/pages/Plugins.stories.ts +1 -1
  96. package/src/pages/Plugins.vue +174 -238
  97. package/src/pages/Projects.vue +62 -60
  98. package/src/pages/Streams.vue +344 -0
  99. package/src/pages/Topology.vue +318 -136
  100. package/src/pages/Trace.vue +174 -412
  101. package/src/pages/__tests__/Home.test.ts +109 -54
  102. package/src/pages/__tests__/Hooks.test.ts +5 -5
  103. package/src/pages/__tests__/Inspect.test.ts +111 -0
  104. package/src/pages/__tests__/Plugins.test.ts +85 -35
  105. package/src/pages/__tests__/Trace.test.ts +117 -0
  106. package/src/pages/operate/CommandsPanel.vue +186 -0
  107. package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
  108. package/src/pages/operate/EndpointPicker.vue +56 -0
  109. package/src/pages/operate/RunPanel.vue +316 -0
  110. package/src/server/__tests__/nwire-read.test.ts +80 -0
  111. package/src/server/nwire-read.ts +63 -0
  112. package/vite.config.ts +220 -2
  113. package/src/lib/__tests__/normalize-cache.test.ts +0 -105
  114. package/src/lib/cache.ts +0 -312
  115. package/src/lib/normalize-cache.ts +0 -92
  116. package/src/pages/Actions.vue +0 -171
  117. package/src/pages/Apps.vue +0 -177
  118. package/src/pages/Commands.vue +0 -262
  119. package/src/pages/Events.vue +0 -210
  120. package/src/pages/Live.vue +0 -249
  121. package/src/pages/Overview.vue +0 -161
  122. package/src/pages/Projections.vue +0 -148
  123. package/src/pages/Queries.vue +0 -148
  124. package/src/pages/Run.vue +0 -618
  125. package/src/pages/Sinks.vue +0 -124
  126. package/src/pages/TraceNode.vue +0 -164
  127. package/src/pages/Workflows.vue +0 -184
  128. package/src/pages/__tests__/Actions.test.ts +0 -98
  129. package/src/pages/__tests__/Projections.test.ts +0 -90
  130. 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
- }
@@ -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>
@@ -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>