@kb-labs/studio-app 0.6.0 → 2.8.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/dist/index.html +1 -0
- package/dist/main.1d68e922.js +522 -0
- package/dist/main.1d68e922.js.map +1 -0
- package/dist/main.80b70197.css +2 -0
- package/dist/main.80b70197.css.map +1 -0
- package/dist/mf-manifest.json +241 -0
- package/dist/mf-stats.json +271 -0
- package/package.json +56 -39
- package/server.js +68 -0
- package/.env.example +0 -10
- package/UI-KIT-COMPONENTS.md +0 -783
- package/UI-KIT-PLAN.md +0 -326
- package/eslint-rules/no-direct-antd-import.js +0 -151
- package/eslint.config.ts +0 -27
- package/index.html +0 -17
- package/postcss.config.js +0 -7
- package/public/favicon.ico +0 -0
- package/rspack.config.mjs +0 -118
- package/src/App.tsx +0 -46
- package/src/components/devtools/devtools-log-feed.module.css +0 -165
- package/src/components/devtools/devtools-log-feed.tsx +0 -300
- package/src/components/devtools/devtools-panel.module.css +0 -121
- package/src/components/devtools/devtools-panel.tsx +0 -115
- package/src/components/empty-state.tsx +0 -28
- package/src/components/error-boundary.tsx +0 -210
- package/src/components/health-banner.module.css +0 -103
- package/src/components/health-banner.tsx +0 -134
- package/src/components/health-indicator.tsx +0 -12
- package/src/components/page-header.tsx +0 -49
- package/src/components/page-transition.tsx +0 -24
- package/src/components/role-switcher.tsx +0 -148
- package/src/components/run-badge.tsx +0 -13
- package/src/components/ui/icons.tsx +0 -509
- package/src/components/ui/index.ts +0 -25
- package/src/components/ui/kb-config-provider.tsx +0 -130
- package/src/components/ui/kb-header.module.css +0 -51
- package/src/components/ui/kb-header.tsx +0 -112
- package/src/components/ui/kb-notification-bell.module.css +0 -103
- package/src/components/ui/kb-notification-bell.tsx +0 -199
- package/src/components/ui/kb-page-layout.tsx +0 -66
- package/src/components/ui/kb-quick-search.module.css +0 -86
- package/src/components/ui/kb-quick-search.tsx +0 -196
- package/src/components/ui/kb-sidebar.module.css +0 -47
- package/src/components/ui/kb-sidebar.tsx +0 -196
- package/src/components/ui/kb-stat-card.module.css +0 -21
- package/src/components/ui/kb-stat-card.tsx +0 -22
- package/src/components/ui/kb-stat-grid.tsx +0 -22
- package/src/components/ui/kb-status-bar.module.css +0 -49
- package/src/components/ui/kb-status-bar.tsx +0 -145
- package/src/components/ui/kb-system-health-indicator.tsx +0 -229
- package/src/components/ui/kb-theme-toggle.tsx +0 -42
- package/src/components/ui/theme-adapter.ts +0 -812
- package/src/components/ui/theme-transition-overlay.tsx +0 -70
- package/src/components/ui/utils.ts +0 -6
- package/src/components/widget-error-boundary.tsx +0 -172
- package/src/config/env.ts +0 -5
- package/src/config/feature-flags.ts +0 -482
- package/src/config/studio.config.schema.ts +0 -88
- package/src/config/studio.config.ts +0 -84
- package/src/env.d.ts +0 -23
- package/src/hooks/use-command-palette.ts +0 -6
- package/src/hooks/use-devtools-store.ts +0 -25
- package/src/hooks/use-feature-flags.ts +0 -101
- package/src/index.css +0 -10
- package/src/layouts/dashboard-grid.tsx +0 -122
- package/src/layouts/index.ts +0 -12
- package/src/layouts/two-pane.tsx +0 -134
- package/src/main.tsx +0 -15
- package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +0 -257
- package/src/modules/analytics/components/analytics-date-range-picker.tsx +0 -121
- package/src/modules/analytics/pages/analytics-cache-page.tsx +0 -285
- package/src/modules/analytics/pages/analytics-embeddings-page.tsx +0 -274
- package/src/modules/analytics/pages/analytics-events-page.tsx +0 -425
- package/src/modules/analytics/pages/analytics-llm-page.tsx +0 -434
- package/src/modules/analytics/pages/analytics-overview-page.tsx +0 -336
- package/src/modules/analytics/pages/analytics-page.tsx +0 -20
- package/src/modules/analytics/pages/analytics-storage-page.tsx +0 -344
- package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +0 -279
- package/src/modules/analytics/routes/index.tsx +0 -133
- package/src/modules/analytics/utils/formatters.test.ts +0 -148
- package/src/modules/analytics/utils/formatters.ts +0 -100
- package/src/modules/auth/pages/login-page.module.css +0 -113
- package/src/modules/auth/pages/login-page.tsx +0 -80
- package/src/modules/auth/routes/index.tsx +0 -24
- package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +0 -199
- package/src/modules/dashboard/components/AiInsightsWidget.tsx +0 -358
- package/src/modules/dashboard/components/AnimatedMetricCard.css +0 -88
- package/src/modules/dashboard/components/AnimatedMetricCard.tsx +0 -125
- package/src/modules/dashboard/components/HeroMetricCard.css +0 -114
- package/src/modules/dashboard/components/HeroMetricCard.tsx +0 -152
- package/src/modules/dashboard/components/HeroMetricsWidget.tsx +0 -146
- package/src/modules/dashboard/components/SystemResourcesWidget.tsx +0 -197
- package/src/modules/dashboard/components/dashboard-empty.tsx +0 -22
- package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +0 -59
- package/src/modules/dashboard/components/dashboard-kpis.tsx +0 -13
- package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +0 -85
- package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +0 -78
- package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +0 -67
- package/src/modules/dashboard/components/dashboard-requests-widget.tsx +0 -62
- package/src/modules/dashboard/layouts/dashboard-layout.tsx +0 -16
- package/src/modules/dashboard/pages/ai-insights-page.tsx +0 -563
- package/src/modules/dashboard/pages/dashboard-animations.css +0 -317
- package/src/modules/dashboard/pages/dashboard-page.tsx +0 -13
- package/src/modules/dashboard/routes/index.tsx +0 -70
- package/src/modules/observability/pages/devkit-page.tsx +0 -263
- package/src/modules/observability/pages/incident-detail-page.tsx +0 -571
- package/src/modules/observability/pages/incidents-page.tsx +0 -370
- package/src/modules/observability/pages/log-detail-page.tsx +0 -508
- package/src/modules/observability/pages/logs-page.tsx +0 -1129
- package/src/modules/observability/pages/prometheus-metrics-page.tsx +0 -486
- package/src/modules/observability/pages/state-broker-page.tsx +0 -229
- package/src/modules/observability/pages/system-events-page.tsx +0 -255
- package/src/modules/observability/routes/index.tsx +0 -133
- package/src/modules/plugins/components/marketplace-card.tsx +0 -153
- package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +0 -374
- package/src/modules/plugins/components/plugin-card.tsx +0 -136
- package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +0 -101
- package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +0 -101
- package/src/modules/plugins/components/plugin-detail/index.ts +0 -7
- package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +0 -64
- package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +0 -30
- package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +0 -68
- package/src/modules/plugins/components/plugin-detail/permissions/index.ts +0 -6
- package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +0 -43
- package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +0 -242
- package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +0 -96
- package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +0 -55
- package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +0 -47
- package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +0 -169
- package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +0 -159
- package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +0 -40
- package/src/modules/plugins/components/plugin-grid.tsx +0 -20
- package/src/modules/plugins/components/plugin-table.tsx +0 -178
- package/src/modules/plugins/pages/marketplace-page.tsx +0 -200
- package/src/modules/plugins/pages/plugin-detail-page.tsx +0 -1145
- package/src/modules/plugins/routes/index.tsx +0 -48
- package/src/modules/plugins/utils/index.ts +0 -1
- package/src/modules/plugins/utils/schema-helpers.ts +0 -21
- package/src/modules/settings/components/api-routes-viewer.tsx +0 -415
- package/src/modules/settings/components/appearance-settings.tsx +0 -200
- package/src/modules/settings/components/configuration-settings.tsx +0 -321
- package/src/modules/settings/components/data-privacy-settings.tsx +0 -111
- package/src/modules/settings/components/experimental-settings.tsx +0 -243
- package/src/modules/settings/components/navigation-settings.tsx +0 -198
- package/src/modules/settings/components/notifications-settings.tsx +0 -40
- package/src/modules/settings/pages/settings-page.tsx +0 -298
- package/src/modules/settings/routes/index.tsx +0 -47
- package/src/pages/README-assistant.md +0 -91
- package/src/pages/login-page.tsx +0 -177
- package/src/pages/not-found-page.tsx +0 -85
- package/src/providers/auth-provider.tsx +0 -40
- package/src/providers/command-palette-provider.tsx +0 -193
- package/src/providers/data-sources-provider.tsx +0 -27
- package/src/providers/devtools-provider.tsx +0 -72
- package/src/providers/navigation-items-provider.tsx +0 -38
- package/src/providers/registry-v2-provider.tsx +0 -93
- package/src/providers/settings-provider.tsx +0 -158
- package/src/router.tsx +0 -302
- package/src/routes/helpers.tsx +0 -25
- package/src/routes/plugin-page-v2.tsx +0 -52
- package/src/routes/types.ts +0 -18
- package/src/styles/antd-overrides.css +0 -211
- package/src/styles/base.css +0 -190
- package/src/styles/settings.css +0 -75
- package/src/styles/variables.css +0 -94
- package/src/utils/analytics.ts +0 -183
- package/src/utils/apply-navigation-categories.ts +0 -83
- package/src/utils/commands.ts +0 -252
- package/src/utils/export.ts +0 -174
- package/src/utils/format-date.ts +0 -166
- package/src/utils/logger.ts +0 -110
- package/tailwind.config.ts +0 -13
- package/tsconfig.json +0 -13
- package/vitest-setup.ts +0 -1
- /package/{public → dist}/favicon.svg +0 -0
|
@@ -1,1129 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
UICard,
|
|
4
|
-
UIAlert,
|
|
5
|
-
UIBadge,
|
|
6
|
-
UITag,
|
|
7
|
-
UIList,
|
|
8
|
-
UIListItem,
|
|
9
|
-
UITypographyText,
|
|
10
|
-
UITypographyParagraph,
|
|
11
|
-
UISpace,
|
|
12
|
-
UIRow,
|
|
13
|
-
UICol,
|
|
14
|
-
UISelect,
|
|
15
|
-
UIButton,
|
|
16
|
-
UIAccordion,
|
|
17
|
-
UIDescriptions,
|
|
18
|
-
UIDescriptionsItem,
|
|
19
|
-
UIMessage,
|
|
20
|
-
UIModal,
|
|
21
|
-
UIInput,
|
|
22
|
-
UISegmented,
|
|
23
|
-
UIInputTextArea,
|
|
24
|
-
UIDivider,
|
|
25
|
-
UISpin,
|
|
26
|
-
UIIcon,
|
|
27
|
-
} from '@kb-labs/studio-ui-kit';
|
|
28
|
-
import { UIRangePicker, UICheckbox } from '@kb-labs/studio-ui-kit';
|
|
29
|
-
import { useState, useMemo, useEffect } from 'react';
|
|
30
|
-
import { useLogStream } from '@kb-labs/studio-data-client';
|
|
31
|
-
import { useDataSources } from '../../../providers/data-sources-provider';
|
|
32
|
-
import type { LogRecord, LogSummarizeResponse } from '@kb-labs/studio-data-client';
|
|
33
|
-
import type { Dayjs } from 'dayjs';
|
|
34
|
-
import dayjs from 'dayjs';
|
|
35
|
-
import { UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Time range presets for quick filtering
|
|
40
|
-
*/
|
|
41
|
-
const TIME_RANGES = {
|
|
42
|
-
'5m': { label: 'Last 5 minutes', minutes: 5 },
|
|
43
|
-
'15m': { label: 'Last 15 minutes', minutes: 15 },
|
|
44
|
-
'1h': { label: 'Last hour', minutes: 60 },
|
|
45
|
-
'6h': { label: 'Last 6 hours', minutes: 360 },
|
|
46
|
-
'24h': { label: 'Last 24 hours', minutes: 1440 },
|
|
47
|
-
custom: { label: 'Custom range', minutes: 0 },
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Quick question templates for AI summarization
|
|
52
|
-
*/
|
|
53
|
-
const QUESTION_TEMPLATES = [
|
|
54
|
-
{ label: 'What happened?', value: 'What happened in these logs?' },
|
|
55
|
-
{ label: 'Show errors', value: 'What errors occurred and what caused them?' },
|
|
56
|
-
{ label: 'Find patterns', value: 'Are there any patterns or repeated issues?' },
|
|
57
|
-
{ label: 'Performance issues', value: 'Are there any performance or latency issues?' },
|
|
58
|
-
{ label: 'Timeline', value: 'Give me a timeline of key events' },
|
|
59
|
-
{ label: 'Root cause', value: 'What is the root cause of the errors?' },
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Format timestamp to time string
|
|
64
|
-
*/
|
|
65
|
-
function formatTime(timestamp: string): string {
|
|
66
|
-
return new Date(timestamp).toLocaleTimeString();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Group logs by a field
|
|
71
|
-
*/
|
|
72
|
-
function groupLogsByField(logs: LogRecord[], field: keyof LogRecord): Map<string, LogRecord[]> {
|
|
73
|
-
const groups = new Map<string, LogRecord[]>();
|
|
74
|
-
for (const log of logs) {
|
|
75
|
-
const key = String(log[field] || 'unknown');
|
|
76
|
-
if (!groups.has(key)) {
|
|
77
|
-
groups.set(key, []);
|
|
78
|
-
}
|
|
79
|
-
groups.get(key)!.push(log);
|
|
80
|
-
}
|
|
81
|
-
return groups;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Calculate log statistics for AI summarization
|
|
86
|
-
*/
|
|
87
|
-
function calculateLogStats(logs: LogRecord[]) {
|
|
88
|
-
const stats = {
|
|
89
|
-
total: logs.length,
|
|
90
|
-
byLevel: {} as Record<string, number>,
|
|
91
|
-
byPlugin: {} as Record<string, number>,
|
|
92
|
-
errorCount: 0,
|
|
93
|
-
warningCount: 0,
|
|
94
|
-
uniqueTraces: new Set<string>(),
|
|
95
|
-
uniqueExecutions: new Set<string>(),
|
|
96
|
-
timeRange: {
|
|
97
|
-
oldest: logs.length > 0 ? logs[0]!.time : null,
|
|
98
|
-
newest: logs.length > 0 ? logs[logs.length - 1]!.time : null,
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
for (const log of logs) {
|
|
103
|
-
// Count by level
|
|
104
|
-
stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
|
|
105
|
-
|
|
106
|
-
// Count by plugin
|
|
107
|
-
if (log.plugin) {
|
|
108
|
-
stats.byPlugin[log.plugin] = (stats.byPlugin[log.plugin] || 0) + 1;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Track errors and warnings
|
|
112
|
-
if (log.level === 'error') {stats.errorCount++;}
|
|
113
|
-
if (log.level === 'warn') {stats.warningCount++;}
|
|
114
|
-
|
|
115
|
-
// Track unique traces and executions
|
|
116
|
-
if (log.trace) {stats.uniqueTraces.add(log.trace);}
|
|
117
|
-
if (log.executionId) {stats.uniqueExecutions.add(log.executionId);}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return stats;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Live Logs Page with AI Summarization support
|
|
125
|
-
*
|
|
126
|
-
* Features:
|
|
127
|
-
* - Real-time log streaming via SSE
|
|
128
|
-
* - Advanced filtering (time, level, plugin, trace)
|
|
129
|
-
* - Log grouping (by trace, execution, plugin)
|
|
130
|
-
* - Expandable log details
|
|
131
|
-
* - Copy traceId/executionId
|
|
132
|
-
* - Statistics for AI summarization
|
|
133
|
-
*/
|
|
134
|
-
export function LogsPage() {
|
|
135
|
-
const sources = useDataSources();
|
|
136
|
-
const { logs: streamLogs, isConnected, error, clearLogs } = useLogStream(sources.observability);
|
|
137
|
-
const [viewMode, setViewMode] = useState<'live' | 'history'>('live');
|
|
138
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
139
|
-
const [historyRefreshToken, setHistoryRefreshToken] = useState(0);
|
|
140
|
-
const [historyPage, setHistoryPage] = useState(1);
|
|
141
|
-
const [historyPageSize, setHistoryPageSize] = useState(50);
|
|
142
|
-
const [historyLogs, setHistoryLogs] = useState<LogRecord[]>([]);
|
|
143
|
-
const [historyTotal, setHistoryTotal] = useState(0);
|
|
144
|
-
const [historyLoading, setHistoryLoading] = useState(false);
|
|
145
|
-
const [historyError, setHistoryError] = useState<Error | null>(null);
|
|
146
|
-
const [historySource, setHistorySource] = useState<string | undefined>(undefined);
|
|
147
|
-
const [historyStats, setHistoryStats] = useState<any>(null);
|
|
148
|
-
|
|
149
|
-
// Pause/Resume state
|
|
150
|
-
const [isPaused, setIsPaused] = useState(false);
|
|
151
|
-
const [frozenLogs, setFrozenLogs] = useState<LogRecord[]>([]);
|
|
152
|
-
|
|
153
|
-
// Update frozen logs when not paused
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (!isPaused) {
|
|
156
|
-
setFrozenLogs(streamLogs);
|
|
157
|
-
}
|
|
158
|
-
}, [streamLogs, isPaused]);
|
|
159
|
-
|
|
160
|
-
// Filters
|
|
161
|
-
const [levelFilter, setLevelFilter] = useState<string | null>(null);
|
|
162
|
-
const [pluginFilter, setPluginFilter] = useState<string | null>(null);
|
|
163
|
-
const [timeRange, setTimeRange] = useState<string>('15m');
|
|
164
|
-
const [customTimeRange, setCustomTimeRange] = useState<[Dayjs, Dayjs] | null>(null);
|
|
165
|
-
const [groupBy, setGroupBy] = useState<'none' | 'trace' | 'execution' | 'plugin'>('none');
|
|
166
|
-
|
|
167
|
-
// AI Summarize Modal
|
|
168
|
-
const [summarizeModalOpen, setSummarizeModalOpen] = useState(false);
|
|
169
|
-
const [customQuestion, setCustomQuestion] = useState('');
|
|
170
|
-
const [includeContext, setIncludeContext] = useState({
|
|
171
|
-
errors: true,
|
|
172
|
-
warnings: true,
|
|
173
|
-
info: false,
|
|
174
|
-
metadata: true,
|
|
175
|
-
stackTraces: true,
|
|
176
|
-
});
|
|
177
|
-
const [summarizing, setSummarizing] = useState(false);
|
|
178
|
-
const [summaryResult, setSummaryResult] = useState<LogSummarizeResponse['data'] | null>(null);
|
|
179
|
-
|
|
180
|
-
useEffect(() => {
|
|
181
|
-
if (viewMode !== 'history') {return;}
|
|
182
|
-
setHistoryPage(1);
|
|
183
|
-
}, [viewMode, levelFilter, pluginFilter, timeRange, customTimeRange, searchQuery]);
|
|
184
|
-
|
|
185
|
-
// Query logs history from backend with server-side filtering/pagination/search
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (viewMode !== 'history') {return;}
|
|
188
|
-
|
|
189
|
-
let cancelled = false;
|
|
190
|
-
|
|
191
|
-
const fetchHistory = async () => {
|
|
192
|
-
setHistoryLoading(true);
|
|
193
|
-
setHistoryError(null);
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
const query: any = {
|
|
197
|
-
limit: historyPageSize,
|
|
198
|
-
offset: (historyPage - 1) * historyPageSize,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
if (levelFilter) {
|
|
202
|
-
query.level = levelFilter;
|
|
203
|
-
}
|
|
204
|
-
if (pluginFilter) {
|
|
205
|
-
query.plugin = pluginFilter;
|
|
206
|
-
}
|
|
207
|
-
if (searchQuery.trim()) {
|
|
208
|
-
query.search = searchQuery.trim();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (timeRange !== 'custom') {
|
|
212
|
-
const rangeMinutes = TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.minutes || 15;
|
|
213
|
-
query.from = dayjs().subtract(rangeMinutes, 'minutes').toISOString();
|
|
214
|
-
query.to = dayjs().toISOString();
|
|
215
|
-
} else if (customTimeRange) {
|
|
216
|
-
query.from = customTimeRange[0].toISOString();
|
|
217
|
-
query.to = customTimeRange[1].toISOString();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const result: any = await sources.observability.queryLogs(query);
|
|
221
|
-
const data = result?.logs ? result : result?.data ?? {};
|
|
222
|
-
|
|
223
|
-
if (cancelled) {return;}
|
|
224
|
-
|
|
225
|
-
const logs = Array.isArray(data.logs) ? data.logs : [];
|
|
226
|
-
setHistoryLogs(logs);
|
|
227
|
-
setHistoryTotal(typeof data.total === 'number' ? data.total : logs.length);
|
|
228
|
-
setHistorySource(typeof data.source === 'string' ? data.source : undefined);
|
|
229
|
-
setHistoryStats(data.stats ?? null);
|
|
230
|
-
} catch (err: any) {
|
|
231
|
-
if (cancelled) {return;}
|
|
232
|
-
setHistoryError(err instanceof Error ? err : new Error(String(err)));
|
|
233
|
-
setHistoryLogs([]);
|
|
234
|
-
setHistoryTotal(0);
|
|
235
|
-
} finally {
|
|
236
|
-
if (!cancelled) {
|
|
237
|
-
setHistoryLoading(false);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
void fetchHistory();
|
|
243
|
-
|
|
244
|
-
return () => {
|
|
245
|
-
cancelled = true;
|
|
246
|
-
};
|
|
247
|
-
}, [
|
|
248
|
-
viewMode,
|
|
249
|
-
historyPage,
|
|
250
|
-
historyPageSize,
|
|
251
|
-
levelFilter,
|
|
252
|
-
pluginFilter,
|
|
253
|
-
timeRange,
|
|
254
|
-
customTimeRange,
|
|
255
|
-
searchQuery,
|
|
256
|
-
historyRefreshToken,
|
|
257
|
-
sources.observability,
|
|
258
|
-
]);
|
|
259
|
-
|
|
260
|
-
// Calculate filtered logs
|
|
261
|
-
const logs = viewMode === 'history' ? historyLogs : (isPaused ? frozenLogs : streamLogs);
|
|
262
|
-
|
|
263
|
-
const filteredLogs = useMemo(() => {
|
|
264
|
-
let filtered = [...logs];
|
|
265
|
-
|
|
266
|
-
// In history mode level/plugin/time/search are applied on backend.
|
|
267
|
-
// In live mode apply all filters client-side.
|
|
268
|
-
if (viewMode !== 'history') {
|
|
269
|
-
if (levelFilter) {
|
|
270
|
-
filtered = filtered.filter((log) => log.level === levelFilter);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (pluginFilter) {
|
|
274
|
-
filtered = filtered.filter((log) => log.plugin === pluginFilter);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (timeRange !== 'custom') {
|
|
278
|
-
const rangeMinutes = TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.minutes || 15;
|
|
279
|
-
const cutoff = Date.now() - rangeMinutes * 60 * 1000;
|
|
280
|
-
filtered = filtered.filter((log) => new Date(log.time).getTime() >= cutoff);
|
|
281
|
-
} else if (customTimeRange) {
|
|
282
|
-
const [start, end] = customTimeRange;
|
|
283
|
-
filtered = filtered.filter((log) => {
|
|
284
|
-
const logTime = dayjs(log.time);
|
|
285
|
-
return logTime.isAfter(start) && logTime.isBefore(end);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (searchQuery.trim()) {
|
|
290
|
-
const query = searchQuery.trim().toLowerCase();
|
|
291
|
-
filtered = filtered.filter((log) => {
|
|
292
|
-
const haystack = [
|
|
293
|
-
String(log.msg ?? ''),
|
|
294
|
-
String(log.plugin ?? ''),
|
|
295
|
-
String(log.trace ?? (log as any).traceId ?? ''),
|
|
296
|
-
String(log.executionId ?? ''),
|
|
297
|
-
JSON.stringify(log.meta ?? {}),
|
|
298
|
-
].join(' ').toLowerCase();
|
|
299
|
-
return haystack.includes(query);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return filtered;
|
|
305
|
-
}, [logs, viewMode, levelFilter, pluginFilter, timeRange, customTimeRange, searchQuery]);
|
|
306
|
-
|
|
307
|
-
// Calculate statistics (for AI summarization)
|
|
308
|
-
const stats = useMemo(() => calculateLogStats(filteredLogs), [filteredLogs]);
|
|
309
|
-
|
|
310
|
-
// Get unique plugins for filter dropdown
|
|
311
|
-
const uniquePlugins = useMemo(() => {
|
|
312
|
-
return Array.from(new Set([...streamLogs, ...historyLogs].map((log) => log.plugin).filter(Boolean)));
|
|
313
|
-
}, [streamLogs, historyLogs]);
|
|
314
|
-
|
|
315
|
-
// Group logs if needed
|
|
316
|
-
const groupedLogs = useMemo(() => {
|
|
317
|
-
if (groupBy === 'none') {return null;}
|
|
318
|
-
|
|
319
|
-
switch (groupBy) {
|
|
320
|
-
case 'trace':
|
|
321
|
-
return groupLogsByField(filteredLogs, 'traceId');
|
|
322
|
-
case 'execution':
|
|
323
|
-
return groupLogsByField(filteredLogs, 'executionId');
|
|
324
|
-
case 'plugin':
|
|
325
|
-
return groupLogsByField(filteredLogs, 'plugin');
|
|
326
|
-
default:
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
}, [filteredLogs, groupBy]);
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Render single log item with expandable details
|
|
333
|
-
*/
|
|
334
|
-
const renderLogItem = (log: LogRecord, index: number) => {
|
|
335
|
-
// Build clean JSON representation for expansion
|
|
336
|
-
const logData = { ...log };
|
|
337
|
-
|
|
338
|
-
// Safe JSON stringify (handles circular refs and errors)
|
|
339
|
-
let jsonString: string;
|
|
340
|
-
try {
|
|
341
|
-
jsonString = JSON.stringify(logData, null, 2);
|
|
342
|
-
} catch {
|
|
343
|
-
// Fallback if JSON.stringify fails (circular refs, etc.)
|
|
344
|
-
jsonString = JSON.stringify(
|
|
345
|
-
{
|
|
346
|
-
...logData,
|
|
347
|
-
err: logData.err ? {
|
|
348
|
-
name: String(logData.err.name || 'Error'),
|
|
349
|
-
message: String(logData.err.message || ''),
|
|
350
|
-
stack: String(logData.err.stack || '').split('\n').slice(0, 10).join('\n'),
|
|
351
|
-
} : undefined,
|
|
352
|
-
},
|
|
353
|
-
null,
|
|
354
|
-
2
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const labelText = [
|
|
359
|
-
formatTime(log.time),
|
|
360
|
-
`[${log.level.toUpperCase()}]`,
|
|
361
|
-
log.plugin ? `[${log.plugin}]` : '',
|
|
362
|
-
String(log.msg || '(no message)'),
|
|
363
|
-
log.err ? `(${String(log.err.name || 'Error')})` : '',
|
|
364
|
-
].filter(Boolean).join(' ');
|
|
365
|
-
|
|
366
|
-
return (
|
|
367
|
-
<UIAccordion
|
|
368
|
-
ghost
|
|
369
|
-
key={`${log.time}-${index}`}
|
|
370
|
-
items={[
|
|
371
|
-
{
|
|
372
|
-
key: String(index),
|
|
373
|
-
label: labelText,
|
|
374
|
-
children: (
|
|
375
|
-
<div style={{ backgroundColor: '#fafafa', padding: 16, borderRadius: 4, marginLeft: -24, marginRight: -24 }}>
|
|
376
|
-
<UITypographyParagraph
|
|
377
|
-
copyable={{ text: jsonString }}
|
|
378
|
-
style={{
|
|
379
|
-
marginBottom: 0,
|
|
380
|
-
fontSize: 12,
|
|
381
|
-
fontFamily: 'monospace',
|
|
382
|
-
whiteSpace: 'pre-wrap',
|
|
383
|
-
maxHeight: 400,
|
|
384
|
-
overflow: 'auto',
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
{jsonString}
|
|
388
|
-
</UITypographyParagraph>
|
|
389
|
-
</div>
|
|
390
|
-
),
|
|
391
|
-
},
|
|
392
|
-
]}
|
|
393
|
-
/>
|
|
394
|
-
);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Render grouped logs
|
|
399
|
-
*/
|
|
400
|
-
const renderGroupedLogs = () => {
|
|
401
|
-
if (!groupedLogs) {return null;}
|
|
402
|
-
|
|
403
|
-
const groups = Array.from(groupedLogs.entries());
|
|
404
|
-
|
|
405
|
-
return (
|
|
406
|
-
<UIAccordion
|
|
407
|
-
items={groups.map(([key, groupLogs]) => ({
|
|
408
|
-
key,
|
|
409
|
-
label: `${key} (${groupLogs.length} logs)`,
|
|
410
|
-
children: (
|
|
411
|
-
<UIList
|
|
412
|
-
dataSource={groupLogs}
|
|
413
|
-
renderItem={(log, index) => (
|
|
414
|
-
<UIListItem style={{ padding: '8px 0' }}>{renderLogItem(log, index)}</UIListItem>
|
|
415
|
-
)}
|
|
416
|
-
/>
|
|
417
|
-
),
|
|
418
|
-
}))}
|
|
419
|
-
/>
|
|
420
|
-
);
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Handle AI Summarization
|
|
425
|
-
*/
|
|
426
|
-
const handleSummarize = async () => {
|
|
427
|
-
setSummarizing(true);
|
|
428
|
-
setSummaryResult(null);
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
// Build request
|
|
432
|
-
const request = {
|
|
433
|
-
question: customQuestion,
|
|
434
|
-
includeContext,
|
|
435
|
-
timeRange:
|
|
436
|
-
timeRange === 'custom' && customTimeRange
|
|
437
|
-
? {
|
|
438
|
-
from: customTimeRange[0].toISOString(),
|
|
439
|
-
to: customTimeRange[1].toISOString(),
|
|
440
|
-
}
|
|
441
|
-
: timeRange !== 'custom'
|
|
442
|
-
? {
|
|
443
|
-
from: dayjs()
|
|
444
|
-
.subtract(TIME_RANGES[timeRange as keyof typeof TIME_RANGES].minutes, 'minutes')
|
|
445
|
-
.toISOString(),
|
|
446
|
-
to: dayjs().toISOString(),
|
|
447
|
-
}
|
|
448
|
-
: undefined,
|
|
449
|
-
filters: {
|
|
450
|
-
level: levelFilter || undefined,
|
|
451
|
-
plugin: pluginFilter || undefined,
|
|
452
|
-
},
|
|
453
|
-
groupBy: groupBy !== 'none' ? groupBy : undefined,
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// Call API through data source
|
|
457
|
-
// Note: HttpClient auto-unwraps { ok: true, data: {...} } envelope
|
|
458
|
-
const result = await sources.observability.summarizeLogs(request);
|
|
459
|
-
|
|
460
|
-
// Store the full result data for structured rendering
|
|
461
|
-
setSummaryResult(result.data);
|
|
462
|
-
} catch (err: any) {
|
|
463
|
-
UIMessage.error('Failed to generate summary: ' + err.message);
|
|
464
|
-
} finally {
|
|
465
|
-
setSummarizing(false);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Open AI Summarize modal
|
|
471
|
-
*/
|
|
472
|
-
const openSummarizeModal = () => {
|
|
473
|
-
setSummarizeModalOpen(true);
|
|
474
|
-
setSummaryResult(null);
|
|
475
|
-
setCustomQuestion('');
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
return (
|
|
479
|
-
<UIPage width="full">
|
|
480
|
-
<UIPageHeader
|
|
481
|
-
title="Live Logs"
|
|
482
|
-
description="Real-time application logs with AI-ready filtering and grouping"
|
|
483
|
-
/>
|
|
484
|
-
|
|
485
|
-
{/* Connection Status */}
|
|
486
|
-
{!isConnected && !error && viewMode === 'live' && (
|
|
487
|
-
<UIAlert
|
|
488
|
-
message="Connecting to log stream..."
|
|
489
|
-
variant="info"
|
|
490
|
-
showIcon
|
|
491
|
-
icon={<UIIcon name="SyncOutlined" spin />}
|
|
492
|
-
style={{ marginBottom: 24 }}
|
|
493
|
-
/>
|
|
494
|
-
)}
|
|
495
|
-
|
|
496
|
-
{error && viewMode === 'live' && (
|
|
497
|
-
<UIAlert
|
|
498
|
-
message="Connection failed"
|
|
499
|
-
description={error.message + ' - Make sure REST API is running on localhost:5050'}
|
|
500
|
-
variant="error"
|
|
501
|
-
showIcon
|
|
502
|
-
style={{ marginBottom: 24 }}
|
|
503
|
-
/>
|
|
504
|
-
)}
|
|
505
|
-
|
|
506
|
-
{isConnected && viewMode === 'live' && (
|
|
507
|
-
<UIAlert
|
|
508
|
-
message="Connected to log stream"
|
|
509
|
-
variant="success"
|
|
510
|
-
showIcon
|
|
511
|
-
icon={<UIIcon name="CheckCircleOutlined" />}
|
|
512
|
-
style={{ marginBottom: 24 }}
|
|
513
|
-
/>
|
|
514
|
-
)}
|
|
515
|
-
|
|
516
|
-
{viewMode === 'history' && historyError && (
|
|
517
|
-
<UIAlert
|
|
518
|
-
message="Failed to load historical logs"
|
|
519
|
-
description={historyError.message}
|
|
520
|
-
variant="error"
|
|
521
|
-
showIcon
|
|
522
|
-
style={{ marginBottom: 16 }}
|
|
523
|
-
/>
|
|
524
|
-
)}
|
|
525
|
-
|
|
526
|
-
{viewMode === 'history' && (
|
|
527
|
-
<UIAlert
|
|
528
|
-
message={`Historical logs: ${historyTotal} total`}
|
|
529
|
-
description={
|
|
530
|
-
historySource
|
|
531
|
-
? `Source: ${historySource}${historyStats?.persistence ? `, persisted logs: ${historyStats.persistence.totalLogs}` : ''}`
|
|
532
|
-
: 'Showing stored logs with server-side filtering and search'
|
|
533
|
-
}
|
|
534
|
-
variant="info"
|
|
535
|
-
showIcon
|
|
536
|
-
style={{ marginBottom: 16 }}
|
|
537
|
-
/>
|
|
538
|
-
)}
|
|
539
|
-
|
|
540
|
-
{/* Filters & Stats (Collapsible) */}
|
|
541
|
-
<UICard style={{ marginBottom: 16 }}>
|
|
542
|
-
<UIAccordion
|
|
543
|
-
defaultActiveKey={['filters']}
|
|
544
|
-
ghost
|
|
545
|
-
items={[
|
|
546
|
-
{
|
|
547
|
-
key: 'filters',
|
|
548
|
-
label: `Filters & Statistics (${
|
|
549
|
-
viewMode === 'history'
|
|
550
|
-
? `${filteredLogs.length}/${historyTotal}`
|
|
551
|
-
: filteredLogs.length
|
|
552
|
-
} logs)`,
|
|
553
|
-
children: (
|
|
554
|
-
<>
|
|
555
|
-
<UIRow gutter={[16, 16]} style={{ marginBottom: 12 }}>
|
|
556
|
-
<UICol span={6}>
|
|
557
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
558
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
559
|
-
Mode
|
|
560
|
-
</UITypographyText>
|
|
561
|
-
<UISegmented
|
|
562
|
-
value={viewMode}
|
|
563
|
-
onChange={(value) => setViewMode(value as 'live' | 'history')}
|
|
564
|
-
options={[
|
|
565
|
-
{ label: 'Live', value: 'live' },
|
|
566
|
-
{ label: 'History', value: 'history' },
|
|
567
|
-
]}
|
|
568
|
-
/>
|
|
569
|
-
</UISpace>
|
|
570
|
-
</UICol>
|
|
571
|
-
|
|
572
|
-
<UICol span={18}>
|
|
573
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
574
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
575
|
-
<UIIcon name="SearchOutlined" /> Search
|
|
576
|
-
</UITypographyText>
|
|
577
|
-
<UIInput
|
|
578
|
-
value={searchQuery}
|
|
579
|
-
onChange={(value: string) => setSearchQuery(value)}
|
|
580
|
-
placeholder={viewMode === 'history'
|
|
581
|
-
? 'Search in history by message'
|
|
582
|
-
: 'Search in live list'}
|
|
583
|
-
allowClear
|
|
584
|
-
/>
|
|
585
|
-
</UISpace>
|
|
586
|
-
</UICol>
|
|
587
|
-
</UIRow>
|
|
588
|
-
|
|
589
|
-
<UIRow gutter={[16, 16]}>
|
|
590
|
-
<UICol span={6}>
|
|
591
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
592
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
593
|
-
<UIIcon name="FilterOutlined" /> Level
|
|
594
|
-
</UITypographyText>
|
|
595
|
-
<UISelect
|
|
596
|
-
style={{ width: '100%' }}
|
|
597
|
-
placeholder="All levels"
|
|
598
|
-
allowClear
|
|
599
|
-
value={levelFilter ?? undefined}
|
|
600
|
-
onChange={(v) => setLevelFilter(v as string | null)}
|
|
601
|
-
options={[
|
|
602
|
-
{ label: 'Trace', value: 'trace' },
|
|
603
|
-
{ label: 'Debug', value: 'debug' },
|
|
604
|
-
{ label: 'Info', value: 'info' },
|
|
605
|
-
{ label: 'Warning', value: 'warn' },
|
|
606
|
-
{ label: 'Error', value: 'error' },
|
|
607
|
-
]}
|
|
608
|
-
/>
|
|
609
|
-
</UISpace>
|
|
610
|
-
</UICol>
|
|
611
|
-
|
|
612
|
-
<UICol span={6}>
|
|
613
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
614
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
615
|
-
<UIIcon name="FilterOutlined" /> Plugin
|
|
616
|
-
</UITypographyText>
|
|
617
|
-
<UISelect
|
|
618
|
-
style={{ width: '100%' }}
|
|
619
|
-
placeholder="All plugins"
|
|
620
|
-
allowClear
|
|
621
|
-
value={pluginFilter ?? undefined}
|
|
622
|
-
onChange={(v) => setPluginFilter(v as string | null)}
|
|
623
|
-
options={uniquePlugins.map((plugin) => ({ label: String(plugin), value: String(plugin) }))}
|
|
624
|
-
/>
|
|
625
|
-
</UISpace>
|
|
626
|
-
</UICol>
|
|
627
|
-
|
|
628
|
-
<UICol span={6}>
|
|
629
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
630
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
631
|
-
<UIIcon name="ClockCircleOutlined" /> Time Range
|
|
632
|
-
</UITypographyText>
|
|
633
|
-
<UISelect
|
|
634
|
-
style={{ width: '100%' }}
|
|
635
|
-
value={timeRange}
|
|
636
|
-
onChange={(v) => setTimeRange(v as string)}
|
|
637
|
-
options={Object.entries(TIME_RANGES).map(([key, { label }]) => ({
|
|
638
|
-
label,
|
|
639
|
-
value: key,
|
|
640
|
-
}))}
|
|
641
|
-
/>
|
|
642
|
-
</UISpace>
|
|
643
|
-
</UICol>
|
|
644
|
-
|
|
645
|
-
<UICol span={6}>
|
|
646
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
647
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
648
|
-
<UIIcon name="GroupOutlined" /> Group By
|
|
649
|
-
</UITypographyText>
|
|
650
|
-
<UISelect
|
|
651
|
-
style={{ width: '100%' }}
|
|
652
|
-
value={groupBy}
|
|
653
|
-
onChange={(v) => setGroupBy(v as 'none' | 'trace' | 'plugin' | 'execution')}
|
|
654
|
-
options={[
|
|
655
|
-
{ label: 'No grouping', value: 'none' },
|
|
656
|
-
{ label: 'Trace ID', value: 'trace' },
|
|
657
|
-
{ label: 'Execution ID', value: 'execution' },
|
|
658
|
-
{ label: 'Plugin', value: 'plugin' },
|
|
659
|
-
]}
|
|
660
|
-
/>
|
|
661
|
-
</UISpace>
|
|
662
|
-
</UICol>
|
|
663
|
-
|
|
664
|
-
{timeRange === 'custom' && (
|
|
665
|
-
<UICol span={24}>
|
|
666
|
-
<UISpace direction="vertical" style={{ width: '100%' }}>
|
|
667
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
668
|
-
Custom Time Range
|
|
669
|
-
</UITypographyText>
|
|
670
|
-
<UIRangePicker
|
|
671
|
-
showTime
|
|
672
|
-
style={{ width: '100%' }}
|
|
673
|
-
onChange={(dates) =>
|
|
674
|
-
setCustomTimeRange(dates as [Dayjs, Dayjs] | null)
|
|
675
|
-
}
|
|
676
|
-
/>
|
|
677
|
-
</UISpace>
|
|
678
|
-
</UICol>
|
|
679
|
-
)}
|
|
680
|
-
</UIRow>
|
|
681
|
-
|
|
682
|
-
{/* Statistics (for AI Summarization) */}
|
|
683
|
-
<UIRow
|
|
684
|
-
gutter={16}
|
|
685
|
-
style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f0' }}
|
|
686
|
-
>
|
|
687
|
-
<UICol span={4}>
|
|
688
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
689
|
-
Total Logs
|
|
690
|
-
</UITypographyText>
|
|
691
|
-
<div>
|
|
692
|
-
<UITypographyText strong style={{ fontSize: 20 }}>
|
|
693
|
-
{stats.total}
|
|
694
|
-
</UITypographyText>
|
|
695
|
-
</div>
|
|
696
|
-
</UICol>
|
|
697
|
-
<UICol span={4}>
|
|
698
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
699
|
-
Errors
|
|
700
|
-
</UITypographyText>
|
|
701
|
-
<div>
|
|
702
|
-
<UITypographyText type="danger" strong style={{ fontSize: 20 }}>
|
|
703
|
-
{stats.errorCount}
|
|
704
|
-
</UITypographyText>
|
|
705
|
-
</div>
|
|
706
|
-
</UICol>
|
|
707
|
-
<UICol span={4}>
|
|
708
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
709
|
-
Warnings
|
|
710
|
-
</UITypographyText>
|
|
711
|
-
<div>
|
|
712
|
-
<UITypographyText type="warning" strong style={{ fontSize: 20 }}>
|
|
713
|
-
{stats.warningCount}
|
|
714
|
-
</UITypographyText>
|
|
715
|
-
</div>
|
|
716
|
-
</UICol>
|
|
717
|
-
<UICol span={4}>
|
|
718
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
719
|
-
Unique Traces
|
|
720
|
-
</UITypographyText>
|
|
721
|
-
<div>
|
|
722
|
-
<UITypographyText strong style={{ fontSize: 20 }}>
|
|
723
|
-
{stats.uniqueTraces.size}
|
|
724
|
-
</UITypographyText>
|
|
725
|
-
</div>
|
|
726
|
-
</UICol>
|
|
727
|
-
<UICol span={4}>
|
|
728
|
-
<UITypographyText type="secondary" style={{ fontSize: 12 }}>
|
|
729
|
-
Unique Executions
|
|
730
|
-
</UITypographyText>
|
|
731
|
-
<div>
|
|
732
|
-
<UITypographyText strong style={{ fontSize: 20 }}>
|
|
733
|
-
{stats.uniqueExecutions.size}
|
|
734
|
-
</UITypographyText>
|
|
735
|
-
</div>
|
|
736
|
-
</UICol>
|
|
737
|
-
<UICol span={4}>
|
|
738
|
-
<UIButton
|
|
739
|
-
variant="primary"
|
|
740
|
-
icon={<UIIcon name="RobotOutlined" />}
|
|
741
|
-
onClick={openSummarizeModal}
|
|
742
|
-
disabled={filteredLogs.length === 0}
|
|
743
|
-
>
|
|
744
|
-
AI Summarize
|
|
745
|
-
</UIButton>
|
|
746
|
-
</UICol>
|
|
747
|
-
</UIRow>
|
|
748
|
-
</>
|
|
749
|
-
),
|
|
750
|
-
},
|
|
751
|
-
]}
|
|
752
|
-
/>
|
|
753
|
-
</UICard>
|
|
754
|
-
|
|
755
|
-
{/* Logs List */}
|
|
756
|
-
<UICard
|
|
757
|
-
title={
|
|
758
|
-
<UISpace>
|
|
759
|
-
<span>{
|
|
760
|
-
groupBy === 'none'
|
|
761
|
-
? (viewMode === 'history' ? 'Historical Logs' : 'Recent Logs')
|
|
762
|
-
: `Logs grouped by ${groupBy}`
|
|
763
|
-
}</span>
|
|
764
|
-
<UIBadge
|
|
765
|
-
count={viewMode === 'history' ? historyTotal : filteredLogs.length}
|
|
766
|
-
showZero
|
|
767
|
-
style={{ backgroundColor: '#52c41a' }}
|
|
768
|
-
/>
|
|
769
|
-
</UISpace>
|
|
770
|
-
}
|
|
771
|
-
extra={
|
|
772
|
-
<UISpace>
|
|
773
|
-
{viewMode === 'live' ? (
|
|
774
|
-
<>
|
|
775
|
-
<UIButton
|
|
776
|
-
size="small"
|
|
777
|
-
icon={isPaused ? <UIIcon name="SyncOutlined" /> : <UIIcon name="ClockCircleOutlined" />}
|
|
778
|
-
onClick={() => setIsPaused(!isPaused)}
|
|
779
|
-
variant={isPaused ? 'primary' : 'default'}
|
|
780
|
-
>
|
|
781
|
-
{isPaused ? 'Resume' : 'Pause'}
|
|
782
|
-
</UIButton>
|
|
783
|
-
<UIButton size="small" onClick={clearLogs}>
|
|
784
|
-
Clear
|
|
785
|
-
</UIButton>
|
|
786
|
-
</>
|
|
787
|
-
) : (
|
|
788
|
-
<UIButton
|
|
789
|
-
size="small"
|
|
790
|
-
icon={<UIIcon name="ReloadOutlined" />}
|
|
791
|
-
loading={historyLoading}
|
|
792
|
-
onClick={() => setHistoryRefreshToken((v) => v + 1)}
|
|
793
|
-
>
|
|
794
|
-
Refresh
|
|
795
|
-
</UIButton>
|
|
796
|
-
)}
|
|
797
|
-
</UISpace>
|
|
798
|
-
}
|
|
799
|
-
>
|
|
800
|
-
{viewMode === 'history' && historyLoading ? (
|
|
801
|
-
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
|
802
|
-
<UISpin tip="Loading historical logs..." />
|
|
803
|
-
</div>
|
|
804
|
-
) : filteredLogs.length === 0 ? (
|
|
805
|
-
<UIAlert
|
|
806
|
-
message="No logs match current filters"
|
|
807
|
-
description={
|
|
808
|
-
viewMode === 'history'
|
|
809
|
-
? 'Adjust filters or search query for historical data'
|
|
810
|
-
: 'Adjust filters or wait for new logs to arrive'
|
|
811
|
-
}
|
|
812
|
-
variant="info"
|
|
813
|
-
showIcon
|
|
814
|
-
/>
|
|
815
|
-
) : groupBy === 'none' ? (
|
|
816
|
-
<div style={{ maxHeight: 600, overflow: 'auto' }}>
|
|
817
|
-
<UIList
|
|
818
|
-
dataSource={filteredLogs}
|
|
819
|
-
renderItem={(log, index) => (
|
|
820
|
-
<UIListItem style={{ padding: '8px 0' }}>{renderLogItem(log, index)}</UIListItem>
|
|
821
|
-
)}
|
|
822
|
-
pagination={
|
|
823
|
-
viewMode === 'history'
|
|
824
|
-
? {
|
|
825
|
-
current: historyPage,
|
|
826
|
-
total: historyTotal,
|
|
827
|
-
pageSize: historyPageSize,
|
|
828
|
-
showSizeChanger: true,
|
|
829
|
-
pageSizeOptions: ['25', '50', '100', '200'],
|
|
830
|
-
showTotal: (total: number, range: [number, number]) =>
|
|
831
|
-
`Showing ${range[0]}-${range[1]} of ${total} logs`,
|
|
832
|
-
onChange: (page: number, pageSize: number) => {
|
|
833
|
-
if (pageSize !== historyPageSize) {
|
|
834
|
-
setHistoryPageSize(pageSize);
|
|
835
|
-
setHistoryPage(1);
|
|
836
|
-
} else {
|
|
837
|
-
setHistoryPage(page);
|
|
838
|
-
}
|
|
839
|
-
},
|
|
840
|
-
}
|
|
841
|
-
: filteredLogs.length > 50
|
|
842
|
-
? {
|
|
843
|
-
pageSize: 50,
|
|
844
|
-
showSizeChanger: true,
|
|
845
|
-
showTotal: (total: number) => `Total ${total} logs`,
|
|
846
|
-
pageSizeOptions: ['25', '50', '100', '200'],
|
|
847
|
-
}
|
|
848
|
-
: false
|
|
849
|
-
}
|
|
850
|
-
/>
|
|
851
|
-
</div>
|
|
852
|
-
) : (
|
|
853
|
-
renderGroupedLogs()
|
|
854
|
-
)}
|
|
855
|
-
</UICard>
|
|
856
|
-
|
|
857
|
-
{/* AI Summarize Modal */}
|
|
858
|
-
<UIModal
|
|
859
|
-
title={
|
|
860
|
-
<UISpace>
|
|
861
|
-
<UIIcon name="RobotOutlined" />
|
|
862
|
-
<span>AI Log Summarization</span>
|
|
863
|
-
</UISpace>
|
|
864
|
-
}
|
|
865
|
-
open={summarizeModalOpen}
|
|
866
|
-
onCancel={() => setSummarizeModalOpen(false)}
|
|
867
|
-
width={800}
|
|
868
|
-
footer={[
|
|
869
|
-
<UIButton key="cancel" onClick={() => setSummarizeModalOpen(false)}>
|
|
870
|
-
Cancel
|
|
871
|
-
</UIButton>,
|
|
872
|
-
<UIButton
|
|
873
|
-
key="summarize"
|
|
874
|
-
variant="primary"
|
|
875
|
-
icon={<UIIcon name="ThunderboltOutlined" />}
|
|
876
|
-
loading={summarizing}
|
|
877
|
-
onClick={handleSummarize}
|
|
878
|
-
disabled={!customQuestion.trim()}
|
|
879
|
-
>
|
|
880
|
-
Generate Summary
|
|
881
|
-
</UIButton>,
|
|
882
|
-
]}
|
|
883
|
-
>
|
|
884
|
-
<UISpace direction="vertical" style={{ width: '100%' }} size="large">
|
|
885
|
-
{/* Data Preview */}
|
|
886
|
-
<UICard size="small" style={{ backgroundColor: '#fafafa' }}>
|
|
887
|
-
<UIRow gutter={16}>
|
|
888
|
-
<UICol span={6}>
|
|
889
|
-
<UITypographyText type="secondary">Logs to analyze:</UITypographyText>
|
|
890
|
-
<div>
|
|
891
|
-
<UITypographyText strong>{filteredLogs.length}</UITypographyText>
|
|
892
|
-
</div>
|
|
893
|
-
</UICol>
|
|
894
|
-
<UICol span={6}>
|
|
895
|
-
<UITypographyText type="secondary">Errors:</UITypographyText>
|
|
896
|
-
<div>
|
|
897
|
-
<UITypographyText type="danger" strong>
|
|
898
|
-
{stats.errorCount}
|
|
899
|
-
</UITypographyText>
|
|
900
|
-
</div>
|
|
901
|
-
</UICol>
|
|
902
|
-
<UICol span={6}>
|
|
903
|
-
<UITypographyText type="secondary">Warnings:</UITypographyText>
|
|
904
|
-
<div>
|
|
905
|
-
<UITypographyText type="warning" strong>
|
|
906
|
-
{stats.warningCount}
|
|
907
|
-
</UITypographyText>
|
|
908
|
-
</div>
|
|
909
|
-
</UICol>
|
|
910
|
-
<UICol span={6}>
|
|
911
|
-
<UITypographyText type="secondary">Time range:</UITypographyText>
|
|
912
|
-
<div>
|
|
913
|
-
<UITypographyText strong>{TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.label}</UITypographyText>
|
|
914
|
-
</div>
|
|
915
|
-
</UICol>
|
|
916
|
-
</UIRow>
|
|
917
|
-
</UICard>
|
|
918
|
-
|
|
919
|
-
{/* Quick Templates */}
|
|
920
|
-
<div>
|
|
921
|
-
<UITypographyText strong>Quick Questions:</UITypographyText>
|
|
922
|
-
<div style={{ marginTop: 8 }}>
|
|
923
|
-
<UISpace wrap>
|
|
924
|
-
{QUESTION_TEMPLATES.map((template) => (
|
|
925
|
-
<UIButton
|
|
926
|
-
key={template.label}
|
|
927
|
-
size="small"
|
|
928
|
-
onClick={() => setCustomQuestion(template.value)}
|
|
929
|
-
>
|
|
930
|
-
{template.label}
|
|
931
|
-
</UIButton>
|
|
932
|
-
))}
|
|
933
|
-
</UISpace>
|
|
934
|
-
</div>
|
|
935
|
-
</div>
|
|
936
|
-
|
|
937
|
-
<UIDivider style={{ margin: '8px 0' }} />
|
|
938
|
-
|
|
939
|
-
{/* Custom Question */}
|
|
940
|
-
<div>
|
|
941
|
-
<UITypographyText strong>Your Question:</UITypographyText>
|
|
942
|
-
<UIInputTextArea
|
|
943
|
-
value={customQuestion}
|
|
944
|
-
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setCustomQuestion(e.target.value)}
|
|
945
|
-
placeholder="Ask anything about these logs... (e.g., 'What went wrong in the last 5 minutes?')"
|
|
946
|
-
rows={3}
|
|
947
|
-
style={{ marginTop: 8 }}
|
|
948
|
-
/>
|
|
949
|
-
</div>
|
|
950
|
-
|
|
951
|
-
{/* Context Options */}
|
|
952
|
-
<div>
|
|
953
|
-
<UITypographyText strong>Include in context:</UITypographyText>
|
|
954
|
-
<div style={{ marginTop: 8 }}>
|
|
955
|
-
<UISpace direction="vertical">
|
|
956
|
-
<UICheckbox
|
|
957
|
-
checked={includeContext.errors}
|
|
958
|
-
onChange={(checked) =>
|
|
959
|
-
setIncludeContext({ ...includeContext, errors: checked })
|
|
960
|
-
}
|
|
961
|
-
>
|
|
962
|
-
Error messages and stack traces
|
|
963
|
-
</UICheckbox>
|
|
964
|
-
<UICheckbox
|
|
965
|
-
checked={includeContext.warnings}
|
|
966
|
-
onChange={(checked) =>
|
|
967
|
-
setIncludeContext({ ...includeContext, warnings: checked })
|
|
968
|
-
}
|
|
969
|
-
>
|
|
970
|
-
Warning messages
|
|
971
|
-
</UICheckbox>
|
|
972
|
-
<UICheckbox
|
|
973
|
-
checked={includeContext.info}
|
|
974
|
-
onChange={(checked) =>
|
|
975
|
-
setIncludeContext({ ...includeContext, info: checked })
|
|
976
|
-
}
|
|
977
|
-
>
|
|
978
|
-
Info-level logs
|
|
979
|
-
</UICheckbox>
|
|
980
|
-
<UICheckbox
|
|
981
|
-
checked={includeContext.metadata}
|
|
982
|
-
onChange={(checked) =>
|
|
983
|
-
setIncludeContext({ ...includeContext, metadata: checked })
|
|
984
|
-
}
|
|
985
|
-
>
|
|
986
|
-
Metadata (traceId, executionId, etc.)
|
|
987
|
-
</UICheckbox>
|
|
988
|
-
<UICheckbox
|
|
989
|
-
checked={includeContext.stackTraces}
|
|
990
|
-
onChange={(checked) =>
|
|
991
|
-
setIncludeContext({ ...includeContext, stackTraces: checked })
|
|
992
|
-
}
|
|
993
|
-
>
|
|
994
|
-
Full stack traces
|
|
995
|
-
</UICheckbox>
|
|
996
|
-
</UISpace>
|
|
997
|
-
</div>
|
|
998
|
-
</div>
|
|
999
|
-
|
|
1000
|
-
{/* Summary Result */}
|
|
1001
|
-
{summarizing && (
|
|
1002
|
-
<div style={{ textAlign: 'center', padding: '24px 0' }}>
|
|
1003
|
-
<UISpin tip="Analyzing logs with AI..." size="large" />
|
|
1004
|
-
</div>
|
|
1005
|
-
)}
|
|
1006
|
-
|
|
1007
|
-
{summaryResult && !summarizing && (
|
|
1008
|
-
<UISpace direction="vertical" size="middle" style={{ width: '100%', marginTop: 16 }}>
|
|
1009
|
-
{/* Statistics Card */}
|
|
1010
|
-
<UICard
|
|
1011
|
-
title={
|
|
1012
|
-
<UISpace>
|
|
1013
|
-
<UIIcon name="InfoCircleOutlined" />
|
|
1014
|
-
<span>Statistics</span>
|
|
1015
|
-
</UISpace>
|
|
1016
|
-
}
|
|
1017
|
-
size="small"
|
|
1018
|
-
>
|
|
1019
|
-
<UIDescriptions column={2} size="small">
|
|
1020
|
-
<UIDescriptionsItem label="Total Logs">{summaryResult.summary.total}</UIDescriptionsItem>
|
|
1021
|
-
<UIDescriptionsItem label="Time Range">
|
|
1022
|
-
{summaryResult.summary.timeRange.from && summaryResult.summary.timeRange.to
|
|
1023
|
-
? `${summaryResult.summary.timeRange.from} to ${summaryResult.summary.timeRange.to}`
|
|
1024
|
-
: 'All time'}
|
|
1025
|
-
</UIDescriptionsItem>
|
|
1026
|
-
</UIDescriptions>
|
|
1027
|
-
|
|
1028
|
-
{/* Level breakdown */}
|
|
1029
|
-
{summaryResult.summary.stats?.byLevel && Object.keys(summaryResult.summary.stats.byLevel).length > 0 && (
|
|
1030
|
-
<>
|
|
1031
|
-
<UIDivider style={{ margin: '12px 0' }} />
|
|
1032
|
-
<UITypographyText strong>By Level:</UITypographyText>
|
|
1033
|
-
<div style={{ marginTop: 8 }}>
|
|
1034
|
-
<UISpace wrap>
|
|
1035
|
-
{Object.entries(summaryResult.summary.stats.byLevel).map(([level, count]) => (
|
|
1036
|
-
<UITag key={level} color={
|
|
1037
|
-
level === 'error' ? 'red' :
|
|
1038
|
-
level === 'warn' ? 'orange' :
|
|
1039
|
-
level === 'info' ? 'blue' :
|
|
1040
|
-
'default'
|
|
1041
|
-
}>
|
|
1042
|
-
{level}: {count}
|
|
1043
|
-
</UITag>
|
|
1044
|
-
))}
|
|
1045
|
-
</UISpace>
|
|
1046
|
-
</div>
|
|
1047
|
-
</>
|
|
1048
|
-
)}
|
|
1049
|
-
|
|
1050
|
-
{/* Plugin breakdown */}
|
|
1051
|
-
{summaryResult.summary.stats?.byPlugin && Object.keys(summaryResult.summary.stats.byPlugin).length > 0 && (
|
|
1052
|
-
<>
|
|
1053
|
-
<UIDivider style={{ margin: '12px 0' }} />
|
|
1054
|
-
<UITypographyText strong>By Plugin (Top 5):</UITypographyText>
|
|
1055
|
-
<div style={{ marginTop: 8 }}>
|
|
1056
|
-
<UISpace wrap>
|
|
1057
|
-
{Object.entries(summaryResult.summary.stats.byPlugin)
|
|
1058
|
-
.sort((a: any, b: any) => b[1] - a[1])
|
|
1059
|
-
.slice(0, 5)
|
|
1060
|
-
.map(([plugin, count]) => (
|
|
1061
|
-
<UITag key={plugin}>{plugin}: {count}</UITag>
|
|
1062
|
-
))}
|
|
1063
|
-
</UISpace>
|
|
1064
|
-
</div>
|
|
1065
|
-
</>
|
|
1066
|
-
)}
|
|
1067
|
-
</UICard>
|
|
1068
|
-
|
|
1069
|
-
{/* Top Errors Card */}
|
|
1070
|
-
{summaryResult.summary.stats?.topErrors && summaryResult.summary.stats.topErrors.length > 0 && (
|
|
1071
|
-
<UICard
|
|
1072
|
-
title={
|
|
1073
|
-
<UISpace>
|
|
1074
|
-
<UIIcon name="BugOutlined" />
|
|
1075
|
-
<span>Top Errors</span>
|
|
1076
|
-
</UISpace>
|
|
1077
|
-
}
|
|
1078
|
-
size="small"
|
|
1079
|
-
>
|
|
1080
|
-
<UIList
|
|
1081
|
-
size="small"
|
|
1082
|
-
dataSource={summaryResult.summary.stats.topErrors.slice(0, 5)}
|
|
1083
|
-
renderItem={(err: any, idx: number) => (
|
|
1084
|
-
<UIListItem>
|
|
1085
|
-
<UITypographyText>
|
|
1086
|
-
<UITypographyText type="secondary">{idx + 1}.</UITypographyText>{' '}
|
|
1087
|
-
<UITypographyText code>{err.message}</UITypographyText>{' '}
|
|
1088
|
-
<UITypographyText type="secondary">({err.count} times)</UITypographyText>
|
|
1089
|
-
</UITypographyText>
|
|
1090
|
-
</UIListItem>
|
|
1091
|
-
)}
|
|
1092
|
-
/>
|
|
1093
|
-
</UICard>
|
|
1094
|
-
)}
|
|
1095
|
-
|
|
1096
|
-
{/* AI Analysis Card */}
|
|
1097
|
-
{summaryResult.aiSummary && (
|
|
1098
|
-
<UIAlert
|
|
1099
|
-
variant="info"
|
|
1100
|
-
showIcon
|
|
1101
|
-
icon={<UIIcon name="RobotOutlined" />}
|
|
1102
|
-
message="AI Analysis"
|
|
1103
|
-
description={
|
|
1104
|
-
<UISpace direction="vertical" size="small" style={{ width: '100%' }}>
|
|
1105
|
-
{summaryResult.aiSummary.split('\n\n').map((paragraph, idx) => (
|
|
1106
|
-
<UITypographyParagraph key={idx} style={{ marginBottom: 0 }}>
|
|
1107
|
-
{paragraph}
|
|
1108
|
-
</UITypographyParagraph>
|
|
1109
|
-
))}
|
|
1110
|
-
</UISpace>
|
|
1111
|
-
}
|
|
1112
|
-
/>
|
|
1113
|
-
)}
|
|
1114
|
-
|
|
1115
|
-
{/* Info message if AI not available */}
|
|
1116
|
-
{!summaryResult.aiSummary && summaryResult.message && (
|
|
1117
|
-
<UIAlert
|
|
1118
|
-
message={summaryResult.message}
|
|
1119
|
-
variant="info"
|
|
1120
|
-
showIcon
|
|
1121
|
-
/>
|
|
1122
|
-
)}
|
|
1123
|
-
</UISpace>
|
|
1124
|
-
)}
|
|
1125
|
-
</UISpace>
|
|
1126
|
-
</UIModal>
|
|
1127
|
-
</UIPage>
|
|
1128
|
-
);
|
|
1129
|
-
}
|