@camstack/addon-admin-ui 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -0
- package/{index.html → dist/index.html} +3 -1
- package/dist/server/addon.d.ts +11 -0
- package/dist/server/addon.js +50 -0
- package/dist/server/addon.js.map +1 -0
- package/package.json +5 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -339
- package/src/components/addons/AddonUploadZone.tsx +0 -307
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -119
- package/src/components/agents/AgentCard.tsx +0 -281
- package/src/components/agents/AgentLogs.tsx +0 -231
- package/src/components/agents/ProcessList.tsx +0 -127
- package/src/components/agents/ProcessTree.tsx +0 -369
- package/src/components/agents/TaskList.tsx +0 -68
- package/src/components/cameras/CameraCard.tsx +0 -60
- package/src/components/cameras/LiveEventsPanel.tsx +0 -91
- package/src/components/cameras/ProviderSection.tsx +0 -50
- package/src/components/cameras/StreamArea.tsx +0 -107
- package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
- package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
- package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
- package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
- package/src/components/dashboard/BlockPicker.tsx +0 -54
- package/src/components/dashboard/BlockWrapper.tsx +0 -97
- package/src/components/dashboard/DashboardGrid.tsx +0 -160
- package/src/components/dashboard/block-registry.ts +0 -15
- package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
- package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
- package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
- package/src/components/dashboard/blocks/index.ts +0 -32
- package/src/components/device/DeviceHeader.tsx +0 -116
- package/src/components/device/FloatingPanel.tsx +0 -132
- package/src/components/device/FloatingPanelManager.tsx +0 -167
- package/src/components/device/PanelContent.tsx +0 -196
- package/src/components/device/QuickConfigWizard.tsx +0 -507
- package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
- package/src/components/device/tabs/EventsTab.tsx +0 -19
- package/src/components/device/tabs/LogsTab.tsx +0 -22
- package/src/components/device/tabs/OverviewTab.tsx +0 -104
- package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
- package/src/components/device/tabs/RecordingTab.tsx +0 -47
- package/src/components/device/tabs/ReplTab.tsx +0 -153
- package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
- package/src/components/device/tabs/ZonesTab.tsx +0 -98
- package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
- package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
- package/src/components/device/zone-editor/ZoneList.tsx +0 -150
- package/src/components/form-builder/FormBuilder.tsx +0 -135
- package/src/components/form-builder/FormField.tsx +0 -732
- package/src/components/form-builder/ModelSelector.tsx +0 -239
- package/src/components/integrations/AddDeviceDialog.tsx +0 -205
- package/src/components/integrations/CompactDeviceCard.tsx +0 -35
- package/src/components/integrations/DeviceCard.tsx +0 -29
- package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
- package/src/components/integrations/DeviceGrid.tsx +0 -79
- package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
- package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
- package/src/components/integrations/IntegrationCard.tsx +0 -40
- package/src/components/integrations/IntegrationWizard.tsx +0 -171
- package/src/components/integrations/ProviderConfigForm.tsx +0 -89
- package/src/components/integrations/ProviderPicker.tsx +0 -91
- package/src/components/integrations/SnapshotPopover.tsx +0 -68
- package/src/components/metrics/AgentLoad.tsx +0 -113
- package/src/components/metrics/IntegrationUsage.tsx +0 -90
- package/src/components/metrics/PipelineStatus.tsx +0 -105
- package/src/components/metrics/ProcessResources.tsx +0 -139
- package/src/components/pipeline/PhaseSettings.tsx +0 -131
- package/src/components/shared/CapabilityBadges.tsx +0 -30
- package/src/components/shared/ProviderIcon.tsx +0 -42
- package/src/components/shared/StatusBadge.tsx +0 -23
- package/src/components/shared/WebRtcPlayer.tsx +0 -211
- package/src/components/timeline/EventMarker.tsx +0 -32
- package/src/components/timeline/TimelineBar.tsx +0 -131
- package/src/components/ui/ConfirmDialog.tsx +0 -115
- package/src/components/ui/ToastContainer.tsx +0 -92
- package/src/contexts/auth-context.tsx +0 -91
- package/src/hooks/useBackendClient.ts +0 -6
- package/src/hooks/useTheme.ts +0 -1
- package/src/i18n/en.json +0 -164
- package/src/i18n/index.ts +0 -29
- package/src/i18n/it.json +0 -164
- package/src/index.css +0 -63
- package/src/layouts/AddonPageLoader.tsx +0 -120
- package/src/layouts/AppLayout.tsx +0 -238
- package/src/layouts/ProtectedRoute.tsx +0 -25
- package/src/lib/addon-page-context.ts +0 -29
- package/src/lib/backend.ts +0 -16
- package/src/main.tsx +0 -21
- package/src/pages/AccessDenied.tsx +0 -22
- package/src/pages/Cameras.tsx +0 -127
- package/src/pages/Dashboard.tsx +0 -6
- package/src/pages/DeviceDetail.tsx +0 -175
- package/src/pages/IntegrationDetail.tsx +0 -224
- package/src/pages/Integrations.tsx +0 -330
- package/src/pages/Login.tsx +0 -106
- package/src/pages/Metrics.tsx +0 -18
- package/src/pages/PipelineConfig.tsx +0 -282
- package/src/pages/Showroom.tsx +0 -351
- package/src/pages/Timeline.tsx +0 -269
- package/src/pages/system/Addons.tsx +0 -525
- package/src/pages/system/Agents.tsx +0 -362
- package/src/pages/system/Logs.tsx +0 -131
- package/src/pages/system/Models.tsx +0 -102
- package/src/pages/system/Processes.tsx +0 -129
- package/src/pages/system/Repl.tsx +0 -148
- package/src/pages/system/Settings.tsx +0 -168
- package/src/pages/system/Users.tsx +0 -174
- package/src/server/addon.ts +0 -54
- package/src/types/config-ui.ts +0 -210
- package/src/types/dashboard.ts +0 -39
- package/tsconfig.json +0 -29
- package/tsconfig.server.json +0 -16
- package/tsup.config.ts +0 -20
- package/vite.config.ts +0 -68
- /package/{public → dist}/brand/logo-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
- /package/{public → dist}/brand/logo-light.svg +0 -0
- /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
- /package/{public → dist}/brand/logo-wide-light.svg +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
- /package/{public → dist}/vendor/react.mjs +0 -0
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Settings, X, GripVertical } from 'lucide-react'
|
|
3
|
-
import type { DashboardBlock } from '../../types/dashboard'
|
|
4
|
-
|
|
5
|
-
interface BlockWrapperProps {
|
|
6
|
-
block: DashboardBlock
|
|
7
|
-
config: Record<string, unknown>
|
|
8
|
-
size: { w: number; h: number }
|
|
9
|
-
onRemove: () => void
|
|
10
|
-
onConfigChange: (config: Record<string, unknown>) => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function BlockWrapper({ block, config, size, onRemove, onConfigChange }: BlockWrapperProps) {
|
|
14
|
-
const [showConfig, setShowConfig] = useState(false)
|
|
15
|
-
const Component = block.component
|
|
16
|
-
const Icon = block.icon
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="flex flex-col h-full rounded-lg border border-border bg-surface shadow-sm shadow-black/5 overflow-hidden hover:shadow-md hover:shadow-black/8 transition-shadow">
|
|
20
|
-
{/* Title bar */}
|
|
21
|
-
<div className="drag-handle flex items-center h-8 px-2 border-b border-border bg-surface/80 shrink-0 cursor-move group">
|
|
22
|
-
<GripVertical className="h-3 w-3 text-foreground-subtle/30 group-hover:text-foreground-subtle transition-colors mr-1" />
|
|
23
|
-
<Icon className="h-3 w-3 text-primary/70 mr-1.5 shrink-0" />
|
|
24
|
-
<span className="text-[11px] font-semibold text-foreground/80 truncate flex-1">
|
|
25
|
-
{block.name}
|
|
26
|
-
</span>
|
|
27
|
-
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
28
|
-
{block.configSchema && block.configSchema.length > 0 && (
|
|
29
|
-
<button
|
|
30
|
-
onClick={(e) => { e.stopPropagation(); setShowConfig(!showConfig) }}
|
|
31
|
-
className="p-1 rounded-md hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
|
|
32
|
-
>
|
|
33
|
-
<Settings className="h-3 w-3" />
|
|
34
|
-
</button>
|
|
35
|
-
)}
|
|
36
|
-
<button
|
|
37
|
-
onClick={(e) => { e.stopPropagation(); onRemove() }}
|
|
38
|
-
className="p-1 rounded-md hover:bg-danger/10 text-foreground-subtle hover:text-danger transition-colors"
|
|
39
|
-
>
|
|
40
|
-
<X className="h-3 w-3" />
|
|
41
|
-
</button>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
{/* Config panel */}
|
|
46
|
-
{showConfig && block.configSchema && (
|
|
47
|
-
<div className="p-2.5 border-b border-border bg-background/50 space-y-2">
|
|
48
|
-
{block.configSchema.map((field) => (
|
|
49
|
-
<label key={field.key} className="flex items-center gap-2 text-[11px]">
|
|
50
|
-
<span className="text-foreground-subtle w-20 shrink-0">{field.label}</span>
|
|
51
|
-
{field.type === 'number' && (
|
|
52
|
-
<input
|
|
53
|
-
type="number"
|
|
54
|
-
value={(config[field.key] as number) ?? field.defaultValue ?? ''}
|
|
55
|
-
onChange={(e) => onConfigChange({ ...config, [field.key]: Number(e.target.value) })}
|
|
56
|
-
className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
|
|
57
|
-
/>
|
|
58
|
-
)}
|
|
59
|
-
{field.type === 'text' && (
|
|
60
|
-
<input
|
|
61
|
-
type="text"
|
|
62
|
-
value={(config[field.key] as string) ?? field.defaultValue ?? ''}
|
|
63
|
-
onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.value })}
|
|
64
|
-
className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
|
|
65
|
-
/>
|
|
66
|
-
)}
|
|
67
|
-
{field.type === 'boolean' && (
|
|
68
|
-
<input
|
|
69
|
-
type="checkbox"
|
|
70
|
-
checked={(config[field.key] as boolean) ?? field.defaultValue ?? false}
|
|
71
|
-
onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.checked })}
|
|
72
|
-
className="accent-primary"
|
|
73
|
-
/>
|
|
74
|
-
)}
|
|
75
|
-
{field.type === 'select' && (
|
|
76
|
-
<select
|
|
77
|
-
value={(config[field.key] as string) ?? field.defaultValue ?? ''}
|
|
78
|
-
onChange={(e) => onConfigChange({ ...config, [field.key]: e.target.value })}
|
|
79
|
-
className="flex-1 rounded-md border border-border bg-surface px-2 py-1 text-foreground text-[11px] focus:border-primary outline-none"
|
|
80
|
-
>
|
|
81
|
-
{field.options?.map((opt) => (
|
|
82
|
-
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
83
|
-
))}
|
|
84
|
-
</select>
|
|
85
|
-
)}
|
|
86
|
-
</label>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
|
|
91
|
-
{/* Block content */}
|
|
92
|
-
<div className="flex-1 overflow-auto p-3">
|
|
93
|
-
<Component config={config} size={size} />
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react'
|
|
2
|
-
import { ResponsiveGridLayout } from 'react-grid-layout'
|
|
3
|
-
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
const GridLayout = ResponsiveGridLayout as any
|
|
6
|
-
import { Plus, LayoutDashboard } from 'lucide-react'
|
|
7
|
-
import { BlockWrapper } from './BlockWrapper'
|
|
8
|
-
import { BlockPicker } from './BlockPicker'
|
|
9
|
-
import { getBlock } from './block-registry'
|
|
10
|
-
import type { DashboardBlock, DashboardLayoutItem, DashboardState } from '../../types/dashboard'
|
|
11
|
-
import 'react-grid-layout/css/styles.css'
|
|
12
|
-
|
|
13
|
-
const STORAGE_KEY = 'camstack-dashboard-state'
|
|
14
|
-
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 0 }
|
|
15
|
-
const COLS = { lg: 12, md: 8, sm: 4, xs: 2 }
|
|
16
|
-
|
|
17
|
-
function loadState(): DashboardState {
|
|
18
|
-
try {
|
|
19
|
-
const raw = localStorage.getItem(STORAGE_KEY)
|
|
20
|
-
if (raw) return JSON.parse(raw)
|
|
21
|
-
} catch { /* ignore */ }
|
|
22
|
-
return { items: [] }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function saveState(state: DashboardState): void {
|
|
26
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let instanceCounter = 0
|
|
30
|
-
|
|
31
|
-
export function DashboardGrid() {
|
|
32
|
-
const [state, setState] = useState<DashboardState>(loadState)
|
|
33
|
-
const [pickerOpen, setPickerOpen] = useState(false)
|
|
34
|
-
|
|
35
|
-
const updateState = useCallback((next: DashboardState) => {
|
|
36
|
-
setState(next)
|
|
37
|
-
saveState(next)
|
|
38
|
-
}, [])
|
|
39
|
-
|
|
40
|
-
const handleAddBlock = useCallback((block: DashboardBlock) => {
|
|
41
|
-
instanceCounter++
|
|
42
|
-
const instanceId = `${block.id}-${instanceCounter}`
|
|
43
|
-
const defaultConfig: Record<string, unknown> = {}
|
|
44
|
-
block.configSchema?.forEach((f) => {
|
|
45
|
-
if (f.defaultValue !== undefined) defaultConfig[f.key] = f.defaultValue
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const newItem: DashboardLayoutItem = {
|
|
49
|
-
i: instanceId,
|
|
50
|
-
blockId: block.id,
|
|
51
|
-
x: 0,
|
|
52
|
-
y: Infinity, // place at bottom
|
|
53
|
-
w: block.defaultSize.w,
|
|
54
|
-
h: block.defaultSize.h,
|
|
55
|
-
config: defaultConfig,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
updateState({ items: [...state.items, newItem] })
|
|
59
|
-
}, [state, updateState])
|
|
60
|
-
|
|
61
|
-
const handleRemoveBlock = useCallback((instanceId: string) => {
|
|
62
|
-
updateState({ items: state.items.filter((item) => item.i !== instanceId) })
|
|
63
|
-
}, [state, updateState])
|
|
64
|
-
|
|
65
|
-
const handleConfigChange = useCallback((instanceId: string, config: Record<string, unknown>) => {
|
|
66
|
-
updateState({
|
|
67
|
-
items: state.items.map((item) =>
|
|
68
|
-
item.i === instanceId ? { ...item, config } : item,
|
|
69
|
-
),
|
|
70
|
-
})
|
|
71
|
-
}, [state, updateState])
|
|
72
|
-
|
|
73
|
-
const handleLayoutChange = useCallback((layout: any) => {
|
|
74
|
-
updateState({
|
|
75
|
-
items: state.items.map((item) => {
|
|
76
|
-
const layoutItem = layout.find((l: any) => l.i === item.i)
|
|
77
|
-
if (!layoutItem) return item
|
|
78
|
-
return { ...item, x: layoutItem.x, y: layoutItem.y, w: layoutItem.w, h: layoutItem.h }
|
|
79
|
-
}),
|
|
80
|
-
})
|
|
81
|
-
}, [state, updateState])
|
|
82
|
-
|
|
83
|
-
const gridLayout = state.items.map((item) => {
|
|
84
|
-
const block = getBlock(item.blockId)
|
|
85
|
-
return {
|
|
86
|
-
i: item.i,
|
|
87
|
-
x: item.x,
|
|
88
|
-
y: item.y,
|
|
89
|
-
w: item.w,
|
|
90
|
-
h: item.h,
|
|
91
|
-
minW: block?.minSize?.w ?? 2,
|
|
92
|
-
minH: block?.minSize?.h ?? 2,
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<div className="p-4">
|
|
98
|
-
<div className="flex items-center justify-between mb-4">
|
|
99
|
-
<h1 className="text-lg font-semibold text-foreground">Dashboard</h1>
|
|
100
|
-
<button
|
|
101
|
-
onClick={() => setPickerOpen(true)}
|
|
102
|
-
className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
|
|
103
|
-
>
|
|
104
|
-
<Plus className="h-3.5 w-3.5" />
|
|
105
|
-
Add Block
|
|
106
|
-
</button>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
{state.items.length === 0 ? (
|
|
110
|
-
<div className="flex flex-col items-center justify-center py-24 text-foreground-subtle">
|
|
111
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-surface border border-border mb-4">
|
|
112
|
-
<LayoutDashboard className="h-8 w-8 text-foreground-subtle/40" />
|
|
113
|
-
</div>
|
|
114
|
-
<p className="text-sm font-medium text-foreground/70">Your dashboard is empty</p>
|
|
115
|
-
<p className="text-xs text-foreground-subtle mt-1 mb-4">Add blocks to monitor your cameras and system</p>
|
|
116
|
-
<button
|
|
117
|
-
onClick={() => setPickerOpen(true)}
|
|
118
|
-
className="flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground shadow-md shadow-primary/20 hover:shadow-lg transition-all"
|
|
119
|
-
>
|
|
120
|
-
<Plus className="h-3.5 w-3.5" />
|
|
121
|
-
Add your first block
|
|
122
|
-
</button>
|
|
123
|
-
</div>
|
|
124
|
-
) : (
|
|
125
|
-
<GridLayout
|
|
126
|
-
layouts={{ lg: gridLayout }}
|
|
127
|
-
breakpoints={BREAKPOINTS}
|
|
128
|
-
cols={COLS}
|
|
129
|
-
rowHeight={60}
|
|
130
|
-
draggableHandle=".drag-handle"
|
|
131
|
-
onLayoutChange={handleLayoutChange}
|
|
132
|
-
isResizable
|
|
133
|
-
isDraggable
|
|
134
|
-
>
|
|
135
|
-
{state.items.map((item) => {
|
|
136
|
-
const block = getBlock(item.blockId)
|
|
137
|
-
if (!block) return <div key={item.i} />
|
|
138
|
-
return (
|
|
139
|
-
<div key={item.i}>
|
|
140
|
-
<BlockWrapper
|
|
141
|
-
block={block}
|
|
142
|
-
config={item.config}
|
|
143
|
-
size={{ w: item.w, h: item.h }}
|
|
144
|
-
onRemove={() => handleRemoveBlock(item.i)}
|
|
145
|
-
onConfigChange={(config) => handleConfigChange(item.i, config)}
|
|
146
|
-
/>
|
|
147
|
-
</div>
|
|
148
|
-
)
|
|
149
|
-
})}
|
|
150
|
-
</GridLayout>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
<BlockPicker
|
|
154
|
-
open={pickerOpen}
|
|
155
|
-
onClose={() => setPickerOpen(false)}
|
|
156
|
-
onSelect={handleAddBlock}
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { DashboardBlock } from '../../types/dashboard'
|
|
2
|
-
|
|
3
|
-
const registry = new Map<string, DashboardBlock>()
|
|
4
|
-
|
|
5
|
-
export function registerBlock(block: DashboardBlock): void {
|
|
6
|
-
registry.set(block.id, block)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getBlock(id: string): DashboardBlock | undefined {
|
|
10
|
-
return registry.get(id)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getAllBlocks(): DashboardBlock[] {
|
|
14
|
-
return Array.from(registry.values())
|
|
15
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../../hooks/useBackendClient'
|
|
3
|
-
import type { BlockProps } from '../../../types/dashboard'
|
|
4
|
-
|
|
5
|
-
const STAGE_ORDER = [
|
|
6
|
-
'class-filter', 'tracker', 'sub-detection', 'recognition',
|
|
7
|
-
'zone-analysis', 'event-generation', 'object-snapshot',
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
export function PipelineStagesBlock({ config, size }: BlockProps) {
|
|
11
|
-
const client = useBackendClient()
|
|
12
|
-
const { data, isLoading, error } = useQuery({
|
|
13
|
-
queryKey: ['pipelines'],
|
|
14
|
-
queryFn: () => client.listPipelines(),
|
|
15
|
-
refetchInterval: 5_000,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
if (isLoading) return <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
19
|
-
if (error) return <div className="text-xs text-danger">Failed to load pipelines</div>
|
|
20
|
-
|
|
21
|
-
const pipelines = (data ?? []) as Array<Record<string, unknown>>
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="space-y-2 text-xs">
|
|
25
|
-
<div className="flex justify-between text-foreground-subtle">
|
|
26
|
-
<span>Active pipelines</span>
|
|
27
|
-
<span className="font-medium text-foreground">{pipelines.length}</span>
|
|
28
|
-
</div>
|
|
29
|
-
<div className="space-y-0.5">
|
|
30
|
-
{STAGE_ORDER.map((stage) => (
|
|
31
|
-
<div key={stage} className="flex items-center gap-1.5">
|
|
32
|
-
<div className="h-1.5 w-1.5 rounded-full bg-success shrink-0" />
|
|
33
|
-
<span className="text-foreground-subtle">{stage}</span>
|
|
34
|
-
</div>
|
|
35
|
-
))}
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../../hooks/useBackendClient'
|
|
3
|
-
import type { BlockProps } from '../../../types/dashboard'
|
|
4
|
-
|
|
5
|
-
interface SystemInfo {
|
|
6
|
-
version?: string
|
|
7
|
-
uptime?: number
|
|
8
|
-
nodeVersion?: string
|
|
9
|
-
platform?: string
|
|
10
|
-
[key: string]: unknown
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatUptime(seconds: number): string {
|
|
14
|
-
if (seconds < 60) return `${Math.floor(seconds)}s`
|
|
15
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
16
|
-
const h = Math.floor(seconds / 3600)
|
|
17
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
18
|
-
return `${h}h ${m}m`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function StorageBlock({ config: _config, size: _size }: BlockProps) {
|
|
22
|
-
const client = useBackendClient()
|
|
23
|
-
|
|
24
|
-
const { data: systemInfo, isLoading, error } = useQuery<SystemInfo>({
|
|
25
|
-
queryKey: ['system-info'],
|
|
26
|
-
queryFn: async () => {
|
|
27
|
-
const result = await client.getSystemInfo()
|
|
28
|
-
return result as SystemInfo
|
|
29
|
-
},
|
|
30
|
-
refetchInterval: 30_000,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const { data: addons } = useQuery({
|
|
34
|
-
queryKey: ['storage-addons'],
|
|
35
|
-
queryFn: () => client.listAddons(),
|
|
36
|
-
refetchInterval: 60_000,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
if (isLoading) return <div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
40
|
-
if (error) return <div className="text-xs text-danger">Failed to load system info</div>
|
|
41
|
-
|
|
42
|
-
const addonList = (addons ?? []) as Array<{ manifest: { id: string } }>
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-1.5 text-xs">
|
|
46
|
-
<div className="flex justify-between">
|
|
47
|
-
<span className="text-foreground-subtle">Platform</span>
|
|
48
|
-
<span className="text-foreground font-medium">{systemInfo?.platform ?? 'N/A'}</span>
|
|
49
|
-
</div>
|
|
50
|
-
<div className="flex justify-between">
|
|
51
|
-
<span className="text-foreground-subtle">Uptime</span>
|
|
52
|
-
<span className="text-foreground font-medium">
|
|
53
|
-
{systemInfo?.uptime != null ? formatUptime(systemInfo.uptime) : 'N/A'}
|
|
54
|
-
</span>
|
|
55
|
-
</div>
|
|
56
|
-
<div className="flex justify-between">
|
|
57
|
-
<span className="text-foreground-subtle">Node</span>
|
|
58
|
-
<span className="text-foreground font-medium font-mono">{systemInfo?.nodeVersion ?? 'N/A'}</span>
|
|
59
|
-
</div>
|
|
60
|
-
<div className="flex justify-between">
|
|
61
|
-
<span className="text-foreground-subtle">Addons</span>
|
|
62
|
-
<span className="text-foreground font-medium">{addonList.length} installed</span>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { CheckCircle, AlertTriangle, Loader2 } from 'lucide-react'
|
|
3
|
-
import { useBackendClient } from '../../../hooks/useBackendClient'
|
|
4
|
-
import type { BlockProps } from '../../../types/dashboard'
|
|
5
|
-
|
|
6
|
-
export function SystemStatusBlock({ config, size }: BlockProps) {
|
|
7
|
-
const client = useBackendClient()
|
|
8
|
-
const { data, isLoading, error } = useQuery({
|
|
9
|
-
queryKey: ['system-info'],
|
|
10
|
-
queryFn: () => client.getSystemInfo(),
|
|
11
|
-
refetchInterval: 10_000,
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
if (isLoading) {
|
|
15
|
-
return (
|
|
16
|
-
<div className="flex items-center gap-2 text-xs text-foreground-subtle">
|
|
17
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
18
|
-
<span>Connecting to server...</span>
|
|
19
|
-
</div>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (error) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="flex items-center gap-2 text-xs text-danger">
|
|
26
|
-
<AlertTriangle className="h-3.5 w-3.5" />
|
|
27
|
-
<span>Unable to reach server</span>
|
|
28
|
-
</div>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const info = data as Record<string, unknown> | undefined
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className="space-y-2 text-xs">
|
|
36
|
-
<div className="flex items-center gap-1.5 mb-2">
|
|
37
|
-
<CheckCircle className="h-3.5 w-3.5 text-success" />
|
|
38
|
-
<span className="text-success font-medium">Online</span>
|
|
39
|
-
</div>
|
|
40
|
-
<Row label="Version" value={String(info?.version ?? 'N/A')} />
|
|
41
|
-
<Row label="Uptime" value={formatUptime(info?.uptime as number)} />
|
|
42
|
-
<Row label="Memory" value={`${formatMB(info?.memoryUsage as number)} MB`} />
|
|
43
|
-
<Row label="Platform" value={String(info?.platform ?? 'N/A')} />
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function Row({ label, value }: { label: string; value: string }) {
|
|
49
|
-
return (
|
|
50
|
-
<div className="flex justify-between items-center">
|
|
51
|
-
<span className="text-foreground-subtle">{label}</span>
|
|
52
|
-
<span className="text-foreground font-medium tabular-nums">{value}</span>
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function formatUptime(seconds?: number): string {
|
|
58
|
-
if (!seconds) return 'N/A'
|
|
59
|
-
const h = Math.floor(seconds / 3600)
|
|
60
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
61
|
-
return h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatMB(bytes?: number): string {
|
|
65
|
-
if (!bytes) return 'N/A'
|
|
66
|
-
return (bytes / 1024 / 1024).toFixed(0)
|
|
67
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Monitor, Workflow, HardDrive } from 'lucide-react'
|
|
2
|
-
import { registerBlock } from '../block-registry'
|
|
3
|
-
import { SystemStatusBlock } from './SystemStatusBlock'
|
|
4
|
-
import { PipelineStagesBlock } from './PipelineStagesBlock'
|
|
5
|
-
import { StorageBlock } from './StorageBlock'
|
|
6
|
-
|
|
7
|
-
registerBlock({
|
|
8
|
-
id: 'system-status',
|
|
9
|
-
name: 'System Status',
|
|
10
|
-
icon: Monitor,
|
|
11
|
-
defaultSize: { w: 4, h: 2 },
|
|
12
|
-
minSize: { w: 2, h: 2 },
|
|
13
|
-
component: SystemStatusBlock,
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
registerBlock({
|
|
17
|
-
id: 'pipeline-stages',
|
|
18
|
-
name: 'Pipeline Stages',
|
|
19
|
-
icon: Workflow,
|
|
20
|
-
defaultSize: { w: 4, h: 3 },
|
|
21
|
-
minSize: { w: 3, h: 2 },
|
|
22
|
-
component: PipelineStagesBlock,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
registerBlock({
|
|
26
|
-
id: 'storage',
|
|
27
|
-
name: 'Storage',
|
|
28
|
-
icon: HardDrive,
|
|
29
|
-
defaultSize: { w: 4, h: 2 },
|
|
30
|
-
minSize: { w: 2, h: 2 },
|
|
31
|
-
component: StorageBlock,
|
|
32
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Link } from 'react-router-dom'
|
|
3
|
-
import { ChevronRight, Video, ScrollText, Terminal, Bell, Zap } from 'lucide-react'
|
|
4
|
-
import { StatusBadge } from '../shared/StatusBadge'
|
|
5
|
-
import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
|
|
6
|
-
import { CapabilityBadges } from '../shared/CapabilityBadges'
|
|
7
|
-
import { QuickConfigWizard } from './QuickConfigWizard'
|
|
8
|
-
import type { PanelId } from './FloatingPanelManager'
|
|
9
|
-
|
|
10
|
-
interface DeviceHeaderProps {
|
|
11
|
-
deviceId: string
|
|
12
|
-
name: string
|
|
13
|
-
status: string
|
|
14
|
-
provider: string
|
|
15
|
-
capabilities: string[]
|
|
16
|
-
activePanel: PanelId | null
|
|
17
|
-
onTogglePanel: (panel: PanelId) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const PANEL_BUTTONS: Array<{
|
|
21
|
-
id: PanelId
|
|
22
|
-
icon: React.ComponentType<{ className?: string }>
|
|
23
|
-
label: string
|
|
24
|
-
}> = [
|
|
25
|
-
{ id: 'stream', icon: Video, label: 'Stream' },
|
|
26
|
-
{ id: 'logs', icon: ScrollText, label: 'Logs' },
|
|
27
|
-
{ id: 'repl', icon: Terminal, label: 'REPL' },
|
|
28
|
-
{ id: 'events', icon: Bell, label: 'Events' },
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
export function DeviceHeader({
|
|
32
|
-
deviceId,
|
|
33
|
-
name,
|
|
34
|
-
status,
|
|
35
|
-
provider,
|
|
36
|
-
capabilities,
|
|
37
|
-
activePanel,
|
|
38
|
-
onTogglePanel,
|
|
39
|
-
}: DeviceHeaderProps) {
|
|
40
|
-
const [wizardOpen, setWizardOpen] = useState(false)
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<>
|
|
44
|
-
<div className="border-b border-border bg-surface px-6 py-4">
|
|
45
|
-
{/* Breadcrumb */}
|
|
46
|
-
<nav className="flex items-center gap-1 text-[11px] text-foreground-subtle mb-3">
|
|
47
|
-
<Link to="/integrations" className="hover:text-foreground transition-colors">
|
|
48
|
-
Integrations
|
|
49
|
-
</Link>
|
|
50
|
-
<ChevronRight className="h-3 w-3" />
|
|
51
|
-
<span className="text-foreground-subtle">{getProviderLabel(provider)}</span>
|
|
52
|
-
<ChevronRight className="h-3 w-3" />
|
|
53
|
-
<span className="text-foreground">{name}</span>
|
|
54
|
-
</nav>
|
|
55
|
-
|
|
56
|
-
{/* Header row */}
|
|
57
|
-
<div className="flex items-center justify-between gap-4">
|
|
58
|
-
<div className="flex items-center gap-3 min-w-0">
|
|
59
|
-
<ProviderIcon type={provider} size="lg" />
|
|
60
|
-
<div className="min-w-0">
|
|
61
|
-
<div className="flex items-center gap-2">
|
|
62
|
-
<h1 className="text-base font-semibold text-foreground truncate">{name}</h1>
|
|
63
|
-
<StatusBadge status={status} />
|
|
64
|
-
</div>
|
|
65
|
-
<div className="mt-1">
|
|
66
|
-
<CapabilityBadges capabilities={capabilities} />
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
{/* Action buttons */}
|
|
72
|
-
<div className="flex items-center gap-1 flex-shrink-0">
|
|
73
|
-
{/* Quick Config wizard button */}
|
|
74
|
-
<button
|
|
75
|
-
onClick={() => setWizardOpen(true)}
|
|
76
|
-
title="Quick Config"
|
|
77
|
-
className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[11px] font-medium border border-border text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
|
|
78
|
-
>
|
|
79
|
-
<Zap className="h-3.5 w-3.5" />
|
|
80
|
-
Quick Config
|
|
81
|
-
</button>
|
|
82
|
-
|
|
83
|
-
{/* Divider */}
|
|
84
|
-
<div className="h-5 w-px bg-border mx-1" />
|
|
85
|
-
|
|
86
|
-
{/* Floating panel toggle buttons */}
|
|
87
|
-
{PANEL_BUTTONS.map(({ id, icon: Icon, label }) => {
|
|
88
|
-
const isActive = activePanel === id
|
|
89
|
-
return (
|
|
90
|
-
<button
|
|
91
|
-
key={id}
|
|
92
|
-
onClick={() => onTogglePanel(id)}
|
|
93
|
-
title={label}
|
|
94
|
-
className={`inline-flex items-center justify-center rounded-lg p-2 transition-colors ${
|
|
95
|
-
isActive
|
|
96
|
-
? 'bg-primary/15 text-primary'
|
|
97
|
-
: 'text-foreground-subtle hover:text-foreground hover:bg-surface-hover'
|
|
98
|
-
}`}
|
|
99
|
-
>
|
|
100
|
-
<Icon className="h-4 w-4" />
|
|
101
|
-
</button>
|
|
102
|
-
)
|
|
103
|
-
})}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
{/* Quick Config Wizard (modal) */}
|
|
109
|
-
<QuickConfigWizard
|
|
110
|
-
open={wizardOpen}
|
|
111
|
-
onClose={() => setWizardOpen(false)}
|
|
112
|
-
deviceId={deviceId}
|
|
113
|
-
/>
|
|
114
|
-
</>
|
|
115
|
-
)
|
|
116
|
-
}
|