@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.
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 +828 -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
@@ -0,0 +1,271 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import type { LogEntry } from "../../types.js";
3
+ import { AttributesTab } from "./AttributesTab.js";
4
+ import { JsonTreeView } from "./JsonTreeView.js";
5
+
6
+ export interface LogDetailPaneProps {
7
+ log: LogEntry;
8
+ onClose: () => void;
9
+ onTraceLinkClick?: (traceId: string, spanId: string) => void;
10
+ initialTab?: "message" | "attributes" | "resource" | "context";
11
+ wordWrap?: boolean;
12
+ }
13
+
14
+ type TabType = "message" | "attributes" | "resource" | "context";
15
+
16
+ export function LogDetailPane({
17
+ log,
18
+ onClose,
19
+ onTraceLinkClick,
20
+ initialTab = "message",
21
+ wordWrap = true,
22
+ }: LogDetailPaneProps) {
23
+ const hasContext = !!log.traceId;
24
+ const [activeTab, setActiveTab] = useState<TabType>(
25
+ initialTab === "context" && !hasContext ? "message" : initialTab
26
+ );
27
+ const [copiedId, setCopiedId] = useState(false);
28
+ const detailPaneRef = useRef<HTMLDivElement>(null);
29
+
30
+ const handleCopyLogId = useCallback(async () => {
31
+ try {
32
+ await navigator.clipboard.writeText(log.logId);
33
+ setCopiedId(true);
34
+ setTimeout(() => setCopiedId(false), 2000);
35
+ } catch (err) {
36
+ console.error("Failed to copy log ID:", err);
37
+ }
38
+ }, [log.logId]);
39
+
40
+ const handleKeyDown = useCallback(
41
+ (e: React.KeyboardEvent) => {
42
+ if (e.key === "Escape") onClose();
43
+ },
44
+ [onClose]
45
+ );
46
+
47
+ useEffect(() => {
48
+ detailPaneRef.current?.focus();
49
+ }, []);
50
+
51
+ const severityColor = getSeverityColor(log.severityText);
52
+
53
+ return (
54
+ <div
55
+ ref={detailPaneRef}
56
+ className="w-[500px] flex flex-col h-full bg-background border-l border-border outline-none"
57
+ onKeyDown={handleKeyDown}
58
+ role="complementary"
59
+ aria-label="Log details"
60
+ tabIndex={-1}
61
+ >
62
+ {/* Header */}
63
+ <div className="p-4 border-b border-border">
64
+ <div className="flex items-center justify-between mb-3">
65
+ <h2 className="text-lg font-semibold text-foreground">Log Details</h2>
66
+ <button
67
+ onClick={onClose}
68
+ className="p-2 hover:bg-muted rounded transition-colors"
69
+ aria-label="Close detail pane"
70
+ >
71
+ <svg
72
+ className="w-6 h-6 text-muted-foreground"
73
+ fill="none"
74
+ stroke="currentColor"
75
+ viewBox="0 0 24 24"
76
+ >
77
+ <path
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ strokeWidth={2}
81
+ d="M6 18L18 6M6 6l12 12"
82
+ />
83
+ </svg>
84
+ </button>
85
+ </div>
86
+ <div className="flex items-center gap-2 mb-2">
87
+ <div
88
+ className={`text-xs font-semibold px-2 py-0.5 rounded ${severityColor.bg} ${severityColor.text}`}
89
+ >
90
+ {log.severityText}
91
+ </div>
92
+ <div className="text-sm text-muted-foreground">{log.serviceName}</div>
93
+ </div>
94
+ <div className="text-xs font-medium text-muted-foreground mt-3 mb-1">
95
+ Timestamp
96
+ </div>
97
+ <div className="text-xs text-foreground font-mono">
98
+ {new Date(log.timeUnixMs).toISOString()}
99
+ </div>
100
+ <div className="text-xs font-medium text-muted-foreground mt-3 mb-1">
101
+ Log ID
102
+ </div>
103
+ <div className="flex items-center gap-2">
104
+ <button
105
+ onClick={handleCopyLogId}
106
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground font-mono bg-muted px-2 py-1 rounded"
107
+ title="Click to copy log ID"
108
+ >
109
+ <span className="truncate max-w-[200px]">{log.logId}</span>
110
+ <svg
111
+ className="w-3 h-3 flex-shrink-0"
112
+ fill="none"
113
+ stroke="currentColor"
114
+ viewBox="0 0 24 24"
115
+ >
116
+ {copiedId ? (
117
+ <path
118
+ strokeLinecap="round"
119
+ strokeLinejoin="round"
120
+ strokeWidth={2}
121
+ d="M5 13l4 4L19 7"
122
+ />
123
+ ) : (
124
+ <path
125
+ strokeLinecap="round"
126
+ strokeLinejoin="round"
127
+ strokeWidth={2}
128
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
129
+ />
130
+ )}
131
+ </svg>
132
+ </button>
133
+ </div>
134
+ </div>
135
+
136
+ {/* Tabs */}
137
+ <div className="flex border-b border-border">
138
+ {(
139
+ [
140
+ "message",
141
+ "attributes",
142
+ "resource",
143
+ ...(log.traceId ? ["context" as const] : []),
144
+ ] as TabType[]
145
+ ).map((tab) => (
146
+ <button
147
+ key={tab}
148
+ onClick={() => setActiveTab(tab)}
149
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
150
+ activeTab === tab
151
+ ? "text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400"
152
+ : "text-muted-foreground hover:text-foreground"
153
+ }`}
154
+ >
155
+ {tab === "context"
156
+ ? "Trace"
157
+ : tab.charAt(0).toUpperCase() + tab.slice(1)}
158
+ </button>
159
+ ))}
160
+ </div>
161
+
162
+ {/* Content */}
163
+ <div className="flex-1 overflow-y-auto p-4">
164
+ {activeTab === "message" && (
165
+ <pre
166
+ className={`text-sm text-foreground font-mono bg-muted p-3 rounded ${
167
+ wordWrap
168
+ ? "whitespace-pre-wrap break-words"
169
+ : "whitespace-pre overflow-x-auto"
170
+ }`}
171
+ >
172
+ {log.body || "No message body"}
173
+ </pre>
174
+ )}
175
+ {activeTab === "attributes" && <AttributesTab log={log} />}
176
+ {activeTab === "resource" && (
177
+ <div>
178
+ <div className="mb-3">
179
+ <div className="text-sm font-semibold text-foreground mb-2">
180
+ Service Name
181
+ </div>
182
+ <div className="text-sm text-foreground bg-muted p-2 rounded font-mono">
183
+ {log.serviceName}
184
+ </div>
185
+ </div>
186
+ {log.scopeName && (
187
+ <div className="mb-3">
188
+ <div className="text-sm font-semibold text-foreground mb-2">
189
+ Scope Name
190
+ </div>
191
+ <div className="text-sm text-foreground bg-muted p-2 rounded font-mono">
192
+ {log.scopeName}
193
+ </div>
194
+ </div>
195
+ )}
196
+ <div>
197
+ <div className="text-sm font-semibold text-foreground mb-2">
198
+ Resource Attributes
199
+ </div>
200
+ {Object.keys(log.resourceAttributes).length > 0 ? (
201
+ <JsonTreeView data={log.resourceAttributes} />
202
+ ) : (
203
+ <div className="text-sm text-muted-foreground text-center py-4">
204
+ No resource attributes
205
+ </div>
206
+ )}
207
+ </div>
208
+ </div>
209
+ )}
210
+ {activeTab === "context" && log.traceId && (
211
+ <div className="space-y-3">
212
+ <div>
213
+ <div className="text-sm font-semibold text-foreground mb-2">
214
+ Trace ID
215
+ </div>
216
+ <div className="text-sm text-foreground bg-muted p-2 rounded font-mono break-all">
217
+ {log.traceId}
218
+ </div>
219
+ </div>
220
+ {log.spanId && (
221
+ <div>
222
+ <div className="text-sm font-semibold text-foreground mb-2">
223
+ Span ID
224
+ </div>
225
+ <div className="text-sm text-foreground bg-muted p-2 rounded font-mono break-all">
226
+ {log.spanId}
227
+ </div>
228
+ </div>
229
+ )}
230
+ {onTraceLinkClick && log.spanId && (
231
+ <button
232
+ onClick={() => onTraceLinkClick(log.traceId!, log.spanId!)}
233
+ className="w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors text-sm"
234
+ >
235
+ View Trace
236
+ </button>
237
+ )}
238
+ </div>
239
+ )}
240
+ </div>
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function getSeverityColor(severity: string): { text: string; bg: string } {
246
+ const s = severity.toUpperCase();
247
+ if (s === "FATAL" || s === "ERROR")
248
+ return {
249
+ text: "text-red-900 dark:text-red-100",
250
+ bg: "bg-red-100 dark:bg-red-900/30",
251
+ };
252
+ if (s === "WARN" || s === "WARNING")
253
+ return {
254
+ text: "text-orange-900 dark:text-orange-100",
255
+ bg: "bg-orange-100 dark:bg-orange-900/30",
256
+ };
257
+ if (s === "INFO")
258
+ return {
259
+ text: "text-blue-900 dark:text-blue-100",
260
+ bg: "bg-blue-100 dark:bg-blue-900/30",
261
+ };
262
+ if (s === "DEBUG")
263
+ return {
264
+ text: "text-gray-900 dark:text-gray-100",
265
+ bg: "bg-gray-100 dark:bg-gray-700/30",
266
+ };
267
+ return {
268
+ text: "text-gray-600 dark:text-gray-400",
269
+ bg: "bg-gray-50 dark:bg-gray-800/20",
270
+ };
271
+ }
@@ -0,0 +1,66 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import type { dataFilterSchemas } from "@kopai/core";
4
+ import { LogFilter } from "./LogFilter.js";
5
+ import { mockLogRows } from "../__fixtures__/logs.js";
6
+
7
+ type LogsDataFilter = dataFilterSchemas.LogsDataFilter;
8
+
9
+ const meta: Meta<typeof LogFilter> = {
10
+ title: "Observability/LogFilter",
11
+ component: LogFilter,
12
+ decorators: [
13
+ (Story) => (
14
+ <div style={{ width: 800 }}>
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ };
20
+ export default meta;
21
+ type Story = StoryObj<typeof LogFilter>;
22
+
23
+ function Controlled(
24
+ initial: Partial<LogsDataFilter>,
25
+ initialServices: string[] = []
26
+ ) {
27
+ return function Render() {
28
+ const [value, setValue] = useState<Partial<LogsDataFilter>>(initial);
29
+ const [selectedServices, setSelectedServices] =
30
+ useState<string[]>(initialServices);
31
+ return (
32
+ <div className="space-y-4">
33
+ <LogFilter
34
+ value={value}
35
+ onChange={setValue}
36
+ rows={mockLogRows}
37
+ selectedServices={selectedServices}
38
+ onSelectedServicesChange={setSelectedServices}
39
+ />
40
+ <pre className="text-xs text-muted-foreground bg-muted/50 p-3 rounded overflow-auto">
41
+ {JSON.stringify(
42
+ { ...value, _selectedServices: selectedServices },
43
+ null,
44
+ 2
45
+ )}
46
+ </pre>
47
+ </div>
48
+ );
49
+ };
50
+ }
51
+
52
+ export const Default: Story = {
53
+ render: Controlled({}),
54
+ };
55
+
56
+ export const WithValues: Story = {
57
+ render: Controlled(
58
+ {
59
+ severityText: "ERROR",
60
+ bodyContains: "timeout",
61
+ limit: 200,
62
+ sortOrder: "DESC",
63
+ },
64
+ ["api-gateway"]
65
+ ),
66
+ };