@kopai/ui 0.0.4 → 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 +30 -12
- 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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RawDataTable - Display arbitrary tabular data with dynamic columns.
|
|
3
|
+
* Accepts RawTableData directly (unchanged from source).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RawTableData } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export interface RawDataTableProps {
|
|
9
|
+
data: RawTableData;
|
|
10
|
+
maxRows?: number;
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
error?: Error;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SCALE_K = 1e3;
|
|
17
|
+
const SCALE_M = 1e6;
|
|
18
|
+
const SCALE_G = 1e9;
|
|
19
|
+
|
|
20
|
+
function isNumericType(type: string | undefined): boolean {
|
|
21
|
+
if (!type) return false;
|
|
22
|
+
return ["Int", "UInt", "Float", "Decimal"].some((t) => type.includes(t));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatNumber(value: number): string {
|
|
26
|
+
if (Math.abs(value) >= SCALE_G) return `${(value / SCALE_G).toFixed(2)}G`;
|
|
27
|
+
if (Math.abs(value) >= SCALE_M) return `${(value / SCALE_M).toFixed(2)}M`;
|
|
28
|
+
if (Math.abs(value) >= SCALE_K) return `${(value / SCALE_K).toFixed(2)}K`;
|
|
29
|
+
if (Number.isInteger(value)) return value.toLocaleString();
|
|
30
|
+
return value.toFixed(2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatCell(value: unknown): string {
|
|
34
|
+
if (value === null || value === undefined) return "-";
|
|
35
|
+
if (typeof value === "number") return formatNumber(value);
|
|
36
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function RawDataTable({
|
|
41
|
+
data,
|
|
42
|
+
maxRows = 50,
|
|
43
|
+
isLoading = false,
|
|
44
|
+
error,
|
|
45
|
+
className = "",
|
|
46
|
+
}: RawDataTableProps) {
|
|
47
|
+
if (isLoading) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={`bg-background rounded-lg p-4 ${className}`}>
|
|
50
|
+
<div className="animate-pulse" data-testid="raw-data-table-loading">
|
|
51
|
+
<div className="h-10 bg-muted rounded mb-2" />
|
|
52
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
53
|
+
<div key={i} className="h-12 bg-muted/50 rounded mb-1" />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={`bg-background rounded-lg p-4 border border-destructive ${className}`}
|
|
64
|
+
data-testid="raw-data-table-error"
|
|
65
|
+
>
|
|
66
|
+
<p className="text-destructive">Error: {error.message}</p>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { columns, types, rows } = data;
|
|
72
|
+
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={`bg-background rounded-lg p-4 border border-border ${className}`}
|
|
77
|
+
data-testid="raw-data-table-empty"
|
|
78
|
+
>
|
|
79
|
+
<p className="text-muted-foreground text-center py-4">
|
|
80
|
+
No data available
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isTruncated = rows.length > maxRows;
|
|
87
|
+
const displayRows = rows.slice(0, maxRows);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={`bg-background rounded-lg overflow-hidden border border-border ${className}`}
|
|
92
|
+
data-testid="raw-data-table"
|
|
93
|
+
>
|
|
94
|
+
<div className="overflow-x-auto">
|
|
95
|
+
<table className="w-full text-sm">
|
|
96
|
+
<thead>
|
|
97
|
+
<tr className="bg-muted text-muted-foreground text-left">
|
|
98
|
+
{columns.map((col, colIdx) => (
|
|
99
|
+
<th
|
|
100
|
+
key={colIdx}
|
|
101
|
+
className={`px-4 py-3 font-medium whitespace-nowrap ${isNumericType(types[colIdx]) ? "text-right" : "text-left"}`}
|
|
102
|
+
>
|
|
103
|
+
{col}
|
|
104
|
+
</th>
|
|
105
|
+
))}
|
|
106
|
+
</tr>
|
|
107
|
+
</thead>
|
|
108
|
+
<tbody className="divide-y divide-border">
|
|
109
|
+
{displayRows.map((row, rowIdx) => (
|
|
110
|
+
<tr key={rowIdx} className="hover:bg-muted/50 transition-colors">
|
|
111
|
+
{columns.map((_, colIdx) => (
|
|
112
|
+
<td
|
|
113
|
+
key={colIdx}
|
|
114
|
+
className={`px-4 py-3 whitespace-nowrap ${isNumericType(types[colIdx]) ? "text-right text-foreground font-medium" : "text-foreground"}`}
|
|
115
|
+
>
|
|
116
|
+
{formatCell(row[colIdx])}
|
|
117
|
+
</td>
|
|
118
|
+
))}
|
|
119
|
+
</tr>
|
|
120
|
+
))}
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
124
|
+
{isTruncated && (
|
|
125
|
+
<div className="px-4 py-2 bg-muted text-muted-foreground text-xs text-center">
|
|
126
|
+
Showing first {maxRows} of {rows.length} rows
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ServiceList } from "./index.js";
|
|
3
|
+
import { mockServices } from "../__fixtures__/services.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ServiceList> = {
|
|
6
|
+
title: "Observability/ServiceList",
|
|
7
|
+
component: ServiceList,
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof ServiceList>;
|
|
11
|
+
|
|
12
|
+
export const Default: Story = { args: { services: mockServices } };
|
|
13
|
+
export const Loading: Story = { args: { services: [], isLoading: true } };
|
|
14
|
+
export const Error: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
services: [],
|
|
17
|
+
error: new globalThis.Error("Failed to fetch services"),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export const Empty: Story = { args: { services: [] } };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getServiceColor } from "../utils/colors.js";
|
|
2
|
+
|
|
3
|
+
export interface ServiceEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ServiceListProps {
|
|
8
|
+
services: ServiceEntry[];
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
error?: Error;
|
|
11
|
+
onSelect: (name: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ServiceList({
|
|
15
|
+
services,
|
|
16
|
+
isLoading,
|
|
17
|
+
error,
|
|
18
|
+
onSelect,
|
|
19
|
+
}: ServiceListProps) {
|
|
20
|
+
if (isLoading) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex items-center gap-2 text-muted-foreground py-8">
|
|
23
|
+
<div className="w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" />
|
|
24
|
+
Loading services...
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (error) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="text-red-400 py-4">
|
|
32
|
+
Error loading services: {error.message}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (services.length === 0) {
|
|
38
|
+
return <div className="text-muted-foreground py-8">No services found</div>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-1">
|
|
43
|
+
{services.map((svc) => (
|
|
44
|
+
<button
|
|
45
|
+
key={svc.name}
|
|
46
|
+
onClick={() => onSelect(svc.name)}
|
|
47
|
+
className="w-full text-left px-4 py-3 rounded-lg border border-border hover:border-foreground/30 hover:bg-muted/50 transition-colors group"
|
|
48
|
+
>
|
|
49
|
+
<span className="flex items-center gap-2 font-medium text-foreground">
|
|
50
|
+
<span
|
|
51
|
+
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
|
|
52
|
+
style={{ backgroundColor: getServiceColor(svc.name) }}
|
|
53
|
+
/>
|
|
54
|
+
{svc.name}
|
|
55
|
+
</span>
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { TabBar } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof TabBar> = {
|
|
5
|
+
title: "Observability/TabBar",
|
|
6
|
+
component: TabBar,
|
|
7
|
+
};
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof TabBar>;
|
|
10
|
+
|
|
11
|
+
export const Default: Story = {
|
|
12
|
+
args: {
|
|
13
|
+
tabs: [
|
|
14
|
+
{ key: "services", label: "Services" },
|
|
15
|
+
{ key: "logs", label: "Logs" },
|
|
16
|
+
{ key: "metrics", label: "Metrics" },
|
|
17
|
+
],
|
|
18
|
+
active: "services",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ManyTabs: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
tabs: [
|
|
25
|
+
{ key: "overview", label: "Overview" },
|
|
26
|
+
{ key: "services", label: "Services" },
|
|
27
|
+
{ key: "logs", label: "Logs" },
|
|
28
|
+
{ key: "traces", label: "Traces" },
|
|
29
|
+
{ key: "metrics", label: "Metrics" },
|
|
30
|
+
{ key: "alerts", label: "Alerts" },
|
|
31
|
+
],
|
|
32
|
+
active: "traces",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface Tab {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
shortcutKey?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TabBarProps {
|
|
8
|
+
tabs: Tab[];
|
|
9
|
+
active: string;
|
|
10
|
+
onChange: (key: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function renderLabel(label: string, shortcutKey?: string) {
|
|
14
|
+
if (!shortcutKey) return label;
|
|
15
|
+
const idx = label.toLowerCase().indexOf(shortcutKey.toLowerCase());
|
|
16
|
+
if (idx === -1) return label;
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{label.slice(0, idx)}
|
|
20
|
+
<span className="underline underline-offset-4">{label[idx]}</span>
|
|
21
|
+
{label.slice(idx + 1)}
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TabBar({ tabs, active, onChange }: TabBarProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div role="tablist" className="flex border-b border-border mb-6">
|
|
29
|
+
{tabs.map((t) => (
|
|
30
|
+
<button
|
|
31
|
+
key={t.key}
|
|
32
|
+
role="tab"
|
|
33
|
+
aria-selected={active === t.key}
|
|
34
|
+
onClick={() => onChange(t.key)}
|
|
35
|
+
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
36
|
+
active === t.key
|
|
37
|
+
? "border-foreground text-foreground"
|
|
38
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
{renderLabel(t.label, t.shortcutKey)}
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { TraceDetail } from "./index.js";
|
|
3
|
+
import { mockTraceRows, mockErrorTraceRows } from "../__fixtures__/traces.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TraceDetail> = {
|
|
6
|
+
title: "Observability/TraceDetail",
|
|
7
|
+
component: TraceDetail,
|
|
8
|
+
decorators: [
|
|
9
|
+
(Story) => (
|
|
10
|
+
<div style={{ height: "600px" }}>
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof TraceDetail>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
service: "api-gateway",
|
|
22
|
+
traceId: "0af7651916cd43dd8448eb211c80319c",
|
|
23
|
+
rows: mockTraceRows,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ErrorTrace: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
service: "api-gateway",
|
|
30
|
+
traceId: "1bf8762027de54ee9559fc322d91420d",
|
|
31
|
+
rows: mockErrorTraceRows,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Loading: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
service: "api-gateway",
|
|
38
|
+
traceId: "0af7651916cd43dd8448eb211c80319c",
|
|
39
|
+
rows: [],
|
|
40
|
+
isLoading: true,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Error: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
service: "api-gateway",
|
|
47
|
+
traceId: "0af7651916cd43dd8448eb211c80319c",
|
|
48
|
+
rows: [],
|
|
49
|
+
error: new globalThis.Error("Failed to fetch trace"),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { denormalizedSignals } from "@kopai/core";
|
|
2
|
+
import { TraceTimeline } from "../TraceTimeline/index.js";
|
|
3
|
+
import type { SpanNode } from "../types.js";
|
|
4
|
+
|
|
5
|
+
type OtelTracesRow = denormalizedSignals.OtelTracesRow;
|
|
6
|
+
|
|
7
|
+
export interface TraceDetailProps {
|
|
8
|
+
service: string;
|
|
9
|
+
traceId: string;
|
|
10
|
+
rows: OtelTracesRow[];
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
error?: Error;
|
|
13
|
+
selectedSpanId?: string;
|
|
14
|
+
onSpanClick?: (span: SpanNode) => void;
|
|
15
|
+
onBack: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TraceDetail({
|
|
19
|
+
service,
|
|
20
|
+
traceId,
|
|
21
|
+
rows,
|
|
22
|
+
isLoading,
|
|
23
|
+
error,
|
|
24
|
+
selectedSpanId,
|
|
25
|
+
onSpanClick,
|
|
26
|
+
onBack,
|
|
27
|
+
}: TraceDetailProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
{/* Breadcrumb */}
|
|
31
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground mb-4">
|
|
32
|
+
<button
|
|
33
|
+
onClick={onBack}
|
|
34
|
+
className="hover:text-foreground transition-colors"
|
|
35
|
+
>
|
|
36
|
+
Services / {service}
|
|
37
|
+
</button>
|
|
38
|
+
<span>/</span>
|
|
39
|
+
<span className="text-foreground font-mono text-xs">
|
|
40
|
+
{traceId.slice(0, 16)}...
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<TraceTimeline
|
|
45
|
+
rows={rows}
|
|
46
|
+
isLoading={isLoading}
|
|
47
|
+
error={error}
|
|
48
|
+
selectedSpanId={selectedSpanId}
|
|
49
|
+
onSpanClick={onSpanClick}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { TraceSearch } from "./index.js";
|
|
3
|
+
import { mockTraceSummaries } from "../__fixtures__/trace-summaries.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TraceSearch> = {
|
|
6
|
+
title: "Observability/TraceSearch",
|
|
7
|
+
component: TraceSearch,
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof TraceSearch>;
|
|
11
|
+
|
|
12
|
+
export const Default: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
service: "api-gateway",
|
|
15
|
+
traces: mockTraceSummaries,
|
|
16
|
+
operations: [
|
|
17
|
+
"GET /api/users",
|
|
18
|
+
"POST /api/users",
|
|
19
|
+
"GET /api/products",
|
|
20
|
+
"PUT /api/users/42",
|
|
21
|
+
"DELETE /api/sessions",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Loading: Story = {
|
|
27
|
+
args: { service: "api-gateway", traces: [], isLoading: true },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Error: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
service: "api-gateway",
|
|
33
|
+
traces: [],
|
|
34
|
+
error: new globalThis.Error("Failed to fetch traces"),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Empty: Story = {
|
|
39
|
+
args: { service: "api-gateway", traces: [] },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const WithFilters: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
service: "api-gateway",
|
|
45
|
+
traces: mockTraceSummaries,
|
|
46
|
+
operations: ["GET /api/users", "POST /api/users"],
|
|
47
|
+
onSearch: (filters) => console.log("Search:", filters),
|
|
48
|
+
},
|
|
49
|
+
};
|