@alepha/devtools 0.13.6 → 0.13.7
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/dist/index.d.ts +249 -32
- package/dist/index.js +253 -22
- package/dist/index.js.map +1 -1
- package/package.json +12 -6
- package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
- package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
- package/src/api/schemas/DevAtomMetadata.ts +26 -0
- package/src/api/schemas/DevCommandMetadata.ts +9 -0
- package/src/api/schemas/DevEntityMetadata.ts +57 -0
- package/src/api/schemas/DevEnvMetadata.ts +22 -0
- package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
- package/src/api/schemas/DevRouteMetadata.ts +8 -0
- package/src/index.ts +23 -16
- package/src/ui/AppRouter.tsx +85 -2
- package/src/ui/components/DevAtomsViewer.tsx +636 -0
- package/src/ui/components/DevCacheInspector.tsx +423 -0
- package/src/ui/components/DevDashboard.tsx +188 -0
- package/src/ui/components/DevEnvExplorer.tsx +462 -0
- package/src/ui/components/DevLayout.tsx +65 -4
- package/src/ui/components/DevLogViewer.tsx +161 -163
- package/src/ui/components/DevQueueMonitor.tsx +51 -0
- package/src/ui/components/DevTopicsViewer.tsx +690 -0
- package/src/ui/components/actions/ActionGroup.tsx +37 -0
- package/src/ui/components/actions/ActionItem.tsx +138 -0
- package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
- package/src/ui/components/actions/MethodBadge.tsx +18 -0
- package/src/ui/components/actions/SchemaViewer.tsx +21 -0
- package/src/ui/components/actions/TryItPanel.tsx +140 -0
- package/src/ui/components/actions/constants.ts +7 -0
- package/src/ui/components/actions/helpers.ts +18 -0
- package/src/ui/components/actions/index.ts +8 -0
- package/src/ui/components/db/ColumnBadge.tsx +55 -0
- package/src/ui/components/db/DevDbStudio.tsx +485 -0
- package/src/ui/components/db/constants.ts +11 -0
- package/src/ui/components/db/index.ts +4 -0
- package/src/ui/components/db/types.ts +7 -0
- package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
- package/src/ui/components/graph/GraphControls.tsx +162 -0
- package/src/ui/components/graph/NodeDetails.tsx +181 -0
- package/src/ui/components/graph/ProviderNode.tsx +97 -0
- package/src/ui/components/graph/constants.ts +35 -0
- package/src/ui/components/graph/helpers.ts +443 -0
- package/src/ui/components/graph/index.ts +7 -0
- package/src/ui/components/graph/types.ts +28 -0
- package/src/ui/styles.css +0 -6
- package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
- /package/src/{entities → api/entities}/logs.ts +0 -0
- /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
- /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
- /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevTopicMetadata.ts +0 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { useInject } from "@alepha/react";
|
|
2
|
+
import { JsonViewer, ui } from "@alepha/ui";
|
|
3
|
+
import {
|
|
4
|
+
ActionIcon,
|
|
5
|
+
Badge,
|
|
6
|
+
Box,
|
|
7
|
+
Button,
|
|
8
|
+
Flex,
|
|
9
|
+
ScrollArea,
|
|
10
|
+
Stack,
|
|
11
|
+
Tabs,
|
|
12
|
+
Text,
|
|
13
|
+
Textarea,
|
|
14
|
+
TextInput,
|
|
15
|
+
Tooltip,
|
|
16
|
+
} from "@mantine/core";
|
|
17
|
+
import {
|
|
18
|
+
IconChevronDown,
|
|
19
|
+
IconChevronRight,
|
|
20
|
+
IconClock,
|
|
21
|
+
IconFolder,
|
|
22
|
+
IconFolderOpen,
|
|
23
|
+
IconMessageCircle,
|
|
24
|
+
IconPlayerPlay,
|
|
25
|
+
IconSearch,
|
|
26
|
+
IconSend,
|
|
27
|
+
IconServer,
|
|
28
|
+
IconTrash,
|
|
29
|
+
} from "@tabler/icons-react";
|
|
30
|
+
import { HttpClient } from "alepha/server";
|
|
31
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
32
|
+
import { devMetadataSchema } from "../../api/schemas/DevMetadata.ts";
|
|
33
|
+
import type { DevTopicMetadata } from "../../api/schemas/DevTopicMetadata.ts";
|
|
34
|
+
|
|
35
|
+
const PROVIDER_COLORS: Record<string, string> = {
|
|
36
|
+
memory: "#69db7c",
|
|
37
|
+
redis: "#ff6b6b",
|
|
38
|
+
default: "#495057",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getProviderColor = (provider: string): string => {
|
|
42
|
+
return PROVIDER_COLORS[provider] ?? PROVIDER_COLORS.default;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Build tree structure from topic names
|
|
46
|
+
interface TopicTreeNode {
|
|
47
|
+
name: string;
|
|
48
|
+
fullPath: string;
|
|
49
|
+
topic?: DevTopicMetadata;
|
|
50
|
+
children: Map<string, TopicTreeNode>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const buildTopicTree = (topics: DevTopicMetadata[]): TopicTreeNode => {
|
|
54
|
+
const root: TopicTreeNode = { name: "", fullPath: "", children: new Map() };
|
|
55
|
+
|
|
56
|
+
for (const topic of topics) {
|
|
57
|
+
const parts = topic.name.split(/[./]/);
|
|
58
|
+
let current = root;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < parts.length; i++) {
|
|
61
|
+
const part = parts[i];
|
|
62
|
+
const fullPath = parts.slice(0, i + 1).join("/");
|
|
63
|
+
|
|
64
|
+
if (!current.children.has(part)) {
|
|
65
|
+
current.children.set(part, {
|
|
66
|
+
name: part,
|
|
67
|
+
fullPath,
|
|
68
|
+
children: new Map(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
current = current.children.get(part)!;
|
|
72
|
+
}
|
|
73
|
+
current.topic = topic;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return root;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const TopicTreeItem = ({
|
|
80
|
+
node,
|
|
81
|
+
depth,
|
|
82
|
+
selectedTopic,
|
|
83
|
+
onSelectTopic,
|
|
84
|
+
expandedNodes,
|
|
85
|
+
onToggleExpand,
|
|
86
|
+
}: {
|
|
87
|
+
node: TopicTreeNode;
|
|
88
|
+
depth: number;
|
|
89
|
+
selectedTopic: DevTopicMetadata | null;
|
|
90
|
+
onSelectTopic: (topic: DevTopicMetadata) => void;
|
|
91
|
+
expandedNodes: Set<string>;
|
|
92
|
+
onToggleExpand: (path: string) => void;
|
|
93
|
+
}) => {
|
|
94
|
+
const hasChildren = node.children.size > 0;
|
|
95
|
+
const isExpanded = expandedNodes.has(node.fullPath);
|
|
96
|
+
const isSelected = node.topic && selectedTopic?.name === node.topic.name;
|
|
97
|
+
const providerColor = node.topic
|
|
98
|
+
? getProviderColor(node.topic.provider)
|
|
99
|
+
: "#495057";
|
|
100
|
+
|
|
101
|
+
const handleClick = () => {
|
|
102
|
+
if (node.topic) {
|
|
103
|
+
onSelectTopic(node.topic);
|
|
104
|
+
}
|
|
105
|
+
if (hasChildren) {
|
|
106
|
+
onToggleExpand(node.fullPath);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Box>
|
|
112
|
+
<Flex
|
|
113
|
+
align="center"
|
|
114
|
+
gap={4}
|
|
115
|
+
px="sm"
|
|
116
|
+
py={4}
|
|
117
|
+
onClick={handleClick}
|
|
118
|
+
style={{
|
|
119
|
+
cursor: "pointer",
|
|
120
|
+
paddingLeft: depth * 16 + 8,
|
|
121
|
+
backgroundColor: isSelected ? `${providerColor}15` : undefined,
|
|
122
|
+
borderLeft: isSelected
|
|
123
|
+
? `2px solid ${providerColor}`
|
|
124
|
+
: "2px solid transparent",
|
|
125
|
+
borderBottom: `1px solid ${ui.colors.border}`,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{hasChildren ? (
|
|
129
|
+
isExpanded ? (
|
|
130
|
+
<IconChevronDown size={12} opacity={0.5} />
|
|
131
|
+
) : (
|
|
132
|
+
<IconChevronRight size={12} opacity={0.5} />
|
|
133
|
+
)
|
|
134
|
+
) : (
|
|
135
|
+
<Box w={12} />
|
|
136
|
+
)}
|
|
137
|
+
{hasChildren ? (
|
|
138
|
+
isExpanded ? (
|
|
139
|
+
<IconFolderOpen size={14} opacity={0.5} />
|
|
140
|
+
) : (
|
|
141
|
+
<IconFolder size={14} opacity={0.5} />
|
|
142
|
+
)
|
|
143
|
+
) : (
|
|
144
|
+
<IconMessageCircle size={14} opacity={0.5} />
|
|
145
|
+
)}
|
|
146
|
+
<Text
|
|
147
|
+
size="sm"
|
|
148
|
+
style={{ flex: 1 }}
|
|
149
|
+
truncate
|
|
150
|
+
fw={node.topic ? 500 : 400}
|
|
151
|
+
>
|
|
152
|
+
{node.name}
|
|
153
|
+
</Text>
|
|
154
|
+
{node.topic && (
|
|
155
|
+
<Badge
|
|
156
|
+
size="xs"
|
|
157
|
+
variant="light"
|
|
158
|
+
color={node.topic.provider === "redis" ? "red" : "green"}
|
|
159
|
+
>
|
|
160
|
+
{node.topic.provider}
|
|
161
|
+
</Badge>
|
|
162
|
+
)}
|
|
163
|
+
</Flex>
|
|
164
|
+
{hasChildren && isExpanded && (
|
|
165
|
+
<Box>
|
|
166
|
+
{Array.from(node.children.values())
|
|
167
|
+
.sort((a, b) => {
|
|
168
|
+
// Folders first, then alphabetical
|
|
169
|
+
const aHasChildren = a.children.size > 0;
|
|
170
|
+
const bHasChildren = b.children.size > 0;
|
|
171
|
+
if (aHasChildren && !bHasChildren) return -1;
|
|
172
|
+
if (!aHasChildren && bHasChildren) return 1;
|
|
173
|
+
return a.name.localeCompare(b.name);
|
|
174
|
+
})
|
|
175
|
+
.map((child) => (
|
|
176
|
+
<TopicTreeItem
|
|
177
|
+
key={child.fullPath}
|
|
178
|
+
node={child}
|
|
179
|
+
depth={depth + 1}
|
|
180
|
+
selectedTopic={selectedTopic}
|
|
181
|
+
onSelectTopic={onSelectTopic}
|
|
182
|
+
expandedNodes={expandedNodes}
|
|
183
|
+
onToggleExpand={onToggleExpand}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</Box>
|
|
187
|
+
)}
|
|
188
|
+
</Box>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const TopicSidebar = ({
|
|
193
|
+
topics,
|
|
194
|
+
selectedTopic,
|
|
195
|
+
onSelectTopic,
|
|
196
|
+
search,
|
|
197
|
+
onSearchChange,
|
|
198
|
+
}: {
|
|
199
|
+
topics: DevTopicMetadata[];
|
|
200
|
+
selectedTopic: DevTopicMetadata | null;
|
|
201
|
+
onSelectTopic: (topic: DevTopicMetadata) => void;
|
|
202
|
+
search: string;
|
|
203
|
+
onSearchChange: (search: string) => void;
|
|
204
|
+
}) => {
|
|
205
|
+
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
206
|
+
|
|
207
|
+
const tree = useMemo(() => buildTopicTree(topics), [topics]);
|
|
208
|
+
|
|
209
|
+
// Auto-expand all nodes on first load
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const allPaths = new Set<string>();
|
|
212
|
+
const collectPaths = (node: TopicTreeNode) => {
|
|
213
|
+
if (node.fullPath) allPaths.add(node.fullPath);
|
|
214
|
+
for (const child of node.children.values()) {
|
|
215
|
+
collectPaths(child);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
collectPaths(tree);
|
|
219
|
+
setExpandedNodes(allPaths);
|
|
220
|
+
}, [tree]);
|
|
221
|
+
|
|
222
|
+
const handleToggleExpand = useCallback((path: string) => {
|
|
223
|
+
setExpandedNodes((prev) => {
|
|
224
|
+
const next = new Set(prev);
|
|
225
|
+
if (next.has(path)) {
|
|
226
|
+
next.delete(path);
|
|
227
|
+
} else {
|
|
228
|
+
next.add(path);
|
|
229
|
+
}
|
|
230
|
+
return next;
|
|
231
|
+
});
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Stack gap={0} h="100%">
|
|
236
|
+
<Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
|
|
237
|
+
<TextInput
|
|
238
|
+
placeholder="Search topics..."
|
|
239
|
+
leftSection={<IconSearch size={14} />}
|
|
240
|
+
size="xs"
|
|
241
|
+
value={search}
|
|
242
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
243
|
+
/>
|
|
244
|
+
</Box>
|
|
245
|
+
<ScrollArea style={{ flex: 1 }}>
|
|
246
|
+
{Array.from(tree.children.values())
|
|
247
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
248
|
+
.map((node) => (
|
|
249
|
+
<TopicTreeItem
|
|
250
|
+
key={node.fullPath}
|
|
251
|
+
node={node}
|
|
252
|
+
depth={0}
|
|
253
|
+
selectedTopic={selectedTopic}
|
|
254
|
+
onSelectTopic={onSelectTopic}
|
|
255
|
+
expandedNodes={expandedNodes}
|
|
256
|
+
onToggleExpand={handleToggleExpand}
|
|
257
|
+
/>
|
|
258
|
+
))}
|
|
259
|
+
</ScrollArea>
|
|
260
|
+
</Stack>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
interface FakeMessage {
|
|
265
|
+
id: string;
|
|
266
|
+
timestamp: Date;
|
|
267
|
+
payload: any;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const SchemaTab = ({ topic }: { topic: DevTopicMetadata }) => {
|
|
271
|
+
return (
|
|
272
|
+
<ScrollArea h="100%" p="md">
|
|
273
|
+
<Stack gap="lg">
|
|
274
|
+
<Box>
|
|
275
|
+
<Text size="sm" fw={600} mb="xs">
|
|
276
|
+
Topic Info
|
|
277
|
+
</Text>
|
|
278
|
+
<Stack gap="xs">
|
|
279
|
+
<Flex gap="sm">
|
|
280
|
+
<Text size="sm" c="dimmed" w={100}>
|
|
281
|
+
Name
|
|
282
|
+
</Text>
|
|
283
|
+
<Text size="sm" ff="monospace">
|
|
284
|
+
{topic.name}
|
|
285
|
+
</Text>
|
|
286
|
+
</Flex>
|
|
287
|
+
<Flex gap="sm">
|
|
288
|
+
<Text size="sm" c="dimmed" w={100}>
|
|
289
|
+
Provider
|
|
290
|
+
</Text>
|
|
291
|
+
<Badge
|
|
292
|
+
size="xs"
|
|
293
|
+
variant="light"
|
|
294
|
+
color={topic.provider === "redis" ? "red" : "green"}
|
|
295
|
+
>
|
|
296
|
+
{topic.provider}
|
|
297
|
+
</Badge>
|
|
298
|
+
</Flex>
|
|
299
|
+
{topic.description && (
|
|
300
|
+
<Flex gap="sm">
|
|
301
|
+
<Text size="sm" c="dimmed" w={100}>
|
|
302
|
+
Description
|
|
303
|
+
</Text>
|
|
304
|
+
<Text size="sm">{topic.description}</Text>
|
|
305
|
+
</Flex>
|
|
306
|
+
)}
|
|
307
|
+
</Stack>
|
|
308
|
+
</Box>
|
|
309
|
+
|
|
310
|
+
{topic.schema && (
|
|
311
|
+
<Box>
|
|
312
|
+
<Text size="sm" fw={600} mb="xs">
|
|
313
|
+
Message Schema
|
|
314
|
+
</Text>
|
|
315
|
+
<Box
|
|
316
|
+
p="sm"
|
|
317
|
+
style={{
|
|
318
|
+
border: `1px solid ${ui.colors.border}`,
|
|
319
|
+
borderRadius: 8,
|
|
320
|
+
backgroundColor: ui.colors.surface,
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
<JsonViewer data={topic.schema} maxDepth={5} size="xs" />
|
|
324
|
+
</Box>
|
|
325
|
+
</Box>
|
|
326
|
+
)}
|
|
327
|
+
</Stack>
|
|
328
|
+
</ScrollArea>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const MessagesTab = ({ topic }: { topic: DevTopicMetadata }) => {
|
|
333
|
+
const [messages, setMessages] = useState<FakeMessage[]>([]);
|
|
334
|
+
const [isListening, setIsListening] = useState(false);
|
|
335
|
+
|
|
336
|
+
// Generate fake message for demo
|
|
337
|
+
const addFakeMessage = useCallback(() => {
|
|
338
|
+
const fakePayload = topic.schema
|
|
339
|
+
? {
|
|
340
|
+
example: "data",
|
|
341
|
+
topic: topic.name,
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
}
|
|
344
|
+
: { message: `Hello from ${topic.name}` };
|
|
345
|
+
|
|
346
|
+
setMessages((prev) => [
|
|
347
|
+
{
|
|
348
|
+
id: Math.random().toString(36).slice(2),
|
|
349
|
+
timestamp: new Date(),
|
|
350
|
+
payload: fakePayload,
|
|
351
|
+
},
|
|
352
|
+
...prev.slice(0, 49), // Keep last 50 messages
|
|
353
|
+
]);
|
|
354
|
+
}, [topic]);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<Flex direction="column" h="100%">
|
|
358
|
+
<Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
|
|
359
|
+
<Flex gap="sm" align="center" justify="space-between">
|
|
360
|
+
<Flex gap="sm" align="center">
|
|
361
|
+
<Tooltip
|
|
362
|
+
label={
|
|
363
|
+
isListening
|
|
364
|
+
? "Stop listening (coming soon)"
|
|
365
|
+
: "Start listening (coming soon)"
|
|
366
|
+
}
|
|
367
|
+
>
|
|
368
|
+
<Button
|
|
369
|
+
size="xs"
|
|
370
|
+
variant="light"
|
|
371
|
+
color={isListening ? "red" : "green"}
|
|
372
|
+
leftSection={<IconPlayerPlay size={14} />}
|
|
373
|
+
onClick={() => setIsListening(!isListening)}
|
|
374
|
+
disabled
|
|
375
|
+
>
|
|
376
|
+
{isListening ? "Stop" : "Listen"}
|
|
377
|
+
</Button>
|
|
378
|
+
</Tooltip>
|
|
379
|
+
<Text size="xs" c="dimmed">
|
|
380
|
+
{messages.length} messages
|
|
381
|
+
</Text>
|
|
382
|
+
</Flex>
|
|
383
|
+
<Flex gap="sm">
|
|
384
|
+
<Tooltip label="Add fake message (for demo)">
|
|
385
|
+
<ActionIcon size="sm" variant="light" onClick={addFakeMessage}>
|
|
386
|
+
<IconSend size={14} />
|
|
387
|
+
</ActionIcon>
|
|
388
|
+
</Tooltip>
|
|
389
|
+
<Tooltip label="Clear messages">
|
|
390
|
+
<ActionIcon
|
|
391
|
+
size="sm"
|
|
392
|
+
variant="light"
|
|
393
|
+
color="gray"
|
|
394
|
+
onClick={() => setMessages([])}
|
|
395
|
+
>
|
|
396
|
+
<IconTrash size={14} />
|
|
397
|
+
</ActionIcon>
|
|
398
|
+
</Tooltip>
|
|
399
|
+
</Flex>
|
|
400
|
+
</Flex>
|
|
401
|
+
</Box>
|
|
402
|
+
|
|
403
|
+
{messages.length === 0 ? (
|
|
404
|
+
<Flex align="center" justify="center" style={{ flex: 1 }} c="dimmed">
|
|
405
|
+
<Stack align="center" gap="xs">
|
|
406
|
+
<IconMessageCircle size={48} opacity={0.3} />
|
|
407
|
+
<Text size="sm">No messages yet</Text>
|
|
408
|
+
<Text size="xs" c="dimmed">
|
|
409
|
+
Click the send icon to add a fake message for demo
|
|
410
|
+
</Text>
|
|
411
|
+
</Stack>
|
|
412
|
+
</Flex>
|
|
413
|
+
) : (
|
|
414
|
+
<ScrollArea style={{ flex: 1 }}>
|
|
415
|
+
<Stack gap={0}>
|
|
416
|
+
{messages.map((msg) => (
|
|
417
|
+
<Box
|
|
418
|
+
key={msg.id}
|
|
419
|
+
p="sm"
|
|
420
|
+
style={{
|
|
421
|
+
borderBottom: `1px solid ${ui.colors.border}`,
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
<Flex align="center" gap="xs" mb="xs">
|
|
425
|
+
<IconClock size={12} opacity={0.5} />
|
|
426
|
+
<Text size="xs" c="dimmed">
|
|
427
|
+
{msg.timestamp.toLocaleTimeString()}
|
|
428
|
+
</Text>
|
|
429
|
+
</Flex>
|
|
430
|
+
<Box
|
|
431
|
+
p="xs"
|
|
432
|
+
style={{
|
|
433
|
+
backgroundColor: ui.colors.surface,
|
|
434
|
+
borderRadius: 4,
|
|
435
|
+
}}
|
|
436
|
+
>
|
|
437
|
+
<JsonViewer
|
|
438
|
+
data={msg.payload}
|
|
439
|
+
maxDepth={3}
|
|
440
|
+
size="xs"
|
|
441
|
+
copyable={false}
|
|
442
|
+
/>
|
|
443
|
+
</Box>
|
|
444
|
+
</Box>
|
|
445
|
+
))}
|
|
446
|
+
</Stack>
|
|
447
|
+
</ScrollArea>
|
|
448
|
+
)}
|
|
449
|
+
</Flex>
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const PublishTab = ({ topic }: { topic: DevTopicMetadata }) => {
|
|
454
|
+
const [payload, setPayload] = useState(
|
|
455
|
+
JSON.stringify(
|
|
456
|
+
{ message: "Hello", timestamp: new Date().toISOString() },
|
|
457
|
+
null,
|
|
458
|
+
2,
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
const [error, setError] = useState<string | null>(null);
|
|
462
|
+
|
|
463
|
+
const handlePublish = () => {
|
|
464
|
+
try {
|
|
465
|
+
JSON.parse(payload);
|
|
466
|
+
setError(null);
|
|
467
|
+
// Fake publish - would call API in real implementation
|
|
468
|
+
alert(`Message would be published to "${topic.name}" (coming soon)`);
|
|
469
|
+
} catch {
|
|
470
|
+
setError("Invalid JSON");
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<ScrollArea h="100%" p="md">
|
|
476
|
+
<Stack gap="md">
|
|
477
|
+
<Box>
|
|
478
|
+
<Text size="sm" fw={600} mb="xs">
|
|
479
|
+
Publish Message
|
|
480
|
+
</Text>
|
|
481
|
+
<Text size="xs" c="dimmed" mb="sm">
|
|
482
|
+
Send a test message to this topic (coming soon)
|
|
483
|
+
</Text>
|
|
484
|
+
</Box>
|
|
485
|
+
|
|
486
|
+
<Box>
|
|
487
|
+
<Text size="sm" c="dimmed" mb="xs">
|
|
488
|
+
Payload (JSON)
|
|
489
|
+
</Text>
|
|
490
|
+
<Textarea
|
|
491
|
+
value={payload}
|
|
492
|
+
onChange={(e) => setPayload(e.target.value)}
|
|
493
|
+
minRows={8}
|
|
494
|
+
maxRows={15}
|
|
495
|
+
autosize
|
|
496
|
+
ff="monospace"
|
|
497
|
+
size="xs"
|
|
498
|
+
error={error}
|
|
499
|
+
styles={{
|
|
500
|
+
input: {
|
|
501
|
+
backgroundColor: ui.colors.surface,
|
|
502
|
+
},
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
505
|
+
</Box>
|
|
506
|
+
|
|
507
|
+
<Tooltip label="Publish message (coming soon)">
|
|
508
|
+
<Button
|
|
509
|
+
size="sm"
|
|
510
|
+
variant="light"
|
|
511
|
+
leftSection={<IconSend size={16} />}
|
|
512
|
+
onClick={handlePublish}
|
|
513
|
+
disabled
|
|
514
|
+
>
|
|
515
|
+
Publish
|
|
516
|
+
</Button>
|
|
517
|
+
</Tooltip>
|
|
518
|
+
|
|
519
|
+
{topic.schema && (
|
|
520
|
+
<Box>
|
|
521
|
+
<Text size="sm" c="dimmed" mb="xs">
|
|
522
|
+
Expected Schema
|
|
523
|
+
</Text>
|
|
524
|
+
<Box
|
|
525
|
+
p="sm"
|
|
526
|
+
style={{
|
|
527
|
+
border: `1px solid ${ui.colors.border}`,
|
|
528
|
+
borderRadius: 8,
|
|
529
|
+
backgroundColor: ui.colors.surface,
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
<JsonViewer data={topic.schema} maxDepth={3} size="xs" />
|
|
533
|
+
</Box>
|
|
534
|
+
</Box>
|
|
535
|
+
)}
|
|
536
|
+
</Stack>
|
|
537
|
+
</ScrollArea>
|
|
538
|
+
);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const TopicPanel = ({ topic }: { topic: DevTopicMetadata }) => {
|
|
542
|
+
const providerColor = getProviderColor(topic.provider);
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<Flex direction="column" h="100%">
|
|
546
|
+
{/* Header */}
|
|
547
|
+
<Box
|
|
548
|
+
px="md"
|
|
549
|
+
py="sm"
|
|
550
|
+
style={{
|
|
551
|
+
borderBottom: `1px solid ${ui.colors.border}`,
|
|
552
|
+
backgroundColor: `${providerColor}08`,
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
<Flex align="center" gap="sm">
|
|
556
|
+
<IconMessageCircle size={18} opacity={0.7} />
|
|
557
|
+
<Text size="md" fw={600} ff="monospace">
|
|
558
|
+
{topic.name}
|
|
559
|
+
</Text>
|
|
560
|
+
<Badge
|
|
561
|
+
size="xs"
|
|
562
|
+
variant="light"
|
|
563
|
+
color={topic.provider === "redis" ? "red" : "green"}
|
|
564
|
+
>
|
|
565
|
+
{topic.provider}
|
|
566
|
+
</Badge>
|
|
567
|
+
</Flex>
|
|
568
|
+
{topic.description && (
|
|
569
|
+
<Text size="xs" c="dimmed" mt={4}>
|
|
570
|
+
{topic.description}
|
|
571
|
+
</Text>
|
|
572
|
+
)}
|
|
573
|
+
</Box>
|
|
574
|
+
|
|
575
|
+
{/* Tabs */}
|
|
576
|
+
<Tabs
|
|
577
|
+
defaultValue="schema"
|
|
578
|
+
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
|
579
|
+
>
|
|
580
|
+
<Tabs.List px="md">
|
|
581
|
+
<Tabs.Tab value="schema">Schema</Tabs.Tab>
|
|
582
|
+
<Tabs.Tab value="messages">Messages</Tabs.Tab>
|
|
583
|
+
<Tabs.Tab value="publish">Publish</Tabs.Tab>
|
|
584
|
+
</Tabs.List>
|
|
585
|
+
|
|
586
|
+
<Tabs.Panel value="schema" style={{ flex: 1, overflow: "hidden" }}>
|
|
587
|
+
<SchemaTab topic={topic} />
|
|
588
|
+
</Tabs.Panel>
|
|
589
|
+
|
|
590
|
+
<Tabs.Panel value="messages" style={{ flex: 1, overflow: "hidden" }}>
|
|
591
|
+
<MessagesTab topic={topic} />
|
|
592
|
+
</Tabs.Panel>
|
|
593
|
+
|
|
594
|
+
<Tabs.Panel value="publish" style={{ flex: 1, overflow: "hidden" }}>
|
|
595
|
+
<PublishTab topic={topic} />
|
|
596
|
+
</Tabs.Panel>
|
|
597
|
+
</Tabs>
|
|
598
|
+
</Flex>
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const EmptyState = () => (
|
|
603
|
+
<Flex align="center" justify="center" h="100%" c="dimmed">
|
|
604
|
+
<Stack align="center" gap="xs">
|
|
605
|
+
<IconMessageCircle size={48} opacity={0.3} />
|
|
606
|
+
<Text size="sm">Select a topic to view details</Text>
|
|
607
|
+
</Stack>
|
|
608
|
+
</Flex>
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const NoTopicsState = () => (
|
|
612
|
+
<Flex align="center" justify="center" h="100%" c="dimmed">
|
|
613
|
+
<Stack align="center" gap="xs">
|
|
614
|
+
<IconServer size={48} opacity={0.3} />
|
|
615
|
+
<Text>No topics found</Text>
|
|
616
|
+
<Text size="sm" c="dimmed">
|
|
617
|
+
Add topics using $topic primitive to see them here
|
|
618
|
+
</Text>
|
|
619
|
+
</Stack>
|
|
620
|
+
</Flex>
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
export const DevTopicsViewer = () => {
|
|
624
|
+
const http = useInject(HttpClient);
|
|
625
|
+
const [topics, setTopics] = useState<DevTopicMetadata[]>([]);
|
|
626
|
+
const [loading, setLoading] = useState(true);
|
|
627
|
+
const [selectedTopic, setSelectedTopic] = useState<DevTopicMetadata | null>(
|
|
628
|
+
null,
|
|
629
|
+
);
|
|
630
|
+
const [search, setSearch] = useState("");
|
|
631
|
+
|
|
632
|
+
// Fetch topics
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
http
|
|
635
|
+
.fetch("/devtools/api/metadata", {
|
|
636
|
+
schema: { response: devMetadataSchema },
|
|
637
|
+
})
|
|
638
|
+
.then((res) => {
|
|
639
|
+
setTopics(res.data.topics);
|
|
640
|
+
setLoading(false);
|
|
641
|
+
});
|
|
642
|
+
}, []);
|
|
643
|
+
|
|
644
|
+
// Filter topics by search
|
|
645
|
+
const filteredTopics = useMemo(() => {
|
|
646
|
+
if (!search) return topics;
|
|
647
|
+
const searchLower = search.toLowerCase();
|
|
648
|
+
return topics.filter((t) => t.name.toLowerCase().includes(searchLower));
|
|
649
|
+
}, [topics, search]);
|
|
650
|
+
|
|
651
|
+
if (loading) {
|
|
652
|
+
return (
|
|
653
|
+
<Flex align="center" justify="center" h="100%">
|
|
654
|
+
<Text c="dimmed">Loading...</Text>
|
|
655
|
+
</Flex>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (topics.length === 0) {
|
|
660
|
+
return <NoTopicsState />;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
<Flex h="100%" style={{ overflow: "hidden" }}>
|
|
665
|
+
{/* Sidebar */}
|
|
666
|
+
<Box
|
|
667
|
+
w={280}
|
|
668
|
+
style={{
|
|
669
|
+
borderRight: `1px solid ${ui.colors.border}`,
|
|
670
|
+
backgroundColor: ui.colors.surface,
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<TopicSidebar
|
|
674
|
+
topics={filteredTopics}
|
|
675
|
+
selectedTopic={selectedTopic}
|
|
676
|
+
onSelectTopic={setSelectedTopic}
|
|
677
|
+
search={search}
|
|
678
|
+
onSearchChange={setSearch}
|
|
679
|
+
/>
|
|
680
|
+
</Box>
|
|
681
|
+
|
|
682
|
+
{/* Main content */}
|
|
683
|
+
<Box style={{ flex: 1, overflow: "hidden" }}>
|
|
684
|
+
{selectedTopic ? <TopicPanel topic={selectedTopic} /> : <EmptyState />}
|
|
685
|
+
</Box>
|
|
686
|
+
</Flex>
|
|
687
|
+
);
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
export default DevTopicsViewer;
|