@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,129 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import { RotateCcw } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
import { StatusBadge } from '../../components/shared/StatusBadge'
|
|
6
|
-
|
|
7
|
-
export function ProcessesPage() {
|
|
8
|
-
const client = useBackendClient()
|
|
9
|
-
const queryClient = useQueryClient()
|
|
10
|
-
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
|
11
|
-
|
|
12
|
-
const { data: processes, isLoading, isError } = useQuery({
|
|
13
|
-
queryKey: ['processes'],
|
|
14
|
-
queryFn: () => client.listProcesses(),
|
|
15
|
-
refetchInterval: 5000,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const restartMutation = useMutation({
|
|
19
|
-
mutationFn: (id: string) => client.trpc.processes.restartProcess.mutate({ id }),
|
|
20
|
-
onSuccess: () => {
|
|
21
|
-
queryClient.invalidateQueries({ queryKey: ['processes'] })
|
|
22
|
-
setConfirmingId(null)
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const processList = (processes ?? []) as unknown as Array<Record<string, unknown>>
|
|
27
|
-
|
|
28
|
-
function formatUptime(seconds: number): string {
|
|
29
|
-
if (seconds < 60) return `${seconds}s`
|
|
30
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
31
|
-
const h = Math.floor(seconds / 3600)
|
|
32
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
33
|
-
return `${h}h ${m}m`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="p-6 space-y-4">
|
|
38
|
-
<div className="flex items-center justify-between">
|
|
39
|
-
<h1 className="text-lg font-semibold text-foreground">Processes</h1>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
{isLoading && (
|
|
43
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
44
|
-
)}
|
|
45
|
-
|
|
46
|
-
{isError && (
|
|
47
|
-
<div className="text-xs text-danger">Failed to load</div>
|
|
48
|
-
)}
|
|
49
|
-
|
|
50
|
-
{!isLoading && !isError && processList.length === 0 && (
|
|
51
|
-
<div className="text-xs text-foreground-subtle">No data</div>
|
|
52
|
-
)}
|
|
53
|
-
|
|
54
|
-
{processList.length > 0 && (
|
|
55
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
56
|
-
<table className="w-full text-xs">
|
|
57
|
-
<thead>
|
|
58
|
-
<tr>
|
|
59
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Name</th>
|
|
60
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Status</th>
|
|
61
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">CPU %</th>
|
|
62
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Memory MB</th>
|
|
63
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Uptime</th>
|
|
64
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Restarts</th>
|
|
65
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Actions</th>
|
|
66
|
-
</tr>
|
|
67
|
-
</thead>
|
|
68
|
-
<tbody>
|
|
69
|
-
{processList.map((proc) => {
|
|
70
|
-
const id = String(proc.id ?? proc.name ?? '')
|
|
71
|
-
const name = String(proc.name ?? proc.id ?? '')
|
|
72
|
-
const status = String(proc.status ?? proc.state ?? 'stopped')
|
|
73
|
-
const cpu = Number(proc.cpu ?? proc.cpuPercent ?? 0).toFixed(1)
|
|
74
|
-
const memory = Number(proc.memory ?? proc.memoryMb ?? proc.memoryBytes != null
|
|
75
|
-
? Number(proc.memoryBytes ?? 0) / 1024 / 1024
|
|
76
|
-
: proc.memoryMb ?? 0).toFixed(1)
|
|
77
|
-
const uptime = formatUptime(Number(proc.uptime ?? proc.uptimeSeconds ?? 0))
|
|
78
|
-
const restarts = Number(proc.restarts ?? proc.restartCount ?? 0)
|
|
79
|
-
const isConfirming = confirmingId === id
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<tr key={id} className="hover:bg-primary/5">
|
|
83
|
-
<td className="px-3 py-2 text-foreground border-b border-border font-mono">
|
|
84
|
-
{name}
|
|
85
|
-
</td>
|
|
86
|
-
<td className="px-3 py-2 text-foreground border-b border-border">
|
|
87
|
-
<StatusBadge status={status} />
|
|
88
|
-
</td>
|
|
89
|
-
<td className="px-3 py-2 text-foreground border-b border-border">{cpu}%</td>
|
|
90
|
-
<td className="px-3 py-2 text-foreground border-b border-border">{memory} MB</td>
|
|
91
|
-
<td className="px-3 py-2 text-foreground border-b border-border">{uptime}</td>
|
|
92
|
-
<td className="px-3 py-2 text-foreground border-b border-border">{restarts}</td>
|
|
93
|
-
<td className="px-3 py-2 text-foreground border-b border-border">
|
|
94
|
-
{isConfirming ? (
|
|
95
|
-
<div className="flex items-center gap-1.5">
|
|
96
|
-
<button
|
|
97
|
-
onClick={() => restartMutation.mutate(id)}
|
|
98
|
-
disabled={restartMutation.isPending}
|
|
99
|
-
className="rounded px-2 py-0.5 text-[10px] bg-danger/10 text-danger hover:bg-danger/20"
|
|
100
|
-
>
|
|
101
|
-
Confirm
|
|
102
|
-
</button>
|
|
103
|
-
<button
|
|
104
|
-
onClick={() => setConfirmingId(null)}
|
|
105
|
-
className="rounded px-2 py-0.5 text-[10px] bg-surface text-foreground-subtle hover:text-foreground border border-border"
|
|
106
|
-
>
|
|
107
|
-
Cancel
|
|
108
|
-
</button>
|
|
109
|
-
</div>
|
|
110
|
-
) : (
|
|
111
|
-
<button
|
|
112
|
-
onClick={() => setConfirmingId(id)}
|
|
113
|
-
className="inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] bg-primary/10 text-primary hover:bg-primary/20"
|
|
114
|
-
>
|
|
115
|
-
<RotateCcw className="h-3 w-3" />
|
|
116
|
-
Restart
|
|
117
|
-
</button>
|
|
118
|
-
)}
|
|
119
|
-
</td>
|
|
120
|
-
</tr>
|
|
121
|
-
)
|
|
122
|
-
})}
|
|
123
|
-
</tbody>
|
|
124
|
-
</table>
|
|
125
|
-
</div>
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { useState, useRef } from 'react'
|
|
2
|
-
import { useMutation } from '@tanstack/react-query'
|
|
3
|
-
import { Play, ChevronUp, Trash2 } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
interface HistoryEntry {
|
|
7
|
-
id: number
|
|
8
|
-
code: string
|
|
9
|
-
result: string
|
|
10
|
-
error: boolean
|
|
11
|
-
ts: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let historyCounter = 0
|
|
15
|
-
|
|
16
|
-
export function ReplPage() {
|
|
17
|
-
const client = useBackendClient()
|
|
18
|
-
const [code, setCode] = useState('')
|
|
19
|
-
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
20
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
21
|
-
|
|
22
|
-
const evalMutation = useMutation({
|
|
23
|
-
mutationFn: (src: string) => client.replEval(src),
|
|
24
|
-
onSuccess: (data, src) => {
|
|
25
|
-
const result = typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
|
26
|
-
setHistory((prev) => [
|
|
27
|
-
...prev,
|
|
28
|
-
{
|
|
29
|
-
id: ++historyCounter,
|
|
30
|
-
code: src,
|
|
31
|
-
result,
|
|
32
|
-
error: false,
|
|
33
|
-
ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
|
|
34
|
-
},
|
|
35
|
-
])
|
|
36
|
-
setCode('')
|
|
37
|
-
},
|
|
38
|
-
onError: (err: unknown, src) => {
|
|
39
|
-
const result = err instanceof Error ? err.message : String(err)
|
|
40
|
-
setHistory((prev) => [
|
|
41
|
-
...prev,
|
|
42
|
-
{
|
|
43
|
-
id: ++historyCounter,
|
|
44
|
-
code: src,
|
|
45
|
-
result,
|
|
46
|
-
error: true,
|
|
47
|
-
ts: new Date().toLocaleTimeString('en-GB', { hour12: false }),
|
|
48
|
-
},
|
|
49
|
-
])
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
function handleExecute() {
|
|
54
|
-
const trimmed = code.trim()
|
|
55
|
-
if (!trimmed) return
|
|
56
|
-
evalMutation.mutate(trimmed)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
|
60
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
61
|
-
e.preventDefault()
|
|
62
|
-
handleExecute()
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function reuseCode(src: string) {
|
|
67
|
-
setCode(src)
|
|
68
|
-
textareaRef.current?.focus()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className="p-6 space-y-4">
|
|
73
|
-
<div className="flex items-center justify-between">
|
|
74
|
-
<h1 className="text-lg font-semibold text-foreground">REPL</h1>
|
|
75
|
-
{history.length > 0 && (
|
|
76
|
-
<button
|
|
77
|
-
onClick={() => setHistory([])}
|
|
78
|
-
className="inline-flex items-center gap-1 text-[10px] text-foreground-subtle hover:text-foreground"
|
|
79
|
-
>
|
|
80
|
-
<Trash2 className="h-3 w-3" />
|
|
81
|
-
Clear history
|
|
82
|
-
</button>
|
|
83
|
-
)}
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
{/* Input */}
|
|
87
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
88
|
-
<div className="flex items-center justify-between border-b border-border px-3 py-1.5">
|
|
89
|
-
<span className="text-[10px] text-foreground-subtle font-mono">JavaScript — Ctrl+Enter to run</span>
|
|
90
|
-
<button
|
|
91
|
-
onClick={handleExecute}
|
|
92
|
-
disabled={evalMutation.isPending || !code.trim()}
|
|
93
|
-
className="inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-[10px] font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
94
|
-
>
|
|
95
|
-
<Play className="h-3 w-3" />
|
|
96
|
-
{evalMutation.isPending ? 'Running...' : 'Execute'}
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
<textarea
|
|
100
|
-
ref={textareaRef}
|
|
101
|
-
value={code}
|
|
102
|
-
onChange={(e) => setCode(e.target.value)}
|
|
103
|
-
onKeyDown={handleKeyDown}
|
|
104
|
-
rows={6}
|
|
105
|
-
placeholder="// Enter JavaScript code here..."
|
|
106
|
-
className="w-full resize-y bg-background p-3 font-mono text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none"
|
|
107
|
-
spellCheck={false}
|
|
108
|
-
/>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
{/* History */}
|
|
112
|
-
{history.length === 0 && (
|
|
113
|
-
<div className="text-xs text-foreground-subtle">No evaluations yet</div>
|
|
114
|
-
)}
|
|
115
|
-
|
|
116
|
-
{history.length > 0 && (
|
|
117
|
-
<div className="space-y-3">
|
|
118
|
-
{[...history].reverse().map((entry) => (
|
|
119
|
-
<div key={entry.id} className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
120
|
-
{/* Code input */}
|
|
121
|
-
<div className="flex items-start justify-between gap-2 border-b border-border bg-background px-3 py-2">
|
|
122
|
-
<pre className="font-mono text-xs text-foreground whitespace-pre-wrap break-all flex-1">{entry.code}</pre>
|
|
123
|
-
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
124
|
-
<span className="text-[10px] text-foreground-subtle">{entry.ts}</span>
|
|
125
|
-
<button
|
|
126
|
-
onClick={() => reuseCode(entry.code)}
|
|
127
|
-
title="Reuse"
|
|
128
|
-
className="text-foreground-subtle hover:text-foreground"
|
|
129
|
-
>
|
|
130
|
-
<ChevronUp className="h-3.5 w-3.5" />
|
|
131
|
-
</button>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
{/* Result */}
|
|
135
|
-
<pre
|
|
136
|
-
className={`px-3 py-2 font-mono text-xs whitespace-pre-wrap break-all ${
|
|
137
|
-
entry.error ? 'text-danger' : 'text-success'
|
|
138
|
-
}`}
|
|
139
|
-
>
|
|
140
|
-
{entry.result}
|
|
141
|
-
</pre>
|
|
142
|
-
</div>
|
|
143
|
-
))}
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
)
|
|
148
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
4
|
-
import { FormBuilder } from '../../components/form-builder/FormBuilder'
|
|
5
|
-
import type { ConfigUISchema } from '../../types/config-ui'
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// SectionPanel — fetches schema + values from backend
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
interface SectionPanelProps {
|
|
12
|
-
section: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function SectionPanel({ section }: SectionPanelProps) {
|
|
16
|
-
const client = useBackendClient()
|
|
17
|
-
const queryClient = useQueryClient()
|
|
18
|
-
const [localValues, setLocalValues] = useState<Record<string, unknown> | null>(null)
|
|
19
|
-
const [saveSuccess, setSaveSuccess] = useState(false)
|
|
20
|
-
const [saveError, setSaveError] = useState<string | null>(null)
|
|
21
|
-
|
|
22
|
-
// Fetch schema from backend
|
|
23
|
-
const schemaQuery = useQuery({
|
|
24
|
-
queryKey: ['settings-schema', section],
|
|
25
|
-
queryFn: () => client.getSettingsSchema(section as Parameters<typeof client.getSettingsSchema>[0]),
|
|
26
|
-
staleTime: Infinity, // schemas don't change at runtime
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// Fetch values
|
|
30
|
-
const valuesQuery = useQuery({
|
|
31
|
-
queryKey: ['settings', section],
|
|
32
|
-
queryFn: () => client.getSettings(section as Parameters<typeof client.getSettings>[0]),
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const schema = (schemaQuery.data as { schema?: ConfigUISchema } | undefined)?.schema
|
|
36
|
-
const isReadOnly = (schemaQuery.data as { readOnly?: boolean } | undefined)?.readOnly ?? false
|
|
37
|
-
|
|
38
|
-
const remoteValues = ((valuesQuery.data?.data ?? {}) as Record<string, unknown>)
|
|
39
|
-
const values = localValues ?? remoteValues
|
|
40
|
-
|
|
41
|
-
const isDirty = localValues !== null
|
|
42
|
-
|
|
43
|
-
const updateMutation = useMutation({
|
|
44
|
-
mutationFn: (updated: Record<string, unknown>) =>
|
|
45
|
-
client.updateSettings(section as Parameters<typeof client.updateSettings>[0], updated),
|
|
46
|
-
onSuccess: () => {
|
|
47
|
-
setSaveSuccess(true)
|
|
48
|
-
setSaveError(null)
|
|
49
|
-
setLocalValues(null)
|
|
50
|
-
void queryClient.invalidateQueries({ queryKey: ['settings', section] })
|
|
51
|
-
setTimeout(() => setSaveSuccess(false), 2000)
|
|
52
|
-
},
|
|
53
|
-
onError: (err: unknown) => {
|
|
54
|
-
setSaveError(err instanceof Error ? err.message : 'Failed to save')
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
if (schemaQuery.isLoading || valuesQuery.isLoading) {
|
|
59
|
-
return <div className="text-xs text-foreground-subtle animate-pulse py-4">Loading...</div>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (schemaQuery.isError || valuesQuery.isError) {
|
|
63
|
-
return <div className="text-xs text-danger py-4">Failed to load settings</div>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!schema || schema.sections.length === 0) {
|
|
67
|
-
return <div className="text-xs text-foreground-subtle py-4">No configurable settings for this section.</div>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className="space-y-4">
|
|
72
|
-
<FormBuilder
|
|
73
|
-
schema={schema}
|
|
74
|
-
values={values}
|
|
75
|
-
onChange={(updated) => setLocalValues(updated)}
|
|
76
|
-
disabled={isReadOnly}
|
|
77
|
-
/>
|
|
78
|
-
|
|
79
|
-
{!isReadOnly && (
|
|
80
|
-
<div className="flex items-center justify-end gap-3">
|
|
81
|
-
{saveSuccess && (
|
|
82
|
-
<span className="text-xs text-success">Saved successfully</span>
|
|
83
|
-
)}
|
|
84
|
-
{saveError && (
|
|
85
|
-
<span className="text-xs text-danger">{saveError}</span>
|
|
86
|
-
)}
|
|
87
|
-
{isDirty && (
|
|
88
|
-
<span className="text-[10px] text-orange-400">Unsaved changes</span>
|
|
89
|
-
)}
|
|
90
|
-
<button
|
|
91
|
-
type="button"
|
|
92
|
-
onClick={() => updateMutation.mutate(values)}
|
|
93
|
-
disabled={updateMutation.isPending || !isDirty}
|
|
94
|
-
className="rounded-md bg-primary px-4 py-1.5 text-xs font-medium text-white hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
95
|
-
>
|
|
96
|
-
{updateMutation.isPending ? 'Saving...' : 'Save'}
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Page — fetches tabs from backend
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
interface TabEntry {
|
|
109
|
-
readonly id: string
|
|
110
|
-
readonly label: string
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function SettingsPage() {
|
|
114
|
-
const client = useBackendClient()
|
|
115
|
-
|
|
116
|
-
// Fetch all schemas + tabs metadata in one call
|
|
117
|
-
const allQuery = useQuery({
|
|
118
|
-
queryKey: ['settings-schema-all'],
|
|
119
|
-
queryFn: () => client.getSettingsSchema(),
|
|
120
|
-
staleTime: Infinity,
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const tabs: readonly TabEntry[] =
|
|
124
|
-
(allQuery.data as { tabs?: readonly TabEntry[] } | undefined)?.tabs ?? []
|
|
125
|
-
|
|
126
|
-
const [activeTab, setActiveTab] = useState<string | null>(null)
|
|
127
|
-
const currentTab = activeTab ?? (tabs.length > 0 ? tabs[0].id : null)
|
|
128
|
-
|
|
129
|
-
if (allQuery.isLoading) {
|
|
130
|
-
return (
|
|
131
|
-
<div className="p-6">
|
|
132
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading settings...</div>
|
|
133
|
-
</div>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="p-6 space-y-4">
|
|
139
|
-
<div className="flex items-center justify-between">
|
|
140
|
-
<h1 className="text-lg font-semibold text-foreground">Settings</h1>
|
|
141
|
-
<span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium">
|
|
142
|
-
system
|
|
143
|
-
</span>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Tabs — scrollable on small screens */}
|
|
147
|
-
<div className="flex gap-1 border-b border-border overflow-x-auto scrollbar-hide">
|
|
148
|
-
{tabs.map((tab) => (
|
|
149
|
-
<button
|
|
150
|
-
key={tab.id}
|
|
151
|
-
onClick={() => setActiveTab(tab.id)}
|
|
152
|
-
className={[
|
|
153
|
-
'px-3 py-1.5 text-xs font-medium rounded-t transition-colors whitespace-nowrap',
|
|
154
|
-
currentTab === tab.id
|
|
155
|
-
? 'text-primary border-b-2 border-primary -mb-px bg-surface'
|
|
156
|
-
: 'text-foreground-subtle hover:text-foreground',
|
|
157
|
-
].join(' ')}
|
|
158
|
-
>
|
|
159
|
-
{tab.label}
|
|
160
|
-
</button>
|
|
161
|
-
))}
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
{/* Active panel */}
|
|
165
|
-
{currentTab && <SectionPanel key={currentTab} section={currentTab} />}
|
|
166
|
-
</div>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import { Plus, X } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
type Role = 'super_admin' | 'admin' | 'viewer'
|
|
7
|
-
|
|
8
|
-
const ROLE_STYLES: Record<Role, string> = {
|
|
9
|
-
super_admin: 'bg-danger/10 text-danger',
|
|
10
|
-
admin: 'bg-primary/10 text-primary',
|
|
11
|
-
viewer: 'bg-foreground-subtle/10 text-foreground-subtle',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function UsersPage() {
|
|
15
|
-
const client = useBackendClient()
|
|
16
|
-
const queryClient = useQueryClient()
|
|
17
|
-
const [showForm, setShowForm] = useState(false)
|
|
18
|
-
const [username, setUsername] = useState('')
|
|
19
|
-
const [password, setPassword] = useState('')
|
|
20
|
-
const [role, setRole] = useState<Role>('viewer')
|
|
21
|
-
const [formError, setFormError] = useState<string | null>(null)
|
|
22
|
-
|
|
23
|
-
const { data: users, isLoading, isError } = useQuery({
|
|
24
|
-
queryKey: ['users'],
|
|
25
|
-
queryFn: () => client.listUsers(),
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const createMutation = useMutation({
|
|
29
|
-
mutationFn: () => client.createUser(username, password, role),
|
|
30
|
-
onSuccess: () => {
|
|
31
|
-
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
32
|
-
setShowForm(false)
|
|
33
|
-
setUsername('')
|
|
34
|
-
setPassword('')
|
|
35
|
-
setRole('viewer')
|
|
36
|
-
setFormError(null)
|
|
37
|
-
},
|
|
38
|
-
onError: (err: unknown) => {
|
|
39
|
-
setFormError(err instanceof Error ? err.message : 'Failed to create user')
|
|
40
|
-
},
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const userList = (users ?? []) as unknown as Array<Record<string, unknown>>
|
|
44
|
-
|
|
45
|
-
function formatDate(ts: unknown): string {
|
|
46
|
-
if (!ts) return '—'
|
|
47
|
-
const d = new Date(typeof ts === 'number' ? ts : String(ts))
|
|
48
|
-
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div className="p-6 space-y-4">
|
|
53
|
-
<div className="flex items-center justify-between">
|
|
54
|
-
<h1 className="text-lg font-semibold text-foreground">Users</h1>
|
|
55
|
-
<button
|
|
56
|
-
onClick={() => setShowForm(true)}
|
|
57
|
-
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm hover:bg-primary/90"
|
|
58
|
-
>
|
|
59
|
-
<Plus className="h-3.5 w-3.5" />
|
|
60
|
-
Create User
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
{/* Create user form */}
|
|
65
|
-
{showForm && (
|
|
66
|
-
<div className="rounded-lg border border-border bg-surface p-4 space-y-3">
|
|
67
|
-
<div className="flex items-center justify-between">
|
|
68
|
-
<span className="text-xs font-medium text-foreground">New User</span>
|
|
69
|
-
<button onClick={() => setShowForm(false)} className="text-foreground-subtle hover:text-foreground">
|
|
70
|
-
<X className="h-4 w-4" />
|
|
71
|
-
</button>
|
|
72
|
-
</div>
|
|
73
|
-
<div className="grid grid-cols-3 gap-3">
|
|
74
|
-
<div className="space-y-1">
|
|
75
|
-
<label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Username</label>
|
|
76
|
-
<input
|
|
77
|
-
type="text"
|
|
78
|
-
value={username}
|
|
79
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
80
|
-
placeholder="john"
|
|
81
|
-
className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
<div className="space-y-1">
|
|
85
|
-
<label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Password</label>
|
|
86
|
-
<input
|
|
87
|
-
type="password"
|
|
88
|
-
value={password}
|
|
89
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
90
|
-
placeholder="••••••••"
|
|
91
|
-
className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
|
|
92
|
-
/>
|
|
93
|
-
</div>
|
|
94
|
-
<div className="space-y-1">
|
|
95
|
-
<label className="text-[10px] text-foreground-subtle uppercase tracking-wide">Role</label>
|
|
96
|
-
<select
|
|
97
|
-
value={role}
|
|
98
|
-
onChange={(e) => setRole(e.target.value as Role)}
|
|
99
|
-
className="w-full rounded border border-border bg-background px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
100
|
-
>
|
|
101
|
-
<option value="viewer">Viewer</option>
|
|
102
|
-
<option value="admin">Admin</option>
|
|
103
|
-
<option value="super_admin">Super Admin</option>
|
|
104
|
-
</select>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
{formError && <p className="text-[10px] text-danger">{formError}</p>}
|
|
108
|
-
<div className="flex justify-end gap-2">
|
|
109
|
-
<button
|
|
110
|
-
onClick={() => setShowForm(false)}
|
|
111
|
-
className="rounded px-3 py-1.5 text-xs text-foreground-subtle border border-border hover:text-foreground"
|
|
112
|
-
>
|
|
113
|
-
Cancel
|
|
114
|
-
</button>
|
|
115
|
-
<button
|
|
116
|
-
onClick={() => createMutation.mutate()}
|
|
117
|
-
disabled={createMutation.isPending || !username || !password}
|
|
118
|
-
className="rounded bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
119
|
-
>
|
|
120
|
-
{createMutation.isPending ? 'Creating...' : 'Create'}
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
{isLoading && (
|
|
127
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
128
|
-
)}
|
|
129
|
-
|
|
130
|
-
{isError && (
|
|
131
|
-
<div className="text-xs text-danger">Failed to load</div>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{!isLoading && !isError && userList.length === 0 && (
|
|
135
|
-
<div className="text-xs text-foreground-subtle">No data</div>
|
|
136
|
-
)}
|
|
137
|
-
|
|
138
|
-
{userList.length > 0 && (
|
|
139
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
140
|
-
<table className="w-full text-xs">
|
|
141
|
-
<thead>
|
|
142
|
-
<tr>
|
|
143
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Username</th>
|
|
144
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Role</th>
|
|
145
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Created</th>
|
|
146
|
-
</tr>
|
|
147
|
-
</thead>
|
|
148
|
-
<tbody>
|
|
149
|
-
{userList.map((user) => {
|
|
150
|
-
const userRole = String(user.role ?? 'viewer') as Role
|
|
151
|
-
const roleStyle = ROLE_STYLES[userRole] ?? ROLE_STYLES['viewer']
|
|
152
|
-
return (
|
|
153
|
-
<tr key={String(user.id ?? user.username)} className="hover:bg-primary/5">
|
|
154
|
-
<td className="px-3 py-2 text-foreground border-b border-border">
|
|
155
|
-
{String(user.username ?? user.name ?? '')}
|
|
156
|
-
</td>
|
|
157
|
-
<td className="px-3 py-2 text-foreground border-b border-border">
|
|
158
|
-
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${roleStyle}`}>
|
|
159
|
-
{userRole.replace('_', ' ')}
|
|
160
|
-
</span>
|
|
161
|
-
</td>
|
|
162
|
-
<td className="px-3 py-2 text-foreground border-b border-border text-foreground-subtle">
|
|
163
|
-
{formatDate(user.createdAt ?? user.created_at ?? user.createdAt)}
|
|
164
|
-
</td>
|
|
165
|
-
</tr>
|
|
166
|
-
)
|
|
167
|
-
})}
|
|
168
|
-
</tbody>
|
|
169
|
-
</table>
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
)
|
|
174
|
-
}
|
package/src/server/addon.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import type {
|
|
4
|
-
ICamstackAddon,
|
|
5
|
-
AddonManifest,
|
|
6
|
-
AddonContext,
|
|
7
|
-
IAdminUI,
|
|
8
|
-
CapabilityProviderMap,
|
|
9
|
-
} from '@camstack/types'
|
|
10
|
-
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* AdminUIAddon — standalone addon that serves the Vite-built frontend.
|
|
15
|
-
* The dist/ directory containing the built React app sits alongside this
|
|
16
|
-
* compiled file in the package output.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
function resolveAdminUiDistDir(): string {
|
|
20
|
-
// This addon file lives at <package-root>/dist/server/addon.js.
|
|
21
|
-
// Vite assets (index.html, assets/) are at <package-root>/dist/.
|
|
22
|
-
return path.resolve(__dirname, '..')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class AdminUIAddon implements ICamstackAddon {
|
|
26
|
-
readonly id = 'admin-ui'
|
|
27
|
-
|
|
28
|
-
readonly manifest: AddonManifest = {
|
|
29
|
-
id: 'admin-ui',
|
|
30
|
-
name: 'CamStack Admin UI',
|
|
31
|
-
version: '0.1.0',
|
|
32
|
-
packageName: '@camstack/addon-admin-ui',
|
|
33
|
-
description: 'Web-based administration interface for CamStack',
|
|
34
|
-
capabilities: [{ name: 'admin-ui', mode: 'singleton' }],
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async initialize(_ctx: AddonContext): Promise<void> {}
|
|
38
|
-
async shutdown(): Promise<void> {}
|
|
39
|
-
|
|
40
|
-
getCapabilityProvider<K extends keyof CapabilityProviderMap>(
|
|
41
|
-
name: K,
|
|
42
|
-
): CapabilityProviderMap[K] | null {
|
|
43
|
-
if (name === 'admin-ui') {
|
|
44
|
-
const provider: IAdminUI = {
|
|
45
|
-
getStaticDir: () => resolveAdminUiDistDir(),
|
|
46
|
-
getVersion: () => this.manifest.version,
|
|
47
|
-
}
|
|
48
|
-
return provider as CapabilityProviderMap[K]
|
|
49
|
-
}
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export default AdminUIAddon
|