@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,66 @@
1
+ // Components
2
+ export { TabBar } from "./TabBar/index.js";
3
+ export type { TabBarProps, Tab } from "./TabBar/index.js";
4
+
5
+ export { ServiceList } from "./ServiceList/index.js";
6
+ export type { ServiceListProps, ServiceEntry } from "./ServiceList/index.js";
7
+
8
+ export { TraceSearch } from "./TraceSearch/index.js";
9
+ export type {
10
+ TraceSearchProps,
11
+ TraceSearchFilters,
12
+ TraceSummary,
13
+ } from "./TraceSearch/index.js";
14
+
15
+ export { TraceDetail } from "./TraceDetail/index.js";
16
+ export type { TraceDetailProps } from "./TraceDetail/index.js";
17
+
18
+ export { TraceTimeline } from "./TraceTimeline/index.js";
19
+ export type { TraceTimelineProps } from "./TraceTimeline/index.js";
20
+
21
+ export { LogTimeline } from "./LogTimeline/index.js";
22
+ export type { LogTimelineProps } from "./LogTimeline/index.js";
23
+
24
+ export { LogFilter } from "./LogTimeline/LogFilter.js";
25
+ export type { LogFilterProps } from "./LogTimeline/LogFilter.js";
26
+
27
+ export { MetricTimeSeries } from "./MetricTimeSeries/index.js";
28
+ export type {
29
+ MetricTimeSeriesProps,
30
+ ThresholdLine,
31
+ } from "./MetricTimeSeries/index.js";
32
+
33
+ export { MetricHistogram } from "./MetricHistogram/index.js";
34
+ export type { MetricHistogramProps } from "./MetricHistogram/index.js";
35
+
36
+ export { MetricStat } from "./MetricStat/index.js";
37
+ export type { MetricStatProps, ThresholdConfig } from "./MetricStat/index.js";
38
+
39
+ export { MetricTable } from "./MetricTable/index.js";
40
+ export type { MetricTableProps } from "./MetricTable/index.js";
41
+
42
+ export { RawDataTable } from "./RawDataTable/index.js";
43
+ export type { RawDataTableProps } from "./RawDataTable/index.js";
44
+
45
+ export { KeyboardShortcutsProvider } from "../KeyboardShortcuts/index.js";
46
+ export { ShortcutsHelpDialog } from "../KeyboardShortcuts/index.js";
47
+ export { useRegisterShortcuts } from "../KeyboardShortcuts/index.js";
48
+ export type {
49
+ KeyboardShortcut,
50
+ ShortcutGroup,
51
+ ShortcutsRegistry,
52
+ } from "../KeyboardShortcuts/index.js";
53
+
54
+ // Types
55
+ export type {
56
+ SpanNode,
57
+ SpanEvent,
58
+ SpanLink,
59
+ ParsedTrace,
60
+ LogEntry,
61
+ MetricDataPoint,
62
+ MetricSeries,
63
+ ParsedMetricGroup,
64
+ RawTableData,
65
+ RechartsDataPoint,
66
+ } from "./types.js";
@@ -0,0 +1,77 @@
1
+ import { useMemo } from "react";
2
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
3
+ import type { RendererComponentProps } from "../../../lib/renderer.js";
4
+ import type { MetricsDiscoveryResult } from "@kopai/sdk";
5
+
6
+ type Props = RendererComponentProps<
7
+ typeof observabilityCatalog.components.MetricDiscovery
8
+ >;
9
+
10
+ const TYPE_ORDER: Record<string, number> = {
11
+ Gauge: 0,
12
+ Sum: 1,
13
+ Histogram: 2,
14
+ ExponentialHistogram: 3,
15
+ Summary: 4,
16
+ };
17
+
18
+ export function OtelMetricDiscovery(props: Props) {
19
+ const data = props.hasData
20
+ ? (props.data as MetricsDiscoveryResult | null)
21
+ : null;
22
+ const loading = props.hasData ? props.loading : false;
23
+ const error = props.hasData ? props.error : null;
24
+
25
+ const sorted = useMemo(() => {
26
+ if (!data?.metrics) return [];
27
+ return [...data.metrics].sort(
28
+ (a, b) =>
29
+ a.name.localeCompare(b.name) ||
30
+ (TYPE_ORDER[a.type] ?? 99) - (TYPE_ORDER[b.type] ?? 99)
31
+ );
32
+ }, [data]);
33
+
34
+ if (loading && !sorted.length) {
35
+ return <p className="text-muted-foreground py-4">Loading metrics…</p>;
36
+ }
37
+ if (error) {
38
+ return <p className="text-red-400 py-4">Error: {error.message}</p>;
39
+ }
40
+ if (!sorted.length) {
41
+ return <p className="text-muted-foreground py-4">No metrics discovered.</p>;
42
+ }
43
+
44
+ return (
45
+ <div className="overflow-x-auto">
46
+ <table className="w-full text-sm text-left text-foreground border-collapse">
47
+ <thead className="text-xs uppercase text-muted-foreground border-b border-border">
48
+ <tr>
49
+ <th className="px-3 py-2">Name</th>
50
+ <th className="px-3 py-2">Type</th>
51
+ <th className="px-3 py-2">Unit</th>
52
+ <th className="px-3 py-2">Description</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ {sorted.map((m) => (
57
+ <tr
58
+ key={`${m.name}-${m.type}`}
59
+ className="border-b border-border/50 hover:bg-muted/40"
60
+ >
61
+ <td className="px-3 py-2 font-mono whitespace-nowrap">
62
+ {m.name}
63
+ </td>
64
+ <td className="px-3 py-2 text-muted-foreground">{m.type}</td>
65
+ <td className="px-3 py-2 text-muted-foreground">
66
+ {m.unit || "–"}
67
+ </td>
68
+ <td className="px-3 py-2 text-muted-foreground">
69
+ {m.description || "–"}
70
+ </td>
71
+ </tr>
72
+ ))}
73
+ </tbody>
74
+ </table>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,29 @@
1
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
2
+ import type { RendererComponentProps } from "../../../lib/renderer.js";
3
+ import { MetricHistogram } from "../index.js";
4
+ import type { denormalizedSignals } from "@kopai/core";
5
+
6
+ type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
7
+
8
+ type Props = RendererComponentProps<
9
+ typeof observabilityCatalog.components.MetricHistogram
10
+ >;
11
+
12
+ export function OtelMetricHistogram(props: Props) {
13
+ if (!props.hasData) {
14
+ return (
15
+ <div style={{ padding: 24, color: "var(--muted)" }}>No data source</div>
16
+ );
17
+ }
18
+
19
+ const response = props.data as { data?: OtelMetricsRow[] } | null;
20
+
21
+ return (
22
+ <MetricHistogram
23
+ rows={response?.data ?? []}
24
+ isLoading={props.loading}
25
+ error={props.error ?? undefined}
26
+ height={props.element.props.height ?? 400}
27
+ />
28
+ );
29
+ }
@@ -0,0 +1,44 @@
1
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
2
+ import type { RendererComponentProps } from "../../../lib/renderer.js";
3
+ import { MetricStat } from "../index.js";
4
+ import type { denormalizedSignals } from "@kopai/core";
5
+
6
+ type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
7
+
8
+ type Props = RendererComponentProps<
9
+ typeof observabilityCatalog.components.MetricStat
10
+ >;
11
+
12
+ function formatOtelValue(value: number, unit: string): string {
13
+ // OTel unit "1" = dimensionless ratio → show as percentage
14
+ if (unit === "1") return `${(value * 100).toFixed(1)}%`;
15
+ // OTel curly-brace units like "{request}" → strip braces
16
+ const cleanUnit = unit.replace(/^\{|\}$/g, "");
17
+ let formatted: string;
18
+ if (Math.abs(value) >= 1e6) formatted = `${(value / 1e6).toFixed(1)}M`;
19
+ else if (Math.abs(value) >= 1e3) formatted = `${(value / 1e3).toFixed(1)}K`;
20
+ else if (Number.isInteger(value)) formatted = value.toString();
21
+ else formatted = value.toFixed(2);
22
+ return cleanUnit ? `${formatted} ${cleanUnit}` : formatted;
23
+ }
24
+
25
+ export function OtelMetricStat(props: Props) {
26
+ if (!props.hasData) {
27
+ return (
28
+ <div style={{ padding: 24, color: "var(--muted)" }}>No data source</div>
29
+ );
30
+ }
31
+
32
+ const response = props.data as { data?: OtelMetricsRow[] } | null;
33
+
34
+ return (
35
+ <MetricStat
36
+ rows={response?.data ?? []}
37
+ isLoading={props.loading}
38
+ error={props.error ?? undefined}
39
+ label={props.element.props.label ?? undefined}
40
+ showSparkline={props.element.props.showSparkline ?? false}
41
+ formatValue={formatOtelValue}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,29 @@
1
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
2
+ import type { RendererComponentProps } from "../../../lib/renderer.js";
3
+ import { MetricTable } from "../index.js";
4
+ import type { denormalizedSignals } from "@kopai/core";
5
+
6
+ type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
7
+
8
+ type Props = RendererComponentProps<
9
+ typeof observabilityCatalog.components.MetricTable
10
+ >;
11
+
12
+ export function OtelMetricTable(props: Props) {
13
+ if (!props.hasData) {
14
+ return (
15
+ <div style={{ padding: 24, color: "var(--muted)" }}>No data source</div>
16
+ );
17
+ }
18
+
19
+ const response = props.data as { data?: OtelMetricsRow[] } | null;
20
+
21
+ return (
22
+ <MetricTable
23
+ rows={response?.data ?? []}
24
+ isLoading={props.loading}
25
+ error={props.error ?? undefined}
26
+ maxRows={props.element.props.maxRows ?? 100}
27
+ />
28
+ );
29
+ }
@@ -0,0 +1,30 @@
1
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
2
+ import type { RendererComponentProps } from "../../../lib/renderer.js";
3
+ import { MetricTimeSeries } from "../index.js";
4
+ import type { denormalizedSignals } from "@kopai/core";
5
+
6
+ type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
7
+
8
+ type Props = RendererComponentProps<
9
+ typeof observabilityCatalog.components.MetricTimeSeries
10
+ >;
11
+
12
+ export function OtelMetricTimeSeries(props: Props) {
13
+ if (!props.hasData) {
14
+ return (
15
+ <div style={{ padding: 24, color: "var(--muted)" }}>No data source</div>
16
+ );
17
+ }
18
+
19
+ const response = props.data as { data?: OtelMetricsRow[] } | null;
20
+
21
+ return (
22
+ <MetricTimeSeries
23
+ rows={response?.data ?? []}
24
+ isLoading={props.loading}
25
+ error={props.error ?? undefined}
26
+ height={props.element.props.height ?? 400}
27
+ showBrush={props.element.props.showBrush ?? true}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,5 @@
1
+ export { OtelMetricDiscovery } from "./OtelMetricDiscovery.js";
2
+ export { OtelMetricHistogram } from "./OtelMetricHistogram.js";
3
+ export { OtelMetricStat } from "./OtelMetricStat.js";
4
+ export { OtelMetricTable } from "./OtelMetricTable.js";
5
+ export { OtelMetricTimeSeries } from "./OtelMetricTimeSeries.js";
@@ -0,0 +1,43 @@
1
+ export function LoadingSkeleton() {
2
+ return (
3
+ <div className="flex flex-col h-full bg-background animate-pulse">
4
+ <div className="border-b border-border p-4">
5
+ <div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
6
+ <div className="flex gap-4">
7
+ <div className="h-3 bg-muted rounded w-32"></div>
8
+ <div className="h-3 bg-muted rounded w-24"></div>
9
+ <div className="h-3 bg-muted rounded w-20"></div>
10
+ </div>
11
+ </div>
12
+ <div className="flex-1 p-4 space-y-2">
13
+ {Array.from({ length: 15 }).map((_, i) => (
14
+ <div key={i} className="flex items-start gap-3">
15
+ <div className="h-4 bg-muted rounded w-32"></div>
16
+ <div
17
+ className="h-4 rounded w-16"
18
+ style={{
19
+ backgroundColor:
20
+ i % 4 === 0
21
+ ? "#ef4444"
22
+ : i % 4 === 1
23
+ ? "#f97316"
24
+ : i % 4 === 2
25
+ ? "#3b82f6"
26
+ : "#6b7280",
27
+ opacity: 0.3,
28
+ }}
29
+ ></div>
30
+ <div
31
+ className="h-4 bg-muted rounded"
32
+ style={{ width: `${80 + ((i * 7) % 40)}px` }}
33
+ ></div>
34
+ <div
35
+ className="h-4 bg-muted/80 rounded flex-1"
36
+ style={{ maxWidth: `${300 + ((i * 13) % 200)}px` }}
37
+ ></div>
38
+ </div>
39
+ ))}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Internal types for observability components.
3
+ * Components accept denormalized rows (OtelTracesRow, OtelLogsRow, OtelMetricsRow)
4
+ * from @kopai/core and transform to these internal types in useMemo.
5
+ */
6
+
7
+ // ── Trace types ──
8
+
9
+ export interface SpanNode {
10
+ spanId: string;
11
+ parentSpanId?: string;
12
+ traceId: string;
13
+ name: string;
14
+ startTimeUnixMs: number;
15
+ endTimeUnixMs: number;
16
+ durationMs: number;
17
+ kind: string;
18
+ status: string; // "UNSET" | "OK" | "ERROR"
19
+ statusMessage?: string;
20
+ serviceName: string;
21
+ attributes: Record<string, unknown>;
22
+ resourceAttributes: Record<string, unknown>;
23
+ events: SpanEvent[];
24
+ links: SpanLink[];
25
+ children: SpanNode[];
26
+ }
27
+
28
+ export interface SpanEvent {
29
+ timeUnixMs: number;
30
+ name: string;
31
+ attributes: Record<string, unknown>;
32
+ }
33
+
34
+ export interface SpanLink {
35
+ traceId: string;
36
+ spanId: string;
37
+ attributes: Record<string, unknown>;
38
+ }
39
+
40
+ export interface ParsedTrace {
41
+ traceId: string;
42
+ rootSpans: SpanNode[];
43
+ minTimeMs: number;
44
+ maxTimeMs: number;
45
+ totalSpanCount: number;
46
+ }
47
+
48
+ // ── Log types ──
49
+
50
+ export interface LogEntry {
51
+ logId: string;
52
+ timeUnixMs: number;
53
+ body: string;
54
+ severityText: string;
55
+ severityNumber: number;
56
+ serviceName: string;
57
+ traceId?: string;
58
+ spanId?: string;
59
+ attributes: Record<string, unknown>;
60
+ resourceAttributes: Record<string, unknown>;
61
+ scopeName?: string;
62
+ }
63
+
64
+ // ── Metric types ──
65
+
66
+ export type MetricType =
67
+ | "Gauge"
68
+ | "Sum"
69
+ | "Histogram"
70
+ | "ExponentialHistogram"
71
+ | "Summary";
72
+
73
+ export interface MetricSeries {
74
+ key: string;
75
+ labels: Record<string, string>;
76
+ dataPoints: MetricDataPoint[];
77
+ }
78
+
79
+ export interface MetricDataPoint {
80
+ timestamp: number;
81
+ value: number;
82
+ // Histogram-specific
83
+ count?: number;
84
+ sum?: number;
85
+ bucketCounts?: number[];
86
+ explicitBounds?: number[];
87
+ min?: number;
88
+ max?: number;
89
+ }
90
+
91
+ export interface ParsedMetricGroup {
92
+ name: string;
93
+ description: string;
94
+ unit: string;
95
+ type: MetricType;
96
+ series: MetricSeries[];
97
+ serviceName: string;
98
+ }
99
+
100
+ // ── RawDataTable types ──
101
+
102
+ export interface RawTableData {
103
+ columns: string[];
104
+ types: string[];
105
+ rows: unknown[][];
106
+ }
107
+
108
+ // ── Recharts types ──
109
+
110
+ export interface RechartsDataPoint {
111
+ timestamp: number;
112
+ [seriesKey: string]: number;
113
+ }
@@ -0,0 +1,17 @@
1
+ export function formatAttributeValue(value: unknown): string {
2
+ if (value === null || value === undefined) return "null";
3
+ if (typeof value === "string") return value;
4
+ if (typeof value === "boolean" || typeof value === "number")
5
+ return String(value);
6
+ if (Array.isArray(value) || typeof value === "object")
7
+ return JSON.stringify(value, null, 2);
8
+ return String(value);
9
+ }
10
+
11
+ export function isComplexValue(value: unknown): boolean {
12
+ return (
13
+ typeof value === "object" &&
14
+ value !== null &&
15
+ (Array.isArray(value) || Object.keys(value).length > 0)
16
+ );
17
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Color palette utilities for trace visualization
3
+ * Generates consistent colors for service names using HSL color space
4
+ */
5
+
6
+ function hashString(str: string): number {
7
+ let hash = 5381;
8
+ for (let i = 0; i < str.length; i++) {
9
+ hash = (hash << 5) + hash + str.charCodeAt(i);
10
+ }
11
+ return Math.abs(hash);
12
+ }
13
+
14
+ export function getServiceColor(serviceName: string): string {
15
+ const hash = hashString(serviceName);
16
+ const hue = hash % 360;
17
+ const saturation = 70;
18
+ const lightness = 50;
19
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
20
+ }
21
+
22
+ export const ERROR_COLOR = "#ef4444";
23
+
24
+ export function getSpanBarColor(serviceName: string, isError: boolean): string {
25
+ if (isError) {
26
+ return ERROR_COLOR;
27
+ }
28
+ return getServiceColor(serviceName);
29
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Tree flattening utilities for virtual scrolling
3
+ */
4
+
5
+ import type { SpanNode } from "../types.js";
6
+
7
+ export interface FlattenedSpan {
8
+ span: SpanNode;
9
+ level: number;
10
+ }
11
+
12
+ export function flattenTree(
13
+ rootSpans: SpanNode[],
14
+ collapsedIds: Set<string>
15
+ ): FlattenedSpan[] {
16
+ const result: FlattenedSpan[] = [];
17
+
18
+ function traverse(span: SpanNode, level: number) {
19
+ result.push({ span, level });
20
+ if (!collapsedIds.has(span.spanId)) {
21
+ span.children.forEach((child) => traverse(child, level + 1));
22
+ }
23
+ }
24
+
25
+ rootSpans.forEach((root) => traverse(root, 0));
26
+ return result;
27
+ }
28
+
29
+ export function getAllDescendantIds(span: SpanNode): string[] {
30
+ const ids: string[] = [span.spanId];
31
+
32
+ function traverse(s: SpanNode) {
33
+ s.children.forEach((child) => {
34
+ ids.push(child.spanId);
35
+ traverse(child);
36
+ });
37
+ }
38
+
39
+ traverse(span);
40
+ return ids;
41
+ }
42
+
43
+ export function getAllSpanIds(rootSpans: SpanNode[]): string[] {
44
+ const ids: string[] = [];
45
+
46
+ function traverse(span: SpanNode) {
47
+ ids.push(span.spanId);
48
+ span.children.forEach((child) => traverse(child));
49
+ }
50
+
51
+ rootSpans.forEach((root) => traverse(root));
52
+ return ids;
53
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Largest Triangle Three Buckets (LTTB) downsampling algorithm
3
+ */
4
+
5
+ export interface LTTBPoint {
6
+ x: number;
7
+ y: number;
8
+ }
9
+
10
+ function triangleArea(p1: LTTBPoint, p2: LTTBPoint, p3: LTTBPoint): number {
11
+ return (
12
+ Math.abs((p1.x - p3.x) * (p2.y - p1.y) - (p1.x - p2.x) * (p3.y - p1.y)) / 2
13
+ );
14
+ }
15
+
16
+ export function downsampleLTTB(
17
+ data: LTTBPoint[],
18
+ targetPoints: number
19
+ ): LTTBPoint[] {
20
+ if (data.length <= 2 || targetPoints >= data.length) {
21
+ return data.slice();
22
+ }
23
+ if (targetPoints <= 2) {
24
+ return [data[0]!, data[data.length - 1]!];
25
+ }
26
+
27
+ const sampled: LTTBPoint[] = [];
28
+ const bucketSize = (data.length - 2) / (targetPoints - 2);
29
+
30
+ const firstPoint = data[0];
31
+ if (!firstPoint) return data;
32
+ sampled.push(firstPoint);
33
+
34
+ let prevSelectedIndex = 0;
35
+
36
+ for (let i = 0; i < targetPoints - 2; i++) {
37
+ const bucketStart = Math.floor((i + 0) * bucketSize) + 1;
38
+ const bucketEnd = Math.min(
39
+ Math.floor((i + 1) * bucketSize) + 1,
40
+ data.length - 1
41
+ );
42
+
43
+ const nextBucketStart = Math.floor((i + 1) * bucketSize) + 1;
44
+ const nextBucketEnd = Math.min(
45
+ Math.floor((i + 2) * bucketSize) + 1,
46
+ data.length - 1
47
+ );
48
+
49
+ let avgX = 0;
50
+ let avgY = 0;
51
+ let nextBucketCount = 0;
52
+
53
+ for (let j = nextBucketStart; j < nextBucketEnd; j++) {
54
+ const point = data[j];
55
+ if (point) {
56
+ avgX += point.x;
57
+ avgY += point.y;
58
+ nextBucketCount++;
59
+ }
60
+ }
61
+
62
+ if (nextBucketCount > 0) {
63
+ avgX /= nextBucketCount;
64
+ avgY /= nextBucketCount;
65
+ } else {
66
+ const lastPoint = data[data.length - 1];
67
+ if (lastPoint) {
68
+ avgX = lastPoint.x;
69
+ avgY = lastPoint.y;
70
+ }
71
+ }
72
+
73
+ const avgPoint: LTTBPoint = { x: avgX, y: avgY };
74
+
75
+ let maxArea = -1;
76
+ let maxAreaIndex = bucketStart;
77
+
78
+ const prevPoint = data[prevSelectedIndex];
79
+ if (!prevPoint) continue;
80
+
81
+ for (let j = bucketStart; j < bucketEnd; j++) {
82
+ const currentPoint = data[j];
83
+ if (!currentPoint) continue;
84
+ const area = triangleArea(prevPoint, currentPoint, avgPoint);
85
+ if (area > maxArea) {
86
+ maxArea = area;
87
+ maxAreaIndex = j;
88
+ }
89
+ }
90
+
91
+ const selectedPoint = data[maxAreaIndex];
92
+ if (selectedPoint) {
93
+ sampled.push(selectedPoint);
94
+ }
95
+ prevSelectedIndex = maxAreaIndex;
96
+ }
97
+
98
+ const lastPoint = data[data.length - 1];
99
+ if (lastPoint) {
100
+ sampled.push(lastPoint);
101
+ }
102
+
103
+ return sampled;
104
+ }
105
+
106
+ export function downsampleTimeSeries<
107
+ T extends { timestamp: number; value: number },
108
+ >(data: T[], targetPoints: number): T[] {
109
+ if (targetPoints >= data.length) {
110
+ return data;
111
+ }
112
+
113
+ const points: LTTBPoint[] = data.map((d) => ({
114
+ x: d.timestamp,
115
+ y: d.value,
116
+ }));
117
+
118
+ const sampled = downsampleLTTB(points, targetPoints);
119
+ const sampledTimestamps = new Set(sampled.map((p) => p.x));
120
+ return data.filter((d) => sampledTimestamps.has(d.timestamp));
121
+ }