@camstack/addon-admin-ui 0.1.2 → 0.1.4
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-BoVZEQ1j.js +598 -0
- package/dist/assets/index-DwSc8ann.css +1 -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 +4 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -355
- package/src/components/addons/AddonUploadZone.tsx +0 -69
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -108
- 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 -172
- 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 -105
- package/src/components/metrics/IntegrationUsage.tsx +0 -73
- package/src/components/metrics/PipelineStatus.tsx +0 -74
- package/src/components/metrics/ProcessResources.tsx +0 -123
- 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 -254
- 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 -222
- package/src/pages/Integrations.tsx +0 -333
- 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 -396
- 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 -28
- 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,114 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { Radio, Copy, Check } from 'lucide-react'
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { useBackendClient } from '../../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
interface StreamsTabProps {
|
|
7
|
-
readonly deviceId: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function StreamsTab({ deviceId }: StreamsTabProps) {
|
|
11
|
-
const client = useBackendClient()
|
|
12
|
-
const [copiedUrl, setCopiedUrl] = useState<string | null>(null)
|
|
13
|
-
|
|
14
|
-
const { data: deviceData } = useQuery({
|
|
15
|
-
queryKey: ['device', deviceId],
|
|
16
|
-
queryFn: () => client.getDevice(deviceId),
|
|
17
|
-
enabled: !!deviceId,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const device = (deviceData ?? {}) as Record<string, unknown>
|
|
21
|
-
const serverUrl = (client as unknown as { serverUrl?: string }).serverUrl ?? window.location.origin
|
|
22
|
-
|
|
23
|
-
// Build stream URLs from known patterns
|
|
24
|
-
const streams = [
|
|
25
|
-
{
|
|
26
|
-
id: 'webrtc',
|
|
27
|
-
label: 'WebRTC',
|
|
28
|
-
resolution: 'Native',
|
|
29
|
-
codec: 'H.264',
|
|
30
|
-
url: `${serverUrl}/webrtc/${deviceId}`,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'hls',
|
|
34
|
-
label: 'HLS',
|
|
35
|
-
resolution: 'Native',
|
|
36
|
-
codec: 'H.264',
|
|
37
|
-
url: `${serverUrl}/hls/${deviceId}/index.m3u8`,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'mjpeg',
|
|
41
|
-
label: 'MJPEG',
|
|
42
|
-
resolution: 'Scaled',
|
|
43
|
-
codec: 'MJPEG',
|
|
44
|
-
url: `${serverUrl}/mjpeg/${deviceId}`,
|
|
45
|
-
},
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
async function handleCopy(url: string) {
|
|
49
|
-
try {
|
|
50
|
-
await navigator.clipboard.writeText(url)
|
|
51
|
-
setCopiedUrl(url)
|
|
52
|
-
setTimeout(() => setCopiedUrl(null), 2000)
|
|
53
|
-
} catch {
|
|
54
|
-
// clipboard not available
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="space-y-4">
|
|
60
|
-
{/* Available streams */}
|
|
61
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
62
|
-
<div className="border-b border-border px-4 py-2.5 flex items-center justify-between">
|
|
63
|
-
<h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Available Streams</h2>
|
|
64
|
-
<span className="text-[10px] text-foreground-subtle">{streams.length} streams</span>
|
|
65
|
-
</div>
|
|
66
|
-
<div className="divide-y divide-border">
|
|
67
|
-
{streams.map((stream) => (
|
|
68
|
-
<div key={stream.id} className="flex items-center gap-3 px-4 py-3">
|
|
69
|
-
<div className="flex items-center justify-center rounded-lg h-8 w-8 bg-info/10 flex-shrink-0">
|
|
70
|
-
<Radio className="h-4 w-4 text-info" />
|
|
71
|
-
</div>
|
|
72
|
-
<div className="flex-1 min-w-0">
|
|
73
|
-
<div className="flex items-center gap-2">
|
|
74
|
-
<p className="text-xs font-medium text-foreground">{stream.label}</p>
|
|
75
|
-
<span className="text-[9px] bg-surface-hover rounded px-1.5 py-0.5 text-foreground-subtle">
|
|
76
|
-
{stream.resolution}
|
|
77
|
-
</span>
|
|
78
|
-
<span className="text-[9px] bg-surface-hover rounded px-1.5 py-0.5 text-foreground-subtle">
|
|
79
|
-
{stream.codec}
|
|
80
|
-
</span>
|
|
81
|
-
</div>
|
|
82
|
-
<p className="text-[10px] font-mono text-foreground-subtle mt-0.5 truncate">{stream.url}</p>
|
|
83
|
-
</div>
|
|
84
|
-
<button
|
|
85
|
-
onClick={() => handleCopy(stream.url)}
|
|
86
|
-
className="p-1.5 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors flex-shrink-0"
|
|
87
|
-
title="Copy URL"
|
|
88
|
-
>
|
|
89
|
-
{copiedUrl === stream.url ? (
|
|
90
|
-
<Check className="h-3.5 w-3.5 text-success" />
|
|
91
|
-
) : (
|
|
92
|
-
<Copy className="h-3.5 w-3.5" />
|
|
93
|
-
)}
|
|
94
|
-
</button>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
{/* Restreamer URLs */}
|
|
101
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
102
|
-
<div className="border-b border-border px-4 py-2.5">
|
|
103
|
-
<h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Restreamer</h2>
|
|
104
|
-
</div>
|
|
105
|
-
<div className="px-4 py-8 flex flex-col items-center text-center text-foreground-subtle">
|
|
106
|
-
<p className="text-sm">Restreamer URLs</p>
|
|
107
|
-
<p className="text-xs mt-1 opacity-70">
|
|
108
|
-
Restreamer endpoints will be displayed here when a restreamer is configured
|
|
109
|
-
</p>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { X } from 'lucide-react'
|
|
2
|
-
import { getAllBlocks } from './block-registry'
|
|
3
|
-
import type { DashboardBlock } from '../../types/dashboard'
|
|
4
|
-
|
|
5
|
-
interface BlockPickerProps {
|
|
6
|
-
open: boolean
|
|
7
|
-
onClose: () => void
|
|
8
|
-
onSelect: (block: DashboardBlock) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function BlockPicker({ open, onClose, onSelect }: BlockPickerProps) {
|
|
12
|
-
if (!open) return null
|
|
13
|
-
|
|
14
|
-
const blocks = getAllBlocks()
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
|
18
|
-
<div
|
|
19
|
-
className="w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-2xl shadow-black/20"
|
|
20
|
-
onClick={(e) => e.stopPropagation()}
|
|
21
|
-
>
|
|
22
|
-
<div className="flex items-center justify-between mb-4">
|
|
23
|
-
<h2 className="text-sm font-semibold text-foreground">Add Block</h2>
|
|
24
|
-
<button
|
|
25
|
-
onClick={onClose}
|
|
26
|
-
className="p-1 rounded-md hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
|
|
27
|
-
>
|
|
28
|
-
<X className="h-4 w-4" />
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
31
|
-
<div className="grid grid-cols-2 gap-2">
|
|
32
|
-
{blocks.map((block) => {
|
|
33
|
-
const Icon = block.icon
|
|
34
|
-
return (
|
|
35
|
-
<button
|
|
36
|
-
key={block.id}
|
|
37
|
-
onClick={() => { onSelect(block); onClose() }}
|
|
38
|
-
className="flex items-center gap-3 rounded-lg border border-border p-3 text-left hover:bg-surface-hover hover:border-primary/30 transition-all group"
|
|
39
|
-
>
|
|
40
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary/10 group-hover:bg-primary/15 transition-colors shrink-0">
|
|
41
|
-
<Icon className="h-4 w-4 text-primary" />
|
|
42
|
-
</div>
|
|
43
|
-
<div>
|
|
44
|
-
<span className="text-xs font-medium text-foreground block">{block.name}</span>
|
|
45
|
-
<span className="text-[10px] text-foreground-subtle">{block.defaultSize.w}x{block.defaultSize.h}</span>
|
|
46
|
-
</div>
|
|
47
|
-
</button>
|
|
48
|
-
)
|
|
49
|
-
})}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
@@ -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
|
-
})
|