@alepha/devtools 0.13.5 → 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.
Files changed (62) hide show
  1. package/dist/{index.d.mts → index.d.ts} +250 -32
  2. package/dist/{index.mjs → index.js} +254 -22
  3. package/dist/index.js.map +1 -0
  4. package/package.json +12 -6
  5. package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
  6. package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
  7. package/src/api/schemas/DevAtomMetadata.ts +26 -0
  8. package/src/api/schemas/DevCommandMetadata.ts +9 -0
  9. package/src/api/schemas/DevEntityMetadata.ts +57 -0
  10. package/src/api/schemas/DevEnvMetadata.ts +22 -0
  11. package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
  12. package/src/api/schemas/DevRouteMetadata.ts +8 -0
  13. package/src/index.ts +23 -16
  14. package/src/ui/AppRouter.tsx +85 -2
  15. package/src/ui/components/DevAtomsViewer.tsx +636 -0
  16. package/src/ui/components/DevCacheInspector.tsx +423 -0
  17. package/src/ui/components/DevDashboard.tsx +188 -0
  18. package/src/ui/components/DevEnvExplorer.tsx +462 -0
  19. package/src/ui/components/DevLayout.tsx +65 -4
  20. package/src/ui/components/DevLogViewer.tsx +161 -163
  21. package/src/ui/components/DevQueueMonitor.tsx +51 -0
  22. package/src/ui/components/DevTopicsViewer.tsx +690 -0
  23. package/src/ui/components/actions/ActionGroup.tsx +37 -0
  24. package/src/ui/components/actions/ActionItem.tsx +138 -0
  25. package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
  26. package/src/ui/components/actions/MethodBadge.tsx +18 -0
  27. package/src/ui/components/actions/SchemaViewer.tsx +21 -0
  28. package/src/ui/components/actions/TryItPanel.tsx +140 -0
  29. package/src/ui/components/actions/constants.ts +7 -0
  30. package/src/ui/components/actions/helpers.ts +18 -0
  31. package/src/ui/components/actions/index.ts +8 -0
  32. package/src/ui/components/db/ColumnBadge.tsx +55 -0
  33. package/src/ui/components/db/DevDbStudio.tsx +485 -0
  34. package/src/ui/components/db/constants.ts +11 -0
  35. package/src/ui/components/db/index.ts +4 -0
  36. package/src/ui/components/db/types.ts +7 -0
  37. package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
  38. package/src/ui/components/graph/GraphControls.tsx +162 -0
  39. package/src/ui/components/graph/NodeDetails.tsx +181 -0
  40. package/src/ui/components/graph/ProviderNode.tsx +97 -0
  41. package/src/ui/components/graph/constants.ts +35 -0
  42. package/src/ui/components/graph/helpers.ts +443 -0
  43. package/src/ui/components/graph/index.ts +7 -0
  44. package/src/ui/components/graph/types.ts +28 -0
  45. package/src/ui/styles.css +0 -6
  46. package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
  47. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
  48. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
  49. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
  50. /package/src/{entities → api/entities}/logs.ts +0 -0
  51. /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
  52. /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
  53. /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
  54. /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
  55. /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
  56. /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
  57. /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
  58. /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
  59. /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
  60. /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
  61. /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
  62. /package/src/{schemas → api/schemas}/DevTopicMetadata.ts +0 -0
@@ -0,0 +1,462 @@
1
+ import { useInject } from "@alepha/react";
2
+ import { ui } from "@alepha/ui";
3
+ import {
4
+ Badge,
5
+ Box,
6
+ Code,
7
+ CopyButton,
8
+ Flex,
9
+ ScrollArea,
10
+ Stack,
11
+ Table,
12
+ Text,
13
+ TextInput,
14
+ Tooltip,
15
+ } from "@mantine/core";
16
+ import {
17
+ IconCheck,
18
+ IconCopy,
19
+ IconKey,
20
+ IconSearch,
21
+ IconSettings,
22
+ IconVariable,
23
+ } from "@tabler/icons-react";
24
+ import { HttpClient } from "alepha/server";
25
+ import { useEffect, useMemo, useState } from "react";
26
+ import type { DevEnvMetadata } from "../../api/schemas/DevEnvMetadata.ts";
27
+ import { devMetadataSchema } from "../../api/schemas/DevMetadata.ts";
28
+
29
+ interface EnvVariable {
30
+ name: string;
31
+ value: any;
32
+ type: string;
33
+ description?: string;
34
+ format?: string;
35
+ }
36
+
37
+ const parseEnvVariables = (envs: DevEnvMetadata[]): EnvVariable[] => {
38
+ const variableMap = new Map<string, EnvVariable>();
39
+
40
+ for (const env of envs) {
41
+ const schema = env.schema;
42
+ if (!schema?.properties) continue;
43
+
44
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
45
+ // Skip if already added (deduplication)
46
+ if (variableMap.has(name)) continue;
47
+
48
+ const prop = propSchema as any;
49
+ const value = env.values[name];
50
+
51
+ // Get the actual type from schema
52
+ let type = prop.type ?? "unknown";
53
+ let format = prop.format;
54
+
55
+ // Handle union types (optional)
56
+ if (prop.anyOf) {
57
+ const nonNull = prop.anyOf.find((t: any) => t.type !== "null");
58
+ if (nonNull) {
59
+ type = nonNull.type ?? "unknown";
60
+ format = nonNull.format;
61
+ }
62
+ }
63
+
64
+ variableMap.set(name, {
65
+ name,
66
+ value,
67
+ type,
68
+ description: prop.description,
69
+ format,
70
+ });
71
+ }
72
+ }
73
+
74
+ return Array.from(variableMap.values()).sort((a, b) =>
75
+ a.name.localeCompare(b.name),
76
+ );
77
+ };
78
+
79
+ const EnvSidebar = ({
80
+ variables,
81
+ selectedVar,
82
+ onSelectVar,
83
+ search,
84
+ onSearchChange,
85
+ }: {
86
+ variables: EnvVariable[];
87
+ selectedVar: EnvVariable | null;
88
+ onSelectVar: (v: EnvVariable) => void;
89
+ search: string;
90
+ onSearchChange: (s: string) => void;
91
+ }) => {
92
+ return (
93
+ <Stack gap={0} h="100%">
94
+ <Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
95
+ <TextInput
96
+ placeholder="Search variables..."
97
+ leftSection={<IconSearch size={14} />}
98
+ size="xs"
99
+ value={search}
100
+ onChange={(e) => onSearchChange(e.target.value)}
101
+ />
102
+ </Box>
103
+ <ScrollArea style={{ flex: 1 }}>
104
+ <Box>
105
+ <Text
106
+ size="xs"
107
+ fw={600}
108
+ c="dimmed"
109
+ px="sm"
110
+ py="xs"
111
+ style={{
112
+ backgroundColor: ui.colors.background,
113
+ borderBottom: `1px solid ${ui.colors.border}`,
114
+ textTransform: "uppercase",
115
+ letterSpacing: "0.05em",
116
+ }}
117
+ >
118
+ Variables ({variables.length})
119
+ </Text>
120
+ {variables.map((v) => {
121
+ const isSelected = selectedVar?.name === v.name;
122
+ return (
123
+ <Flex
124
+ key={v.name}
125
+ align="center"
126
+ gap="xs"
127
+ px="sm"
128
+ py={6}
129
+ onClick={() => onSelectVar(v)}
130
+ style={{
131
+ cursor: "pointer",
132
+ backgroundColor: isSelected ? "#228be615" : undefined,
133
+ borderLeft: isSelected
134
+ ? "2px solid #228be6"
135
+ : "2px solid transparent",
136
+ borderBottom: `1px solid ${ui.colors.border}`,
137
+ }}
138
+ >
139
+ <IconVariable size={14} opacity={0.5} />
140
+ <Text size="sm" style={{ flex: 1 }} truncate ff="monospace">
141
+ {v.name}
142
+ </Text>
143
+ </Flex>
144
+ );
145
+ })}
146
+ </Box>
147
+ </ScrollArea>
148
+ </Stack>
149
+ );
150
+ };
151
+
152
+ const EnvPanel = ({ variable }: { variable: EnvVariable }) => {
153
+ const hasValue = variable.value !== undefined && variable.value !== "";
154
+ const displayValue = hasValue ? String(variable.value) : "(not set)";
155
+ const isSensitive =
156
+ variable.name.toLowerCase().includes("secret") ||
157
+ variable.name.toLowerCase().includes("password") ||
158
+ variable.name.toLowerCase().includes("key") ||
159
+ variable.name.toLowerCase().includes("token");
160
+
161
+ return (
162
+ <Flex direction="column" h="100%">
163
+ {/* Header */}
164
+ <Box
165
+ px="md"
166
+ py="sm"
167
+ style={{
168
+ borderBottom: `1px solid ${ui.colors.border}`,
169
+ backgroundColor: "#228be608",
170
+ }}
171
+ >
172
+ <Flex align="center" gap="sm">
173
+ <IconVariable size={18} opacity={0.7} />
174
+ <Text size="md" fw={600} ff="monospace">
175
+ {variable.name}
176
+ </Text>
177
+ <Badge size="xs" variant="outline" color="blue">
178
+ {variable.format || variable.type}
179
+ </Badge>
180
+ </Flex>
181
+ </Box>
182
+
183
+ <ScrollArea style={{ flex: 1 }} p="md">
184
+ <Stack gap="lg">
185
+ {/* Description */}
186
+ {variable.description && (
187
+ <Box>
188
+ <Text size="sm" fw={600} mb="xs">
189
+ Description
190
+ </Text>
191
+ <Text size="sm" c="dimmed">
192
+ {variable.description}
193
+ </Text>
194
+ </Box>
195
+ )}
196
+
197
+ {/* Current Value */}
198
+ <Box>
199
+ <Text size="sm" fw={600} mb="xs">
200
+ Current Value
201
+ </Text>
202
+ <Flex align="center" gap="sm">
203
+ <Code
204
+ block
205
+ style={{
206
+ flex: 1,
207
+ filter: isSensitive && hasValue ? "blur(4px)" : undefined,
208
+ transition: "filter 0.2s",
209
+ }}
210
+ onMouseEnter={(e) => {
211
+ if (isSensitive) e.currentTarget.style.filter = "none";
212
+ }}
213
+ onMouseLeave={(e) => {
214
+ if (isSensitive && hasValue)
215
+ e.currentTarget.style.filter = "blur(4px)";
216
+ }}
217
+ >
218
+ {displayValue}
219
+ </Code>
220
+ {hasValue && (
221
+ <CopyButton value={String(variable.value)}>
222
+ {({ copied, copy }) => (
223
+ <Tooltip label={copied ? "Copied!" : "Copy value"}>
224
+ <Box
225
+ onClick={copy}
226
+ style={{ cursor: "pointer" }}
227
+ c={copied ? "teal" : "dimmed"}
228
+ >
229
+ {copied ? (
230
+ <IconCheck size={16} />
231
+ ) : (
232
+ <IconCopy size={16} />
233
+ )}
234
+ </Box>
235
+ </Tooltip>
236
+ )}
237
+ </CopyButton>
238
+ )}
239
+ </Flex>
240
+ {isSensitive && (
241
+ <Text size="xs" c="dimmed" mt="xs">
242
+ Hover to reveal sensitive value
243
+ </Text>
244
+ )}
245
+ </Box>
246
+
247
+ {/* Schema Info */}
248
+ <Box>
249
+ <Text size="sm" fw={600} mb="xs">
250
+ Schema
251
+ </Text>
252
+ <Table striped highlightOnHover withTableBorder>
253
+ <Table.Tbody>
254
+ <Table.Tr>
255
+ <Table.Td w={120}>
256
+ <Text size="sm" c="dimmed">
257
+ Type
258
+ </Text>
259
+ </Table.Td>
260
+ <Table.Td>
261
+ <Text size="sm" ff="monospace">
262
+ {variable.type}
263
+ </Text>
264
+ </Table.Td>
265
+ </Table.Tr>
266
+ {variable.format && (
267
+ <Table.Tr>
268
+ <Table.Td>
269
+ <Text size="sm" c="dimmed">
270
+ Format
271
+ </Text>
272
+ </Table.Td>
273
+ <Table.Td>
274
+ <Text size="sm" ff="monospace">
275
+ {variable.format}
276
+ </Text>
277
+ </Table.Td>
278
+ </Table.Tr>
279
+ )}
280
+ </Table.Tbody>
281
+ </Table>
282
+ </Box>
283
+
284
+ {/* Usage hint */}
285
+ <Box>
286
+ <Text size="sm" fw={600} mb="xs">
287
+ Usage
288
+ </Text>
289
+ <Code block>
290
+ {`# Set in .env file or environment
291
+ ${variable.name}=${hasValue ? variable.value : "<value>"}`}
292
+ </Code>
293
+ </Box>
294
+ </Stack>
295
+ </ScrollArea>
296
+ </Flex>
297
+ );
298
+ };
299
+
300
+ const EmptyState = () => (
301
+ <Flex align="center" justify="center" h="100%" c="dimmed">
302
+ <Stack align="center" gap="xs">
303
+ <IconVariable size={48} opacity={0.3} />
304
+ <Text size="sm">Select a variable to view details</Text>
305
+ </Stack>
306
+ </Flex>
307
+ );
308
+
309
+ const NoEnvsState = () => (
310
+ <Flex align="center" justify="center" h="100%" c="dimmed">
311
+ <Stack align="center" gap="xs">
312
+ <IconSettings size={48} opacity={0.3} />
313
+ <Text>No environment variables found</Text>
314
+ <Text size="sm" c="dimmed">
315
+ Use $env primitive to define expected environment variables
316
+ </Text>
317
+ </Stack>
318
+ </Flex>
319
+ );
320
+
321
+ const AllEnvsTable = ({ variables }: { variables: EnvVariable[] }) => {
322
+ return (
323
+ <ScrollArea h="100%" p="md">
324
+ <Stack gap="lg">
325
+ <Box>
326
+ <Flex align="center" gap="xs" mb="sm">
327
+ <IconKey size={18} opacity={0.7} />
328
+ <Text size="md" fw={600}>
329
+ All Environment Variables
330
+ </Text>
331
+ <Badge size="sm" variant="light">
332
+ {variables.length} variables
333
+ </Badge>
334
+ </Flex>
335
+ <Table striped highlightOnHover withTableBorder>
336
+ <Table.Thead>
337
+ <Table.Tr>
338
+ <Table.Th>Name</Table.Th>
339
+ <Table.Th>Type</Table.Th>
340
+ <Table.Th>Value</Table.Th>
341
+ </Table.Tr>
342
+ </Table.Thead>
343
+ <Table.Tbody>
344
+ {variables.map((v) => {
345
+ const hasValue = v.value !== undefined && v.value !== "";
346
+ const isSensitive =
347
+ v.name.toLowerCase().includes("secret") ||
348
+ v.name.toLowerCase().includes("password") ||
349
+ v.name.toLowerCase().includes("key") ||
350
+ v.name.toLowerCase().includes("token");
351
+ return (
352
+ <Table.Tr key={v.name}>
353
+ <Table.Td>
354
+ <Text size="sm" ff="monospace" fw={500}>
355
+ {v.name}
356
+ </Text>
357
+ </Table.Td>
358
+ <Table.Td>
359
+ <Badge size="xs" variant="outline" color="blue">
360
+ {v.format || v.type}
361
+ </Badge>
362
+ </Table.Td>
363
+ <Table.Td>
364
+ {hasValue ? (
365
+ <Text
366
+ size="sm"
367
+ ff="monospace"
368
+ c="dimmed"
369
+ style={{
370
+ filter: isSensitive ? "blur(4px)" : undefined,
371
+ }}
372
+ >
373
+ {String(v.value).slice(0, 30)}
374
+ {String(v.value).length > 30 ? "..." : ""}
375
+ </Text>
376
+ ) : (
377
+ <Text size="sm" c="dimmed" fs="italic">
378
+ (not set)
379
+ </Text>
380
+ )}
381
+ </Table.Td>
382
+ </Table.Tr>
383
+ );
384
+ })}
385
+ </Table.Tbody>
386
+ </Table>
387
+ </Box>
388
+ </Stack>
389
+ </ScrollArea>
390
+ );
391
+ };
392
+
393
+ export const DevEnvExplorer = () => {
394
+ const http = useInject(HttpClient);
395
+ const [envs, setEnvs] = useState<DevEnvMetadata[]>([]);
396
+ const [loading, setLoading] = useState(true);
397
+ const [selectedVar, setSelectedVar] = useState<EnvVariable | null>(null);
398
+ const [search, setSearch] = useState("");
399
+
400
+ useEffect(() => {
401
+ http
402
+ .fetch("/devtools/api/metadata", {
403
+ schema: { response: devMetadataSchema },
404
+ })
405
+ .then((res) => {
406
+ setEnvs(res.data.envs);
407
+ setLoading(false);
408
+ });
409
+ }, []);
410
+
411
+ const variables = useMemo(() => parseEnvVariables(envs), [envs]);
412
+
413
+ const filteredVariables = useMemo(() => {
414
+ if (!search) return variables;
415
+ const searchLower = search.toLowerCase();
416
+ return variables.filter((v) => v.name.toLowerCase().includes(searchLower));
417
+ }, [variables, search]);
418
+
419
+ if (loading) {
420
+ return (
421
+ <Flex align="center" justify="center" h="100%">
422
+ <Text c="dimmed">Loading...</Text>
423
+ </Flex>
424
+ );
425
+ }
426
+
427
+ if (variables.length === 0) {
428
+ return <NoEnvsState />;
429
+ }
430
+
431
+ return (
432
+ <Flex h="100%" style={{ overflow: "hidden" }}>
433
+ {/* Sidebar */}
434
+ <Box
435
+ w={280}
436
+ style={{
437
+ borderRight: `1px solid ${ui.colors.border}`,
438
+ backgroundColor: ui.colors.surface,
439
+ }}
440
+ >
441
+ <EnvSidebar
442
+ variables={filteredVariables}
443
+ selectedVar={selectedVar}
444
+ onSelectVar={setSelectedVar}
445
+ search={search}
446
+ onSearchChange={setSearch}
447
+ />
448
+ </Box>
449
+
450
+ {/* Main content */}
451
+ <Box style={{ flex: 1, overflow: "hidden" }}>
452
+ {selectedVar ? (
453
+ <EnvPanel variable={selectedVar} />
454
+ ) : (
455
+ <AllEnvsTable variables={variables} />
456
+ )}
457
+ </Box>
458
+ </Flex>
459
+ );
460
+ };
461
+
462
+ export default DevEnvExplorer;
@@ -3,11 +3,23 @@ import {
3
3
  ActionButton,
4
4
  AdminShell,
5
5
  DarkModeButton,
6
- Flex,
7
6
  OmnibarButton,
8
7
  ui,
9
8
  } from "@alepha/ui";
10
- import { IconDashboard, IconLogs, IconTools } from "@tabler/icons-react";
9
+ import { Flex } from "@mantine/core";
10
+ import {
11
+ IconApi,
12
+ IconArchive,
13
+ IconAtom,
14
+ IconDashboard,
15
+ IconDatabase,
16
+ IconLogs,
17
+ IconMessageCircle,
18
+ IconStack2,
19
+ IconTools,
20
+ IconTopologyRing,
21
+ IconVariable,
22
+ } from "@tabler/icons-react";
11
23
 
12
24
  export const DevLayout = () => {
13
25
  return (
@@ -34,6 +46,49 @@ export const DevLayout = () => {
34
46
  icon: <IconDashboard />,
35
47
  href: "/",
36
48
  },
49
+ { type: "divider" },
50
+ {
51
+ label: "Actions",
52
+ icon: <IconApi />,
53
+ href: "/actions",
54
+ },
55
+ {
56
+ label: "Queues",
57
+ icon: <IconStack2 />,
58
+ href: "/queues",
59
+ },
60
+ {
61
+ label: "Topics",
62
+ icon: <IconMessageCircle />,
63
+ href: "/topics",
64
+ },
65
+ {
66
+ label: "Caches",
67
+ icon: <IconArchive />,
68
+ href: "/caches",
69
+ },
70
+ {
71
+ label: "DB Studio",
72
+ icon: <IconDatabase />,
73
+ href: "/db",
74
+ },
75
+ { type: "divider" },
76
+ {
77
+ label: "Environment",
78
+ icon: <IconVariable />,
79
+ href: "/env",
80
+ },
81
+ {
82
+ label: "Atoms",
83
+ icon: <IconAtom />,
84
+ href: "/atoms",
85
+ },
86
+ { type: "divider" },
87
+ {
88
+ label: "Graph",
89
+ icon: <IconTopologyRing />,
90
+ href: "/graph",
91
+ },
37
92
  {
38
93
  label: "Logs",
39
94
  icon: <IconLogs />,
@@ -47,7 +102,12 @@ export const DevLayout = () => {
47
102
  {
48
103
  position: "left",
49
104
  element: (
50
- <ActionButton icon={<IconTools />} href={"/"} active={false}>
105
+ <ActionButton
106
+ intent={"none"}
107
+ icon={IconTools}
108
+ href={"/"}
109
+ active={false}
110
+ >
51
111
  Devtools
52
112
  </ActionButton>
53
113
  ),
@@ -64,11 +124,12 @@ export const DevLayout = () => {
64
124
  }}
65
125
  >
66
126
  <Flex
127
+ className={"overflow-auto"}
67
128
  w={"100%"}
68
129
  flex={1}
130
+ direction={"column"}
69
131
  bd={`1px solid ${ui.colors.border}`}
70
132
  bg={ui.colors.elevated}
71
- p={"xl"}
72
133
  ml={-16}
73
134
  mr={-16}
74
135
  mt={-16}