@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
@@ -0,0 +1,174 @@
1
+ import { memo, useMemo } from "react";
2
+ import type { LogEntry } from "../types.js";
3
+ import { getServiceColor } from "../utils/colors.js";
4
+
5
+ export interface LogRowProps {
6
+ log: LogEntry;
7
+ isSelected: boolean;
8
+ onClick: () => void;
9
+ searchText?: string;
10
+ relativeTime?: boolean;
11
+ referenceTimeMs?: number;
12
+ }
13
+
14
+ function formatTimestamp(timeMs: number): string {
15
+ const date = new Date(timeMs);
16
+ const hours = String(date.getHours()).padStart(2, "0");
17
+ const minutes = String(date.getMinutes()).padStart(2, "0");
18
+ const seconds = String(date.getSeconds()).padStart(2, "0");
19
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
20
+ return `${hours}:${minutes}:${seconds}.${ms}`;
21
+ }
22
+
23
+ function formatRelativeTime(timeMs: number, referenceMs: number): string {
24
+ const diffMs = timeMs - referenceMs;
25
+ const sign = diffMs >= 0 ? "+" : "-";
26
+ const abs = Math.abs(diffMs);
27
+ if (abs < 1000) return `${sign}${abs.toFixed(4)}ms`;
28
+ if (abs < 60_000) return `${sign}${(abs / 1000).toFixed(4)}s`;
29
+ const mins = Math.floor(abs / 60_000);
30
+ const secs = ((abs % 60_000) / 1000).toFixed(4);
31
+ return `${sign}${mins}m${secs}s`;
32
+ }
33
+
34
+ function truncateMessage(message: string, maxLength = 120): string {
35
+ if (message.length <= maxLength) return message;
36
+ return message.slice(0, maxLength) + "...";
37
+ }
38
+
39
+ function getSeverityColor(severity: string): { text: string; bg: string } {
40
+ const s = severity.toUpperCase();
41
+ if (s === "FATAL" || s === "ERROR")
42
+ return {
43
+ text: "text-red-900 dark:text-red-100",
44
+ bg: "bg-red-100 dark:bg-red-900/30",
45
+ };
46
+ if (s === "WARN" || s === "WARNING")
47
+ return {
48
+ text: "text-orange-900 dark:text-orange-100",
49
+ bg: "bg-orange-100 dark:bg-orange-900/30",
50
+ };
51
+ if (s === "INFO")
52
+ return {
53
+ text: "text-blue-900 dark:text-blue-100",
54
+ bg: "bg-blue-100 dark:bg-blue-900/30",
55
+ };
56
+ if (s === "DEBUG")
57
+ return {
58
+ text: "text-gray-900 dark:text-gray-100",
59
+ bg: "bg-gray-100 dark:bg-gray-700/30",
60
+ };
61
+ if (s === "TRACE")
62
+ return {
63
+ text: "text-gray-700 dark:text-gray-300",
64
+ bg: "bg-gray-50 dark:bg-gray-800/30",
65
+ };
66
+ return {
67
+ text: "text-gray-600 dark:text-gray-400",
68
+ bg: "bg-gray-50 dark:bg-gray-800/20",
69
+ };
70
+ }
71
+
72
+ function highlightSearchText(
73
+ text: string,
74
+ searchText: string
75
+ ): React.ReactNode {
76
+ if (!searchText || !text) return text;
77
+
78
+ const parts: React.ReactNode[] = [];
79
+ let lastIndex = 0;
80
+ const searchLower = searchText.toLowerCase();
81
+ const textLower = text.toLowerCase();
82
+ let index = textLower.indexOf(searchLower);
83
+
84
+ while (index !== -1) {
85
+ if (index > lastIndex) parts.push(text.slice(lastIndex, index));
86
+ const matchText = text.slice(index, index + searchText.length);
87
+ parts.push(
88
+ <mark
89
+ key={`${index}-${matchText}`}
90
+ className="bg-yellow-200 dark:bg-yellow-700 text-foreground"
91
+ >
92
+ {matchText}
93
+ </mark>
94
+ );
95
+ lastIndex = index + searchText.length;
96
+ index = textLower.indexOf(searchLower, lastIndex);
97
+ }
98
+
99
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
100
+ return parts.length > 0 ? <>{parts}</> : text;
101
+ }
102
+
103
+ export const LogRow = memo(function LogRow({
104
+ log,
105
+ isSelected,
106
+ onClick,
107
+ searchText,
108
+ relativeTime,
109
+ referenceTimeMs,
110
+ }: LogRowProps) {
111
+ const severityColor = getSeverityColor(log.severityText);
112
+ const message = useMemo(() => log.body || "", [log.body]);
113
+ const timestamp =
114
+ relativeTime && referenceTimeMs != null
115
+ ? formatRelativeTime(log.timeUnixMs, referenceTimeMs)
116
+ : formatTimestamp(log.timeUnixMs);
117
+ const lineCount = message.split("\n").length;
118
+ const hasMultipleLines = lineCount > 1;
119
+
120
+ return (
121
+ <div
122
+ style={{ contain: "layout style paint" }}
123
+ className={`flex items-center gap-3 px-4 h-[44px] border-b border-border cursor-pointer overflow-hidden outline-none ${
124
+ isSelected
125
+ ? "bg-blue-50 dark:bg-blue-900/30 border-l-4 border-l-blue-500"
126
+ : "hover:bg-muted"
127
+ }`}
128
+ onClick={onClick}
129
+ role="button"
130
+ tabIndex={0}
131
+ onKeyDown={(e) => {
132
+ if (e.key === "Enter" || e.key === " ") {
133
+ e.preventDefault();
134
+ onClick();
135
+ }
136
+ }}
137
+ >
138
+ <div className="flex-shrink-0 w-28 font-mono text-xs text-muted-foreground">
139
+ {timestamp}
140
+ </div>
141
+ <div
142
+ className={`flex-shrink-0 w-24 text-xs font-semibold px-2 py-0.5 rounded truncate ${severityColor.bg} ${severityColor.text}`}
143
+ >
144
+ {log.severityText}
145
+ </div>
146
+ <div
147
+ className="flex-shrink-0 w-32 text-xs truncate"
148
+ style={{ color: getServiceColor(log.serviceName) }}
149
+ >
150
+ {log.serviceName}
151
+ </div>
152
+ <div className="flex-1 min-w-0 flex items-center gap-2">
153
+ <div className="text-sm text-foreground truncate">
154
+ {searchText
155
+ ? highlightSearchText(
156
+ truncateMessage(message.split("\n")[0] || "", 100),
157
+ searchText
158
+ )
159
+ : truncateMessage(message.split("\n")[0] || "", 100)}
160
+ </div>
161
+ {hasMultipleLines && (
162
+ <span className="flex-shrink-0 text-xs text-muted-foreground">
163
+ +{lineCount - 1} lines
164
+ </span>
165
+ )}
166
+ {log.traceId && (
167
+ <span className="flex-shrink-0 text-xs text-indigo-600 dark:text-indigo-400">
168
+ trace: {log.traceId.slice(0, 16)}...
169
+ </span>
170
+ )}
171
+ </div>
172
+ </div>
173
+ );
174
+ });
@@ -0,0 +1,154 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import type { denormalizedSignals, dataFilterSchemas } from "@kopai/core";
4
+ import { LogTimeline } from "./index.js";
5
+ import { LogFilter } from "./LogFilter.js";
6
+ import { mockLogRows } from "../__fixtures__/logs.js";
7
+
8
+ type OtelLogsRow = denormalizedSignals.OtelLogsRow;
9
+
10
+ const meta: Meta<typeof LogTimeline> = {
11
+ title: "Observability/LogTimeline",
12
+ component: LogTimeline,
13
+ decorators: [
14
+ (Story) => (
15
+ <div style={{ height: "600px" }}>
16
+ <Story />
17
+ </div>
18
+ ),
19
+ ],
20
+ };
21
+ export default meta;
22
+ type Story = StoryObj<typeof LogTimeline>;
23
+
24
+ export const Default: Story = { args: { rows: mockLogRows } };
25
+ export const Loading: Story = { args: { rows: [], isLoading: true } };
26
+ export const Error: Story = {
27
+ args: { rows: [], error: new globalThis.Error("Failed to fetch logs") },
28
+ };
29
+ export const Empty: Story = { args: { rows: [] } };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Live Streaming story
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const SERVICES = ["api-gateway", "auth-service", "user-service"];
36
+ const BODIES = [
37
+ "Request received: GET /api/users",
38
+ "User authenticated successfully",
39
+ "Database query completed in 42ms",
40
+ "Cache hit for session token",
41
+ "Response sent: 200 OK",
42
+ "Rate limit check passed",
43
+ "Health check passed",
44
+ "Connection pool: 12/20 active",
45
+ "Retry attempt 1/3 for upstream call",
46
+ "Metrics flush: 256 data points exported",
47
+ ];
48
+ const SEVERITIES: [string, number][] = [
49
+ ["INFO", 9],
50
+ ["DEBUG", 5],
51
+ ["WARN", 13],
52
+ ["ERROR", 17],
53
+ ];
54
+
55
+ let nextTs = BigInt(Date.now()) * 1000000n;
56
+
57
+ function randomRow(): OtelLogsRow {
58
+ nextTs += BigInt(Math.floor(Math.random() * 500 + 10)) * 1000000n;
59
+ const sev = SEVERITIES[Math.floor(Math.random() * SEVERITIES.length)]!;
60
+ return {
61
+ Timestamp: nextTs.toString(),
62
+ Body: BODIES[Math.floor(Math.random() * BODIES.length)]!,
63
+ ServiceName: SERVICES[Math.floor(Math.random() * SERVICES.length)]!,
64
+ SeverityText: sev[0],
65
+ SeverityNumber: sev[1],
66
+ };
67
+ }
68
+
69
+ function LiveStreamingDemo() {
70
+ const [rows, setRows] = useState<OtelLogsRow[]>(() =>
71
+ Array.from({ length: 20 }, () => randomRow())
72
+ );
73
+ const [isLive, setIsLive] = useState(true);
74
+
75
+ useEffect(() => {
76
+ if (!isLive) return;
77
+ const id = setInterval(() => {
78
+ const count = Math.floor(Math.random() * 3) + 1;
79
+ const newRows = Array.from({ length: count }, () => randomRow());
80
+ setRows((prev) => {
81
+ const all = [...prev, ...newRows];
82
+ return all.length > 1000 ? all.slice(all.length - 1000) : all;
83
+ });
84
+ }, 2000);
85
+ return () => clearInterval(id);
86
+ }, [isLive]);
87
+
88
+ const handleAtBottomChange = useCallback((atBottom: boolean) => {
89
+ setIsLive(atBottom);
90
+ }, []);
91
+
92
+ return (
93
+ <LogTimeline
94
+ rows={rows}
95
+ streaming={isLive}
96
+ onAtBottomChange={handleAtBottomChange}
97
+ />
98
+ );
99
+ }
100
+
101
+ export const LiveStreaming: Story = {
102
+ render: () => <LiveStreamingDemo />,
103
+ };
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // WithFilter story
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function WithFilterDemo() {
110
+ const [filters, setFilters] = useState<
111
+ Partial<dataFilterSchemas.LogsDataFilter>
112
+ >({ limit: 200, sortOrder: "DESC" });
113
+ const [selectedServices, setSelectedServices] = useState<string[]>([]);
114
+
115
+ // Client-side filter on the mock data for demo purposes
116
+ const filtered = mockLogRows.filter((row) => {
117
+ if (
118
+ selectedServices.length > 0 &&
119
+ !selectedServices.includes(row.ServiceName ?? "")
120
+ )
121
+ return false;
122
+ if (filters.severityText && row.SeverityText !== filters.severityText)
123
+ return false;
124
+ if (
125
+ filters.bodyContains &&
126
+ !(row.Body ?? "")
127
+ .toLowerCase()
128
+ .includes(filters.bodyContains.toLowerCase())
129
+ )
130
+ return false;
131
+ return true;
132
+ });
133
+
134
+ return (
135
+ <div className="flex flex-col" style={{ height: 600 }}>
136
+ <div className="shrink-0 mb-3">
137
+ <LogFilter
138
+ value={filters}
139
+ onChange={setFilters}
140
+ rows={mockLogRows}
141
+ selectedServices={selectedServices}
142
+ onSelectedServicesChange={setSelectedServices}
143
+ />
144
+ </div>
145
+ <div className="flex-1 min-h-0">
146
+ <LogTimeline rows={filtered} />
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ export const WithFilter: Story = {
153
+ render: () => <WithFilterDemo />,
154
+ };