@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,112 @@
1
+ import * as React from 'react';
2
+ import { Layout, Dropdown, Avatar, type MenuProps } from 'antd';
3
+ import { User, Search } from 'lucide-react';
4
+ import { UIButton } from '@kb-labs/studio-ui-kit';
5
+ import { KBThemeToggle } from './kb-theme-toggle';
6
+ import { type SystemHealthData } from './kb-system-health-indicator';
7
+ import { KBNotificationBell, type LogNotification } from './kb-notification-bell';
8
+ import styles from './kb-header.module.css';
9
+
10
+ const { Header: AntHeader } = Layout;
11
+
12
+ export interface KBHeaderProps {
13
+ logo?: React.ReactNode;
14
+ logoLink?: string;
15
+ LinkComponent?: React.ComponentType<{ to: string; children: React.ReactNode; className?: string }>;
16
+ onLogout?: () => void;
17
+ profileMenuItems?: MenuProps['items'];
18
+ userAvatar?: string;
19
+ userName?: string;
20
+ systemHealth?: SystemHealthData;
21
+ systemHealthLoading?: boolean;
22
+ onSearchClick?: () => void;
23
+ sidebarCollapsed?: boolean;
24
+ onSidebarCollapse?: (collapsed: boolean) => void;
25
+ sidebarWidth?: number;
26
+ notifications?: LogNotification[];
27
+ unreadNotificationsCount?: number;
28
+ onMarkNotificationAsRead?: (id: string) => void;
29
+ onMarkAllNotificationsAsRead?: () => void;
30
+ onClearAllNotifications?: () => void;
31
+ onClearNotification?: (id: string) => void;
32
+ onNotificationClick?: (notification: LogNotification) => void;
33
+ }
34
+
35
+ export function KBHeader({
36
+ logo = 'KB Labs',
37
+ logoLink = '/',
38
+ LinkComponent,
39
+ onLogout,
40
+ profileMenuItems,
41
+ userAvatar,
42
+ userName = 'User',
43
+ onSearchClick,
44
+ sidebarWidth = 220,
45
+ notifications = [],
46
+ unreadNotificationsCount = 0,
47
+ onMarkNotificationAsRead,
48
+ onMarkAllNotificationsAsRead,
49
+ onClearAllNotifications,
50
+ onClearNotification,
51
+ onNotificationClick,
52
+ }: KBHeaderProps) {
53
+ const profileItems: MenuProps['items'] = profileMenuItems || [
54
+ ...(onLogout ? [{ key: 'logout', label: 'Logout', danger: true, onClick: onLogout }] : []),
55
+ ];
56
+
57
+ return (
58
+ <AntHeader
59
+ className={styles.header}
60
+ style={{ paddingLeft: sidebarWidth + 16 }}
61
+ >
62
+ {LinkComponent ? (
63
+ <LinkComponent to={logoLink} className={styles.logo}>
64
+ {logo}
65
+ </LinkComponent>
66
+ ) : (
67
+ <a href={logoLink} className={styles.logo}>
68
+ {logo}
69
+ </a>
70
+ )}
71
+
72
+ <div className={styles.actions}>
73
+ {onSearchClick && (
74
+ <UIButton
75
+ variant="text"
76
+ icon={<Search size={16} />}
77
+ onClick={onSearchClick}
78
+ className={styles.actionBtn}
79
+ />
80
+ )}
81
+
82
+ {onMarkNotificationAsRead &&
83
+ onMarkAllNotificationsAsRead &&
84
+ onClearAllNotifications &&
85
+ onClearNotification && (
86
+ <KBNotificationBell
87
+ notifications={notifications}
88
+ unreadCount={unreadNotificationsCount}
89
+ onMarkAsRead={onMarkNotificationAsRead}
90
+ onMarkAllAsRead={onMarkAllNotificationsAsRead}
91
+ onClearAll={onClearAllNotifications}
92
+ onClearNotification={onClearNotification}
93
+ onNotificationClick={onNotificationClick}
94
+ />
95
+ )}
96
+
97
+ <KBThemeToggle />
98
+
99
+ <Dropdown menu={{ items: profileItems }} placement="bottomRight">
100
+ <UIButton variant="text" className={styles.userBtn}>
101
+ <Avatar
102
+ size={20}
103
+ icon={!userAvatar && <User size={12} />}
104
+ src={userAvatar}
105
+ />
106
+ {userName && <span className={styles.userName}>{userName}</span>}
107
+ </UIButton>
108
+ </Dropdown>
109
+ </div>
110
+ </AntHeader>
111
+ );
112
+ }
@@ -0,0 +1,103 @@
1
+ .bell {
2
+ display: flex;
3
+ align-items: center;
4
+ }
5
+
6
+ .panel {
7
+ width: 400px;
8
+ max-height: 500px;
9
+ display: flex;
10
+ flex-direction: column;
11
+ background-color: var(--bg-secondary);
12
+ border: 1px solid var(--border-primary);
13
+ border-radius: 8px;
14
+ box-shadow: 0 4px 16px var(--shadow);
15
+ }
16
+
17
+ .panelHeader {
18
+ padding: 12px 16px;
19
+ border-bottom: 1px solid var(--border-primary);
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ .panelList {
27
+ overflow-y: auto;
28
+ flex: 1;
29
+ }
30
+
31
+ .emptyState {
32
+ padding: 48px 16px;
33
+ }
34
+
35
+ .notifItem {
36
+ padding: 12px 16px;
37
+ border-bottom: 1px solid var(--border-primary);
38
+ cursor: pointer;
39
+ transition: background-color 0.15s;
40
+ }
41
+
42
+ .notifItem:hover {
43
+ background-color: var(--bg-hover);
44
+ }
45
+
46
+ .notifItem.unread {
47
+ background-color: var(--bg-tertiary);
48
+ }
49
+
50
+ .notifItem.unread:hover {
51
+ background-color: var(--bg-hover);
52
+ }
53
+
54
+ .notifRow {
55
+ display: flex;
56
+ gap: 8px;
57
+ }
58
+
59
+ .notifIcon {
60
+ flex-shrink: 0;
61
+ padding-top: 2px;
62
+ }
63
+
64
+ .notifBody {
65
+ flex: 1;
66
+ min-width: 0;
67
+ }
68
+
69
+ .notifMeta {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 8px;
73
+ margin-bottom: 4px;
74
+ }
75
+
76
+ .notifTime {
77
+ margin-left: auto;
78
+ }
79
+
80
+ .notifMessage {
81
+ margin: 0;
82
+ font-size: 13px;
83
+ color: var(--text-primary);
84
+ word-break: break-word;
85
+ }
86
+
87
+ .notifExecId {
88
+ font-size: 11px;
89
+ font-family: monospace;
90
+ display: block;
91
+ margin-top: 4px;
92
+ }
93
+
94
+ .notifClear {
95
+ flex-shrink: 0;
96
+ }
97
+
98
+ .panelFooter {
99
+ padding: 8px 16px;
100
+ border-top: 1px solid var(--border-primary);
101
+ text-align: center;
102
+ flex-shrink: 0;
103
+ }
@@ -0,0 +1,199 @@
1
+ import * as React from 'react';
2
+ import { Badge, Dropdown, List, Typography, Tag, Empty, Space, Tooltip } from 'antd';
3
+ import { Bell, AlertTriangle, XCircle, X, CheckCheck } from 'lucide-react';
4
+ import { UIButton } from '@kb-labs/studio-ui-kit';
5
+ import styles from './kb-notification-bell.module.css';
6
+
7
+ const { Text, Paragraph } = Typography;
8
+
9
+ export interface LogNotification {
10
+ id: string;
11
+ timestamp: string;
12
+ level: 'warn' | 'error';
13
+ message: string;
14
+ plugin?: string;
15
+ executionId?: string;
16
+ error?: {
17
+ name: string;
18
+ message: string;
19
+ };
20
+ read: boolean;
21
+ }
22
+
23
+ export interface KBNotificationBellProps {
24
+ notifications: LogNotification[];
25
+ unreadCount: number;
26
+ onMarkAsRead: (id: string) => void;
27
+ onMarkAllAsRead: () => void;
28
+ onClearAll: () => void;
29
+ onClearNotification: (id: string) => void;
30
+ onNotificationClick?: (notification: LogNotification) => void;
31
+ }
32
+
33
+ export function KBNotificationBell({
34
+ notifications,
35
+ unreadCount,
36
+ onMarkAsRead,
37
+ onMarkAllAsRead,
38
+ onClearAll,
39
+ onClearNotification,
40
+ onNotificationClick,
41
+ }: KBNotificationBellProps) {
42
+ const [open, setOpen] = React.useState(false);
43
+
44
+ const formatTimestamp = (timestamp: string) => {
45
+ const date = new Date(timestamp);
46
+ const now = new Date();
47
+ const diffMs = now.getTime() - date.getTime();
48
+ const diffMins = Math.floor(diffMs / 60000);
49
+ const diffHours = Math.floor(diffMs / 3600000);
50
+ const diffDays = Math.floor(diffMs / 86400000);
51
+
52
+ if (diffMins < 1) { return 'Just now'; }
53
+ if (diffMins < 60) { return `${diffMins}m ago`; }
54
+ if (diffHours < 24) { return `${diffHours}h ago`; }
55
+ return `${diffDays}d ago`;
56
+ };
57
+
58
+ const getLevelIcon = (level: 'warn' | 'error') => {
59
+ if (level === 'error') {
60
+ return <XCircle size={16} style={{ color: 'var(--error)' }} />;
61
+ }
62
+ return <AlertTriangle size={16} style={{ color: 'var(--warning)' }} />;
63
+ };
64
+
65
+ const getLevelColor = (level: 'warn' | 'error') => {
66
+ return level === 'error' ? 'red' : 'orange';
67
+ };
68
+
69
+ const notificationContent = (
70
+ <div className={styles.panel}>
71
+ <div className={styles.panelHeader}>
72
+ <Space>
73
+ <Text strong>Notifications</Text>
74
+ {unreadCount > 0 && (
75
+ <Badge count={unreadCount} style={{ backgroundColor: 'var(--error)' }} />
76
+ )}
77
+ </Space>
78
+ <Space size="small">
79
+ {notifications.length > 0 && (
80
+ <>
81
+ <Tooltip title="Mark all as read">
82
+ <UIButton
83
+ variant="text"
84
+ size="small"
85
+ icon={<CheckCheck size={14} />}
86
+ onClick={(e) => { e.stopPropagation(); onMarkAllAsRead(); }}
87
+ />
88
+ </Tooltip>
89
+ <Tooltip title="Clear all">
90
+ <UIButton
91
+ variant="text"
92
+ size="small"
93
+ danger
94
+ icon={<X size={14} />}
95
+ onClick={(e) => { e.stopPropagation(); onClearAll(); }}
96
+ />
97
+ </Tooltip>
98
+ </>
99
+ )}
100
+ </Space>
101
+ </div>
102
+
103
+ <div className={styles.panelList}>
104
+ {notifications.length === 0 ? (
105
+ <div className={styles.emptyState}>
106
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="No critical logs" />
107
+ </div>
108
+ ) : (
109
+ <List
110
+ dataSource={notifications}
111
+ renderItem={(notification) => (
112
+ <div
113
+ className={`${styles.notifItem} ${!notification.read ? styles.unread : ''}`}
114
+ onClick={() => {
115
+ if (!notification.read) { onMarkAsRead(notification.id); }
116
+ if (onNotificationClick) {
117
+ setOpen(false);
118
+ onNotificationClick(notification);
119
+ }
120
+ }}
121
+ >
122
+ <div className={styles.notifRow}>
123
+ <div className={styles.notifIcon}>{getLevelIcon(notification.level)}</div>
124
+ <div className={styles.notifBody}>
125
+ <div className={styles.notifMeta}>
126
+ <Tag color={getLevelColor(notification.level)} style={{ margin: 0 }}>
127
+ {notification.level.toUpperCase()}
128
+ </Tag>
129
+ {notification.plugin && (
130
+ <Tag color="purple" style={{ margin: 0, fontSize: 11 }}>
131
+ {notification.plugin}
132
+ </Tag>
133
+ )}
134
+ <Text type="secondary" className={styles.notifTime} style={{ fontSize: 11 }}>
135
+ {formatTimestamp(notification.timestamp)}
136
+ </Text>
137
+ </div>
138
+ <Paragraph className={styles.notifMessage} ellipsis={{ rows: 2 }}>
139
+ {notification.error && typeof notification.error === 'object'
140
+ ? `${notification.error.name || 'Error'}: ${notification.error.message || 'Unknown error'}`
141
+ : notification.message || 'No message'}
142
+ </Paragraph>
143
+ {notification.executionId && (
144
+ <Text type="secondary" className={styles.notifExecId}>
145
+ exec: {notification.executionId.slice(0, 8)}
146
+ </Text>
147
+ )}
148
+ </div>
149
+ <div className={styles.notifClear}>
150
+ <UIButton
151
+ variant="text"
152
+ size="small"
153
+ icon={<X size={12} />}
154
+ onClick={(e) => { e.stopPropagation(); onClearNotification(notification.id); }}
155
+ />
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )}
160
+ />
161
+ )}
162
+ </div>
163
+
164
+ {notifications.length > 0 && (
165
+ <div className={styles.panelFooter}>
166
+ <UIButton
167
+ variant="link"
168
+ size="small"
169
+ onClick={() => {
170
+ setOpen(false);
171
+ window.location.href = '/observability/logs';
172
+ }}
173
+ >
174
+ View all logs →
175
+ </UIButton>
176
+ </div>
177
+ )}
178
+ </div>
179
+ );
180
+
181
+ return (
182
+ <Dropdown
183
+ open={open}
184
+ onOpenChange={setOpen}
185
+ popupRender={() => notificationContent}
186
+ trigger={['click']}
187
+ placement="bottomRight"
188
+ >
189
+ <Badge count={unreadCount} offset={[-2, 2]} size="small">
190
+ <UIButton
191
+ variant="text"
192
+ icon={<Bell size={18} />}
193
+ className={styles.bell}
194
+ style={unreadCount > 0 ? { color: 'var(--error)' } : undefined}
195
+ />
196
+ </Badge>
197
+ </Dropdown>
198
+ );
199
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import { Layout as AntLayout } from 'antd';
3
+ import { KBHeader, type KBHeaderProps } from './kb-header';
4
+ import { KBSidebar, type KBSidebarProps, type NavigationItem } from './kb-sidebar';
5
+
6
+ const { Content: AntContent } = AntLayout;
7
+
8
+ export interface KBPageLayoutProps {
9
+ headerProps?: KBHeaderProps;
10
+ sidebarProps: Omit<KBSidebarProps, 'collapsed' | 'onCollapse'>;
11
+ sidebarCollapsed?: boolean;
12
+ onSidebarCollapse?: (collapsed: boolean) => void;
13
+ statusBar?: React.ReactNode;
14
+ children?: React.ReactNode;
15
+ }
16
+
17
+ export function KBPageLayout({
18
+ headerProps,
19
+ sidebarProps,
20
+ sidebarCollapsed,
21
+ onSidebarCollapse,
22
+ statusBar,
23
+ children,
24
+ }: KBPageLayoutProps) {
25
+ const [internalCollapsed, setInternalCollapsed] = React.useState(false);
26
+
27
+ const collapsed = sidebarCollapsed !== undefined ? sidebarCollapsed : internalCollapsed;
28
+ const setCollapsed = React.useCallback(
29
+ (newCollapsed: boolean) => {
30
+ if (sidebarCollapsed === undefined) {
31
+ setInternalCollapsed(newCollapsed);
32
+ }
33
+ onSidebarCollapse?.(newCollapsed);
34
+ },
35
+ [sidebarCollapsed, onSidebarCollapse]
36
+ );
37
+
38
+ const sidebarWidth = collapsed ? (sidebarProps.collapsedWidth || 64) : (sidebarProps.width || 220);
39
+
40
+ return (
41
+ <AntLayout style={{ minHeight: '100vh' }}>
42
+ <KBHeader
43
+ {...headerProps}
44
+ sidebarCollapsed={collapsed}
45
+ onSidebarCollapse={setCollapsed}
46
+ sidebarWidth={sidebarWidth}
47
+ />
48
+ <AntLayout style={{ marginTop: 64 }}>
49
+ <KBSidebar {...sidebarProps} collapsed={collapsed} onCollapse={setCollapsed} />
50
+ <AntLayout
51
+ style={{
52
+ marginLeft: sidebarWidth,
53
+ transition: 'margin-left 0.2s',
54
+ minHeight: 'calc(100vh - 64px)',
55
+ paddingBottom: statusBar ? 28 : 0,
56
+ }}
57
+ >
58
+ <AntContent>{children}</AntContent>
59
+ </AntLayout>
60
+ </AntLayout>
61
+ {statusBar}
62
+ </AntLayout>
63
+ );
64
+ }
65
+
66
+ export type { KBHeaderProps, KBSidebarProps, NavigationItem };
@@ -0,0 +1,86 @@
1
+ .searchWrap {
2
+ padding: 16px 16px 0;
3
+ }
4
+
5
+ .searchInput {
6
+ margin-bottom: 12px;
7
+ }
8
+
9
+ .list {
10
+ max-height: 400px;
11
+ overflow-y: auto;
12
+ }
13
+
14
+ .emptyState {
15
+ padding: 40px 16px;
16
+ }
17
+
18
+ .resultItem {
19
+ padding: 12px 16px;
20
+ cursor: pointer;
21
+ border-left: 3px solid transparent;
22
+ transition: background-color 0.15s, border-color 0.15s;
23
+ }
24
+
25
+ .resultItem.selected {
26
+ background-color: var(--bg-tertiary);
27
+ border-left-color: var(--primary-color, #1890ff);
28
+ }
29
+
30
+ .categoryIcon {
31
+ width: 32px;
32
+ height: 32px;
33
+ border-radius: 6px;
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ }
38
+
39
+ .itemTitle {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 8px;
43
+ }
44
+
45
+ .itemDesc {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ }
50
+
51
+ .enterHint {
52
+ font-size: 11px;
53
+ color: var(--text-tertiary);
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 4px;
57
+ }
58
+
59
+ .footer {
60
+ padding: 12px 16px;
61
+ border-top: 1px solid var(--border-primary);
62
+ display: flex;
63
+ justify-content: space-between;
64
+ align-items: center;
65
+ font-size: 12px;
66
+ color: var(--text-tertiary);
67
+ }
68
+
69
+ .footerLeft {
70
+ display: flex;
71
+ gap: 12px;
72
+ }
73
+
74
+ .footerHint {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 4px;
78
+ }
79
+
80
+ kbd {
81
+ padding: 2px 4px;
82
+ background: var(--bg-tertiary);
83
+ border: 1px solid var(--border-primary);
84
+ border-radius: 2px;
85
+ font-size: 11px;
86
+ }