@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,118 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { defineConfig } from '@rspack/cli';
4
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
5
+ import { HtmlRspackPlugin } from '@rspack/core';
6
+ import RefreshPlugin from '@rspack/plugin-react-refresh';
7
+ import { STUDIO_SHARED_DEPS } from '@kb-labs/studio-plugin-tools';
8
+
9
+ const isDev = process.env.NODE_ENV !== 'production';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // Gateway URL for proxying API and plugin widget requests
14
+ const proxyTarget = process.env.API_PROXY_TARGET || 'http://localhost:4000';
15
+
16
+ // Expose KB_* env vars via import.meta.env
17
+ const envDefines = {
18
+ 'import.meta.env.MODE': JSON.stringify(isDev ? 'development' : 'production'),
19
+ 'import.meta.env.DEV': JSON.stringify(isDev),
20
+ 'import.meta.env.PROD': JSON.stringify(!isDev),
21
+ };
22
+ for (const [key, value] of Object.entries(process.env)) {
23
+ if (key.startsWith('KB_') && value !== undefined) {
24
+ envDefines[`import.meta.env.${key}`] = JSON.stringify(value);
25
+ }
26
+ }
27
+
28
+ export default defineConfig({
29
+ experiments: {
30
+ css: true,
31
+ },
32
+ entry: { main: './src/main.tsx' },
33
+ output: {
34
+ path: path.resolve(__dirname, 'dist'),
35
+ publicPath: 'auto',
36
+ filename: isDev ? '[name].js' : '[name].[contenthash:8].js',
37
+ clean: true,
38
+ },
39
+ resolve: {
40
+ extensions: ['.tsx', '.ts', '.jsx', '.js'],
41
+ alias: {
42
+ '@': path.resolve(__dirname, 'src'),
43
+ '@kb-labs/studio-ui-core': path.resolve(__dirname, '../../packages/studio-ui-core/src'),
44
+ '@kb-labs/studio-data-client': path.resolve(__dirname, '../../packages/studio-data-client/src'),
45
+ },
46
+ },
47
+ module: {
48
+ parser: {
49
+ 'css/auto': {
50
+ namedExports: false,
51
+ },
52
+ css: {
53
+ namedExports: false,
54
+ },
55
+ },
56
+ generator: {
57
+ 'css/auto': {
58
+ exportsOnly: false,
59
+ exportsConvention: 'as-is',
60
+ },
61
+ css: {
62
+ exportsOnly: false,
63
+ exportsConvention: 'as-is',
64
+ },
65
+ },
66
+ rules: [
67
+ {
68
+ test: /\.tsx?$/,
69
+ exclude: /node_modules/,
70
+ use: {
71
+ loader: 'builtin:swc-loader',
72
+ options: {
73
+ jsc: {
74
+ parser: { syntax: 'typescript', tsx: true },
75
+ transform: { react: { runtime: 'automatic', development: isDev, refresh: isDev } },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ {
81
+ test: /\.css$/,
82
+ use: ['postcss-loader'],
83
+ type: 'css/auto',
84
+ },
85
+ {
86
+ test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)$/,
87
+ type: 'asset',
88
+ },
89
+ ],
90
+ },
91
+ plugins: [
92
+ new HtmlRspackPlugin({
93
+ template: './index.html',
94
+ favicon: './public/favicon.svg',
95
+ }),
96
+ new ModuleFederationPlugin({
97
+ name: 'studioHost',
98
+ remotes: {},
99
+ shared: Object.fromEntries(
100
+ Object.entries(STUDIO_SHARED_DEPS).map(([key, val]) => [key, { ...val, eager: true }]),
101
+ ),
102
+ }),
103
+ isDev && new RefreshPlugin(),
104
+ ].filter(Boolean),
105
+ devServer: {
106
+ port: 3000,
107
+ hot: true,
108
+ historyApiFallback: true,
109
+ proxy: [
110
+ { context: ['/api'], target: proxyTarget, changeOrigin: true },
111
+ { context: ['/plugins'], target: proxyTarget, changeOrigin: true },
112
+ ],
113
+ },
114
+ builtins: {
115
+ define: envDefines,
116
+ },
117
+ devtool: isDev ? 'source-map' : 'source-map',
118
+ });
package/src/App.tsx ADDED
@@ -0,0 +1,46 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
3
+ import { RouterProvider } from 'react-router-dom';
4
+ import { KBConfigProvider } from '@/components/ui/kb-config-provider';
5
+ import { DataSourcesProvider } from './providers/data-sources-provider';
6
+ import { AuthProvider } from './providers/auth-provider';
7
+ import { RegistryV2Provider } from './providers/registry-v2-provider';
8
+ import { SettingsProvider } from './providers/settings-provider';
9
+ import { EventBusProvider } from '@kb-labs/studio-event-bus';
10
+ import { router } from './router';
11
+ import { studioConfig } from './config/studio.config';
12
+ import { DevToolsProvider } from './providers/devtools-provider';
13
+ import { DevToolsPanel } from './components/devtools/devtools-panel';
14
+
15
+ const queryClient = new QueryClient({
16
+ defaultOptions: {
17
+ queries: {
18
+ staleTime: 30000,
19
+ refetchOnWindowFocus: false,
20
+ },
21
+ },
22
+ });
23
+
24
+ export function App() {
25
+ return (
26
+ <KBConfigProvider defaultTheme="light" storageKey="studio-ui-theme">
27
+ <SettingsProvider>
28
+ <AuthProvider>
29
+ <QueryClientProvider client={queryClient}>
30
+ <DataSourcesProvider>
31
+ <DevToolsProvider>
32
+ <RegistryV2Provider apiBaseUrl={studioConfig.apiBaseUrl}>
33
+ <EventBusProvider>
34
+ <RouterProvider router={router} />
35
+ <ReactQueryDevtools initialIsOpen={false} />
36
+ </EventBusProvider>
37
+ </RegistryV2Provider>
38
+ <DevToolsPanel />
39
+ </DevToolsProvider>
40
+ </DataSourcesProvider>
41
+ </QueryClientProvider>
42
+ </AuthProvider>
43
+ </SettingsProvider>
44
+ </KBConfigProvider>
45
+ );
46
+ }
@@ -0,0 +1,165 @@
1
+ /* ── Log feed container ──────────────────────────────────────────────── */
2
+
3
+ .container {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ }
9
+
10
+ /* ── Toolbar ─────────────────────────────────────────────────────────── */
11
+
12
+ .toolbar {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: 8px;
16
+ padding: 5px 10px;
17
+ border-bottom: 1px solid var(--border-primary);
18
+ flex-shrink: 0;
19
+ min-height: 36px;
20
+ background: var(--bg-secondary);
21
+ }
22
+
23
+ .toolbarTitle {
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .toolbarSearch {
28
+ flex: 0 1 220px;
29
+ }
30
+
31
+ .toolbarChips {
32
+ flex: 1;
33
+ display: flex;
34
+ gap: 4px;
35
+ flex-wrap: wrap;
36
+ }
37
+
38
+ .chip {
39
+ cursor: pointer;
40
+ user-select: none;
41
+ font-size: 11px !important;
42
+ }
43
+
44
+ .clearBtn {
45
+ font-size: 11px;
46
+ color: var(--text-secondary);
47
+ cursor: pointer;
48
+ flex-shrink: 0;
49
+ user-select: none;
50
+ background: none;
51
+ border: none;
52
+ padding: 2px 4px;
53
+ border-radius: 3px;
54
+ transition: color 0.15s;
55
+ }
56
+
57
+ .clearBtn:hover {
58
+ color: var(--text-primary);
59
+ }
60
+
61
+ /* ── Log list ────────────────────────────────────────────────────────── */
62
+
63
+ .logList {
64
+ flex: 1;
65
+ overflow-y: auto;
66
+ font-family: var(--font-mono, 'JetBrains Mono', 'Fira Code', 'Menlo', monospace);
67
+ }
68
+
69
+ /* ── Log row ─────────────────────────────────────────────────────────── */
70
+
71
+ .row {
72
+ padding: 3px 8px;
73
+ border-bottom: 1px solid var(--border-primary);
74
+ cursor: default;
75
+ }
76
+
77
+ .row.clickable {
78
+ cursor: pointer;
79
+ }
80
+
81
+ .row.clickable:hover {
82
+ background: var(--bg-hover);
83
+ }
84
+
85
+ .row.expanded {
86
+ background: var(--bg-tertiary);
87
+ }
88
+
89
+ .rowError {
90
+ background: rgba(220, 38, 38, 0.06);
91
+ border-left: 2px solid var(--error);
92
+ }
93
+
94
+ .rowWarning {
95
+ background: rgba(245, 158, 11, 0.06);
96
+ border-left: 2px solid var(--warning);
97
+ }
98
+
99
+ .rowPending {
100
+ opacity: 0.6;
101
+ }
102
+
103
+ /* ── Status badge ────────────────────────────────────────────────────── */
104
+
105
+ .statusBadge {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ min-width: 52px;
110
+ padding: 1px 5px;
111
+ border-radius: 3px;
112
+ font-size: 10px;
113
+ font-weight: 600;
114
+ letter-spacing: 0.03em;
115
+ text-transform: uppercase;
116
+ flex-shrink: 0;
117
+ }
118
+
119
+ .statusSuccess {
120
+ background: rgba(22, 163, 74, 0.12);
121
+ color: var(--success);
122
+ }
123
+
124
+ .statusWarning {
125
+ background: rgba(245, 158, 11, 0.15);
126
+ color: var(--warning);
127
+ }
128
+
129
+ .statusError {
130
+ background: rgba(220, 38, 38, 0.12);
131
+ color: var(--error);
132
+ }
133
+
134
+ .statusPending {
135
+ background: var(--bg-tertiary);
136
+ color: var(--text-secondary);
137
+ }
138
+
139
+ /* ── Expand detail ───────────────────────────────────────────────────── */
140
+
141
+ .detail {
142
+ padding: 4px 8px 4px 16px;
143
+ background: var(--bg-tertiary);
144
+ }
145
+
146
+ .detailPre {
147
+ margin: 0;
148
+ padding: 8px;
149
+ background: var(--bg-primary);
150
+ border: 1px solid var(--border-primary);
151
+ border-radius: 4px;
152
+ font-size: 11px;
153
+ font-family: inherit;
154
+ overflow-x: auto;
155
+ white-space: pre-wrap;
156
+ word-break: break-word;
157
+ color: var(--text-primary);
158
+ }
159
+
160
+ /* ── Empty state ─────────────────────────────────────────────────────── */
161
+
162
+ .empty {
163
+ padding: 24px 16px;
164
+ text-align: center;
165
+ }
@@ -0,0 +1,300 @@
1
+ import * as React from 'react';
2
+ import { useState, useMemo, useCallback } from 'react';
3
+ import { UITag, UIBadge, UIFlex, UIInput, UITypographyText } from '@kb-labs/studio-ui-kit';
4
+ import type { DevToolsChannel, MFEvent, EventBusEvent } from '@kb-labs/studio-devtools';
5
+ import { useDevToolsStore } from '@/hooks/use-devtools-store';
6
+ import styles from './devtools-log-feed.module.css';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ interface LogEntry {
13
+ id: string;
14
+ channelId: string;
15
+ channelLabel: string;
16
+ timestamp: number;
17
+ status: 'success' | 'warning' | 'error' | 'pending';
18
+ summary: string;
19
+ meta?: string;
20
+ raw: unknown;
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Row builders
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function buildMFEntry(event: MFEvent, channelLabel: string): LogEntry {
28
+ let status: LogEntry['status'] = 'pending';
29
+ if (event.status === 'success') {status = 'success';}
30
+ else if (event.status === 'warning') {status = 'warning';}
31
+ else if (event.status === 'error') {status = 'error';}
32
+
33
+ return {
34
+ id: event.id,
35
+ channelId: 'mf-events',
36
+ channelLabel,
37
+ timestamp: event.startedAt,
38
+ status,
39
+ summary: `${event.remoteName} / ${event.exposedModule}`,
40
+ meta: event.durationMs != null ? `${event.durationMs}ms` : undefined,
41
+ raw: event,
42
+ };
43
+ }
44
+
45
+ function buildEventBusEntry(event: EventBusEvent, channelLabel: string): LogEntry {
46
+ return {
47
+ id: event.id,
48
+ channelId: 'eventbus',
49
+ channelLabel,
50
+ timestamp: event.timestamp,
51
+ status: 'success',
52
+ summary: event.event,
53
+ meta: event.sourcePluginId !== '' ? event.sourcePluginId : undefined,
54
+ raw: event,
55
+ };
56
+ }
57
+
58
+ function buildGenericEntry(event: unknown, channelId: string, channelLabel: string): LogEntry {
59
+ const id = (event as { id?: string }).id ?? `${Date.now()}-${Math.random()}`;
60
+ const timestamp =
61
+ (event as { timestamp?: number }).timestamp ??
62
+ (event as { startedAt?: number }).startedAt ??
63
+ Date.now();
64
+ return {
65
+ id, channelId, channelLabel, timestamp,
66
+ status: 'success',
67
+ summary: JSON.stringify(event).slice(0, 80),
68
+ raw: event,
69
+ };
70
+ }
71
+
72
+ function channelToEntries(channel: DevToolsChannel): LogEntry[] {
73
+ return (channel.getEvents() as unknown[]).map((e) => {
74
+ if (channel.id === 'mf-events') {return buildMFEntry(e as MFEvent, channel.label);}
75
+ if (channel.id === 'eventbus') {return buildEventBusEntry(e as EventBusEvent, channel.label);}
76
+ return buildGenericEntry(e, channel.id, channel.label);
77
+ });
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Status badge
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const STATUS_LABEL: Record<LogEntry['status'], string> = {
85
+ success: 'ok',
86
+ warning: 'warn',
87
+ error: 'error',
88
+ pending: '...',
89
+ };
90
+
91
+ function StatusBadge({ status }: { status: LogEntry['status'] }) {
92
+ const cls = {
93
+ success: styles.statusSuccess,
94
+ warning: styles.statusWarning,
95
+ error: styles.statusError,
96
+ pending: styles.statusPending,
97
+ }[status];
98
+ return <span className={`${styles.statusBadge} ${cls}`}>{STATUS_LABEL[status]}</span>;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Expanded detail
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function ExpandedDetail({ entry }: { entry: LogEntry }) {
106
+ if (entry.channelId === 'mf-events') {
107
+ const mf = entry.raw as MFEvent;
108
+ return (
109
+ <div className={styles.detail}>
110
+ {mf.isDefaultUndefined && (
111
+ <UITag color="orange" style={{ marginBottom: 8 }}>
112
+ Missing default export — add <code>export default</code> to the component
113
+ </UITag>
114
+ )}
115
+ {mf.error && (
116
+ <pre className={styles.detailPre}>
117
+ {mf.error.message}
118
+ {mf.error.cause ? `\n\nCause: ${mf.error.cause}` : ''}
119
+ </pre>
120
+ )}
121
+ {!mf.isDefaultUndefined && !mf.error && (
122
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
123
+ attempt {mf.attempt}
124
+ </UITypographyText>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ if (entry.channelId === 'eventbus') {
131
+ const eb = entry.raw as EventBusEvent;
132
+ return (
133
+ <div className={styles.detail}>
134
+ <pre className={styles.detailPre}>
135
+ {JSON.stringify(eb.payload, null, 2)}
136
+ </pre>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ return (
142
+ <div className={styles.detail}>
143
+ <pre className={styles.detailPre}>
144
+ {JSON.stringify(entry.raw, null, 2)}
145
+ </pre>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Log row
152
+ // ---------------------------------------------------------------------------
153
+
154
+ function LogRow({ entry }: { entry: LogEntry }) {
155
+ const [expanded, setExpanded] = useState(false);
156
+ const hasDetail =
157
+ entry.channelId === 'mf-events'
158
+ ? !!(entry.raw as MFEvent).error || !!(entry.raw as MFEvent).isDefaultUndefined
159
+ : entry.channelId === 'eventbus';
160
+
161
+ const time = new Date(entry.timestamp).toLocaleTimeString('en-GB', {
162
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
163
+ });
164
+
165
+ const rowStatusCls = entry.status === 'error' ? styles.rowError
166
+ : entry.status === 'warning' ? styles.rowWarning
167
+ : entry.status === 'pending' ? styles.rowPending
168
+ : '';
169
+
170
+ return (
171
+ <>
172
+ <div
173
+ className={`${styles.row} ${rowStatusCls} ${hasDetail ? styles.clickable : ''} ${expanded ? styles.expanded : ''}`}
174
+ onClick={hasDetail ? () => setExpanded((v) => !v) : undefined}
175
+ >
176
+ <UIFlex align="center" gap={8}>
177
+ <span style={{ fontSize: 10, width: 8, color: 'var(--text-secondary)', flexShrink: 0 }}>
178
+ {hasDetail ? (expanded ? '▼' : '▶') : ''}
179
+ </span>
180
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)', width: 64, flexShrink: 0 }}>
181
+ {time}
182
+ </span>
183
+ <UITag
184
+ color={entry.channelId === 'mf-events' ? 'blue' : 'purple'}
185
+ style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', flexShrink: 0 }}
186
+ >
187
+ {entry.channelLabel}
188
+ </UITag>
189
+ <StatusBadge status={entry.status} />
190
+ <span style={{ fontSize: 12, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--text-primary)' }}>
191
+ {entry.summary}
192
+ {(entry.raw as MFEvent).isDefaultUndefined && (
193
+ <UITag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>default=undefined</UITag>
194
+ )}
195
+ </span>
196
+ {entry.meta && (
197
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)', flexShrink: 0 }}>
198
+ {entry.meta}
199
+ </span>
200
+ )}
201
+ </UIFlex>
202
+ </div>
203
+ {expanded && hasDetail && <ExpandedDetail entry={entry} />}
204
+ </>
205
+ );
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Main log feed
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export function DevToolsLogFeed() {
213
+ const { channels, clear } = useDevToolsStore();
214
+ const [activeChannel, setActiveChannel] = useState<string>('all');
215
+ const [search, setSearch] = useState('');
216
+
217
+ const allEntries = useMemo<LogEntry[]>(() => {
218
+ const entries: LogEntry[] = [];
219
+ channels.forEach((ch) => entries.push(...channelToEntries(ch)));
220
+ return entries.sort((a, b) => a.timestamp - b.timestamp);
221
+ }, [channels]);
222
+
223
+ const filtered = useMemo(() => {
224
+ let result = activeChannel === 'all'
225
+ ? allEntries
226
+ : allEntries.filter((e) => e.channelId === activeChannel);
227
+ if (search.trim()) {
228
+ const q = search.toLowerCase();
229
+ result = result.filter((e) =>
230
+ e.summary.toLowerCase().includes(q) ||
231
+ e.channelLabel.toLowerCase().includes(q) ||
232
+ (e.meta ?? '').toLowerCase().includes(q),
233
+ );
234
+ }
235
+ return result;
236
+ }, [allEntries, activeChannel, search]);
237
+
238
+ const errorCount = allEntries.filter((e) => e.status === 'error' || e.status === 'warning').length;
239
+
240
+ const countFor = useCallback(
241
+ (id: string) => id === 'all' ? allEntries.length : allEntries.filter((e) => e.channelId === id).length,
242
+ [allEntries],
243
+ );
244
+
245
+ return (
246
+ <div className={styles.container}>
247
+ <div className={styles.toolbar}>
248
+ <UIFlex align="center" gap={4} className={styles.toolbarTitle}>
249
+ <UITypographyText strong style={{ fontSize: 12 }}>DevTools</UITypographyText>
250
+ {errorCount > 0 && <UIBadge count={errorCount} size="small" />}
251
+ </UIFlex>
252
+
253
+ <UIInput
254
+ className={styles.toolbarSearch}
255
+ placeholder="Search..."
256
+ value={search}
257
+ onChange={(value) => setSearch(value)}
258
+ allowClear
259
+ size="small"
260
+ />
261
+
262
+ <div className={styles.toolbarChips}>
263
+ <UITag
264
+ className={styles.chip}
265
+ color={activeChannel === 'all' ? 'blue' : undefined}
266
+ onClick={() => setActiveChannel('all')}
267
+ >
268
+ All{countFor('all') > 0 ? ` ×${countFor('all')}` : ''}
269
+ </UITag>
270
+ {channels.map((ch) => (
271
+ <UITag
272
+ key={ch.id}
273
+ className={styles.chip}
274
+ color={activeChannel === ch.id ? 'blue' : undefined}
275
+ onClick={() => setActiveChannel(ch.id)}
276
+ >
277
+ {ch.label}{countFor(ch.id) > 0 ? ` ×${countFor(ch.id)}` : ''}
278
+ </UITag>
279
+ ))}
280
+ </div>
281
+
282
+ <button className={styles.clearBtn} onClick={clear}>Clear</button>
283
+ </div>
284
+
285
+ <div className={styles.logList}>
286
+ {filtered.length === 0 ? (
287
+ <div className={styles.empty}>
288
+ <UITypographyText type="secondary" style={{ fontSize: 12 }}>
289
+ {allEntries.length === 0
290
+ ? 'No events captured yet. Navigate to a plugin page to see MF events.'
291
+ : 'No events match the current filter.'}
292
+ </UITypographyText>
293
+ </div>
294
+ ) : (
295
+ filtered.map((entry) => <LogRow key={entry.id} entry={entry} />)
296
+ )}
297
+ </div>
298
+ </div>
299
+ );
300
+ }