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