@nwire/studio 0.9.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/components.json +19 -0
  4. package/index.html +12 -0
  5. package/package.json +66 -0
  6. package/src/App.vue +305 -0
  7. package/src/components/EmptyState.stories.ts +53 -0
  8. package/src/components/EmptyState.vue +28 -0
  9. package/src/components/ErrorBoundary.vue +60 -0
  10. package/src/components/FilterInput.stories.ts +32 -0
  11. package/src/components/FilterInput.vue +33 -0
  12. package/src/components/JsonView.stories.ts +38 -0
  13. package/src/components/JsonView.vue +34 -0
  14. package/src/components/KindBadge.stories.ts +72 -0
  15. package/src/components/KindBadge.vue +59 -0
  16. package/src/components/ListRow.stories.ts +56 -0
  17. package/src/components/ListRow.vue +48 -0
  18. package/src/components/MasterDetail.stories.ts +74 -0
  19. package/src/components/MasterDetail.vue +35 -0
  20. package/src/components/MonacoViewer.vue +143 -0
  21. package/src/components/PageHeader.stories.ts +45 -0
  22. package/src/components/PageHeader.vue +46 -0
  23. package/src/components/SchemaNode.vue +208 -0
  24. package/src/components/SchemaTree.vue +65 -0
  25. package/src/components/SourceDrawer.vue +136 -0
  26. package/src/components/SourcePill.vue +103 -0
  27. package/src/components/__tests__/EmptyState.test.ts +28 -0
  28. package/src/components/__tests__/ErrorBoundary.test.ts +52 -0
  29. package/src/components/__tests__/FilterInput.test.ts +38 -0
  30. package/src/components/__tests__/JsonView.test.ts +33 -0
  31. package/src/components/__tests__/KindBadge.test.ts +39 -0
  32. package/src/components/__tests__/ListRow.test.ts +39 -0
  33. package/src/components/__tests__/MasterDetail.test.ts +40 -0
  34. package/src/components/__tests__/PageHeader.test.ts +42 -0
  35. package/src/components/index.ts +17 -0
  36. package/src/components/ui/badge/Badge.vue +17 -0
  37. package/src/components/ui/badge/index.ts +25 -0
  38. package/src/components/ui/button/Button.vue +28 -0
  39. package/src/components/ui/button/index.ts +34 -0
  40. package/src/components/ui/card/Card.vue +14 -0
  41. package/src/components/ui/card/CardContent.vue +14 -0
  42. package/src/components/ui/card/CardDescription.vue +14 -0
  43. package/src/components/ui/card/CardFooter.vue +14 -0
  44. package/src/components/ui/card/CardHeader.vue +14 -0
  45. package/src/components/ui/card/CardTitle.vue +14 -0
  46. package/src/components/ui/card/index.ts +6 -0
  47. package/src/components/ui/dialog/Dialog.vue +15 -0
  48. package/src/components/ui/dialog/DialogClose.vue +12 -0
  49. package/src/components/ui/dialog/DialogContent.vue +47 -0
  50. package/src/components/ui/dialog/DialogDescription.vue +22 -0
  51. package/src/components/ui/dialog/DialogFooter.vue +12 -0
  52. package/src/components/ui/dialog/DialogHeader.vue +14 -0
  53. package/src/components/ui/dialog/DialogScrollContent.vue +60 -0
  54. package/src/components/ui/dialog/DialogTitle.vue +22 -0
  55. package/src/components/ui/dialog/DialogTrigger.vue +12 -0
  56. package/src/components/ui/dialog/index.ts +9 -0
  57. package/src/components/ui/input/Input.vue +32 -0
  58. package/src/components/ui/input/index.ts +1 -0
  59. package/src/components/ui/scroll-area/ScrollArea.vue +22 -0
  60. package/src/components/ui/scroll-area/ScrollBar.vue +32 -0
  61. package/src/components/ui/scroll-area/index.ts +2 -0
  62. package/src/components/ui/separator/Separator.vue +27 -0
  63. package/src/components/ui/separator/index.ts +1 -0
  64. package/src/components/ui/skeleton/Skeleton.vue +14 -0
  65. package/src/components/ui/skeleton/index.ts +1 -0
  66. package/src/components/ui/tabs/Tabs.vue +15 -0
  67. package/src/components/ui/tabs/TabsContent.vue +25 -0
  68. package/src/components/ui/tabs/TabsList.vue +25 -0
  69. package/src/components/ui/tabs/TabsTrigger.vue +29 -0
  70. package/src/components/ui/tabs/index.ts +4 -0
  71. package/src/components/ui/tooltip/Tooltip.vue +15 -0
  72. package/src/components/ui/tooltip/TooltipContent.vue +40 -0
  73. package/src/components/ui/tooltip/TooltipProvider.vue +12 -0
  74. package/src/components/ui/tooltip/TooltipTrigger.vue +12 -0
  75. package/src/components/ui/tooltip/index.ts +4 -0
  76. package/src/composables/useCopy.ts +31 -0
  77. package/src/lib/__tests__/normalize-cache.test.ts +104 -0
  78. package/src/lib/cache.ts +334 -0
  79. package/src/lib/normalize-cache.ts +92 -0
  80. package/src/lib/project-catalog.ts +125 -0
  81. package/src/lib/utils.ts +6 -0
  82. package/src/main.ts +112 -0
  83. package/src/pages/Actions.vue +180 -0
  84. package/src/pages/Commands.vue +262 -0
  85. package/src/pages/Dispatch.vue +431 -0
  86. package/src/pages/Events.vue +166 -0
  87. package/src/pages/Home.stories.ts +47 -0
  88. package/src/pages/Home.vue +485 -0
  89. package/src/pages/Hooks.vue +297 -0
  90. package/src/pages/Live.vue +249 -0
  91. package/src/pages/Modules.vue +174 -0
  92. package/src/pages/Overview.vue +159 -0
  93. package/src/pages/Plugins.stories.ts +44 -0
  94. package/src/pages/Plugins.vue +403 -0
  95. package/src/pages/Projects.vue +272 -0
  96. package/src/pages/Run.vue +479 -0
  97. package/src/pages/Topology.vue +164 -0
  98. package/src/pages/Trace.vue +511 -0
  99. package/src/pages/TraceNode.vue +166 -0
  100. package/src/pages/Workflows.vue +191 -0
  101. package/src/pages/__tests__/Actions.test.ts +98 -0
  102. package/src/pages/__tests__/Home.test.ts +98 -0
  103. package/src/pages/__tests__/Hooks.test.ts +119 -0
  104. package/src/pages/__tests__/Plugins.test.ts +80 -0
  105. package/src/style.css +40 -0
  106. package/tsconfig.json +20 -0
  107. package/vite.config.ts +892 -0
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import { nextTick, onMounted, ref, watch } from "vue";
3
+ import { useRoute, useRouter } from "vue-router";
4
+ import { useCache } from "@/lib/cache";
5
+ import { Boxes, ArrowDownRight, ArrowUpRight } from "lucide-vue-next";
6
+
7
+ const route = useRoute();
8
+ const router = useRouter();
9
+ const { cache } = useCache();
10
+
11
+ /**
12
+ * Preselect — /modules?name=<moduleName>. The page is grid-of-cards (no
13
+ * master/detail), so "preselect" means: highlight the card + scroll it
14
+ * into view. First module with the matching name wins (modules are unique
15
+ * within an app, and module-name collisions across apps are rare in
16
+ * practice — if it ever happens the operator can disambiguate visually).
17
+ */
18
+ const selected = ref<string | null>(null);
19
+
20
+ function applyQueryPreselect(): Promise<void> | void {
21
+ const name = route.query.name;
22
+ if (typeof name !== "string" || name.length === 0) {
23
+ selected.value = null;
24
+ return;
25
+ }
26
+ const found = cache.value?.modules.find((m) => m.name === name);
27
+ if (!found) return;
28
+ const key = `${found.app}::${found.name}`;
29
+ selected.value = key;
30
+ return nextTick(() => {
31
+ const el = document.querySelector(`[data-module-key="${key}"]`);
32
+ if (el instanceof HTMLElement) {
33
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
34
+ }
35
+ });
36
+ }
37
+
38
+ onMounted(() => {
39
+ void applyQueryPreselect();
40
+ });
41
+ watch(
42
+ () => route.query.name,
43
+ () => {
44
+ void applyQueryPreselect();
45
+ },
46
+ );
47
+ watch(
48
+ () => cache.value,
49
+ () => {
50
+ void applyQueryPreselect();
51
+ },
52
+ );
53
+
54
+ const moduleKey = (m: { app: string; name: string }) => `${m.app}::${m.name}`;
55
+
56
+ function openEvent(name: string): void {
57
+ void router.push({ path: "/events", query: { name } });
58
+ }
59
+ function openAction(name: string): void {
60
+ void router.push({ path: "/actions", query: { name } });
61
+ }
62
+ </script>
63
+
64
+ <template>
65
+ <div v-if="cache" class="p-6 space-y-3">
66
+ <div>
67
+ <h1 class="text-2xl font-semibold tracking-tight">Modules</h1>
68
+ <p class="text-sm text-zinc-500 mt-1">Bounded contexts and their dependency graph</p>
69
+ </div>
70
+
71
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
72
+ <div
73
+ v-for="m in cache.modules"
74
+ :key="moduleKey(m)"
75
+ :data-module-key="moduleKey(m)"
76
+ :data-testid="`module-card-${m.name}`"
77
+ class="rounded-lg border bg-zinc-900/30 px-4 py-3 space-y-2 transition-colors"
78
+ :class="
79
+ selected === moduleKey(m)
80
+ ? 'border-emerald-500/70 ring-1 ring-emerald-500/40'
81
+ : 'border-zinc-800'
82
+ "
83
+ >
84
+ <div class="flex items-center justify-between">
85
+ <div class="flex items-center gap-2">
86
+ <Boxes class="w-4 h-4 text-emerald-400" />
87
+ <span class="font-mono font-medium">{{ m.name }}</span>
88
+ <span
89
+ class="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-zinc-800 text-zinc-400"
90
+ >
91
+ {{ m.app }}
92
+ </span>
93
+ </div>
94
+ <div class="text-xs text-zinc-500 tabular-nums" data-testid="module-counts">
95
+ {{ m.counts.actions }}A · {{ m.counts.events }}E · {{ m.counts.actors }}@ ·
96
+ {{ m.counts.projections }}P · {{ m.counts.queries }}Q · {{ m.counts.workflows }}W
97
+ </div>
98
+ </div>
99
+
100
+ <div v-if="m.provides.events.length || m.provides.actions.length" class="text-xs">
101
+ <div class="flex items-center gap-1 text-zinc-500 mb-1">
102
+ <ArrowUpRight class="w-3 h-3 text-emerald-400" />
103
+ Provides
104
+ </div>
105
+ <div class="flex flex-wrap gap-1 pl-4">
106
+ <button
107
+ v-for="e in m.provides.events"
108
+ :key="`p-e-${e}`"
109
+ type="button"
110
+ class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-purple-950/30 border border-purple-900/50 text-purple-300 hover:bg-purple-900/40"
111
+ :data-testid="`event-link-${e}`"
112
+ @click="openEvent(e)"
113
+ >
114
+ {{ e }}
115
+ </button>
116
+ <button
117
+ v-for="a in m.provides.actions"
118
+ :key="`p-a-${a}`"
119
+ type="button"
120
+ class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-amber-950/30 border border-amber-900/50 text-amber-300 hover:bg-amber-900/40"
121
+ :data-testid="`action-link-${a}`"
122
+ @click="openAction(a)"
123
+ >
124
+ {{ a }}
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ <div
130
+ v-if="m.needs.events.length || m.needs.externalEvents.length || m.needs.actions.length"
131
+ class="text-xs"
132
+ >
133
+ <div class="flex items-center gap-1 text-zinc-500 mb-1">
134
+ <ArrowDownRight class="w-3 h-3 text-blue-400" />
135
+ Needs
136
+ </div>
137
+ <div class="flex flex-wrap gap-1 pl-4">
138
+ <button
139
+ v-for="e in m.needs.events"
140
+ :key="`n-e-${e}`"
141
+ type="button"
142
+ class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-zinc-900 border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
143
+ :data-testid="`event-link-${e}`"
144
+ @click="openEvent(e)"
145
+ >
146
+ {{ e }}
147
+ </button>
148
+ <button
149
+ v-for="e in m.needs.externalEvents"
150
+ :key="`n-x-${e}`"
151
+ type="button"
152
+ class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-blue-950/30 border border-blue-900/50 text-blue-300 hover:bg-blue-900/40"
153
+ :data-testid="`event-link-${e}`"
154
+ title="cross-service"
155
+ @click="openEvent(e)"
156
+ >
157
+ {{ e }} ⨯
158
+ </button>
159
+ <button
160
+ v-for="a in m.needs.actions"
161
+ :key="`n-a-${a}`"
162
+ type="button"
163
+ class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-zinc-900 border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
164
+ :data-testid="`action-link-${a}`"
165
+ @click="openAction(a)"
166
+ >
167
+ {{ a }}
168
+ </button>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </template>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { RouterLink } from "vue-router";
4
+ import { useCache } from "@/lib/cache";
5
+ import {
6
+ Boxes,
7
+ Zap,
8
+ Radio,
9
+ Layers,
10
+ Database,
11
+ GitBranch,
12
+ Network,
13
+ LayoutDashboard,
14
+ Waves,
15
+ Send,
16
+ Play,
17
+ Workflow,
18
+ } from "lucide-vue-next";
19
+ import { PageHeader, KindBadge } from "@/components";
20
+
21
+ const { cache } = useCache();
22
+
23
+ // "Daily" shortcuts — the high-frequency entry points.
24
+ const quickActions = [
25
+ {
26
+ to: "/live",
27
+ label: "Trace",
28
+ desc: "Watch actions / events / workflows fire in real time",
29
+ icon: Waves,
30
+ color: "text-cyan-400",
31
+ },
32
+ {
33
+ to: "/dispatch",
34
+ label: "Try",
35
+ desc: "Dispatch any action with a form generated from its schema",
36
+ icon: Send,
37
+ color: "text-amber-400",
38
+ },
39
+ {
40
+ to: "/run",
41
+ label: "Processes",
42
+ desc: "Start, stop, and watch logs for your wires",
43
+ icon: Play,
44
+ color: "text-emerald-400",
45
+ },
46
+ {
47
+ to: "/eventstorm",
48
+ label: "Flow",
49
+ desc: "See the causal graph: action → event → workflow → action",
50
+ icon: Workflow,
51
+ color: "text-violet-400",
52
+ },
53
+ ];
54
+
55
+ const hasData = computed(() => !!cache.value && cache.value.apps.length > 0);
56
+
57
+ const stats = computed(() => {
58
+ if (!cache.value) return [];
59
+ return [
60
+ { label: "Apps", value: cache.value.apps.length, icon: Network, color: "text-blue-400" },
61
+ { label: "Modules", value: cache.value.modules.length, icon: Boxes, color: "text-emerald-400" },
62
+ { label: "Actions", value: cache.value.actions.length, icon: Zap, color: "text-amber-400" },
63
+ { label: "Events", value: cache.value.events.length, icon: Radio, color: "text-purple-400" },
64
+ { label: "Actors", value: cache.value.actors.length, icon: Layers, color: "text-pink-400" },
65
+ {
66
+ label: "Projections",
67
+ value: cache.value.projections.length,
68
+ icon: Database,
69
+ color: "text-cyan-400",
70
+ },
71
+ {
72
+ label: "Workflows",
73
+ value: cache.value.workflows.length,
74
+ icon: GitBranch,
75
+ color: "text-violet-400",
76
+ },
77
+ {
78
+ label: "Resolvers",
79
+ value: cache.value.resolvers.length,
80
+ icon: Network,
81
+ color: "text-rose-400",
82
+ },
83
+ ];
84
+ });
85
+ </script>
86
+
87
+ <template>
88
+ <div v-if="cache" class="p-6 space-y-6" data-testid="overview-page">
89
+ <PageHeader
90
+ title="Overview"
91
+ :icon="LayoutDashboard"
92
+ icon-color="text-emerald-400"
93
+ :subtitle="
94
+ hasData
95
+ ? 'Snapshot from .nwire/manifest.json'
96
+ : 'No apps discovered yet — try the shortcuts below'
97
+ "
98
+ />
99
+
100
+ <!-- Quick actions — what you actually do here daily. -->
101
+ <div>
102
+ <h2 class="text-sm font-medium text-zinc-300 uppercase tracking-wide mb-3">Start here</h2>
103
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
104
+ <RouterLink
105
+ v-for="a in quickActions"
106
+ :key="a.to"
107
+ :to="a.to"
108
+ class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 hover:bg-zinc-900 hover:border-zinc-700 transition-colors block"
109
+ >
110
+ <div class="flex items-center gap-2 mb-1">
111
+ <component :is="a.icon" class="w-4 h-4" :class="a.color" />
112
+ <span class="font-medium text-zinc-100">{{ a.label }}</span>
113
+ </div>
114
+ <div class="text-xs text-zinc-400 leading-relaxed">{{ a.desc }}</div>
115
+ </RouterLink>
116
+ </div>
117
+ </div>
118
+
119
+ <div v-if="hasData" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3">
120
+ <div
121
+ v-for="s in stats"
122
+ :key="s.label"
123
+ class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3"
124
+ data-testid="overview-stat"
125
+ >
126
+ <div class="flex items-center justify-between mb-1">
127
+ <span class="text-xs text-zinc-500 uppercase tracking-wide">{{ s.label }}</span>
128
+ <component :is="s.icon" class="w-3.5 h-3.5" :class="s.color" />
129
+ </div>
130
+ <div class="text-2xl font-semibold tabular-nums">{{ s.value }}</div>
131
+ </div>
132
+ </div>
133
+
134
+ <div v-if="hasData">
135
+ <h2 class="text-sm font-medium text-zinc-300 uppercase tracking-wide mb-3">Apps</h2>
136
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
137
+ <div
138
+ v-for="app in cache.apps"
139
+ :key="app.name"
140
+ class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3"
141
+ data-testid="overview-app-card"
142
+ >
143
+ <div class="flex items-center justify-between">
144
+ <span class="font-medium">{{ app.name }}</span>
145
+ <span class="text-xs text-zinc-500 tabular-nums"> {{ app.modules.length }} BCs </span>
146
+ </div>
147
+ <div v-if="app.description" class="text-sm text-zinc-400 mt-1">
148
+ {{ app.description }}
149
+ </div>
150
+ <div class="mt-2 flex flex-wrap gap-1">
151
+ <KindBadge v-for="m in app.modules" :key="m" variant="neutral">
152
+ {{ m }}
153
+ </KindBadge>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </template>
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from "@storybook/vue3-vite";
2
+ import { createRouter, createMemoryHistory } from "vue-router";
3
+ import Plugins from "./Plugins.vue";
4
+
5
+ /**
6
+ * Storybook for the Plugins page. The manifest fetch + SSE will fail
7
+ * inside Storybook, so the page degrades to its empty / no-live state —
8
+ * which is exactly the surface we want to document.
9
+ */
10
+ const router = createRouter({
11
+ history: createMemoryHistory(),
12
+ routes: [
13
+ { path: "/plugins", name: "plugins", component: Plugins },
14
+ { path: "/hooks", name: "hooks", component: { template: "<div/>" } },
15
+ ],
16
+ });
17
+
18
+ const meta: Meta<typeof Plugins> = {
19
+ title: "Pages/Plugins",
20
+ component: Plugins,
21
+ decorators: [
22
+ (story) => ({
23
+ components: { story },
24
+ template: '<div class="h-screen bg-zinc-950 text-zinc-100"><story /></div>',
25
+ }),
26
+ ],
27
+ parameters: {
28
+ layout: "fullscreen",
29
+ vueRouter: { router },
30
+ },
31
+ render: () => ({
32
+ components: { Plugins },
33
+ setup() {
34
+ return {};
35
+ },
36
+ template: "<Plugins />",
37
+ }),
38
+ };
39
+ export default meta;
40
+
41
+ type Story = StoryObj<typeof Plugins>;
42
+
43
+ /** Default: no wire running — empty cache + stream errored. */
44
+ export const NoLiveData: Story = {};