@camstack/addon-admin-ui 0.1.2 → 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 +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,132 +0,0 @@
|
|
|
1
|
-
import { useRef, useCallback } from 'react'
|
|
2
|
-
import { X, Minimize2, Maximize2, GripHorizontal } from 'lucide-react'
|
|
3
|
-
import type { LucideIcon } from 'lucide-react'
|
|
4
|
-
|
|
5
|
-
interface FloatingPanelProps {
|
|
6
|
-
id: string
|
|
7
|
-
title: string
|
|
8
|
-
icon: LucideIcon
|
|
9
|
-
isOpen: boolean
|
|
10
|
-
isFloating: boolean
|
|
11
|
-
position?: { x: number; y: number }
|
|
12
|
-
size?: { w: number; h: number }
|
|
13
|
-
onClose: () => void
|
|
14
|
-
onToggleFloat: () => void
|
|
15
|
-
onPositionChange?: (pos: { x: number; y: number }) => void
|
|
16
|
-
children: React.ReactNode
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function FloatingPanel({
|
|
20
|
-
title,
|
|
21
|
-
icon: Icon,
|
|
22
|
-
isFloating,
|
|
23
|
-
position = { x: 80, y: 80 },
|
|
24
|
-
size = { w: 400, h: 480 },
|
|
25
|
-
onClose,
|
|
26
|
-
onToggleFloat,
|
|
27
|
-
onPositionChange,
|
|
28
|
-
children,
|
|
29
|
-
}: FloatingPanelProps) {
|
|
30
|
-
const panelRef = useRef<HTMLDivElement>(null)
|
|
31
|
-
const dragging = useRef(false)
|
|
32
|
-
const dragOffset = useRef({ x: 0, y: 0 })
|
|
33
|
-
|
|
34
|
-
const handleMouseDown = useCallback(
|
|
35
|
-
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
36
|
-
if (!isFloating) return
|
|
37
|
-
dragging.current = true
|
|
38
|
-
dragOffset.current = {
|
|
39
|
-
x: e.clientX - position.x,
|
|
40
|
-
y: e.clientY - position.y,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function onMouseMove(ev: MouseEvent) {
|
|
44
|
-
if (!dragging.current) return
|
|
45
|
-
onPositionChange?.({
|
|
46
|
-
x: ev.clientX - dragOffset.current.x,
|
|
47
|
-
y: ev.clientY - dragOffset.current.y,
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function onMouseUp() {
|
|
52
|
-
dragging.current = false
|
|
53
|
-
window.removeEventListener('mousemove', onMouseMove)
|
|
54
|
-
window.removeEventListener('mouseup', onMouseUp)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
window.addEventListener('mousemove', onMouseMove)
|
|
58
|
-
window.addEventListener('mouseup', onMouseUp)
|
|
59
|
-
},
|
|
60
|
-
[isFloating, position.x, position.y, onPositionChange],
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if (isFloating) {
|
|
64
|
-
return (
|
|
65
|
-
<div
|
|
66
|
-
ref={panelRef}
|
|
67
|
-
className="fixed z-40 flex flex-col rounded-xl border border-border bg-surface shadow-2xl overflow-hidden"
|
|
68
|
-
style={{
|
|
69
|
-
left: position.x,
|
|
70
|
-
top: position.y,
|
|
71
|
-
width: size.w,
|
|
72
|
-
height: size.h,
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{/* Title bar — drag handle */}
|
|
76
|
-
<div
|
|
77
|
-
onMouseDown={handleMouseDown}
|
|
78
|
-
className="flex items-center gap-2 border-b border-border bg-surface-hover px-3 py-2 cursor-grab select-none active:cursor-grabbing flex-shrink-0"
|
|
79
|
-
>
|
|
80
|
-
<GripHorizontal className="h-3.5 w-3.5 text-foreground-subtle" />
|
|
81
|
-
<Icon className="h-3.5 w-3.5 text-primary" />
|
|
82
|
-
<span className="flex-1 text-xs font-semibold text-foreground">{title}</span>
|
|
83
|
-
<button
|
|
84
|
-
onClick={onToggleFloat}
|
|
85
|
-
title="Dock panel"
|
|
86
|
-
className="text-foreground-subtle hover:text-foreground transition-colors p-0.5 rounded"
|
|
87
|
-
>
|
|
88
|
-
<Minimize2 className="h-3.5 w-3.5" />
|
|
89
|
-
</button>
|
|
90
|
-
<button
|
|
91
|
-
onClick={onClose}
|
|
92
|
-
title="Close"
|
|
93
|
-
className="text-foreground-subtle hover:text-danger transition-colors p-0.5 rounded"
|
|
94
|
-
>
|
|
95
|
-
<X className="h-3.5 w-3.5" />
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Content */}
|
|
100
|
-
<div className="flex-1 overflow-y-auto p-3 text-xs">{children}</div>
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Docked mode — rendered by FloatingPanelManager in the right column
|
|
106
|
-
return (
|
|
107
|
-
<div className="flex flex-col flex-1 min-h-0 border-b border-border last:border-b-0">
|
|
108
|
-
{/* Header */}
|
|
109
|
-
<div className="flex items-center gap-2 border-b border-border bg-surface-hover px-3 py-2 flex-shrink-0">
|
|
110
|
-
<Icon className="h-3.5 w-3.5 text-primary" />
|
|
111
|
-
<span className="flex-1 text-xs font-semibold text-foreground">{title}</span>
|
|
112
|
-
<button
|
|
113
|
-
onClick={onToggleFloat}
|
|
114
|
-
title="Detach panel"
|
|
115
|
-
className="text-foreground-subtle hover:text-foreground transition-colors p-0.5 rounded"
|
|
116
|
-
>
|
|
117
|
-
<Maximize2 className="h-3.5 w-3.5" />
|
|
118
|
-
</button>
|
|
119
|
-
<button
|
|
120
|
-
onClick={onClose}
|
|
121
|
-
title="Close"
|
|
122
|
-
className="text-foreground-subtle hover:text-danger transition-colors p-0.5 rounded"
|
|
123
|
-
>
|
|
124
|
-
<X className="h-3.5 w-3.5" />
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{/* Content */}
|
|
129
|
-
<div className="flex-1 overflow-y-auto p-3 text-xs">{children}</div>
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react'
|
|
2
|
-
import { Video, ScrollText, Terminal, Bell } from 'lucide-react'
|
|
3
|
-
import type { LucideIcon } from 'lucide-react'
|
|
4
|
-
import { FloatingPanel } from './FloatingPanel'
|
|
5
|
-
import {
|
|
6
|
-
StreamPanelContent,
|
|
7
|
-
LogsPanelContent,
|
|
8
|
-
ReplPanelContent,
|
|
9
|
-
EventsPanelContent,
|
|
10
|
-
} from './PanelContent'
|
|
11
|
-
|
|
12
|
-
export type PanelId = 'stream' | 'logs' | 'repl' | 'events'
|
|
13
|
-
|
|
14
|
-
interface PanelState {
|
|
15
|
-
isOpen: boolean
|
|
16
|
-
isFloating: boolean
|
|
17
|
-
position: { x: number; y: number }
|
|
18
|
-
size: { w: number; h: number }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface PanelConfig {
|
|
22
|
-
id: PanelId
|
|
23
|
-
title: string
|
|
24
|
-
icon: LucideIcon
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const PANEL_CONFIGS: PanelConfig[] = [
|
|
28
|
-
{ id: 'stream', title: 'Stream', icon: Video },
|
|
29
|
-
{ id: 'logs', title: 'Logs', icon: ScrollText },
|
|
30
|
-
{ id: 'repl', title: 'REPL', icon: Terminal },
|
|
31
|
-
{ id: 'events', title: 'Events', icon: Bell },
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
const DEFAULT_PANEL_STATE: PanelState = {
|
|
35
|
-
isOpen: false,
|
|
36
|
-
isFloating: false,
|
|
37
|
-
position: { x: 80, y: 80 },
|
|
38
|
-
size: { w: 400, h: 480 },
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getInitialPanelStates(): Record<PanelId, PanelState> {
|
|
42
|
-
return {
|
|
43
|
-
stream: { ...DEFAULT_PANEL_STATE, position: { x: 80, y: 80 } },
|
|
44
|
-
logs: { ...DEFAULT_PANEL_STATE, position: { x: 120, y: 120 } },
|
|
45
|
-
repl: { ...DEFAULT_PANEL_STATE, position: { x: 160, y: 160 } },
|
|
46
|
-
events: { ...DEFAULT_PANEL_STATE, position: { x: 200, y: 200 } },
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface FloatingPanelManagerProps {
|
|
51
|
-
deviceId: string
|
|
52
|
-
/** Externally controlled open panels — toggled from DeviceHeader */
|
|
53
|
-
openPanelId: PanelId | null
|
|
54
|
-
onPanelClosed: (id: PanelId) => void
|
|
55
|
-
children: React.ReactNode
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function FloatingPanelManager({
|
|
59
|
-
deviceId,
|
|
60
|
-
openPanelId,
|
|
61
|
-
onPanelClosed,
|
|
62
|
-
children,
|
|
63
|
-
}: FloatingPanelManagerProps) {
|
|
64
|
-
const [panelStates, setPanelStates] = useState<Record<PanelId, PanelState>>(getInitialPanelStates)
|
|
65
|
-
|
|
66
|
-
const updatePanel = useCallback((id: PanelId, patch: Partial<PanelState>) => {
|
|
67
|
-
setPanelStates((prev) => ({
|
|
68
|
-
...prev,
|
|
69
|
-
[id]: { ...prev[id], ...patch },
|
|
70
|
-
}))
|
|
71
|
-
}, [])
|
|
72
|
-
|
|
73
|
-
function handleClose(id: PanelId) {
|
|
74
|
-
updatePanel(id, { isOpen: false, isFloating: false })
|
|
75
|
-
onPanelClosed(id)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function handleToggleFloat(id: PanelId) {
|
|
79
|
-
updatePanel(id, { isFloating: !panelStates[id].isFloating })
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function handlePositionChange(id: PanelId, pos: { x: number; y: number }) {
|
|
83
|
-
updatePanel(id, { position: pos })
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Determine which panels are visible (open or just toggled open externally)
|
|
87
|
-
const visiblePanels = PANEL_CONFIGS.filter(({ id }) => {
|
|
88
|
-
const state = panelStates[id]
|
|
89
|
-
return state.isOpen || id === openPanelId
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// Sync external toggle: if openPanelId was set externally and panel isn't already open, open it
|
|
93
|
-
if (openPanelId !== null && !panelStates[openPanelId].isOpen) {
|
|
94
|
-
updatePanel(openPanelId, { isOpen: true })
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const dockedPanels = visiblePanels.filter(({ id }) => !panelStates[id].isFloating)
|
|
98
|
-
const floatingPanels = visiblePanels.filter(({ id }) => panelStates[id].isFloating)
|
|
99
|
-
|
|
100
|
-
function renderPanelContent(id: PanelId) {
|
|
101
|
-
switch (id) {
|
|
102
|
-
case 'stream':
|
|
103
|
-
return <StreamPanelContent deviceId={deviceId} />
|
|
104
|
-
case 'logs':
|
|
105
|
-
return <LogsPanelContent />
|
|
106
|
-
case 'repl':
|
|
107
|
-
return <ReplPanelContent deviceId={deviceId} />
|
|
108
|
-
case 'events':
|
|
109
|
-
return <EventsPanelContent />
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<>
|
|
115
|
-
{/* Main layout: tab content + docked panels column */}
|
|
116
|
-
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
117
|
-
{/* Tab content area */}
|
|
118
|
-
<div className="flex-1 overflow-y-auto p-6">{children}</div>
|
|
119
|
-
|
|
120
|
-
{/* Docked panels column */}
|
|
121
|
-
{dockedPanels.length > 0 && (
|
|
122
|
-
<div className="w-[360px] flex-shrink-0 border-l border-border bg-surface flex flex-col overflow-hidden">
|
|
123
|
-
{dockedPanels.map(({ id, title, icon }) => {
|
|
124
|
-
const state = panelStates[id]
|
|
125
|
-
return (
|
|
126
|
-
<FloatingPanel
|
|
127
|
-
key={id}
|
|
128
|
-
id={id}
|
|
129
|
-
title={title}
|
|
130
|
-
icon={icon}
|
|
131
|
-
isOpen={state.isOpen}
|
|
132
|
-
isFloating={false}
|
|
133
|
-
onClose={() => handleClose(id)}
|
|
134
|
-
onToggleFloat={() => handleToggleFloat(id)}
|
|
135
|
-
>
|
|
136
|
-
{renderPanelContent(id)}
|
|
137
|
-
</FloatingPanel>
|
|
138
|
-
)
|
|
139
|
-
})}
|
|
140
|
-
</div>
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{/* Floating panels (overlays) */}
|
|
145
|
-
{floatingPanels.map(({ id, title, icon }) => {
|
|
146
|
-
const state = panelStates[id]
|
|
147
|
-
return (
|
|
148
|
-
<FloatingPanel
|
|
149
|
-
key={id}
|
|
150
|
-
id={id}
|
|
151
|
-
title={title}
|
|
152
|
-
icon={icon}
|
|
153
|
-
isOpen={state.isOpen}
|
|
154
|
-
isFloating={true}
|
|
155
|
-
position={state.position}
|
|
156
|
-
size={state.size}
|
|
157
|
-
onClose={() => handleClose(id)}
|
|
158
|
-
onToggleFloat={() => handleToggleFloat(id)}
|
|
159
|
-
onPositionChange={(pos) => handlePositionChange(id, pos)}
|
|
160
|
-
>
|
|
161
|
-
{renderPanelContent(id)}
|
|
162
|
-
</FloatingPanel>
|
|
163
|
-
)
|
|
164
|
-
})}
|
|
165
|
-
</>
|
|
166
|
-
)
|
|
167
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { ScrollText, Terminal, Bell, Play, Trash2, ChevronUp } from 'lucide-react'
|
|
2
|
-
import { useState, useRef } from 'react'
|
|
3
|
-
import { useMutation } from '@tanstack/react-query'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
import { WebRtcPlayer } from '../shared/WebRtcPlayer'
|
|
6
|
-
|
|
7
|
-
// ---- Stream Panel ----
|
|
8
|
-
interface StreamPanelContentProps {
|
|
9
|
-
deviceId: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function StreamPanelContent({ deviceId }: StreamPanelContentProps) {
|
|
13
|
-
const client = useBackendClient()
|
|
14
|
-
const serverUrl = (client as unknown as { serverUrl?: string }).serverUrl ?? window.location.origin
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="flex flex-col h-full gap-2">
|
|
18
|
-
<WebRtcPlayer
|
|
19
|
-
serverUrl={serverUrl}
|
|
20
|
-
streamId={deviceId}
|
|
21
|
-
className="flex-1 min-h-0 rounded-lg"
|
|
22
|
-
/>
|
|
23
|
-
</div>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---- Logs Panel ----
|
|
28
|
-
export function LogsPanelContent() {
|
|
29
|
-
return (
|
|
30
|
-
<div className="flex flex-col items-center justify-center h-full py-8 text-foreground-subtle">
|
|
31
|
-
<ScrollText className="h-8 w-8 mb-3 opacity-30" />
|
|
32
|
-
<p className="text-sm font-medium text-foreground">Device logs</p>
|
|
33
|
-
<p className="text-xs mt-1 opacity-70 text-center">Coming with backend integration</p>
|
|
34
|
-
</div>
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---- REPL Panel ----
|
|
39
|
-
interface ReplPanelContentProps {
|
|
40
|
-
deviceId: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface HistoryEntry {
|
|
44
|
-
id: number
|
|
45
|
-
code: string
|
|
46
|
-
result: string
|
|
47
|
-
error: boolean
|
|
48
|
-
ts: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let replPanelCounter = 0
|
|
52
|
-
|
|
53
|
-
export function ReplPanelContent({ deviceId }: ReplPanelContentProps) {
|
|
54
|
-
const client = useBackendClient()
|
|
55
|
-
const [code, setCode] = useState('')
|
|
56
|
-
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
57
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
58
|
-
|
|
59
|
-
const evalMutation = useMutation({
|
|
60
|
-
mutationFn: (src: string) =>
|
|
61
|
-
client.replEval(src, { type: 'device' as const, deviceId }),
|
|
62
|
-
onSuccess: (data, src) => {
|
|
63
|
-
const result = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
|
64
|
-
setHistory((prev) => [
|
|
65
|
-
...prev,
|
|
66
|
-
{
|
|
67
|
-
id: ++replPanelCounter,
|
|
68
|
-
code: src,
|
|
69
|
-
result,
|
|
70
|
-
error: false,
|
|
71
|
-
ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
|
|
72
|
-
},
|
|
73
|
-
])
|
|
74
|
-
setCode('')
|
|
75
|
-
},
|
|
76
|
-
onError: (err: unknown, src) => {
|
|
77
|
-
const result = err instanceof Error ? err.message : String(err)
|
|
78
|
-
setHistory((prev) => [
|
|
79
|
-
...prev,
|
|
80
|
-
{
|
|
81
|
-
id: ++replPanelCounter,
|
|
82
|
-
code: src,
|
|
83
|
-
result,
|
|
84
|
-
error: true,
|
|
85
|
-
ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
|
|
86
|
-
},
|
|
87
|
-
])
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
function handleExecute() {
|
|
92
|
-
const trimmed = code.trim()
|
|
93
|
-
if (!trimmed) return
|
|
94
|
-
evalMutation.mutate(trimmed)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
|
98
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
99
|
-
e.preventDefault()
|
|
100
|
-
handleExecute()
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function reuseCode(src: string) {
|
|
105
|
-
setCode(src)
|
|
106
|
-
textareaRef.current?.focus()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div className="flex flex-col gap-2 h-full">
|
|
111
|
-
<div className="flex items-center justify-between">
|
|
112
|
-
<span className="text-[10px] text-foreground-subtle font-mono">
|
|
113
|
-
device: <span className="text-foreground">{deviceId}</span>
|
|
114
|
-
</span>
|
|
115
|
-
{history.length > 0 && (
|
|
116
|
-
<button
|
|
117
|
-
onClick={() => setHistory([])}
|
|
118
|
-
className="inline-flex items-center gap-1 text-[10px] text-foreground-subtle hover:text-foreground"
|
|
119
|
-
>
|
|
120
|
-
<Trash2 className="h-3 w-3" />
|
|
121
|
-
Clear
|
|
122
|
-
</button>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{/* Input */}
|
|
127
|
-
<div className="rounded border border-border bg-surface overflow-hidden flex-shrink-0">
|
|
128
|
-
<div className="flex items-center justify-between border-b border-border px-2 py-1">
|
|
129
|
-
<span className="text-[10px] text-foreground-subtle font-mono">Ctrl+Enter to run</span>
|
|
130
|
-
<button
|
|
131
|
-
onClick={handleExecute}
|
|
132
|
-
disabled={evalMutation.isPending || !code.trim()}
|
|
133
|
-
className="inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
134
|
-
>
|
|
135
|
-
<Play className="h-3 w-3" />
|
|
136
|
-
{evalMutation.isPending ? '...' : 'Run'}
|
|
137
|
-
</button>
|
|
138
|
-
</div>
|
|
139
|
-
<textarea
|
|
140
|
-
ref={textareaRef}
|
|
141
|
-
value={code}
|
|
142
|
-
onChange={(e) => setCode(e.target.value)}
|
|
143
|
-
onKeyDown={handleKeyDown}
|
|
144
|
-
rows={3}
|
|
145
|
-
placeholder="// JavaScript..."
|
|
146
|
-
className="w-full resize-none bg-background p-2 font-mono text-[11px] text-foreground placeholder:text-foreground-subtle focus:outline-none"
|
|
147
|
-
spellCheck={false}
|
|
148
|
-
/>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
{/* History */}
|
|
152
|
-
<div className="flex-1 overflow-y-auto space-y-2">
|
|
153
|
-
{history.length === 0 && (
|
|
154
|
-
<div className="text-[10px] text-foreground-subtle">No evaluations yet</div>
|
|
155
|
-
)}
|
|
156
|
-
{[...history].reverse().map((entry) => (
|
|
157
|
-
<div key={entry.id} className="rounded border border-border bg-surface overflow-hidden">
|
|
158
|
-
<div className="flex items-start justify-between gap-1 border-b border-border bg-background px-2 py-1">
|
|
159
|
-
<pre className="font-mono text-[10px] text-foreground whitespace-pre-wrap break-all flex-1">
|
|
160
|
-
{entry.code}
|
|
161
|
-
</pre>
|
|
162
|
-
<div className="flex items-center gap-1 flex-shrink-0">
|
|
163
|
-
<span className="text-[9px] text-foreground-subtle">{entry.ts}</span>
|
|
164
|
-
<button
|
|
165
|
-
onClick={() => reuseCode(entry.code)}
|
|
166
|
-
title="Reuse"
|
|
167
|
-
className="text-foreground-subtle hover:text-foreground"
|
|
168
|
-
>
|
|
169
|
-
<ChevronUp className="h-3 w-3" />
|
|
170
|
-
</button>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
<pre
|
|
174
|
-
className={`px-2 py-1.5 font-mono text-[10px] whitespace-pre-wrap break-all ${
|
|
175
|
-
entry.error ? 'text-danger' : 'text-success'
|
|
176
|
-
}`}
|
|
177
|
-
>
|
|
178
|
-
{entry.result}
|
|
179
|
-
</pre>
|
|
180
|
-
</div>
|
|
181
|
-
))}
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ---- Events Panel ----
|
|
188
|
-
export function EventsPanelContent() {
|
|
189
|
-
return (
|
|
190
|
-
<div className="flex flex-col items-center justify-center h-full py-8 text-foreground-subtle">
|
|
191
|
-
<Bell className="h-8 w-8 mb-3 opacity-30" />
|
|
192
|
-
<p className="text-sm font-medium text-foreground">No events</p>
|
|
193
|
-
<p className="text-xs mt-1 opacity-70 text-center">Device events will appear here as they occur</p>
|
|
194
|
-
</div>
|
|
195
|
-
)
|
|
196
|
-
}
|