@kopai/ui 0.0.5 → 0.1.0
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/README.md +137 -0
- package/dist/index.cjs +5069 -3
- package/dist/index.d.cts +301 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +302 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5010 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -7
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +113 -0
- package/src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx +82 -0
- package/src/components/KeyboardShortcuts/context.ts +23 -0
- package/src/components/KeyboardShortcuts/index.ts +8 -0
- package/src/components/KeyboardShortcuts/types.ts +11 -0
- package/src/components/dashboard/Badge/Badge.stories.tsx +29 -0
- package/src/components/dashboard/Badge/index.tsx +32 -0
- package/src/components/dashboard/Button/Button.stories.tsx +107 -0
- package/src/components/dashboard/Button/index.tsx +63 -0
- package/src/components/dashboard/Card/Card.stories.tsx +81 -0
- package/src/components/dashboard/Card/index.tsx +58 -0
- package/src/components/dashboard/Chart/Chart.stories.tsx +48 -0
- package/src/components/dashboard/Chart/index.tsx +74 -0
- package/src/components/dashboard/DatePicker/DatePicker.stories.tsx +33 -0
- package/src/components/dashboard/DatePicker/index.tsx +41 -0
- package/src/components/dashboard/Divider/Divider.stories.tsx +17 -0
- package/src/components/dashboard/Divider/index.tsx +49 -0
- package/src/components/dashboard/Empty/Empty.stories.tsx +48 -0
- package/src/components/dashboard/Empty/index.tsx +46 -0
- package/src/components/dashboard/Grid/Grid.stories.tsx +52 -0
- package/src/components/dashboard/Grid/index.tsx +26 -0
- package/src/components/dashboard/Heading/Heading.stories.tsx +25 -0
- package/src/components/dashboard/Heading/index.tsx +27 -0
- package/src/components/dashboard/List/List.stories.tsx +37 -0
- package/src/components/dashboard/List/index.tsx +24 -0
- package/src/components/dashboard/Metric/Metric.stories.tsx +65 -0
- package/src/components/dashboard/Metric/index.tsx +36 -0
- package/src/components/dashboard/Stack/Stack.stories.tsx +61 -0
- package/src/components/dashboard/Stack/index.tsx +33 -0
- package/src/components/dashboard/Table/Table.stories.tsx +38 -0
- package/src/components/dashboard/Table/index.tsx +104 -0
- package/src/components/dashboard/Text/Text.stories.tsx +53 -0
- package/src/components/dashboard/Text/index.tsx +18 -0
- package/src/components/dashboard/index.ts +46 -0
- package/src/components/index.ts +17 -0
- package/src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx +56 -0
- package/src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx +139 -0
- package/src/components/observability/LogTimeline/LogDetailPane/index.tsx +271 -0
- package/src/components/observability/LogTimeline/LogFilter.stories.tsx +66 -0
- package/src/components/observability/LogTimeline/LogFilter.test.tsx +696 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +674 -0
- package/src/components/observability/LogTimeline/LogRow.tsx +174 -0
- package/src/components/observability/LogTimeline/LogTimeline.stories.tsx +154 -0
- package/src/components/observability/LogTimeline/index.tsx +542 -0
- package/src/components/observability/LogTimeline/shortcuts.ts +18 -0
- package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +20 -0
- package/src/components/observability/MetricHistogram/index.tsx +303 -0
- package/src/components/observability/MetricStat/MetricStat.stories.tsx +30 -0
- package/src/components/observability/MetricStat/index.tsx +281 -0
- package/src/components/observability/MetricTable/MetricTable.stories.tsx +20 -0
- package/src/components/observability/MetricTable/index.tsx +194 -0
- package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +28 -0
- package/src/components/observability/MetricTimeSeries/index.tsx +462 -0
- package/src/components/observability/RawDataTable/RawDataTable.stories.tsx +27 -0
- package/src/components/observability/RawDataTable/index.tsx +131 -0
- package/src/components/observability/ServiceList/ServiceList.stories.tsx +20 -0
- package/src/components/observability/ServiceList/index.tsx +60 -0
- package/src/components/observability/ServiceList/shortcuts.ts +6 -0
- package/src/components/observability/TabBar/TabBar.stories.tsx +34 -0
- package/src/components/observability/TabBar/index.tsx +46 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +51 -0
- package/src/components/observability/TraceDetail/index.tsx +53 -0
- package/src/components/observability/TraceSearch/TraceSearch.stories.tsx +49 -0
- package/src/components/observability/TraceSearch/index.tsx +292 -0
- package/src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx +152 -0
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +128 -0
- package/src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx +210 -0
- package/src/components/observability/TraceTimeline/DetailPane/index.tsx +174 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +173 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +41 -0
- package/src/components/observability/TraceTimeline/Tooltip.tsx +42 -0
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +88 -0
- package/src/components/observability/TraceTimeline/TraceTimeline.stories.tsx +25 -0
- package/src/components/observability/TraceTimeline/index.tsx +478 -0
- package/src/components/observability/TraceTimeline/shortcuts.ts +16 -0
- package/src/components/observability/__fixtures__/logs.ts +476 -0
- package/src/components/observability/__fixtures__/metrics.ts +216 -0
- package/src/components/observability/__fixtures__/raw-table.ts +204 -0
- package/src/components/observability/__fixtures__/services.ts +8 -0
- package/src/components/observability/__fixtures__/trace-summaries.ts +81 -0
- package/src/components/observability/__fixtures__/traces.ts +396 -0
- package/src/components/observability/index.ts +66 -0
- package/src/components/observability/renderers/OtelMetricDiscovery.tsx +77 -0
- package/src/components/observability/renderers/OtelMetricHistogram.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricStat.tsx +44 -0
- package/src/components/observability/renderers/OtelMetricTable.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +30 -0
- package/src/components/observability/renderers/index.ts +5 -0
- package/src/components/observability/shared/LoadingSkeleton.tsx +43 -0
- package/src/components/observability/types.ts +113 -0
- package/src/components/observability/utils/attributes.ts +17 -0
- package/src/components/observability/utils/colors.ts +29 -0
- package/src/components/observability/utils/flatten-tree.ts +53 -0
- package/src/components/observability/utils/lttb.ts +121 -0
- package/src/components/observability/utils/time.ts +46 -0
- package/src/hooks/use-kopai-data.test.ts +296 -0
- package/src/hooks/use-kopai-data.ts +64 -0
- package/src/hooks/use-live-logs.test.ts +193 -0
- package/src/hooks/use-live-logs.ts +113 -0
- package/src/index.ts +15 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +33 -0
- package/src/lib/catalog.ts +165 -0
- package/src/lib/component-catalog.test.ts +357 -0
- package/src/lib/component-catalog.ts +171 -0
- package/src/lib/dashboard-datasource.ts +76 -0
- package/src/lib/generate-prompt-instructions.test.ts +27 -0
- package/src/lib/generate-prompt-instructions.ts +185 -0
- package/src/lib/log-buffer.test.ts +88 -0
- package/src/lib/log-buffer.ts +62 -0
- package/src/lib/observability-catalog.ts +143 -0
- package/src/lib/renderer.test.tsx +693 -0
- package/src/lib/renderer.tsx +276 -0
- package/src/pages/observability.tsx +828 -0
- package/src/providers/kopai-provider.tsx +51 -0
- package/src/styles/globals.css +46 -0
- package/src/vite-env.d.ts +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as ObservabilityPage } from "./pages/observability.js";
|
|
2
|
+
export { createCatalog } from "./lib/component-catalog.js";
|
|
3
|
+
export { generatePromptInstructions } from "./lib/generate-prompt-instructions.js";
|
|
4
|
+
export {
|
|
5
|
+
Renderer,
|
|
6
|
+
createRendererFromCatalog,
|
|
7
|
+
type RendererComponentProps,
|
|
8
|
+
type ComponentRenderProps,
|
|
9
|
+
type ComponentRenderer,
|
|
10
|
+
} from "./lib/renderer.js";
|
|
11
|
+
export {
|
|
12
|
+
KopaiSDKProvider,
|
|
13
|
+
useKopaiSDK,
|
|
14
|
+
type KopaiClient,
|
|
15
|
+
} from "./providers/kopai-provider.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`generatePromptInstructions > generates full prompt instructions 1`] = `
|
|
4
|
+
"## Available Components
|
|
5
|
+
|
|
6
|
+
### Card
|
|
7
|
+
A card container
|
|
8
|
+
|
|
9
|
+
Props:
|
|
10
|
+
- title: string (required)
|
|
11
|
+
Accepts children: yes
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### Button
|
|
16
|
+
Clickable button
|
|
17
|
+
|
|
18
|
+
Props:
|
|
19
|
+
- label: string (required)
|
|
20
|
+
Accepts dataSource: yes
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Output Schema
|
|
25
|
+
|
|
26
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"root":{"type":"string","description":"root uiElement key in the elements array"},"elements":{"type":"object","propertyNames":{"type":"string","description":"equal to the element key"},"additionalProperties":{"oneOf":[{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Card"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false},{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Button"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false}]}}},"required":["root","elements"],"additionalProperties":false,"$defs":{"DataSource":{"$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"type":"object","properties":{"method":{"type":"string","const":"searchTracesPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All spans from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"parentSpanId":{"description":"The span_id of this span's parent span. Empty if this is a root span.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"spanName":{"description":"Description of the span's operation. E.g., qualified method name or file name with line number.","type":"string"},"spanKind":{"description":"Type of span (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER). Used to identify relationships between spans.","type":"string"},"statusCode":{"description":"Status code (UNSET, OK, ERROR).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timestampMin":{"description":"Minimum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"durationMin":{"description":"Minimum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"durationMax":{"description":"Maximum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"spanAttributes":{"description":"Key/value pairs describing the span.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"eventsAttributes":{"description":"Attribute key/value pairs on the event.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"linksAttributes":{"description":"Attribute key/value pairs on the link.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchLogsPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All logs from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"severityText":{"description":"Severity text (also known as log level). Original string representation as known at the source.","type":"string"},"severityNumberMin":{"description":"Minimum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"severityNumberMax":{"description":"Maximum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"bodyContains":{"description":"Filter logs where body contains this substring.","type":"string"},"timestampMin":{"description":"Minimum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"logAttributes":{"description":"Additional attributes that describe the specific event occurrence.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchMetricsPage"},"params":{"type":"object","properties":{"metricType":{"type":"string","enum":["Gauge","Sum","Histogram","ExponentialHistogram","Summary"],"description":"Metric type to query."},"metricName":{"description":"The name of the metric.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timeUnixMin":{"description":"Minimum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"timeUnixMax":{"description":"Maximum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"attributes":{"description":"Key/value pairs that uniquely identify the timeseries.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"required":["metricType"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getTrace"},"params":{"type":"object","properties":{"traceId":{"type":"string"}},"required":["traceId"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"discoverMetrics"},"params":{"type":"object","properties":{},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method"],"additionalProperties":false}]}}}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
{"root":"card-1","elements":{"card-1":{"key":"card-1","type":"Card","props":{},"children":["button-1"]},"button-1":{"key":"button-1","type":"Button","props":{},"parentKey":"card-1","dataSource":{"method":"searchTracesPage","params":{"limit":10}}}}}"
|
|
33
|
+
`;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createCatalog } from "./component-catalog.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const dashboardCatalog = createCatalog({
|
|
5
|
+
name: "dashboard",
|
|
6
|
+
components: {
|
|
7
|
+
// Layout Components
|
|
8
|
+
Card: {
|
|
9
|
+
props: z.object({
|
|
10
|
+
title: z.string().nullable(),
|
|
11
|
+
description: z.string().nullable(),
|
|
12
|
+
padding: z.enum(["sm", "md", "lg"]).nullable(),
|
|
13
|
+
}),
|
|
14
|
+
hasChildren: true,
|
|
15
|
+
description: "A card container with optional title",
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
Grid: {
|
|
19
|
+
props: z.object({
|
|
20
|
+
columns: z.number().min(1).max(4).nullable(),
|
|
21
|
+
gap: z.enum(["sm", "md", "lg"]).nullable(),
|
|
22
|
+
}),
|
|
23
|
+
hasChildren: true,
|
|
24
|
+
description: "Grid layout with configurable columns",
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
Stack: {
|
|
28
|
+
props: z.object({
|
|
29
|
+
direction: z.enum(["horizontal", "vertical"]).nullable(),
|
|
30
|
+
gap: z.enum(["sm", "md", "lg"]).nullable(),
|
|
31
|
+
align: z.enum(["start", "center", "end", "stretch"]).nullable(),
|
|
32
|
+
}),
|
|
33
|
+
hasChildren: true,
|
|
34
|
+
description: "Flex stack for horizontal or vertical layouts",
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Data Display Components
|
|
38
|
+
Metric: {
|
|
39
|
+
props: z.object({
|
|
40
|
+
label: z.string(),
|
|
41
|
+
valuePath: z.string(),
|
|
42
|
+
format: z.enum(["number", "currency", "percent"]).nullable(),
|
|
43
|
+
trend: z.enum(["up", "down", "neutral"]).nullable(),
|
|
44
|
+
trendValue: z.string().nullable(),
|
|
45
|
+
}),
|
|
46
|
+
hasChildren: false,
|
|
47
|
+
description: "Display a single metric with optional trend indicator",
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
Chart: {
|
|
51
|
+
props: z.object({
|
|
52
|
+
type: z.enum(["bar", "line", "pie", "area"]),
|
|
53
|
+
dataPath: z.string(),
|
|
54
|
+
title: z.string().nullable(),
|
|
55
|
+
height: z.number().nullable(),
|
|
56
|
+
}),
|
|
57
|
+
hasChildren: false,
|
|
58
|
+
description: "Display a chart from array data",
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
Table: {
|
|
62
|
+
props: z.object({
|
|
63
|
+
dataPath: z.string(),
|
|
64
|
+
columns: z.array(
|
|
65
|
+
z.object({
|
|
66
|
+
key: z.string(),
|
|
67
|
+
label: z.string(),
|
|
68
|
+
format: z.enum(["text", "currency", "date", "badge"]).nullable(),
|
|
69
|
+
})
|
|
70
|
+
),
|
|
71
|
+
}),
|
|
72
|
+
hasChildren: false,
|
|
73
|
+
description: "Display tabular data",
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
List: {
|
|
77
|
+
props: z.object({
|
|
78
|
+
dataPath: z.string(),
|
|
79
|
+
emptyMessage: z.string().nullable(),
|
|
80
|
+
}),
|
|
81
|
+
hasChildren: true,
|
|
82
|
+
description: "Render a list from array data",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Interactive Components
|
|
86
|
+
Button: {
|
|
87
|
+
props: z.object({
|
|
88
|
+
label: z.string(),
|
|
89
|
+
variant: z.enum(["primary", "secondary", "danger", "ghost"]).nullable(),
|
|
90
|
+
size: z.enum(["sm", "md", "lg"]).nullable(),
|
|
91
|
+
action: z.string(),
|
|
92
|
+
disabled: z.boolean().nullable(),
|
|
93
|
+
}),
|
|
94
|
+
hasChildren: false,
|
|
95
|
+
description: "Clickable button with action",
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
DatePicker: {
|
|
99
|
+
props: z.object({
|
|
100
|
+
label: z.string().nullable(),
|
|
101
|
+
bindPath: z.string(),
|
|
102
|
+
placeholder: z.string().nullable(),
|
|
103
|
+
}),
|
|
104
|
+
hasChildren: false,
|
|
105
|
+
description: "Date picker input",
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Typography
|
|
109
|
+
Heading: {
|
|
110
|
+
props: z.object({
|
|
111
|
+
text: z.string(),
|
|
112
|
+
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
|
113
|
+
}),
|
|
114
|
+
hasChildren: false,
|
|
115
|
+
description: "Section heading",
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
Text: {
|
|
119
|
+
props: z.object({
|
|
120
|
+
content: z.string(),
|
|
121
|
+
variant: z.enum(["body", "caption", "label"]).nullable(),
|
|
122
|
+
color: z
|
|
123
|
+
.enum(["default", "muted", "success", "warning", "danger"])
|
|
124
|
+
.nullable(),
|
|
125
|
+
}),
|
|
126
|
+
hasChildren: false,
|
|
127
|
+
description: "Text paragraph",
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Status Components
|
|
131
|
+
Badge: {
|
|
132
|
+
props: z.object({
|
|
133
|
+
text: z.string(),
|
|
134
|
+
variant: z
|
|
135
|
+
.enum(["default", "success", "warning", "danger", "info"])
|
|
136
|
+
.nullable(),
|
|
137
|
+
}),
|
|
138
|
+
hasChildren: false,
|
|
139
|
+
description: "Small status badge",
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Special Components
|
|
143
|
+
Divider: {
|
|
144
|
+
props: z.object({
|
|
145
|
+
label: z.string().nullable(),
|
|
146
|
+
}),
|
|
147
|
+
hasChildren: false,
|
|
148
|
+
description: "Visual divider",
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
Empty: {
|
|
152
|
+
props: z.object({
|
|
153
|
+
title: z.string(),
|
|
154
|
+
description: z.string().nullable(),
|
|
155
|
+
action: z.string().nullable(),
|
|
156
|
+
actionLabel: z.string().nullable(),
|
|
157
|
+
}),
|
|
158
|
+
hasChildren: false,
|
|
159
|
+
description: "Empty state placeholder",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Export the component list for the AI prompt
|
|
165
|
+
export const componentList = Object.keys(dashboardCatalog.components);
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { dataSourceSchema, createCatalog } from "./component-catalog.js";
|
|
4
|
+
import type { CatalogueComponentProps } from "./component-catalog.js";
|
|
5
|
+
|
|
6
|
+
describe("schemas", () => {
|
|
7
|
+
it("datasource", () => {
|
|
8
|
+
expect.assertions(0);
|
|
9
|
+
type DataSource = z.infer<typeof dataSourceSchema>;
|
|
10
|
+
|
|
11
|
+
const _testDataSource1 = {
|
|
12
|
+
method: "searchTracesPage",
|
|
13
|
+
params: {
|
|
14
|
+
// @ts-expect-error - invalid param
|
|
15
|
+
nonExisting: "",
|
|
16
|
+
},
|
|
17
|
+
} satisfies DataSource;
|
|
18
|
+
|
|
19
|
+
const _testDataSource2 = {
|
|
20
|
+
method: "searchTracesPage",
|
|
21
|
+
params: {
|
|
22
|
+
cursor: "test",
|
|
23
|
+
eventsAttributes: {
|
|
24
|
+
foo: "bar",
|
|
25
|
+
},
|
|
26
|
+
limit: 3,
|
|
27
|
+
},
|
|
28
|
+
} satisfies DataSource;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("createCatalog", () => {
|
|
32
|
+
it("creates uiTreeSchema that validates component props", () => {
|
|
33
|
+
const catalog = createCatalog({
|
|
34
|
+
name: "test catalog",
|
|
35
|
+
components: {
|
|
36
|
+
TestComponent: {
|
|
37
|
+
description: "test",
|
|
38
|
+
hasChildren: false,
|
|
39
|
+
props: z.object({ isImportant: z.boolean() }),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Valid data passes
|
|
45
|
+
const validResult = catalog.uiTreeSchema.safeParse({
|
|
46
|
+
root: "test-1",
|
|
47
|
+
elements: {
|
|
48
|
+
TestComponent: {
|
|
49
|
+
key: "test-1",
|
|
50
|
+
type: "TestComponent",
|
|
51
|
+
children: [],
|
|
52
|
+
parentKey: "",
|
|
53
|
+
props: { isImportant: true },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(validResult.success).toBe(true);
|
|
58
|
+
|
|
59
|
+
// Invalid props fail
|
|
60
|
+
const invalidResult = catalog.uiTreeSchema.safeParse({
|
|
61
|
+
root: "test-1",
|
|
62
|
+
elements: {
|
|
63
|
+
TestComponent: {
|
|
64
|
+
key: "test-1",
|
|
65
|
+
type: "TestComponent",
|
|
66
|
+
children: [],
|
|
67
|
+
parentKey: "",
|
|
68
|
+
props: { isImportant: "not-a-boolean" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(invalidResult.success).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns components with exact types from config", () => {
|
|
76
|
+
expect.assertions(0);
|
|
77
|
+
|
|
78
|
+
const catalog = createCatalog({
|
|
79
|
+
name: "test catalog",
|
|
80
|
+
components: {
|
|
81
|
+
TestComponent: {
|
|
82
|
+
description: "test",
|
|
83
|
+
hasChildren: false,
|
|
84
|
+
props: z.object({ isImportant: z.boolean() }),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Valid: accessing defined component
|
|
90
|
+
const _validAccess = catalog.components.TestComponent;
|
|
91
|
+
|
|
92
|
+
// @ts-expect-error - non-existent component
|
|
93
|
+
const _invalidAccess = catalog.components.NonExistentComponent;
|
|
94
|
+
|
|
95
|
+
// Props type preserved - can infer schema type
|
|
96
|
+
type Props = z.infer<typeof catalog.components.TestComponent.props>;
|
|
97
|
+
const _validProps: Props = { isImportant: true };
|
|
98
|
+
// @ts-expect-error - wrong prop type
|
|
99
|
+
const _invalidProps: Props = { isImportant: "not-boolean" };
|
|
100
|
+
// @ts-expect-error - non-existent prop
|
|
101
|
+
const _missingProps: Props = { nonExistent: true };
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("validates multiple components with dataSource", () => {
|
|
105
|
+
const catalog = createCatalog({
|
|
106
|
+
name: "multi catalog",
|
|
107
|
+
components: {
|
|
108
|
+
Button: {
|
|
109
|
+
description: "button",
|
|
110
|
+
hasChildren: false,
|
|
111
|
+
props: z.object({ label: z.string() }),
|
|
112
|
+
},
|
|
113
|
+
Card: {
|
|
114
|
+
description: "card",
|
|
115
|
+
hasChildren: true,
|
|
116
|
+
props: z.object({ title: z.string(), bordered: z.boolean() }),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(catalog.name).toBe("multi catalog");
|
|
122
|
+
|
|
123
|
+
// Valid with dataSource
|
|
124
|
+
const validResult = catalog.uiTreeSchema.safeParse({
|
|
125
|
+
root: "card-1",
|
|
126
|
+
elements: {
|
|
127
|
+
Card: {
|
|
128
|
+
key: "card-1",
|
|
129
|
+
type: "Card",
|
|
130
|
+
children: ["btn-1"],
|
|
131
|
+
parentKey: "",
|
|
132
|
+
props: { title: "Hello", bordered: true },
|
|
133
|
+
dataSource: {
|
|
134
|
+
method: "searchTracesPage",
|
|
135
|
+
params: { limit: 10 },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
Button: {
|
|
139
|
+
key: "btn-1",
|
|
140
|
+
type: "Button",
|
|
141
|
+
children: [],
|
|
142
|
+
parentKey: "card-1",
|
|
143
|
+
props: { label: "Click" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
expect(validResult.success).toBe(true);
|
|
148
|
+
|
|
149
|
+
// Invalid dataSource params
|
|
150
|
+
const invalidDataSource = catalog.uiTreeSchema.safeParse({
|
|
151
|
+
root: "btn-1",
|
|
152
|
+
elements: {
|
|
153
|
+
Button: {
|
|
154
|
+
key: "btn-1",
|
|
155
|
+
type: "Button",
|
|
156
|
+
children: [],
|
|
157
|
+
parentKey: "",
|
|
158
|
+
props: { label: "Click" },
|
|
159
|
+
dataSource: {
|
|
160
|
+
method: "searchTracesPage",
|
|
161
|
+
params: { limit: null },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
expect(invalidDataSource.success).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("types multiple components correctly", () => {
|
|
170
|
+
expect.assertions(0);
|
|
171
|
+
|
|
172
|
+
const catalog = createCatalog({
|
|
173
|
+
name: "multi catalog",
|
|
174
|
+
components: {
|
|
175
|
+
Button: {
|
|
176
|
+
description: "button",
|
|
177
|
+
hasChildren: false,
|
|
178
|
+
props: z.object({ label: z.string() }),
|
|
179
|
+
},
|
|
180
|
+
Card: {
|
|
181
|
+
description: "card",
|
|
182
|
+
hasChildren: true,
|
|
183
|
+
props: z.object({ title: z.string() }),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Both components accessible
|
|
189
|
+
const _button = catalog.components.Button;
|
|
190
|
+
const _card = catalog.components.Card;
|
|
191
|
+
|
|
192
|
+
// @ts-expect-error - non-existent
|
|
193
|
+
const _missing = catalog.components.Missing;
|
|
194
|
+
|
|
195
|
+
// Props types preserved per component
|
|
196
|
+
type ButtonProps = z.infer<typeof catalog.components.Button.props>;
|
|
197
|
+
type CardProps = z.infer<typeof catalog.components.Card.props>;
|
|
198
|
+
|
|
199
|
+
const _validButton: ButtonProps = { label: "hi" };
|
|
200
|
+
const _validCard: CardProps = { title: "hi" };
|
|
201
|
+
|
|
202
|
+
// @ts-expect-error - wrong component props
|
|
203
|
+
const _wrongButton: ButtonProps = { title: "hi" };
|
|
204
|
+
// @ts-expect-error - wrong component props
|
|
205
|
+
const _wrongCard: CardProps = { label: "hi" };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("types uiTreeSchema elements with dataSource", () => {
|
|
209
|
+
expect.assertions(0);
|
|
210
|
+
|
|
211
|
+
const _catalog = createCatalog({
|
|
212
|
+
name: "test catalog",
|
|
213
|
+
components: {
|
|
214
|
+
Button: {
|
|
215
|
+
description: "button",
|
|
216
|
+
hasChildren: false,
|
|
217
|
+
props: z.object({ label: z.string() }),
|
|
218
|
+
},
|
|
219
|
+
Card: {
|
|
220
|
+
description: "card",
|
|
221
|
+
hasChildren: true,
|
|
222
|
+
props: z.object({ title: z.string() }),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
type UiTree = z.infer<typeof _catalog.uiTreeSchema>;
|
|
228
|
+
|
|
229
|
+
// Valid: elements with dataSource
|
|
230
|
+
const _validTree: UiTree = {
|
|
231
|
+
root: "card-1",
|
|
232
|
+
elements: {
|
|
233
|
+
"card-1": {
|
|
234
|
+
key: "card-1",
|
|
235
|
+
type: "Card",
|
|
236
|
+
children: ["btn-1"],
|
|
237
|
+
parentKey: "",
|
|
238
|
+
props: { title: "Hello" },
|
|
239
|
+
dataSource: {
|
|
240
|
+
method: "searchTracesPage",
|
|
241
|
+
params: { limit: 10 },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
"btn-1": {
|
|
245
|
+
key: "btn-1",
|
|
246
|
+
type: "Button",
|
|
247
|
+
children: [],
|
|
248
|
+
parentKey: "card-1",
|
|
249
|
+
props: { label: "Click" },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const _invalidType: UiTree = {
|
|
255
|
+
root: "x",
|
|
256
|
+
elements: {
|
|
257
|
+
"random-1": {
|
|
258
|
+
key: "random-1",
|
|
259
|
+
// @ts-expect-error - non-existent type
|
|
260
|
+
type: "NonExistant",
|
|
261
|
+
children: [],
|
|
262
|
+
parentKey: "",
|
|
263
|
+
// @ts-expect-error - props don't match any valid type
|
|
264
|
+
props: {},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const _invalidProps: UiTree = {
|
|
270
|
+
root: "btn-1",
|
|
271
|
+
elements: {
|
|
272
|
+
Button: {
|
|
273
|
+
key: "btn-1",
|
|
274
|
+
type: "Button",
|
|
275
|
+
children: [],
|
|
276
|
+
parentKey: "",
|
|
277
|
+
// @ts-expect-error - wrong props type for element
|
|
278
|
+
props: { title: "wrong prop" },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const _invalidDataSource: UiTree = {
|
|
284
|
+
root: "btn-1",
|
|
285
|
+
elements: {
|
|
286
|
+
Button: {
|
|
287
|
+
key: "btn-1",
|
|
288
|
+
type: "Button",
|
|
289
|
+
children: [],
|
|
290
|
+
parentKey: "",
|
|
291
|
+
props: { label: "Click" },
|
|
292
|
+
dataSource: {
|
|
293
|
+
// @ts-expect-error - invalid dataSource method
|
|
294
|
+
method: "invalidMethod",
|
|
295
|
+
params: {},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const _invalidDataSourceParams: UiTree = {
|
|
302
|
+
root: "btn-1",
|
|
303
|
+
elements: {
|
|
304
|
+
Button: {
|
|
305
|
+
key: "btn-1",
|
|
306
|
+
type: "Button",
|
|
307
|
+
children: [],
|
|
308
|
+
parentKey: "",
|
|
309
|
+
props: { label: "Click" },
|
|
310
|
+
dataSource: {
|
|
311
|
+
method: "searchTracesPage",
|
|
312
|
+
// @ts-expect-error - invalid params for searchTracesPage
|
|
313
|
+
params: { foo: 1 },
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("types CatalogueComponentProps based on hasChildren", () => {
|
|
321
|
+
expect.assertions(0);
|
|
322
|
+
|
|
323
|
+
type WithChildren = {
|
|
324
|
+
hasChildren: true;
|
|
325
|
+
description: string;
|
|
326
|
+
props: { title: string };
|
|
327
|
+
};
|
|
328
|
+
type NoChildren = {
|
|
329
|
+
hasChildren: false;
|
|
330
|
+
description: string;
|
|
331
|
+
props: { label: string };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// hasChildren: true -> includes children prop
|
|
335
|
+
type PropsWithChildren = CatalogueComponentProps<WithChildren>;
|
|
336
|
+
const _validWithChildren: PropsWithChildren = {
|
|
337
|
+
element: { props: { title: "hi" } },
|
|
338
|
+
children: null,
|
|
339
|
+
};
|
|
340
|
+
// @ts-expect-error - missing children
|
|
341
|
+
const _missingChildren: PropsWithChildren = {
|
|
342
|
+
element: { props: { title: "hi" } },
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// hasChildren: false -> no children prop
|
|
346
|
+
type PropsNoChildren = CatalogueComponentProps<NoChildren>;
|
|
347
|
+
const _validNoChildren: PropsNoChildren = {
|
|
348
|
+
element: { props: { label: "hi" } },
|
|
349
|
+
};
|
|
350
|
+
const _extraChildren: PropsNoChildren = {
|
|
351
|
+
element: { props: { label: "hi" } },
|
|
352
|
+
// @ts-expect-error - children not allowed
|
|
353
|
+
children: null,
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|