@kb-labs/studio-app 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.env.example +10 -0
  2. package/README.md +74 -0
  3. package/UI-KIT-COMPONENTS.md +783 -0
  4. package/UI-KIT-PLAN.md +326 -0
  5. package/eslint-rules/no-direct-antd-import.js +151 -0
  6. package/eslint.config.ts +27 -0
  7. package/index.html +17 -0
  8. package/package.json +72 -0
  9. package/postcss.config.js +7 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/favicon.svg +7 -0
  12. package/rspack.config.mjs +118 -0
  13. package/src/App.tsx +46 -0
  14. package/src/components/devtools/devtools-log-feed.module.css +165 -0
  15. package/src/components/devtools/devtools-log-feed.tsx +300 -0
  16. package/src/components/devtools/devtools-panel.module.css +121 -0
  17. package/src/components/devtools/devtools-panel.tsx +115 -0
  18. package/src/components/empty-state.tsx +28 -0
  19. package/src/components/error-boundary.tsx +210 -0
  20. package/src/components/health-banner.module.css +103 -0
  21. package/src/components/health-banner.tsx +134 -0
  22. package/src/components/health-indicator.tsx +12 -0
  23. package/src/components/page-header.tsx +49 -0
  24. package/src/components/page-transition.tsx +24 -0
  25. package/src/components/role-switcher.tsx +148 -0
  26. package/src/components/run-badge.tsx +13 -0
  27. package/src/components/ui/icons.tsx +509 -0
  28. package/src/components/ui/index.ts +25 -0
  29. package/src/components/ui/kb-config-provider.tsx +130 -0
  30. package/src/components/ui/kb-header.module.css +51 -0
  31. package/src/components/ui/kb-header.tsx +112 -0
  32. package/src/components/ui/kb-notification-bell.module.css +103 -0
  33. package/src/components/ui/kb-notification-bell.tsx +199 -0
  34. package/src/components/ui/kb-page-layout.tsx +66 -0
  35. package/src/components/ui/kb-quick-search.module.css +86 -0
  36. package/src/components/ui/kb-quick-search.tsx +196 -0
  37. package/src/components/ui/kb-sidebar.module.css +47 -0
  38. package/src/components/ui/kb-sidebar.tsx +196 -0
  39. package/src/components/ui/kb-stat-card.module.css +21 -0
  40. package/src/components/ui/kb-stat-card.tsx +22 -0
  41. package/src/components/ui/kb-stat-grid.tsx +22 -0
  42. package/src/components/ui/kb-status-bar.module.css +49 -0
  43. package/src/components/ui/kb-status-bar.tsx +145 -0
  44. package/src/components/ui/kb-system-health-indicator.tsx +229 -0
  45. package/src/components/ui/kb-theme-toggle.tsx +42 -0
  46. package/src/components/ui/theme-adapter.ts +812 -0
  47. package/src/components/ui/theme-transition-overlay.tsx +70 -0
  48. package/src/components/ui/utils.ts +6 -0
  49. package/src/components/widget-error-boundary.tsx +172 -0
  50. package/src/config/env.ts +5 -0
  51. package/src/config/feature-flags.ts +482 -0
  52. package/src/config/studio.config.schema.ts +88 -0
  53. package/src/config/studio.config.ts +84 -0
  54. package/src/env.d.ts +23 -0
  55. package/src/hooks/use-command-palette.ts +6 -0
  56. package/src/hooks/use-devtools-store.ts +25 -0
  57. package/src/hooks/use-feature-flags.ts +101 -0
  58. package/src/index.css +10 -0
  59. package/src/layouts/dashboard-grid.tsx +122 -0
  60. package/src/layouts/index.ts +12 -0
  61. package/src/layouts/two-pane.tsx +134 -0
  62. package/src/main.tsx +15 -0
  63. package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +257 -0
  64. package/src/modules/analytics/components/analytics-date-range-picker.tsx +121 -0
  65. package/src/modules/analytics/pages/analytics-cache-page.tsx +285 -0
  66. package/src/modules/analytics/pages/analytics-embeddings-page.tsx +274 -0
  67. package/src/modules/analytics/pages/analytics-events-page.tsx +425 -0
  68. package/src/modules/analytics/pages/analytics-llm-page.tsx +434 -0
  69. package/src/modules/analytics/pages/analytics-overview-page.tsx +336 -0
  70. package/src/modules/analytics/pages/analytics-page.tsx +20 -0
  71. package/src/modules/analytics/pages/analytics-storage-page.tsx +344 -0
  72. package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +279 -0
  73. package/src/modules/analytics/routes/index.tsx +133 -0
  74. package/src/modules/analytics/utils/formatters.test.ts +148 -0
  75. package/src/modules/analytics/utils/formatters.ts +100 -0
  76. package/src/modules/auth/pages/login-page.module.css +113 -0
  77. package/src/modules/auth/pages/login-page.tsx +80 -0
  78. package/src/modules/auth/routes/index.tsx +24 -0
  79. package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +199 -0
  80. package/src/modules/dashboard/components/AiInsightsWidget.tsx +358 -0
  81. package/src/modules/dashboard/components/AnimatedMetricCard.css +88 -0
  82. package/src/modules/dashboard/components/AnimatedMetricCard.tsx +125 -0
  83. package/src/modules/dashboard/components/HeroMetricCard.css +114 -0
  84. package/src/modules/dashboard/components/HeroMetricCard.tsx +152 -0
  85. package/src/modules/dashboard/components/HeroMetricsWidget.tsx +146 -0
  86. package/src/modules/dashboard/components/SystemResourcesWidget.tsx +197 -0
  87. package/src/modules/dashboard/components/dashboard-empty.tsx +22 -0
  88. package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +59 -0
  89. package/src/modules/dashboard/components/dashboard-kpis.tsx +13 -0
  90. package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +85 -0
  91. package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +78 -0
  92. package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +67 -0
  93. package/src/modules/dashboard/components/dashboard-requests-widget.tsx +62 -0
  94. package/src/modules/dashboard/layouts/dashboard-layout.tsx +16 -0
  95. package/src/modules/dashboard/pages/ai-insights-page.tsx +563 -0
  96. package/src/modules/dashboard/pages/dashboard-animations.css +317 -0
  97. package/src/modules/dashboard/pages/dashboard-page.tsx +13 -0
  98. package/src/modules/dashboard/routes/index.tsx +70 -0
  99. package/src/modules/observability/pages/devkit-page.tsx +263 -0
  100. package/src/modules/observability/pages/incident-detail-page.tsx +571 -0
  101. package/src/modules/observability/pages/incidents-page.tsx +370 -0
  102. package/src/modules/observability/pages/log-detail-page.tsx +508 -0
  103. package/src/modules/observability/pages/logs-page.tsx +1129 -0
  104. package/src/modules/observability/pages/prometheus-metrics-page.tsx +486 -0
  105. package/src/modules/observability/pages/state-broker-page.tsx +229 -0
  106. package/src/modules/observability/pages/system-events-page.tsx +255 -0
  107. package/src/modules/observability/routes/index.tsx +133 -0
  108. package/src/modules/plugins/components/marketplace-card.tsx +153 -0
  109. package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +374 -0
  110. package/src/modules/plugins/components/plugin-card.tsx +136 -0
  111. package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +101 -0
  112. package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +101 -0
  113. package/src/modules/plugins/components/plugin-detail/index.ts +7 -0
  114. package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +64 -0
  115. package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +30 -0
  116. package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +68 -0
  117. package/src/modules/plugins/components/plugin-detail/permissions/index.ts +6 -0
  118. package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +43 -0
  119. package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +242 -0
  120. package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +96 -0
  121. package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +55 -0
  122. package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +47 -0
  123. package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +169 -0
  124. package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +159 -0
  125. package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +40 -0
  126. package/src/modules/plugins/components/plugin-grid.tsx +20 -0
  127. package/src/modules/plugins/components/plugin-table.tsx +178 -0
  128. package/src/modules/plugins/pages/marketplace-page.tsx +200 -0
  129. package/src/modules/plugins/pages/plugin-detail-page.tsx +1145 -0
  130. package/src/modules/plugins/routes/index.tsx +48 -0
  131. package/src/modules/plugins/utils/index.ts +1 -0
  132. package/src/modules/plugins/utils/schema-helpers.ts +21 -0
  133. package/src/modules/settings/components/api-routes-viewer.tsx +415 -0
  134. package/src/modules/settings/components/appearance-settings.tsx +200 -0
  135. package/src/modules/settings/components/configuration-settings.tsx +321 -0
  136. package/src/modules/settings/components/data-privacy-settings.tsx +111 -0
  137. package/src/modules/settings/components/experimental-settings.tsx +243 -0
  138. package/src/modules/settings/components/navigation-settings.tsx +198 -0
  139. package/src/modules/settings/components/notifications-settings.tsx +40 -0
  140. package/src/modules/settings/pages/settings-page.tsx +298 -0
  141. package/src/modules/settings/routes/index.tsx +47 -0
  142. package/src/pages/README-assistant.md +91 -0
  143. package/src/pages/login-page.tsx +177 -0
  144. package/src/pages/not-found-page.tsx +85 -0
  145. package/src/providers/auth-provider.tsx +40 -0
  146. package/src/providers/command-palette-provider.tsx +193 -0
  147. package/src/providers/data-sources-provider.tsx +27 -0
  148. package/src/providers/devtools-provider.tsx +72 -0
  149. package/src/providers/navigation-items-provider.tsx +38 -0
  150. package/src/providers/registry-v2-provider.tsx +93 -0
  151. package/src/providers/settings-provider.tsx +158 -0
  152. package/src/router.tsx +302 -0
  153. package/src/routes/helpers.tsx +25 -0
  154. package/src/routes/plugin-page-v2.tsx +52 -0
  155. package/src/routes/types.ts +18 -0
  156. package/src/styles/antd-overrides.css +211 -0
  157. package/src/styles/base.css +190 -0
  158. package/src/styles/settings.css +75 -0
  159. package/src/styles/variables.css +94 -0
  160. package/src/utils/analytics.ts +183 -0
  161. package/src/utils/apply-navigation-categories.ts +83 -0
  162. package/src/utils/commands.ts +252 -0
  163. package/src/utils/export.ts +174 -0
  164. package/src/utils/format-date.ts +166 -0
  165. package/src/utils/logger.ts +110 -0
  166. package/tailwind.config.ts +13 -0
  167. package/tsconfig.json +13 -0
  168. package/vitest-setup.ts +1 -0
@@ -0,0 +1,425 @@
1
+ import { useState, useMemo, useCallback } from 'react';
2
+ import { UITable, UIAlert, UISelect, UISpace, UIButton, UIDrawer, UIDescriptions, UIDescriptionsItem, UITag, UITooltip, UIRangePicker, UIText, UICard, UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
3
+ import {
4
+ ReloadOutlined,
5
+ ClearOutlined,
6
+ } from '@ant-design/icons';
7
+ import {
8
+ useAnalyticsEvents,
9
+ useAnalyticsStats,
10
+ } from '@kb-labs/studio-data-client';
11
+ import { useDataSources } from '../../../providers/data-sources-provider';
12
+ import type { AnalyticsEvent } from '@kb-labs/studio-data-client';
13
+ import { type Dayjs } from 'dayjs';
14
+
15
+ const PAGE_SIZE = 50;
16
+
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ function formatRelativeTime(isoString: string): string {
20
+ const diffMs = Date.now() - new Date(isoString).getTime();
21
+ const minutes = Math.floor(diffMs / 60000);
22
+ const hours = Math.floor(diffMs / 3600000);
23
+ const days = Math.floor(diffMs / 86400000);
24
+ if (days > 0) {return `${days}d ago`;}
25
+ if (hours > 0) {return `${hours}h ago`;}
26
+ if (minutes > 0) {return `${minutes}m ago`;}
27
+ return 'just now';
28
+ }
29
+
30
+ function formatAbsoluteTime(isoString: string): string {
31
+ return new Date(isoString).toLocaleString();
32
+ }
33
+
34
+ function getActorColor(actorType: 'user' | 'agent' | 'ci'): string {
35
+ switch (actorType) {
36
+ case 'user': return 'blue';
37
+ case 'agent': return 'purple';
38
+ case 'ci': return 'orange';
39
+ default: return 'default';
40
+ }
41
+ }
42
+
43
+ function getTypeColor(type: string): string {
44
+ if (type.startsWith('llm.')) {return 'volcano';}
45
+ if (type.startsWith('mind.')) {return 'geekblue';}
46
+ if (type.startsWith('workflow.')) {return 'green';}
47
+ if (type.startsWith('embeddings.')) {return 'cyan';}
48
+ if (type.startsWith('vectorstore.')) {return 'purple';}
49
+ if (type.startsWith('commit.')) {return 'gold';}
50
+ if (type.startsWith('plugin.')) {return 'magenta';}
51
+ return 'default';
52
+ }
53
+
54
+ // ─── Event Detail Drawer ──────────────────────────────────────────────────────
55
+
56
+ interface EventDrawerProps {
57
+ event: AnalyticsEvent | null;
58
+ onClose: () => void;
59
+ }
60
+
61
+ function EventDrawer({ event, onClose }: EventDrawerProps) {
62
+ if (!event) {return null;}
63
+
64
+ return (
65
+ <UIDrawer
66
+ title={
67
+ <UISpace>
68
+ <UITag color={getTypeColor(event.type)}>{event.type}</UITag>
69
+ </UISpace>
70
+ }
71
+ open={!!event}
72
+ onClose={onClose}
73
+ width={520}
74
+ styles={{ body: { padding: '16px 24px' } }}
75
+ >
76
+ <UIDescriptions column={1} size="small" bordered style={{ marginBottom: 20 }}>
77
+ <UIDescriptionsItem label="Time">
78
+ <UITooltip title={event.ts}>
79
+ {formatAbsoluteTime(event.ts)}
80
+ </UITooltip>
81
+ </UIDescriptionsItem>
82
+ <UIDescriptionsItem label="Source">
83
+ {event.source.product} <UIText color="secondary">v{event.source.version}</UIText>
84
+ </UIDescriptionsItem>
85
+ {event.actor && (
86
+ <UIDescriptionsItem label="Actor">
87
+ <UITag color={getActorColor(event.actor.type)}>
88
+ {event.actor.name || event.actor.type}
89
+ </UITag>
90
+ {event.actor.id && (
91
+ <UIText color="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
92
+ {event.actor.id}
93
+ </UIText>
94
+ )}
95
+ </UIDescriptionsItem>
96
+ )}
97
+ <UIDescriptionsItem label="Run ID">
98
+ <UIText style={{ fontFamily: 'monospace', fontSize: 12 }}>{event.runId}</UIText>
99
+ </UIDescriptionsItem>
100
+ <UIDescriptionsItem label="Ingest">
101
+ <UIText color="secondary" style={{ fontSize: 12 }}>
102
+ {formatAbsoluteTime(event.ingestTs)}
103
+ </UIText>
104
+ </UIDescriptionsItem>
105
+ </UIDescriptions>
106
+
107
+ {!!event.payload && (
108
+ <div style={{ marginBottom: 20 }}>
109
+ <UIText weight="semibold" style={{ display: 'block', marginBottom: 8 }}>
110
+ Payload
111
+ </UIText>
112
+ <pre
113
+ style={{
114
+ background: 'var(--color-fill-quaternary, #f5f5f5)',
115
+ borderRadius: 6,
116
+ padding: '10px 14px',
117
+ fontSize: 12,
118
+ lineHeight: 1.6,
119
+ overflowX: 'auto',
120
+ maxHeight: 320,
121
+ overflowY: 'auto',
122
+ margin: 0,
123
+ }}
124
+ >
125
+ {JSON.stringify(event.payload, null, 2)}
126
+ </pre>
127
+ </div>
128
+ )}
129
+
130
+ {event.ctx && Object.keys(event.ctx).length > 0 && (
131
+ <div>
132
+ <UIText weight="semibold" style={{ display: 'block', marginBottom: 8 }}>
133
+ Context
134
+ </UIText>
135
+ <pre
136
+ style={{
137
+ background: 'var(--color-fill-quaternary, #f5f5f5)',
138
+ borderRadius: 6,
139
+ padding: '10px 14px',
140
+ fontSize: 12,
141
+ lineHeight: 1.6,
142
+ overflowX: 'auto',
143
+ maxHeight: 200,
144
+ overflowY: 'auto',
145
+ margin: 0,
146
+ }}
147
+ >
148
+ {JSON.stringify(event.ctx, null, 2)}
149
+ </pre>
150
+ </div>
151
+ )}
152
+ </UIDrawer>
153
+ );
154
+ }
155
+
156
+ // ─── Main Page ────────────────────────────────────────────────────────────────
157
+
158
+ export function AnalyticsEventsPage() {
159
+ const sources = useDataSources();
160
+
161
+ // ── Filter state
162
+ const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
163
+ const [selectedSource, setSelectedSource] = useState<string | undefined>();
164
+ const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null);
165
+ const [limit, setLimit] = useState(PAGE_SIZE);
166
+
167
+ // ── Drawer state
168
+ const [selectedEvent, setSelectedEvent] = useState<AnalyticsEvent | null>(null);
169
+
170
+ // ── Determine if any filter is active (disables auto-refresh for live tail)
171
+ const hasFilters = selectedTypes.length > 0 || !!selectedSource || !!dateRange;
172
+
173
+ // ── Build query
174
+ const query = useMemo(() => ({
175
+ type: selectedTypes.length > 0 ? selectedTypes : undefined,
176
+ source: selectedSource,
177
+ from: dateRange ? dateRange[0].toISOString() : undefined,
178
+ to: dateRange ? dateRange[1].endOf('day').toISOString() : undefined,
179
+ limit,
180
+ }), [selectedTypes, selectedSource, dateRange, limit]);
181
+
182
+ const {
183
+ data: eventsData,
184
+ isLoading: eventsLoading,
185
+ isError: isEventsError,
186
+ error: eventsError,
187
+ refetch,
188
+ dataUpdatedAt,
189
+ } = useAnalyticsEvents(sources.analytics, query);
190
+
191
+ // ── Stats for filter options
192
+ const { data: statsData } = useAnalyticsStats(sources.analytics);
193
+
194
+ const typeOptions = useMemo(() =>
195
+ Object.keys(statsData?.byType ?? {})
196
+ .sort()
197
+ .map((t) => ({ label: t, value: t })),
198
+ [statsData]
199
+ );
200
+
201
+ const sourceOptions = useMemo(() =>
202
+ Object.keys(statsData?.bySource ?? {})
203
+ .sort()
204
+ .map((s) => ({ label: s, value: s })),
205
+ [statsData]
206
+ );
207
+
208
+ // ── Handlers
209
+ const handleClearFilters = useCallback(() => {
210
+ setSelectedTypes([]);
211
+ setSelectedSource(undefined);
212
+ setDateRange(null);
213
+ setLimit(PAGE_SIZE);
214
+ }, []);
215
+
216
+ const handleLoadMore = useCallback(() => {
217
+ setLimit((prev) => prev + PAGE_SIZE);
218
+ }, []);
219
+
220
+ const handleRowClick = useCallback((record: AnalyticsEvent) => {
221
+ setSelectedEvent(record);
222
+ }, []);
223
+
224
+ // ── Error: analytics not configured
225
+ const analyticsNotConfigured =
226
+ isEventsError &&
227
+ eventsError instanceof Error &&
228
+ eventsError.message.includes('not support');
229
+
230
+ if (analyticsNotConfigured) {
231
+ return (
232
+ <UIPage>
233
+ <UIPageHeader title="Event Log" description="Platform event stream" />
234
+ <UIAlert
235
+ message="Analytics Not Configured"
236
+ description="Configure an analytics adapter in kb.config.json to view events."
237
+ variant="info"
238
+ showIcon
239
+ />
240
+ </UIPage>
241
+ );
242
+ }
243
+
244
+ if (isEventsError) {
245
+ return (
246
+ <UIPage>
247
+ <UIPageHeader title="Event Log" description="Platform event stream" />
248
+ <UIAlert
249
+ message="Failed to load events"
250
+ description={eventsError instanceof Error ? eventsError.message : 'Unknown error'}
251
+ variant="error"
252
+ showIcon
253
+ />
254
+ </UIPage>
255
+ );
256
+ }
257
+
258
+ const events = eventsData?.events ?? [];
259
+ const total = eventsData?.total ?? 0;
260
+ const hasMore = eventsData?.hasMore ?? false;
261
+
262
+ const lastUpdated = dataUpdatedAt
263
+ ? formatRelativeTime(new Date(dataUpdatedAt).toISOString())
264
+ : null;
265
+
266
+ return (
267
+ <UIPage width="full">
268
+ <UIPageHeader
269
+ title="Event Log"
270
+ description={
271
+ hasFilters
272
+ ? `${total.toLocaleString()} events matching filters`
273
+ : `Live tail · auto-refresh every 30s${lastUpdated ? ` · updated ${lastUpdated}` : ''}`
274
+ }
275
+ />
276
+
277
+ <UICard style={{ marginBottom: 16 }}>
278
+ <UISpace wrap size="small" style={{ width: '100%', justifyContent: 'space-between' }}>
279
+ <UISpace wrap size="small">
280
+ <UISelect
281
+ mode="multiple"
282
+ placeholder="Event type"
283
+ value={selectedTypes}
284
+ onChange={(v) => setSelectedTypes(v as string[])}
285
+ options={typeOptions}
286
+ style={{ minWidth: 200 }}
287
+ maxTagCount={2}
288
+ allowClear
289
+ />
290
+ <UISelect
291
+ placeholder="Source"
292
+ value={selectedSource}
293
+ onChange={(v) => setSelectedSource(v as string | undefined)}
294
+ options={sourceOptions}
295
+ style={{ width: 140 }}
296
+ allowClear
297
+ />
298
+ <UIRangePicker
299
+ value={dateRange}
300
+ onChange={(dates) => {
301
+ if (dates && dates[0] && dates[1]) {
302
+ setDateRange([dates[0], dates[1]]);
303
+ } else {
304
+ setDateRange(null);
305
+ }
306
+ }}
307
+ format="DD/MM/YYYY"
308
+ allowClear
309
+ />
310
+ {hasFilters && (
311
+ <UIButton
312
+ icon={<ClearOutlined />}
313
+ onClick={handleClearFilters}
314
+ size="middle"
315
+ >
316
+ Clear
317
+ </UIButton>
318
+ )}
319
+ </UISpace>
320
+ <UIButton
321
+ icon={<ReloadOutlined />}
322
+ onClick={() => refetch()}
323
+ loading={eventsLoading}
324
+ size="middle"
325
+ >
326
+ Refresh
327
+ </UIButton>
328
+ </UISpace>
329
+ </UICard>
330
+
331
+ <UICard>
332
+ <UITable<AnalyticsEvent>
333
+ dataSource={events}
334
+ rowKey="id"
335
+ loading={eventsLoading}
336
+ pagination={false}
337
+ size="small"
338
+ onRow={(record) => ({
339
+ onClick: () => handleRowClick(record),
340
+ style: { cursor: 'pointer' },
341
+ })}
342
+ columns={[
343
+ {
344
+ title: 'Time',
345
+ dataIndex: 'ts',
346
+ key: 'ts',
347
+ width: 100,
348
+ render: (ts: string) => (
349
+ <UITooltip title={formatAbsoluteTime(ts)}>
350
+ <UIText color="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
351
+ {formatRelativeTime(ts)}
352
+ </UIText>
353
+ </UITooltip>
354
+ ),
355
+ },
356
+ {
357
+ title: 'Type',
358
+ dataIndex: 'type',
359
+ key: 'type',
360
+ render: (type: string) => (
361
+ <UITag color={getTypeColor(type)} style={{ fontFamily: 'monospace', fontSize: 11 }}>
362
+ {type}
363
+ </UITag>
364
+ ),
365
+ },
366
+ {
367
+ title: 'Source',
368
+ dataIndex: 'source',
369
+ key: 'source',
370
+ width: 100,
371
+ render: (source: { product: string; version: string }) => (
372
+ <UITag>{source.product}</UITag>
373
+ ),
374
+ },
375
+ {
376
+ title: 'Actor',
377
+ dataIndex: 'actor',
378
+ key: 'actor',
379
+ width: 120,
380
+ render: (actor?: { type: 'user' | 'agent' | 'ci'; name?: string }) =>
381
+ actor ? (
382
+ <UITag color={getActorColor(actor.type)}>
383
+ {actor.name || actor.type}
384
+ </UITag>
385
+ ) : (
386
+ <UIText color="secondary">—</UIText>
387
+ ),
388
+ },
389
+ {
390
+ title: 'Run ID',
391
+ dataIndex: 'runId',
392
+ key: 'runId',
393
+ width: 110,
394
+ render: (runId: string) => (
395
+ <UIText color="secondary" style={{ fontSize: 11, fontFamily: 'monospace' }}>
396
+ {runId ? runId.slice(0, 10) : '—'}
397
+ </UIText>
398
+ ),
399
+ },
400
+ ]}
401
+ />
402
+ {hasMore ? (
403
+ <div style={{ textAlign: 'center', paddingTop: 8 }}>
404
+ <UIButton onClick={handleLoadMore} loading={eventsLoading}>
405
+ Load {PAGE_SIZE} more
406
+ </UIButton>
407
+ <UIText color="secondary" style={{ marginLeft: 12, fontSize: 12 }}>
408
+ Showing {events.length} of {total.toLocaleString()}
409
+ </UIText>
410
+ </div>
411
+ ) : (
412
+ <div style={{ textAlign: 'center', paddingTop: 4 }}>
413
+ <UIText color="secondary" style={{ fontSize: 12 }}>
414
+ {total > 0
415
+ ? `All ${total.toLocaleString()} events loaded`
416
+ : 'No events found'}
417
+ </UIText>
418
+ </div>
419
+ )}
420
+ </UICard>
421
+
422
+ <EventDrawer event={selectedEvent} onClose={() => setSelectedEvent(null)} />
423
+ </UIPage>
424
+ );
425
+ }