@alepha/devtools 0.13.6 → 0.13.8

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 (103) hide show
  1. package/assets/devtools/asset.BZV40eAE.css +1 -0
  2. package/assets/devtools/asset.Bhpm0ujk.css +1 -0
  3. package/assets/devtools/chunk.17gtbQUO.js +1 -0
  4. package/assets/devtools/chunk.1mem8WHh.js +1 -0
  5. package/assets/devtools/chunk.B0aYes_4.js +1 -0
  6. package/assets/devtools/chunk.BULoWCgJ.js +7 -0
  7. package/assets/devtools/chunk.BYrPfJRg.js +1 -0
  8. package/assets/devtools/chunk.BjFrJKj1.js +1 -0
  9. package/assets/devtools/chunk.BlqFPyLh.js +1 -0
  10. package/assets/devtools/chunk.BmJ7-uBd.js +1 -0
  11. package/assets/devtools/chunk.BzE7YYkj.js +6 -0
  12. package/assets/devtools/chunk.C49FcqzR.js +1 -0
  13. package/assets/devtools/chunk.CBGZOsrp.js +9 -0
  14. package/assets/devtools/chunk.CHRbA_gU.js +1 -0
  15. package/assets/devtools/chunk.Ct58VlQl.js +1 -0
  16. package/assets/devtools/chunk.Cw2RCl4F.js +1 -0
  17. package/assets/devtools/chunk.CyQeq1kA.js +1 -0
  18. package/assets/devtools/chunk.CzbujtK7.js +1 -0
  19. package/assets/devtools/chunk.DA9XnVAa.js +1 -0
  20. package/assets/devtools/chunk.DEUHUxKv.js +1 -0
  21. package/assets/devtools/chunk.DGW-W4Kc.js +1 -0
  22. package/assets/devtools/chunk.DHWcJNNS.js +1 -0
  23. package/assets/devtools/chunk.DIfRZc20.js +1 -0
  24. package/assets/devtools/chunk.DJJIo7HU.js +1 -0
  25. package/assets/devtools/chunk.DJOi4_So.js +1 -0
  26. package/assets/devtools/chunk.DR0SHXXd.js +1 -0
  27. package/assets/devtools/chunk.DinJSUfH.js +1 -0
  28. package/assets/devtools/chunk.DooL4OcT.js +1 -0
  29. package/assets/devtools/chunk.Dry2LXOT.js +1 -0
  30. package/assets/devtools/chunk.IVvrfXp1.js +1 -0
  31. package/assets/devtools/chunk.OlMI8g2F.js +1 -0
  32. package/assets/devtools/chunk.Pj_uCbSv.js +1 -0
  33. package/assets/devtools/chunk.RTodzvo0.js +2 -0
  34. package/assets/devtools/chunk.YXYL4YAO.js +2 -0
  35. package/assets/devtools/chunk.fpKvkQeU.js +1 -0
  36. package/assets/devtools/chunk.qDx9cjbN.js +1 -0
  37. package/assets/devtools/chunk.rJToME5k.js +1 -0
  38. package/assets/devtools/chunk.rohGhT-A.js +1 -0
  39. package/assets/devtools/chunk.uyVen0u2.js +1 -0
  40. package/assets/devtools/entry.BY4L2Uc6.js +75 -0
  41. package/assets/devtools/index.html +11 -0
  42. package/dist/index.d.ts +249 -32
  43. package/dist/index.js +253 -22
  44. package/dist/index.js.map +1 -1
  45. package/package.json +18 -12
  46. package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
  47. package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
  48. package/src/api/schemas/DevAtomMetadata.ts +26 -0
  49. package/src/api/schemas/DevCommandMetadata.ts +9 -0
  50. package/src/api/schemas/DevEntityMetadata.ts +57 -0
  51. package/src/api/schemas/DevEnvMetadata.ts +22 -0
  52. package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
  53. package/src/api/schemas/DevRouteMetadata.ts +8 -0
  54. package/src/index.ts +23 -16
  55. package/src/ui/AppRouter.tsx +85 -2
  56. package/src/ui/components/DevAtomsViewer.tsx +636 -0
  57. package/src/ui/components/DevCacheInspector.tsx +423 -0
  58. package/src/ui/components/DevDashboard.tsx +188 -0
  59. package/src/ui/components/DevEnvExplorer.tsx +462 -0
  60. package/src/ui/components/DevLayout.tsx +65 -4
  61. package/src/ui/components/DevLogViewer.tsx +161 -163
  62. package/src/ui/components/DevQueueMonitor.tsx +51 -0
  63. package/src/ui/components/DevTopicsViewer.tsx +690 -0
  64. package/src/ui/components/actions/ActionGroup.tsx +37 -0
  65. package/src/ui/components/actions/ActionItem.tsx +138 -0
  66. package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
  67. package/src/ui/components/actions/MethodBadge.tsx +18 -0
  68. package/src/ui/components/actions/SchemaViewer.tsx +21 -0
  69. package/src/ui/components/actions/TryItPanel.tsx +140 -0
  70. package/src/ui/components/actions/constants.ts +7 -0
  71. package/src/ui/components/actions/helpers.ts +18 -0
  72. package/src/ui/components/actions/index.ts +8 -0
  73. package/src/ui/components/db/ColumnBadge.tsx +55 -0
  74. package/src/ui/components/db/DevDbStudio.tsx +485 -0
  75. package/src/ui/components/db/constants.ts +11 -0
  76. package/src/ui/components/db/index.ts +4 -0
  77. package/src/ui/components/db/types.ts +7 -0
  78. package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
  79. package/src/ui/components/graph/GraphControls.tsx +162 -0
  80. package/src/ui/components/graph/NodeDetails.tsx +181 -0
  81. package/src/ui/components/graph/ProviderNode.tsx +97 -0
  82. package/src/ui/components/graph/constants.ts +35 -0
  83. package/src/ui/components/graph/helpers.ts +443 -0
  84. package/src/ui/components/graph/index.ts +7 -0
  85. package/src/ui/components/graph/types.ts +28 -0
  86. package/src/ui/styles.css +0 -6
  87. package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
  88. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
  89. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
  90. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
  91. /package/src/{entities → api/entities}/logs.ts +0 -0
  92. /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
  93. /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
  94. /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
  95. /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
  96. /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
  97. /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
  98. /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
  99. /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
  100. /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
  101. /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
  102. /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
  103. /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;