@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,152 @@
1
+ import React from 'react';
2
+ import { UICard } from '@kb-labs/studio-ui-kit';
3
+ import type { UICardVariant as UICardStatus } from '@kb-labs/studio-ui-kit';
4
+ import './HeroMetricCard.css';
5
+
6
+ export type MetricStatus = 'healthy' | 'warning' | 'critical' | 'live' | 'default';
7
+
8
+ export interface HeroMetricCardProps {
9
+ title: string;
10
+ value: string | number;
11
+ subtitle?: string;
12
+ sparklineData?: number[];
13
+ trend?: {
14
+ direction: 'up' | 'down';
15
+ value: number;
16
+ isPositive?: boolean;
17
+ };
18
+ status?: MetricStatus;
19
+ pulsing?: boolean;
20
+ onClick?: () => void;
21
+ icon?: React.ReactNode;
22
+ }
23
+
24
+ const statusColors: Record<MetricStatus, string> = {
25
+ healthy: 'var(--success)',
26
+ warning: 'var(--warning)',
27
+ critical: 'var(--error)',
28
+ live: 'var(--info)',
29
+ default: 'var(--text-secondary)',
30
+ };
31
+
32
+ const statusToVariant: Record<MetricStatus, UICardStatus> = {
33
+ healthy: 'success',
34
+ warning: 'warning',
35
+ critical: 'error',
36
+ live: 'info',
37
+ default: 'default',
38
+ };
39
+
40
+ export function HeroMetricCard({
41
+ title,
42
+ value,
43
+ subtitle,
44
+ sparklineData,
45
+ trend,
46
+ status = 'default',
47
+ pulsing = false,
48
+ onClick,
49
+ icon,
50
+ }: HeroMetricCardProps) {
51
+ const mainColor = statusColors[status];
52
+
53
+ return (
54
+ <UICard
55
+ className={`hero-metric-card ${onClick ? 'clickable' : ''} ${pulsing ? 'pulsing' : ''}`}
56
+ status={statusToVariant[status]}
57
+ onClick={onClick}
58
+ styles={{ body: { padding: '20px' } }}
59
+ style={{ cursor: onClick ? 'pointer' : 'default' }}
60
+ >
61
+ <div className="hero-metric-header">
62
+ {icon && <div className="hero-metric-icon">{icon}</div>}
63
+ <div className="hero-metric-title">{title}</div>
64
+ {pulsing && (
65
+ <div className="hero-metric-pulse" style={{ backgroundColor: mainColor }} />
66
+ )}
67
+ </div>
68
+
69
+ <div className="hero-metric-value" style={{ color: mainColor }}>
70
+ {value}
71
+ </div>
72
+
73
+ {/* Always render subtitle for consistent height */}
74
+ <div className="hero-metric-subtitle">
75
+ {subtitle || '\u00A0'}
76
+ </div>
77
+
78
+ {/* Always show footer for consistent card height */}
79
+ <div className="hero-metric-footer">
80
+ <div className="hero-metric-sparkline">
81
+ {sparklineData && sparklineData.length > 1 && (
82
+ <Sparkline data={sparklineData} color={mainColor} />
83
+ )}
84
+ </div>
85
+
86
+ <div className="hero-metric-trend">
87
+ {trend && (
88
+ <TrendIndicator
89
+ direction={trend.direction}
90
+ value={trend.value}
91
+ isPositive={trend.isPositive ?? trend.direction === 'up'}
92
+ />
93
+ )}
94
+ </div>
95
+ </div>
96
+ </UICard>
97
+ );
98
+ }
99
+
100
+ // Sparkline Component
101
+ interface SparklineProps {
102
+ data: number[];
103
+ color: string;
104
+ width?: number;
105
+ height?: number;
106
+ }
107
+
108
+ function Sparkline({ data, color, width = 100, height = 30 }: SparklineProps) {
109
+ if (data.length < 2) {return null;}
110
+
111
+ const min = Math.min(...data);
112
+ const max = Math.max(...data);
113
+ const range = max - min || 1;
114
+
115
+ const points = data.map((value, index) => {
116
+ const x = (index / (data.length - 1)) * width;
117
+ const y = height - ((value - min) / range) * height;
118
+ return `${x},${y}`;
119
+ }).join(' ');
120
+
121
+ return (
122
+ <svg width={width} height={height} className="sparkline">
123
+ <polyline
124
+ points={points}
125
+ fill="none"
126
+ stroke={color}
127
+ strokeWidth="2"
128
+ strokeLinecap="round"
129
+ strokeLinejoin="round"
130
+ />
131
+ </svg>
132
+ );
133
+ }
134
+
135
+ // TrendIndicator Component
136
+ interface TrendIndicatorProps {
137
+ direction: 'up' | 'down';
138
+ value: number;
139
+ isPositive: boolean;
140
+ }
141
+
142
+ function TrendIndicator({ direction, value, isPositive }: TrendIndicatorProps) {
143
+ const color = isPositive ? 'var(--success)' : 'var(--error)';
144
+ const arrow = direction === 'up' ? '↑' : '↓';
145
+
146
+ return (
147
+ <div className="trend-indicator" style={{ color }}>
148
+ <span className="trend-arrow">{arrow}</span>
149
+ <span className="trend-value">{Math.abs(value).toFixed(1)}%</span>
150
+ </div>
151
+ );
152
+ }
@@ -0,0 +1,146 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { UIRow, UICol } from '@kb-labs/studio-ui-kit';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import {
5
+ RiseOutlined,
6
+ ApiOutlined,
7
+ ClockCircleOutlined,
8
+ } from '@ant-design/icons';
9
+ import { HeroMetricCard } from './HeroMetricCard';
10
+ import { useDataSources } from '../../../providers/data-sources-provider';
11
+ import {
12
+ usePrometheusMetrics,
13
+ useStateBrokerStats,
14
+ } from '@kb-labs/studio-data-client';
15
+
16
+ interface MetricsHistoryPoint {
17
+ timestamp: number;
18
+ requests: number;
19
+ uptime: number;
20
+ }
21
+
22
+ export function HeroMetricsWidget() {
23
+ const sources = useDataSources();
24
+ const _navigate = useNavigate();
25
+
26
+ // Data hooks with auto-refresh
27
+ const metrics = usePrometheusMetrics(sources.observability);
28
+ const stateBroker = useStateBrokerStats(sources.observability);
29
+
30
+ // Historical data for sparklines (last 20 data points)
31
+ const [metricsHistory, setMetricsHistory] = useState<MetricsHistoryPoint[]>([]);
32
+
33
+ // Collect metrics history
34
+ useEffect(() => {
35
+ if (metrics.data) {
36
+ const newPoint: MetricsHistoryPoint = {
37
+ timestamp: Date.now(),
38
+ requests: metrics.data.requests?.total ?? 0,
39
+ uptime: calculateUptime(metrics.data),
40
+ };
41
+
42
+ setMetricsHistory(prev => {
43
+ // Keep last 20 points
44
+ return [...prev, newPoint].slice(-20);
45
+ });
46
+ }
47
+ }, [metrics.data]);
48
+
49
+ // Calculate uptime percentage
50
+ const calculateUptime = (metricsData: any) => {
51
+ if (!metricsData?.requests) {return 100;}
52
+ const total = metricsData.requests.total ?? 0;
53
+ const success = metricsData.requests.success ?? 0;
54
+ return total > 0 ? (success / total) * 100 : 100;
55
+ };
56
+
57
+ // Calculate trend from history
58
+ const calculateTrend = (data: number[]) => {
59
+ if (data.length < 2) {return null;}
60
+ const recent = data.slice(-5); // Last 5 points
61
+ const older = data.slice(-10, -5); // Previous 5 points
62
+ if (older.length === 0 || recent.length === 0) {return null;}
63
+
64
+ const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
65
+ const olderAvg = older.reduce((a, b) => a + b, 0) / older.length;
66
+
67
+ if (olderAvg === 0) {return null;}
68
+
69
+ const change = ((recentAvg - olderAvg) / olderAvg) * 100;
70
+ return {
71
+ direction: change >= 0 ? 'up' as const : 'down' as const,
72
+ value: Math.abs(change),
73
+ isPositive: change >= 0,
74
+ };
75
+ };
76
+
77
+ // Format uptime seconds to human-readable
78
+ const formatUptime = (seconds: number | undefined) => {
79
+ if (!seconds) {return '0s';}
80
+
81
+ const days = Math.floor(seconds / 86400);
82
+ const hours = Math.floor((seconds % 86400) / 3600);
83
+ const minutes = Math.floor((seconds % 3600) / 60);
84
+
85
+ if (days > 0) {return `${days}d ${hours}h`;}
86
+ if (hours > 0) {return `${hours}h ${minutes}m`;}
87
+ return `${minutes}m`;
88
+ };
89
+
90
+ // Extract sparkline data
91
+ const requestsSparkline = metricsHistory.map(p => p.requests);
92
+ const uptimeSparkline = metricsHistory.map(p => p.uptime);
93
+
94
+ // Calculate trends
95
+ const requestsTrend = calculateTrend(requestsSparkline);
96
+
97
+ // Metric values
98
+ const currentUptime = calculateUptime(metrics.data);
99
+ const totalRequests = metrics.data?.requests?.total ?? 0;
100
+ const runtimeSeconds = metrics.data?.uptime?.seconds ?? 0;
101
+
102
+ // Determine uptime status
103
+ const getUptimeStatus = () => {
104
+ if (currentUptime >= 99) {return 'healthy';}
105
+ if (currentUptime >= 95) {return 'warning';}
106
+ return 'critical';
107
+ };
108
+
109
+ return (
110
+ <UIRow gutter={[16, 16]}>
111
+ <UICol xs={24} sm={12} lg={8}>
112
+ <HeroMetricCard
113
+ title="Uptime"
114
+ value={`${currentUptime.toFixed(2)}%`}
115
+ subtitle={stateBroker.data ? `Cache: ${(stateBroker.data.hitRate * 100).toFixed(1)}%` : 'Success rate'}
116
+ sparklineData={uptimeSparkline}
117
+ status={getUptimeStatus()}
118
+ icon={<RiseOutlined />}
119
+ />
120
+ </UICol>
121
+
122
+ <UICol xs={24} sm={12} lg={8}>
123
+ <HeroMetricCard
124
+ title="Total Requests"
125
+ value={totalRequests.toLocaleString()}
126
+ subtitle={metrics.data ? `Success: ${metrics.data.requests.success.toLocaleString()}` : 'Loading...'}
127
+ sparklineData={requestsSparkline}
128
+ trend={requestsTrend ?? undefined}
129
+ status="default"
130
+ icon={<ApiOutlined />}
131
+ />
132
+ </UICol>
133
+
134
+ <UICol xs={24} sm={12} lg={8}>
135
+ <HeroMetricCard
136
+ title="Runtime"
137
+ value={formatUptime(runtimeSeconds)}
138
+ subtitle="LIVE"
139
+ status="live"
140
+ pulsing={true}
141
+ icon={<ClockCircleOutlined />}
142
+ />
143
+ </UICol>
144
+ </UIRow>
145
+ );
146
+ }
@@ -0,0 +1,197 @@
1
+ import React from 'react';
2
+ import {
3
+ UIProgress,
4
+ UIStatistic,
5
+ UIRow,
6
+ UICol,
7
+ UITag,
8
+ UIEmptyState,
9
+ UICard,
10
+ } from '@kb-labs/studio-ui-kit';
11
+ import {
12
+ CloudServerOutlined,
13
+ ThunderboltOutlined,
14
+ DatabaseOutlined,
15
+ ClockCircleOutlined,
16
+ } from '@ant-design/icons';
17
+ import { useHealthStatus } from '@kb-labs/studio-data-client';
18
+ import { useDataSources } from '../../../providers/data-sources-provider';
19
+
20
+ function formatBytes(bytes: number): string {
21
+ if (bytes === 0) {return '0 B';}
22
+ const k = 1024;
23
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
24
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
25
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
26
+ }
27
+
28
+ function formatUptime(seconds: number): string {
29
+ const days = Math.floor(seconds / 86400);
30
+ const hours = Math.floor((seconds % 86400) / 3600);
31
+ const minutes = Math.floor((seconds % 3600) / 60);
32
+
33
+ if (days > 0) {return `${days}d ${hours}h ${minutes}m`;}
34
+ if (hours > 0) {return `${hours}h ${minutes}m`;}
35
+ return `${minutes}m`;
36
+ }
37
+
38
+ function getMetricColor(value: number, warningThreshold: number, criticalThreshold: number): string {
39
+ if (value >= criticalThreshold) {return 'var(--error)';}
40
+ if (value >= warningThreshold) {return 'var(--warning)';}
41
+ return 'var(--success)';
42
+ }
43
+
44
+ export function SystemResourcesWidget() {
45
+ const sources = useDataSources();
46
+ const healthQuery = useHealthStatus(sources.system);
47
+ const snapshot = healthQuery.data?.snapshot;
48
+ const resources = snapshot?.snapshot;
49
+ const topOperations = snapshot?.topOperations ?? [];
50
+ const state = snapshot?.state ?? 'insufficient_data';
51
+ const cpuPercent = resources?.cpuPercent ?? 0;
52
+ const rssBytes = resources?.rssBytes ?? 0;
53
+ const heapUsedBytes = resources?.heapUsedBytes ?? 0;
54
+ const eventLoopLagMs = resources?.eventLoopLagMs ?? 0;
55
+ const activeOperations = resources?.activeOperations ?? 0;
56
+
57
+ return (
58
+ <UICard
59
+ title={
60
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
61
+ <CloudServerOutlined />
62
+ <span>Runtime Resources</span>
63
+ {snapshot && (
64
+ <UITag color={state === 'active' ? 'green' : state === 'partial_observability' ? 'orange' : 'red'}>
65
+ {state}
66
+ </UITag>
67
+ )}
68
+ </div>
69
+ }
70
+ style={{ height: '100%' }}
71
+ >
72
+ {healthQuery.isLoading ? (
73
+ <div style={{ height: 220, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-tertiary)' }}>
74
+ Loading runtime resources...
75
+ </div>
76
+ ) : !snapshot || !resources ? (
77
+ <UIEmptyState
78
+ image={UIEmptyState.PRESENTED_IMAGE_SIMPLE}
79
+ description="No runtime snapshot available"
80
+ style={{ marginTop: 40 }}
81
+ />
82
+ ) : (
83
+ <>
84
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
85
+ <CloudServerOutlined style={{ fontSize: 18, color: 'var(--info)' }} />
86
+ <span style={{ fontSize: 14, fontWeight: 500 }}>{snapshot.instanceId}</span>
87
+ <UITag color={snapshot.status === 'healthy' ? 'green' : snapshot.status === 'degraded' ? 'orange' : 'red'}>
88
+ {snapshot.status}
89
+ </UITag>
90
+ </div>
91
+
92
+ <UIRow gutter={[16, 16]}>
93
+ <UICol span={12}>
94
+ <div>
95
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
96
+ <ThunderboltOutlined style={{ color: getMetricColor(cpuPercent, 70, 90) }} />
97
+ <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>CPU Usage</span>
98
+ </div>
99
+ <UIProgress
100
+ percent={parseFloat(cpuPercent.toFixed(1))}
101
+ strokeColor={getMetricColor(cpuPercent, 70, 90)}
102
+ size="small"
103
+ format={(percent) => `${percent}%`}
104
+ />
105
+ </div>
106
+ </UICol>
107
+
108
+ <UICol span={12}>
109
+ <UIStatistic
110
+ title={
111
+ <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
112
+ <DatabaseOutlined style={{ marginRight: 4 }} />
113
+ RSS
114
+ </span>
115
+ }
116
+ value={formatBytes(rssBytes)}
117
+ valueStyle={{ fontSize: 14 }}
118
+ />
119
+ </UICol>
120
+
121
+ <UICol span={12}>
122
+ <UIStatistic
123
+ title={
124
+ <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
125
+ <DatabaseOutlined style={{ marginRight: 4 }} />
126
+ Heap Used
127
+ </span>
128
+ }
129
+ value={formatBytes(heapUsedBytes)}
130
+ valueStyle={{ fontSize: 14 }}
131
+ />
132
+ </UICol>
133
+
134
+ <UICol span={12}>
135
+ <UIStatistic
136
+ title={
137
+ <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
138
+ <ClockCircleOutlined style={{ marginRight: 4 }} />
139
+ Uptime
140
+ </span>
141
+ }
142
+ value={formatUptime(snapshot.uptimeSec)}
143
+ valueStyle={{ fontSize: 14 }}
144
+ />
145
+ </UICol>
146
+
147
+ <UICol span={12}>
148
+ <UIStatistic
149
+ title="Event Loop Lag"
150
+ value={eventLoopLagMs.toFixed(1)}
151
+ suffix="ms"
152
+ valueStyle={{
153
+ fontSize: 20,
154
+ color: getMetricColor(eventLoopLagMs, 100, 200),
155
+ }}
156
+ />
157
+ </UICol>
158
+
159
+ <UICol span={12}>
160
+ <UIStatistic
161
+ title="Active Operations"
162
+ value={activeOperations}
163
+ valueStyle={{ fontSize: 20 }}
164
+ />
165
+ </UICol>
166
+ </UIRow>
167
+
168
+ <div style={{ marginTop: 24 }}>
169
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 8 }}>Top Operations</div>
170
+ {topOperations.length > 0 ? (
171
+ topOperations.map((operation) => (
172
+ <div
173
+ key={operation.operation}
174
+ style={{
175
+ display: 'flex',
176
+ justifyContent: 'space-between',
177
+ alignItems: 'center',
178
+ padding: '8px 0',
179
+ borderTop: '1px solid var(--border-secondary)',
180
+ fontSize: 12,
181
+ }}
182
+ >
183
+ <span style={{ color: 'var(--text-primary)' }}>{operation.operation.replace(/^http\./, '')}</span>
184
+ <span style={{ color: 'var(--text-secondary)' }}>
185
+ {operation.count ?? 0} req · {(operation.avgDurationMs ?? 0).toFixed(1)}ms avg
186
+ </span>
187
+ </div>
188
+ ))
189
+ ) : (
190
+ <div style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>No operation samples available</div>
191
+ )}
192
+ </div>
193
+ </>
194
+ )}
195
+ </UICard>
196
+ );
197
+ }
@@ -0,0 +1,22 @@
1
+ import { UIEmptyState, UIButton, UIFlex } from '@kb-labs/studio-ui-kit';
2
+ import { PlusOutlined } from '@ant-design/icons';
3
+
4
+ interface DashboardEmptyProps {
5
+ onAddWidget: () => void;
6
+ }
7
+
8
+ export function DashboardEmpty({ onAddWidget }: DashboardEmptyProps) {
9
+ return (
10
+ <UIFlex align="center" justify="center" style={{ minHeight: '400px' }}>
11
+ <UIEmptyState
12
+ useDefaultImage
13
+ description="No widgets added yet"
14
+ action={
15
+ <UIButton variant="primary" icon={<PlusOutlined />} onClick={onAddWidget}>
16
+ Add Widget
17
+ </UIButton>
18
+ }
19
+ />
20
+ </UIFlex>
21
+ );
22
+ }
@@ -0,0 +1,59 @@
1
+ import { UICard, UIBadge, UIRow, UICol } from '@kb-labs/studio-ui-kit';
2
+ import { HolderOutlined } from '@ant-design/icons';
3
+ import { useRegistryV2 } from '../../../providers/registry-v2-provider';
4
+ import { KBStatCard } from '@/components/ui';
5
+
6
+ export function DashboardKpisWidget() {
7
+ const { registry } = useRegistryV2();
8
+ const health = null as { pluginsMounted: number; pluginsFailed: number; ready: boolean; redisHealthy: boolean; redisEnabled: boolean } | null; // TODO: health status via separate endpoint
9
+
10
+ const totalPlugins = registry.plugins?.length ?? 0;
11
+ const pluginsMounted = health?.pluginsMounted ?? 0;
12
+ const pluginsFailed = health?.pluginsFailed ?? 0;
13
+ const systemStatus = health?.ready ? 'Healthy' : 'Degraded';
14
+ const statusType = health?.ready ? 'success' : 'error';
15
+
16
+ return (
17
+ <UICard
18
+ title={
19
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
20
+ <HolderOutlined className="drag-handle" style={{ cursor: 'grab', color: 'var(--text-tertiary)' }} />
21
+ <span>System Status</span>
22
+ <UIBadge variant={statusType as 'success' | 'error'} />
23
+ </div>
24
+ }
25
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
26
+ styles={{ body: { flex: 1, padding: '16px' } }}
27
+ >
28
+ <UIRow gutter={[16, 16]} style={{ height: '100%' }}>
29
+ <UICol span={6}>
30
+ <KBStatCard
31
+ label="Status"
32
+ value={systemStatus}
33
+ variant={health?.ready ? 'positive' : 'negative'}
34
+ />
35
+ </UICol>
36
+ <UICol span={6}>
37
+ <KBStatCard
38
+ label="Plugins Mounted"
39
+ value={`${pluginsMounted} of ${totalPlugins}`}
40
+ />
41
+ </UICol>
42
+ <UICol span={6}>
43
+ <KBStatCard
44
+ label="Failed"
45
+ value={pluginsFailed.toString()}
46
+ variant={pluginsFailed > 0 ? 'negative' : 'positive'}
47
+ />
48
+ </UICol>
49
+ <UICol span={6}>
50
+ <KBStatCard
51
+ label="Redis"
52
+ value={health?.redisHealthy ? 'Healthy' : health?.redisEnabled ? 'Unhealthy' : 'Disabled'}
53
+ variant={health?.redisHealthy ? 'positive' : health?.redisEnabled ? 'negative' : 'default'}
54
+ />
55
+ </UICol>
56
+ </UIRow>
57
+ </UICard>
58
+ );
59
+ }
@@ -0,0 +1,13 @@
1
+ import { KBStatCard, KBStatGrid } from '@/components/ui';
2
+
3
+ export function DashboardKpis() {
4
+ return (
5
+ <KBStatGrid columns={4}>
6
+ <KBStatCard label="Audit Status" value="Unavailable" />
7
+ <KBStatCard label="Packages OK" value="-" />
8
+ <KBStatCard label="Failed" value="-" />
9
+ <KBStatCard label="Warnings" value="-" />
10
+ </KBStatGrid>
11
+ );
12
+ }
13
+
@@ -0,0 +1,85 @@
1
+ import { UICard } from '@kb-labs/studio-ui-kit';
2
+ import { HolderOutlined } from '@ant-design/icons';
3
+ import { Column } from '@ant-design/plots';
4
+ import { usePrometheusMetrics } from '@kb-labs/studio-data-client';
5
+ import { useDataSources } from '../../../providers/data-sources-provider';
6
+
7
+ export function DashboardLatencyPercentilesWidget() {
8
+ const sources = useDataSources();
9
+ const { data: metrics } = usePrometheusMetrics(sources.observability);
10
+
11
+ const min = metrics?.latency?.min ?? 0;
12
+ const average = metrics?.latency?.average ?? 0;
13
+ const p50 = metrics?.latency?.p50 ?? 0;
14
+ const p95 = metrics?.latency?.p95 ?? 0;
15
+ const p99 = metrics?.latency?.p99 ?? 0;
16
+ const max = metrics?.latency?.max ?? 0;
17
+
18
+ const chartData = [
19
+ { metric: 'Min', value: min, color: 'var(--success)' },
20
+ { metric: 'Avg', value: average, color: 'var(--info)' },
21
+ { metric: 'p50', value: p50, color: 'var(--info)' },
22
+ { metric: 'p95', value: p95, color: 'var(--warning)' },
23
+ { metric: 'p99', value: p99, color: 'var(--warning)' },
24
+ { metric: 'Max', value: max, color: 'var(--error)' },
25
+ ];
26
+
27
+ const config = {
28
+ data: chartData,
29
+ xField: 'metric',
30
+ yField: 'value',
31
+ seriesField: 'metric',
32
+ columnStyle: (datum: any) => {
33
+ return {
34
+ fill: datum.color,
35
+ };
36
+ },
37
+ label: {
38
+ position: 'top' as const,
39
+ formatter: (datum: any) => `${datum.value.toFixed(1)}ms`,
40
+ style: {
41
+ fontSize: 12,
42
+ fill: 'var(--text-secondary)',
43
+ },
44
+ },
45
+ xAxis: {
46
+ label: {
47
+ style: {
48
+ fontSize: 12,
49
+ fill: 'var(--text-secondary)',
50
+ },
51
+ },
52
+ },
53
+ yAxis: {
54
+ label: {
55
+ formatter: (v: string) => `${v}ms`,
56
+ style: {
57
+ fontSize: 12,
58
+ fill: 'var(--text-secondary)',
59
+ },
60
+ },
61
+ },
62
+ legend: false,
63
+ height: 280,
64
+ };
65
+
66
+ return (
67
+ <UICard
68
+ title={
69
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
70
+ <HolderOutlined className="drag-handle" style={{ cursor: 'grab', color: 'var(--text-tertiary)' }} />
71
+ <span>Latency Distribution (ms)</span>
72
+ </div>
73
+ }
74
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
75
+ styles={{ body: { flex: 1, padding: '16px', display: 'flex', flexDirection: 'column' } }}
76
+ >
77
+ {metrics && <Column {...config} />}
78
+ {!metrics && (
79
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1 }}>
80
+ Loading...
81
+ </div>
82
+ )}
83
+ </UICard>
84
+ );
85
+ }