@kopai/ui 0.0.5 → 0.2.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.
Files changed (125) hide show
  1. package/README.md +137 -0
  2. package/dist/index.cjs +5069 -3
  3. package/dist/index.d.cts +301 -3
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +302 -3
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +5010 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +25 -7
  10. package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +113 -0
  11. package/src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx +82 -0
  12. package/src/components/KeyboardShortcuts/context.ts +23 -0
  13. package/src/components/KeyboardShortcuts/index.ts +8 -0
  14. package/src/components/KeyboardShortcuts/types.ts +11 -0
  15. package/src/components/dashboard/Badge/Badge.stories.tsx +29 -0
  16. package/src/components/dashboard/Badge/index.tsx +32 -0
  17. package/src/components/dashboard/Button/Button.stories.tsx +107 -0
  18. package/src/components/dashboard/Button/index.tsx +63 -0
  19. package/src/components/dashboard/Card/Card.stories.tsx +81 -0
  20. package/src/components/dashboard/Card/index.tsx +58 -0
  21. package/src/components/dashboard/Chart/Chart.stories.tsx +48 -0
  22. package/src/components/dashboard/Chart/index.tsx +74 -0
  23. package/src/components/dashboard/DatePicker/DatePicker.stories.tsx +33 -0
  24. package/src/components/dashboard/DatePicker/index.tsx +41 -0
  25. package/src/components/dashboard/Divider/Divider.stories.tsx +17 -0
  26. package/src/components/dashboard/Divider/index.tsx +49 -0
  27. package/src/components/dashboard/Empty/Empty.stories.tsx +48 -0
  28. package/src/components/dashboard/Empty/index.tsx +46 -0
  29. package/src/components/dashboard/Grid/Grid.stories.tsx +52 -0
  30. package/src/components/dashboard/Grid/index.tsx +26 -0
  31. package/src/components/dashboard/Heading/Heading.stories.tsx +25 -0
  32. package/src/components/dashboard/Heading/index.tsx +27 -0
  33. package/src/components/dashboard/List/List.stories.tsx +37 -0
  34. package/src/components/dashboard/List/index.tsx +24 -0
  35. package/src/components/dashboard/Metric/Metric.stories.tsx +65 -0
  36. package/src/components/dashboard/Metric/index.tsx +36 -0
  37. package/src/components/dashboard/Stack/Stack.stories.tsx +61 -0
  38. package/src/components/dashboard/Stack/index.tsx +33 -0
  39. package/src/components/dashboard/Table/Table.stories.tsx +38 -0
  40. package/src/components/dashboard/Table/index.tsx +104 -0
  41. package/src/components/dashboard/Text/Text.stories.tsx +53 -0
  42. package/src/components/dashboard/Text/index.tsx +18 -0
  43. package/src/components/dashboard/index.ts +46 -0
  44. package/src/components/index.ts +17 -0
  45. package/src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx +56 -0
  46. package/src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx +139 -0
  47. package/src/components/observability/LogTimeline/LogDetailPane/index.tsx +271 -0
  48. package/src/components/observability/LogTimeline/LogFilter.stories.tsx +66 -0
  49. package/src/components/observability/LogTimeline/LogFilter.test.tsx +696 -0
  50. package/src/components/observability/LogTimeline/LogFilter.tsx +674 -0
  51. package/src/components/observability/LogTimeline/LogRow.tsx +174 -0
  52. package/src/components/observability/LogTimeline/LogTimeline.stories.tsx +154 -0
  53. package/src/components/observability/LogTimeline/index.tsx +542 -0
  54. package/src/components/observability/LogTimeline/shortcuts.ts +18 -0
  55. package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +20 -0
  56. package/src/components/observability/MetricHistogram/index.tsx +303 -0
  57. package/src/components/observability/MetricStat/MetricStat.stories.tsx +30 -0
  58. package/src/components/observability/MetricStat/index.tsx +281 -0
  59. package/src/components/observability/MetricTable/MetricTable.stories.tsx +20 -0
  60. package/src/components/observability/MetricTable/index.tsx +194 -0
  61. package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +28 -0
  62. package/src/components/observability/MetricTimeSeries/index.tsx +462 -0
  63. package/src/components/observability/RawDataTable/RawDataTable.stories.tsx +27 -0
  64. package/src/components/observability/RawDataTable/index.tsx +131 -0
  65. package/src/components/observability/ServiceList/ServiceList.stories.tsx +20 -0
  66. package/src/components/observability/ServiceList/index.tsx +60 -0
  67. package/src/components/observability/ServiceList/shortcuts.ts +6 -0
  68. package/src/components/observability/TabBar/TabBar.stories.tsx +34 -0
  69. package/src/components/observability/TabBar/index.tsx +46 -0
  70. package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +51 -0
  71. package/src/components/observability/TraceDetail/index.tsx +53 -0
  72. package/src/components/observability/TraceSearch/TraceSearch.stories.tsx +49 -0
  73. package/src/components/observability/TraceSearch/index.tsx +292 -0
  74. package/src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx +152 -0
  75. package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +128 -0
  76. package/src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx +210 -0
  77. package/src/components/observability/TraceTimeline/DetailPane/index.tsx +174 -0
  78. package/src/components/observability/TraceTimeline/SpanRow.tsx +173 -0
  79. package/src/components/observability/TraceTimeline/TimelineBar.tsx +41 -0
  80. package/src/components/observability/TraceTimeline/Tooltip.tsx +42 -0
  81. package/src/components/observability/TraceTimeline/TraceHeader.tsx +88 -0
  82. package/src/components/observability/TraceTimeline/TraceTimeline.stories.tsx +25 -0
  83. package/src/components/observability/TraceTimeline/index.tsx +478 -0
  84. package/src/components/observability/TraceTimeline/shortcuts.ts +16 -0
  85. package/src/components/observability/__fixtures__/logs.ts +476 -0
  86. package/src/components/observability/__fixtures__/metrics.ts +216 -0
  87. package/src/components/observability/__fixtures__/raw-table.ts +204 -0
  88. package/src/components/observability/__fixtures__/services.ts +8 -0
  89. package/src/components/observability/__fixtures__/trace-summaries.ts +81 -0
  90. package/src/components/observability/__fixtures__/traces.ts +396 -0
  91. package/src/components/observability/index.ts +66 -0
  92. package/src/components/observability/renderers/OtelMetricDiscovery.tsx +77 -0
  93. package/src/components/observability/renderers/OtelMetricHistogram.tsx +29 -0
  94. package/src/components/observability/renderers/OtelMetricStat.tsx +44 -0
  95. package/src/components/observability/renderers/OtelMetricTable.tsx +29 -0
  96. package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +30 -0
  97. package/src/components/observability/renderers/index.ts +5 -0
  98. package/src/components/observability/shared/LoadingSkeleton.tsx +43 -0
  99. package/src/components/observability/types.ts +113 -0
  100. package/src/components/observability/utils/attributes.ts +17 -0
  101. package/src/components/observability/utils/colors.ts +29 -0
  102. package/src/components/observability/utils/flatten-tree.ts +53 -0
  103. package/src/components/observability/utils/lttb.ts +121 -0
  104. package/src/components/observability/utils/time.ts +46 -0
  105. package/src/hooks/use-kopai-data.test.ts +296 -0
  106. package/src/hooks/use-kopai-data.ts +64 -0
  107. package/src/hooks/use-live-logs.test.ts +193 -0
  108. package/src/hooks/use-live-logs.ts +113 -0
  109. package/src/index.ts +15 -0
  110. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +33 -0
  111. package/src/lib/catalog.ts +165 -0
  112. package/src/lib/component-catalog.test.ts +357 -0
  113. package/src/lib/component-catalog.ts +171 -0
  114. package/src/lib/dashboard-datasource.ts +76 -0
  115. package/src/lib/generate-prompt-instructions.test.ts +27 -0
  116. package/src/lib/generate-prompt-instructions.ts +185 -0
  117. package/src/lib/log-buffer.test.ts +88 -0
  118. package/src/lib/log-buffer.ts +62 -0
  119. package/src/lib/observability-catalog.ts +143 -0
  120. package/src/lib/renderer.test.tsx +693 -0
  121. package/src/lib/renderer.tsx +276 -0
  122. package/src/pages/observability.tsx +825 -0
  123. package/src/providers/kopai-provider.tsx +51 -0
  124. package/src/styles/globals.css +46 -0
  125. 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
+ });