@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,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
|
+
}
|