@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,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,6 @@
1
+ import type { ShortcutGroup } from "../../KeyboardShortcuts/types.js";
2
+
3
+ export const SERVICES_SHORTCUTS: ShortcutGroup = {
4
+ name: "Services",
5
+ shortcuts: [{ keys: ["Backspace"], description: "Go back" }],
6
+ };
@@ -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
+ };