@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,355 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { ChevronDown, ChevronUp, Plus, Trash2, Save, Loader2, Package, Puzzle, Download } from 'lucide-react'
|
|
3
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { VersionBadge } from '@camstack/ui'
|
|
5
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
6
|
-
import { useConfirm } from '../ui/ConfirmDialog'
|
|
7
|
-
import { FormBuilder } from '../form-builder/FormBuilder'
|
|
8
|
-
import type { ConfigUISchema } from '../../types/config-ui'
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Types
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
export interface AddonManifest {
|
|
15
|
-
id: string
|
|
16
|
-
name: string
|
|
17
|
-
version: string
|
|
18
|
-
description?: string
|
|
19
|
-
capabilities: readonly (string | { name: string; mode?: string })[]
|
|
20
|
-
requiredFeatures?: readonly string[]
|
|
21
|
-
removable?: boolean
|
|
22
|
-
protected?: boolean
|
|
23
|
-
components?: string[]
|
|
24
|
-
packageName: string
|
|
25
|
-
packageVersion: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface AddonListItem {
|
|
29
|
-
manifest: AddonManifest
|
|
30
|
-
enabled: boolean
|
|
31
|
-
hasConfigSchema: boolean
|
|
32
|
-
group?: 'core' | 'addon' | 'provider' | 'page'
|
|
33
|
-
source?: 'core' | 'installed' | 'workspace'
|
|
34
|
-
installedOn?: string[]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Icon letter
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
const GROUP_COLORS: Record<string, string> = {
|
|
42
|
-
core: 'bg-blue-500/20 text-blue-300',
|
|
43
|
-
addon: 'bg-purple-500/20 text-purple-300',
|
|
44
|
-
provider: 'bg-green-500/20 text-green-300',
|
|
45
|
-
page: 'bg-orange-500/20 text-orange-300',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function AddonIcon({ name, group, isBundle }: { name: string; group?: string; isBundle?: boolean }) {
|
|
49
|
-
const color = GROUP_COLORS[group ?? 'addon'] ?? GROUP_COLORS['addon']!
|
|
50
|
-
return (
|
|
51
|
-
<div className={`flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center ${color}`}>
|
|
52
|
-
{isBundle ? (
|
|
53
|
-
<Package className="h-4 w-4" />
|
|
54
|
-
) : (
|
|
55
|
-
<Puzzle className="h-4 w-4" />
|
|
56
|
-
)}
|
|
57
|
-
</div>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// AddonCard
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
export interface AgentInfo {
|
|
66
|
-
id: string
|
|
67
|
-
name: string
|
|
68
|
-
isHub: boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface AddonCardProps {
|
|
72
|
-
addon: AddonListItem
|
|
73
|
-
agents?: AgentInfo[]
|
|
74
|
-
/** Hide version when addon is part of a package bundle */
|
|
75
|
-
hideVersion?: boolean
|
|
76
|
-
/** Latest available version from npm (if update available) */
|
|
77
|
-
availableUpdate?: string
|
|
78
|
-
/** Callback when user clicks the update button */
|
|
79
|
-
onUpdate?: () => void
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function AddonCard({ addon, agents = [], hideVersion, availableUpdate, onUpdate }: AddonCardProps) {
|
|
83
|
-
const [expanded, setExpanded] = useState(false)
|
|
84
|
-
const [agentDropdownOpen, setAgentDropdownOpen] = useState(false)
|
|
85
|
-
const client = useBackendClient()
|
|
86
|
-
const queryClient = useQueryClient()
|
|
87
|
-
const confirm = useConfirm()
|
|
88
|
-
|
|
89
|
-
const { manifest } = addon
|
|
90
|
-
const installedOn = addon.installedOn ?? []
|
|
91
|
-
const removable = !manifest.protected && manifest.removable !== false
|
|
92
|
-
// Only expandable if addon has config, components, or agents to show
|
|
93
|
-
const hasExpandableContent = addon.hasConfigSchema || (manifest.components && manifest.components.length > 0) || (agents.length >= 2)
|
|
94
|
-
|
|
95
|
-
// Load config schema when expanded and addon has config
|
|
96
|
-
const { data: configSchema, isLoading: schemaLoading } = useQuery({
|
|
97
|
-
queryKey: ['addon-config-schema', manifest.id],
|
|
98
|
-
queryFn: () => client.trpc.addons.getConfigSchema.query({ addonId: manifest.id }) as Promise<ConfigUISchema | null>,
|
|
99
|
-
enabled: expanded && addon.hasConfigSchema,
|
|
100
|
-
staleTime: 60_000,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// Load current config values
|
|
104
|
-
const { data: configValues } = useQuery({
|
|
105
|
-
queryKey: ['addon-config', manifest.id],
|
|
106
|
-
queryFn: () => client.trpc.addons.getConfig.query({ addonId: manifest.id }) as Promise<Record<string, unknown> | null>,
|
|
107
|
-
enabled: expanded && addon.hasConfigSchema,
|
|
108
|
-
staleTime: 30_000,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
// Local state for editing
|
|
112
|
-
const [editValues, setEditValues] = useState<Record<string, unknown>>({})
|
|
113
|
-
const [dirty, setDirty] = useState(false)
|
|
114
|
-
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (configValues) {
|
|
117
|
-
setEditValues(configValues)
|
|
118
|
-
setDirty(false)
|
|
119
|
-
}
|
|
120
|
-
}, [configValues])
|
|
121
|
-
|
|
122
|
-
const handleChange = (values: Record<string, unknown>) => {
|
|
123
|
-
setEditValues(values)
|
|
124
|
-
setDirty(true)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Save config
|
|
128
|
-
const saveMutation = useMutation({
|
|
129
|
-
mutationFn: () => client.trpc.addons.updateConfig.mutate({ addonId: manifest.id, config: editValues }),
|
|
130
|
-
onSuccess: () => {
|
|
131
|
-
setDirty(false)
|
|
132
|
-
queryClient.invalidateQueries({ queryKey: ['addon-config', manifest.id] })
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Uninstall
|
|
137
|
-
const uninstallMutation = useMutation({
|
|
138
|
-
mutationFn: () => {
|
|
139
|
-
console.log(`[AddonCard] Uninstalling ${manifest.packageName}`)
|
|
140
|
-
return client.trpc.bridgeAddons.uninstallPackage.mutate({ packageName: manifest.packageName })
|
|
141
|
-
},
|
|
142
|
-
onSuccess: () => {
|
|
143
|
-
console.log(`[AddonCard] Uninstalled ${manifest.packageName}`)
|
|
144
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
145
|
-
queryClient.invalidateQueries({ queryKey: ['addon-pages'] })
|
|
146
|
-
queryClient.invalidateQueries({ queryKey: ['capabilities'] })
|
|
147
|
-
},
|
|
148
|
-
onError: (err) => {
|
|
149
|
-
console.error(`[AddonCard] Uninstall failed:`, err)
|
|
150
|
-
},
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
155
|
-
{/* Header row */}
|
|
156
|
-
<div
|
|
157
|
-
className={[
|
|
158
|
-
'flex items-start gap-3 px-4 py-3 transition-colors',
|
|
159
|
-
hasExpandableContent ? 'cursor-pointer hover:bg-surface-hover/50' : '',
|
|
160
|
-
].join(' ')}
|
|
161
|
-
onClick={() => hasExpandableContent && setExpanded((e) => !e)}
|
|
162
|
-
>
|
|
163
|
-
<AddonIcon name={manifest.name} group={addon.group} isBundle={!!manifest.components && manifest.components.length > 1} />
|
|
164
|
-
|
|
165
|
-
<div className="flex-1 min-w-0">
|
|
166
|
-
<div className="flex items-center gap-2">
|
|
167
|
-
<span className="text-sm font-semibold text-foreground truncate">{manifest.name}</span>
|
|
168
|
-
<span className="text-[10px] text-foreground-subtle font-mono shrink-0">({manifest.packageName})</span>
|
|
169
|
-
{addon.source === 'workspace' && (
|
|
170
|
-
<span className="text-[10px] rounded-full bg-orange-500/15 text-orange-400 px-2 py-0.5 font-medium shrink-0">
|
|
171
|
-
DEV
|
|
172
|
-
</span>
|
|
173
|
-
)}
|
|
174
|
-
{!hideVersion && (
|
|
175
|
-
<span className="ml-auto shrink-0 inline-flex items-center gap-1.5">
|
|
176
|
-
{availableUpdate && onUpdate && (
|
|
177
|
-
<button
|
|
178
|
-
type="button"
|
|
179
|
-
onClick={(e) => { e.stopPropagation(); onUpdate() }}
|
|
180
|
-
className="rounded-full bg-primary/10 text-primary hover:bg-primary/20 p-1 transition-colors"
|
|
181
|
-
title={`Update to v${availableUpdate}`}
|
|
182
|
-
>
|
|
183
|
-
<Download className="h-3 w-3" />
|
|
184
|
-
</button>
|
|
185
|
-
)}
|
|
186
|
-
<VersionBadge version={manifest.version} />
|
|
187
|
-
</span>
|
|
188
|
-
)}
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{manifest.description && (
|
|
192
|
-
<p className="text-[10px] text-foreground-subtle mt-0.5 line-clamp-1">{manifest.description}</p>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
{/* Actions: uninstall + expand chevron */}
|
|
198
|
-
<div className="flex items-center gap-2 shrink-0 pt-1">
|
|
199
|
-
{removable && (
|
|
200
|
-
<button
|
|
201
|
-
type="button"
|
|
202
|
-
onClick={async (e) => {
|
|
203
|
-
e.stopPropagation()
|
|
204
|
-
console.log('[AddonCard] Confirm dialog opening...')
|
|
205
|
-
const confirmed = await confirm({
|
|
206
|
-
title: `Uninstall ${manifest.name}?`,
|
|
207
|
-
message: `This will remove the addon package and all its data. This action cannot be undone.`,
|
|
208
|
-
confirmLabel: 'Uninstall',
|
|
209
|
-
variant: 'danger',
|
|
210
|
-
})
|
|
211
|
-
if (confirmed) uninstallMutation.mutate()
|
|
212
|
-
}}
|
|
213
|
-
disabled={uninstallMutation.isPending}
|
|
214
|
-
className="flex items-center gap-1 rounded-md px-2 py-1 text-[10px] text-red-400 hover:bg-red-500/10 disabled:opacity-50 transition-colors"
|
|
215
|
-
title={`Uninstall ${manifest.name}`}
|
|
216
|
-
>
|
|
217
|
-
<Trash2 className="h-3 w-3" />
|
|
218
|
-
{uninstallMutation.isPending && 'Removing...'}
|
|
219
|
-
</button>
|
|
220
|
-
)}
|
|
221
|
-
{hasExpandableContent && (
|
|
222
|
-
expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle" />
|
|
223
|
-
)}
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
{/* Expanded: settings inline */}
|
|
228
|
-
{expanded && (
|
|
229
|
-
<div className="border-t border-border px-4 py-3 space-y-4">
|
|
230
|
-
|
|
231
|
-
{/* Config form — loaded from backend */}
|
|
232
|
-
{addon.hasConfigSchema && (
|
|
233
|
-
<div>
|
|
234
|
-
{schemaLoading && (
|
|
235
|
-
<div className="flex items-center gap-2 text-xs text-foreground-subtle py-2">
|
|
236
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
237
|
-
Loading configuration...
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
|
|
241
|
-
{configSchema && configSchema.sections?.some((s: any) => s.fields?.length > 0) && (
|
|
242
|
-
<div className="space-y-3">
|
|
243
|
-
<FormBuilder
|
|
244
|
-
schema={configSchema}
|
|
245
|
-
values={editValues}
|
|
246
|
-
onChange={handleChange}
|
|
247
|
-
/>
|
|
248
|
-
{dirty && (
|
|
249
|
-
<div className="flex items-center justify-between">
|
|
250
|
-
<span className="text-[10px] text-orange-400">Unsaved changes</span>
|
|
251
|
-
<button
|
|
252
|
-
type="button"
|
|
253
|
-
onClick={(e) => { e.stopPropagation(); saveMutation.mutate() }}
|
|
254
|
-
disabled={saveMutation.isPending}
|
|
255
|
-
className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
256
|
-
>
|
|
257
|
-
{saveMutation.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <Save className="h-3 w-3" />}
|
|
258
|
-
{saveMutation.isPending ? 'Saving...' : 'Save'}
|
|
259
|
-
</button>
|
|
260
|
-
</div>
|
|
261
|
-
)}
|
|
262
|
-
{saveMutation.isError && (
|
|
263
|
-
<div className="text-[10px] text-red-400">
|
|
264
|
-
Save failed: {saveMutation.error instanceof Error ? saveMutation.error.message : 'Unknown error'}
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
|
|
270
|
-
{/* No fallback message — addons without config don't show the expander */}
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
|
|
274
|
-
{/* No message for addons without config — just show other sections */}
|
|
275
|
-
|
|
276
|
-
{/* Components list (for core) */}
|
|
277
|
-
{manifest.components && manifest.components.length > 0 && (
|
|
278
|
-
<div>
|
|
279
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-2">
|
|
280
|
-
Components ({manifest.components.length})
|
|
281
|
-
</div>
|
|
282
|
-
<div className="flex flex-wrap gap-1">
|
|
283
|
-
{manifest.components.map((comp) => (
|
|
284
|
-
<span key={comp} className="text-[10px] rounded bg-background border border-border px-1.5 py-0.5 text-foreground-subtle font-mono">
|
|
285
|
-
{comp}
|
|
286
|
-
</span>
|
|
287
|
-
))}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
)}
|
|
291
|
-
|
|
292
|
-
{/* Installed on agents — only show when remote agents exist (2+ total) */}
|
|
293
|
-
{agents.length >= 2 && (
|
|
294
|
-
<div>
|
|
295
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-2">
|
|
296
|
-
Installed on
|
|
297
|
-
</div>
|
|
298
|
-
<div className="flex items-center gap-1.5 flex-wrap">
|
|
299
|
-
{installedOn.length === 0 && (
|
|
300
|
-
<span className="text-[10px] text-foreground-subtle">Hub (local)</span>
|
|
301
|
-
)}
|
|
302
|
-
{installedOn.map((agentName) => (
|
|
303
|
-
<span
|
|
304
|
-
key={agentName}
|
|
305
|
-
className="rounded-full bg-surface-hover border border-border px-2 py-0.5 text-[10px] text-foreground-subtle"
|
|
306
|
-
>
|
|
307
|
-
{agentName}
|
|
308
|
-
</span>
|
|
309
|
-
))}
|
|
310
|
-
<div className="relative">
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
onClick={(e) => { e.stopPropagation(); setAgentDropdownOpen((o) => !o) }}
|
|
314
|
-
className="inline-flex items-center gap-1 rounded-full border border-dashed border-border px-2 py-0.5 text-[10px] text-foreground-subtle hover:text-foreground hover:border-primary transition-colors"
|
|
315
|
-
title="Install on more agents"
|
|
316
|
-
>
|
|
317
|
-
<Plus className="h-3 w-3" />
|
|
318
|
-
Add agent
|
|
319
|
-
</button>
|
|
320
|
-
{agentDropdownOpen && (
|
|
321
|
-
<div className="absolute left-0 top-full mt-1 z-10 min-w-[160px] rounded-md border border-border bg-surface shadow-lg overflow-hidden">
|
|
322
|
-
{agents
|
|
323
|
-
.filter((a) => !a.isHub && !installedOn.includes(a.name))
|
|
324
|
-
.map((agent) => (
|
|
325
|
-
<button
|
|
326
|
-
key={agent.id}
|
|
327
|
-
type="button"
|
|
328
|
-
onClick={(e) => {
|
|
329
|
-
e.stopPropagation()
|
|
330
|
-
setAgentDropdownOpen(false)
|
|
331
|
-
// TODO: wire up install-on-agent mutation
|
|
332
|
-
}}
|
|
333
|
-
className="w-full text-left px-3 py-1.5 text-[10px] text-foreground hover:bg-surface-hover transition-colors"
|
|
334
|
-
>
|
|
335
|
-
{agent.name}
|
|
336
|
-
</button>
|
|
337
|
-
))}
|
|
338
|
-
{agents.filter((a) => !a.isHub && !installedOn.includes(a.name)).length === 0 && (
|
|
339
|
-
<div className="px-3 py-1.5 text-[10px] text-foreground-subtle">
|
|
340
|
-
No available agents
|
|
341
|
-
</div>
|
|
342
|
-
)}
|
|
343
|
-
</div>
|
|
344
|
-
)}
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
)}
|
|
349
|
-
|
|
350
|
-
{/* Uninstall button is now inline in the header */}
|
|
351
|
-
</div>
|
|
352
|
-
)}
|
|
353
|
-
</div>
|
|
354
|
-
)
|
|
355
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { useRef, useCallback, useState } from 'react'
|
|
2
|
-
import { Upload, Loader2 } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
interface AddonUploadZoneProps {
|
|
5
|
-
onUploadSuccess: () => void
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const ACCEPT_MIME = '.tgz,.tar.gz,.zip'
|
|
9
|
-
|
|
10
|
-
export function AddonUploadZone({ onUploadSuccess }: AddonUploadZoneProps) {
|
|
11
|
-
const [uploading, setUploading] = useState(false)
|
|
12
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
13
|
-
|
|
14
|
-
const uploadFile = useCallback(
|
|
15
|
-
async (file: File) => {
|
|
16
|
-
setUploading(true)
|
|
17
|
-
const formData = new FormData()
|
|
18
|
-
formData.append('file', file)
|
|
19
|
-
const token = localStorage.getItem('camstack_admin_token') ?? ''
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const res = await fetch('/api/addons/upload', {
|
|
23
|
-
method: 'POST',
|
|
24
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
25
|
-
body: formData,
|
|
26
|
-
})
|
|
27
|
-
const body = await res.json()
|
|
28
|
-
if (body.success) onUploadSuccess()
|
|
29
|
-
} finally {
|
|
30
|
-
setUploading(false)
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
[onUploadSuccess],
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
const handleFileSelect = useCallback(
|
|
37
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
38
|
-
const file = e.target.files?.[0]
|
|
39
|
-
if (file) uploadFile(file)
|
|
40
|
-
e.target.value = ''
|
|
41
|
-
},
|
|
42
|
-
[uploadFile],
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<>
|
|
47
|
-
<input
|
|
48
|
-
ref={fileInputRef}
|
|
49
|
-
type="file"
|
|
50
|
-
accept={ACCEPT_MIME}
|
|
51
|
-
onChange={handleFileSelect}
|
|
52
|
-
className="hidden"
|
|
53
|
-
/>
|
|
54
|
-
<button
|
|
55
|
-
type="button"
|
|
56
|
-
onClick={() => fileInputRef.current?.click()}
|
|
57
|
-
disabled={uploading}
|
|
58
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-surface hover:bg-surface-hover border border-border disabled:opacity-50 transition-colors"
|
|
59
|
-
>
|
|
60
|
-
{uploading ? (
|
|
61
|
-
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
62
|
-
) : (
|
|
63
|
-
<Upload className="w-3.5 h-3.5" />
|
|
64
|
-
)}
|
|
65
|
-
{uploading ? 'Uploading...' : 'Upload'}
|
|
66
|
-
</button>
|
|
67
|
-
</>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const CAPABILITY_COLORS: Record<string, string> = {
|
|
2
|
-
// detection / vision
|
|
3
|
-
detection: 'bg-green-500/10 text-green-400',
|
|
4
|
-
detector: 'bg-green-500/10 text-green-400',
|
|
5
|
-
// streaming
|
|
6
|
-
streaming: 'bg-blue-500/10 text-blue-400',
|
|
7
|
-
decode: 'bg-blue-500/10 text-blue-400',
|
|
8
|
-
decoder: 'bg-blue-500/10 text-blue-400',
|
|
9
|
-
// recording
|
|
10
|
-
recording: 'bg-orange-500/10 text-orange-400',
|
|
11
|
-
recorder: 'bg-orange-500/10 text-orange-400',
|
|
12
|
-
// transcoding
|
|
13
|
-
transcoding: 'bg-purple-500/10 text-purple-400',
|
|
14
|
-
transcoder: 'bg-purple-500/10 text-purple-400',
|
|
15
|
-
transcode: 'bg-purple-500/10 text-purple-400',
|
|
16
|
-
// restream
|
|
17
|
-
restream: 'bg-cyan-500/10 text-cyan-400',
|
|
18
|
-
restreamer: 'bg-cyan-500/10 text-cyan-400',
|
|
19
|
-
// storage
|
|
20
|
-
storage: 'bg-yellow-500/10 text-yellow-400',
|
|
21
|
-
// notification
|
|
22
|
-
notification: 'bg-pink-500/10 text-pink-400',
|
|
23
|
-
notifier: 'bg-pink-500/10 text-pink-400',
|
|
24
|
-
// faces
|
|
25
|
-
faces: 'bg-indigo-500/10 text-indigo-400',
|
|
26
|
-
// default
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface CapabilityBadgeProps {
|
|
30
|
-
/** Capability name (string) or declaration object ({ name, mode }) */
|
|
31
|
-
capability: string | { name: string; mode?: string }
|
|
32
|
-
/** Optional mode label shown after a separator (overrides object mode) */
|
|
33
|
-
mode?: string
|
|
34
|
-
size?: 'sm' | 'xs'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function CapabilityBadge({ capability, mode, size = 'xs' }: CapabilityBadgeProps) {
|
|
38
|
-
const capName = typeof capability === 'string' ? capability : capability.name
|
|
39
|
-
const capMode = mode ?? (typeof capability === 'object' ? capability.mode : undefined)
|
|
40
|
-
const lower = capName.toLowerCase()
|
|
41
|
-
const color = CAPABILITY_COLORS[lower] ?? 'bg-primary/10 text-primary'
|
|
42
|
-
const textSize = size === 'sm' ? 'text-xs' : 'text-[10px]'
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<span className={`inline-flex items-center rounded-md px-1.5 py-0.5 font-medium gap-1 ${textSize} ${color}`}>
|
|
46
|
-
{capName}
|
|
47
|
-
{capMode && (
|
|
48
|
-
<>
|
|
49
|
-
<span className="opacity-40">·</span>
|
|
50
|
-
<span className="opacity-70">{capMode}</span>
|
|
51
|
-
</>
|
|
52
|
-
)}
|
|
53
|
-
</span>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { CheckCircle } from 'lucide-react'
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Types
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
export type CapabilityMode = 'singleton' | 'collection'
|
|
8
|
-
|
|
9
|
-
export interface CapabilityProvider {
|
|
10
|
-
addonId: string
|
|
11
|
-
addonName: string
|
|
12
|
-
active: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CapabilityEntry {
|
|
16
|
-
name: string
|
|
17
|
-
mode: CapabilityMode
|
|
18
|
-
providers: CapabilityProvider[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Color helpers
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const CAPABILITY_BORDER_COLORS: Record<string, string> = {
|
|
26
|
-
detection: 'border-green-500',
|
|
27
|
-
detector: 'border-green-500',
|
|
28
|
-
streaming: 'border-blue-500',
|
|
29
|
-
decode: 'border-blue-500',
|
|
30
|
-
decoder: 'border-blue-500',
|
|
31
|
-
recording: 'border-orange-500',
|
|
32
|
-
recorder: 'border-orange-500',
|
|
33
|
-
transcoding: 'border-purple-500',
|
|
34
|
-
transcoder: 'border-purple-500',
|
|
35
|
-
restream: 'border-cyan-500',
|
|
36
|
-
restreamer: 'border-cyan-500',
|
|
37
|
-
storage: 'border-yellow-500',
|
|
38
|
-
notification: 'border-pink-500',
|
|
39
|
-
notifier: 'border-pink-500',
|
|
40
|
-
faces: 'border-indigo-500',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const CAPABILITY_BG_COLORS: Record<string, string> = {
|
|
44
|
-
detection: 'bg-green-500/5',
|
|
45
|
-
detector: 'bg-green-500/5',
|
|
46
|
-
streaming: 'bg-blue-500/5',
|
|
47
|
-
decode: 'bg-blue-500/5',
|
|
48
|
-
decoder: 'bg-blue-500/5',
|
|
49
|
-
recording: 'bg-orange-500/5',
|
|
50
|
-
recorder: 'bg-orange-500/5',
|
|
51
|
-
transcoding: 'bg-purple-500/5',
|
|
52
|
-
transcoder: 'bg-purple-500/5',
|
|
53
|
-
restream: 'bg-cyan-500/5',
|
|
54
|
-
restreamer: 'bg-cyan-500/5',
|
|
55
|
-
storage: 'bg-yellow-500/5',
|
|
56
|
-
notification: 'bg-pink-500/5',
|
|
57
|
-
notifier: 'bg-pink-500/5',
|
|
58
|
-
faces: 'bg-indigo-500/5',
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// CapabilityCard
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
function CapabilityCard({ entry }: { entry: CapabilityEntry }) {
|
|
66
|
-
const lower = entry.name.toLowerCase()
|
|
67
|
-
const borderColor = CAPABILITY_BORDER_COLORS[lower] ?? 'border-primary'
|
|
68
|
-
const bgColor = CAPABILITY_BG_COLORS[lower] ?? 'bg-primary/5'
|
|
69
|
-
const activeProvider = entry.providers.find((p) => p.active)
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className={`rounded-lg border-t-2 border border-border ${borderColor} ${bgColor} p-3 space-y-2`}>
|
|
73
|
-
<div className="flex items-center justify-between gap-2">
|
|
74
|
-
<span className="text-xs font-semibold text-foreground capitalize">{entry.name}</span>
|
|
75
|
-
<span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${entry.mode === 'singleton' ? 'bg-primary/10 text-primary' : 'bg-info/10 text-info'}`}>
|
|
76
|
-
{entry.mode}
|
|
77
|
-
</span>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
{entry.mode === 'singleton' ? (
|
|
81
|
-
<div>
|
|
82
|
-
{activeProvider ? (
|
|
83
|
-
<div className="flex items-center gap-1.5 text-[10px] text-foreground">
|
|
84
|
-
<CheckCircle className="h-3 w-3 text-green-400 shrink-0" />
|
|
85
|
-
<span className="font-medium">{activeProvider.addonName}</span>
|
|
86
|
-
<span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium ml-auto">ACTIVE</span>
|
|
87
|
-
</div>
|
|
88
|
-
) : (
|
|
89
|
-
<div className="text-[10px] text-foreground-subtle italic">No active provider</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
) : (
|
|
93
|
-
<div className="space-y-1">
|
|
94
|
-
{entry.providers.length === 0 && (
|
|
95
|
-
<div className="text-[10px] text-foreground-subtle italic">No providers</div>
|
|
96
|
-
)}
|
|
97
|
-
{entry.providers.map((p) => (
|
|
98
|
-
<div key={p.addonId} className="flex items-center justify-between gap-2 text-[10px]">
|
|
99
|
-
<span className="text-foreground-subtle">{p.addonName}</span>
|
|
100
|
-
{p.active && (
|
|
101
|
-
<span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium">ACTIVE</span>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// CapabilityMap
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
interface CapabilityMapProps {
|
|
116
|
-
capabilities: CapabilityEntry[]
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function CapabilityMap({ capabilities }: CapabilityMapProps) {
|
|
120
|
-
if (capabilities.length === 0) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="text-xs text-foreground-subtle text-center py-8">No capabilities registered</div>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
|
128
|
-
{capabilities.map((cap) => (
|
|
129
|
-
<CapabilityCard key={cap.name} entry={cap} />
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
)
|
|
133
|
-
}
|