@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<title>CamStack Admin</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<!-- Import map: allows addon page bundles to import 'react' and get the host instance -->
|
|
9
|
+
|
|
9
10
|
<script type="importmap">
|
|
10
11
|
{
|
|
11
12
|
"imports": {
|
|
@@ -14,9 +15,10 @@
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
</script>
|
|
18
|
+
<script type="module" crossorigin src="/assets/index-w55PwKyu.js"></script>
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DjELGD4R.css">
|
|
17
20
|
</head>
|
|
18
21
|
<body>
|
|
19
22
|
<div id="root"></div>
|
|
20
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
21
23
|
</body>
|
|
22
24
|
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ICamstackAddon, AddonManifest, AddonContext, CapabilityProviderMap } from '@camstack/types';
|
|
2
|
+
|
|
3
|
+
declare class AdminUIAddon implements ICamstackAddon {
|
|
4
|
+
readonly id = "admin-ui";
|
|
5
|
+
readonly manifest: AddonManifest;
|
|
6
|
+
initialize(_ctx: AddonContext): Promise<void>;
|
|
7
|
+
shutdown(): Promise<void>;
|
|
8
|
+
getCapabilityProvider<K extends keyof CapabilityProviderMap>(name: K): CapabilityProviderMap[K] | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { AdminUIAddon, AdminUIAddon as default };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/server/addon.ts
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function resolveAdminUiDistDir() {
|
|
9
|
+
return path.resolve(__dirname, "..");
|
|
10
|
+
}
|
|
11
|
+
__name(resolveAdminUiDistDir, "resolveAdminUiDistDir");
|
|
12
|
+
var AdminUIAddon = class {
|
|
13
|
+
static {
|
|
14
|
+
__name(this, "AdminUIAddon");
|
|
15
|
+
}
|
|
16
|
+
id = "admin-ui";
|
|
17
|
+
manifest = {
|
|
18
|
+
id: "admin-ui",
|
|
19
|
+
name: "CamStack Admin UI",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
packageName: "@camstack/addon-admin-ui",
|
|
22
|
+
description: "Web-based administration interface for CamStack",
|
|
23
|
+
capabilities: [
|
|
24
|
+
{
|
|
25
|
+
name: "admin-ui",
|
|
26
|
+
mode: "singleton"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
async initialize(_ctx) {
|
|
31
|
+
}
|
|
32
|
+
async shutdown() {
|
|
33
|
+
}
|
|
34
|
+
getCapabilityProvider(name) {
|
|
35
|
+
if (name === "admin-ui") {
|
|
36
|
+
const provider = {
|
|
37
|
+
getStaticDir: /* @__PURE__ */ __name(() => resolveAdminUiDistDir(), "getStaticDir"),
|
|
38
|
+
getVersion: /* @__PURE__ */ __name(() => this.manifest.version, "getVersion")
|
|
39
|
+
};
|
|
40
|
+
return provider;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var addon_default = AdminUIAddon;
|
|
46
|
+
export {
|
|
47
|
+
AdminUIAddon,
|
|
48
|
+
addon_default as default
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=addon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/addon.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IAdminUI,\n CapabilityProviderMap,\n} from '@camstack/types'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\n/**\n * AdminUIAddon — standalone addon that serves the Vite-built frontend.\n * The dist/ directory containing the built React app sits alongside this\n * compiled file in the package output.\n */\n\nfunction resolveAdminUiDistDir(): string {\n // This addon file lives at <package-root>/dist/server/addon.js.\n // Vite assets (index.html, assets/) are at <package-root>/dist/.\n return path.resolve(__dirname, '..')\n}\n\nexport class AdminUIAddon implements ICamstackAddon {\n readonly id = 'admin-ui'\n\n readonly manifest: AddonManifest = {\n id: 'admin-ui',\n name: 'CamStack Admin UI',\n version: '0.1.0',\n packageName: '@camstack/addon-admin-ui',\n description: 'Web-based administration interface for CamStack',\n capabilities: [{ name: 'admin-ui', mode: 'singleton' }],\n }\n\n async initialize(_ctx: AddonContext): Promise<void> {}\n async shutdown(): Promise<void> {}\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'admin-ui') {\n const provider: IAdminUI = {\n getStaticDir: () => resolveAdminUiDistDir(),\n getVersion: () => this.manifest.version,\n }\n return provider as CapabilityProviderMap[K]\n }\n return null\n }\n}\n\nexport default AdminUIAddon\n"],"mappings":";;;;AAAA,OAAOA,UAAU;AACjB,SAASC,qBAAqB;AAS9B,IAAMC,YAAYC,KAAKC,QAAQC,cAAc,YAAYC,GAAG,CAAA;AAQ5D,SAASC,wBAAAA;AAGP,SAAOJ,KAAKK,QAAQN,WAAW,IAAA;AACjC;AAJSK;AAMF,IAAME,eAAN,MAAMA;EAxBb,OAwBaA;;;EACFC,KAAK;EAELC,WAA0B;IACjCD,IAAI;IACJE,MAAM;IACNC,SAAS;IACTC,aAAa;IACbC,aAAa;IACbC,cAAc;MAAC;QAAEJ,MAAM;QAAYK,MAAM;MAAY;;EACvD;EAEA,MAAMC,WAAWC,MAAmC;EAAC;EACrD,MAAMC,WAA0B;EAAC;EAEjCC,sBACET,MACiC;AACjC,QAAIA,SAAS,YAAY;AACvB,YAAMU,WAAqB;QACzBC,cAAc,6BAAMhB,sBAAAA,GAAN;QACdiB,YAAY,6BAAM,KAAKb,SAASE,SAApB;MACd;AACA,aAAOS;IACT;AACA,WAAO;EACT;AACF;AAEA,IAAA,gBAAeb;","names":["path","fileURLToPath","__dirname","path","dirname","fileURLToPath","url","resolveAdminUiDistDir","resolve","AdminUIAddon","id","manifest","name","version","packageName","description","capabilities","mode","initialize","_ctx","shutdown","getCapabilityProvider","provider","getStaticDir","getVersion"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-admin-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "CamStack Admin UI — Vite frontend build and server-side addon",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"exports": {
|
|
8
11
|
"./server/addon": {
|
|
9
12
|
"types": "./dist/server/addon.d.ts",
|
package/src/App.tsx
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|
2
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
-
import { ThemeProvider } from '@camstack/ui'
|
|
4
|
-
import { AuthProvider } from './contexts/auth-context'
|
|
5
|
-
import { ProtectedRoute } from './layouts/ProtectedRoute'
|
|
6
|
-
import { AppLayout } from './layouts/AppLayout'
|
|
7
|
-
import { LoginPage } from './pages/Login'
|
|
8
|
-
import { AccessDeniedPage } from './pages/AccessDenied'
|
|
9
|
-
import { DashboardPage } from './pages/Dashboard'
|
|
10
|
-
import { CamerasPage } from './pages/Cameras'
|
|
11
|
-
import { IntegrationsPage } from './pages/Integrations'
|
|
12
|
-
import { IntegrationDetailPage } from './pages/IntegrationDetail'
|
|
13
|
-
import { TimelinePage } from './pages/Timeline'
|
|
14
|
-
import { PipelineConfigPage } from './pages/PipelineConfig'
|
|
15
|
-
import { DeviceDetailPage } from './pages/DeviceDetail'
|
|
16
|
-
import { AddonsPage } from './pages/system/Addons'
|
|
17
|
-
import { AgentsPage } from './pages/system/Agents'
|
|
18
|
-
import { LogsPage } from './pages/system/Logs'
|
|
19
|
-
import { UsersPage } from './pages/system/Users'
|
|
20
|
-
import { SettingsPage } from './pages/system/Settings'
|
|
21
|
-
import { ShowroomPage } from './pages/Showroom'
|
|
22
|
-
import { AddonPageLoader } from './layouts/AddonPageLoader'
|
|
23
|
-
import { ConfirmDialogProvider } from './components/ui/ConfirmDialog'
|
|
24
|
-
import { ToastContainer } from './components/ui/ToastContainer'
|
|
25
|
-
|
|
26
|
-
const queryClient = new QueryClient({
|
|
27
|
-
defaultOptions: {
|
|
28
|
-
queries: {
|
|
29
|
-
staleTime: 30_000,
|
|
30
|
-
retry: false,
|
|
31
|
-
refetchOnWindowFocus: false,
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
export function App() {
|
|
37
|
-
return (
|
|
38
|
-
<ThemeProvider defaultMode="dark">
|
|
39
|
-
<QueryClientProvider client={queryClient}>
|
|
40
|
-
<ConfirmDialogProvider>
|
|
41
|
-
<AuthProvider>
|
|
42
|
-
<BrowserRouter>
|
|
43
|
-
<Routes>
|
|
44
|
-
<Route path="/login" element={<LoginPage />} />
|
|
45
|
-
<Route path="/access-denied" element={<AccessDeniedPage />} />
|
|
46
|
-
<Route path="/showroom" element={<ShowroomPage />} />
|
|
47
|
-
<Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
|
|
48
|
-
<Route index element={<Navigate to="/dashboard" replace />} />
|
|
49
|
-
<Route path="/dashboard" element={<DashboardPage />} />
|
|
50
|
-
<Route path="/cameras" element={<CamerasPage />} />
|
|
51
|
-
<Route path="/integrations" element={<IntegrationsPage />} />
|
|
52
|
-
<Route path="/integrations/:integrationId" element={<IntegrationDetailPage />} />
|
|
53
|
-
<Route path="/timeline" element={<TimelinePage />} />
|
|
54
|
-
<Route path="/pipeline" element={<PipelineConfigPage />} />
|
|
55
|
-
<Route path="/devices/:deviceId" element={<DeviceDetailPage />} />
|
|
56
|
-
<Route path="/system/agents" element={<ProtectedRoute requiredRole="admin"><AgentsPage /></ProtectedRoute>} />
|
|
57
|
-
<Route path="/system/logs" element={<ProtectedRoute requiredRole="admin"><LogsPage /></ProtectedRoute>} />
|
|
58
|
-
<Route path="/system/users" element={<ProtectedRoute requiredRole="admin"><UsersPage /></ProtectedRoute>} />
|
|
59
|
-
<Route path="/system/settings" element={<ProtectedRoute requiredRole="admin"><SettingsPage /></ProtectedRoute>} />
|
|
60
|
-
<Route path="/system/addons" element={<ProtectedRoute requiredRole="admin"><AddonsPage /></ProtectedRoute>} />
|
|
61
|
-
<Route path="/addon/:pagePath" element={<ProtectedRoute requiredRole="admin"><AddonPageLoader /></ProtectedRoute>} />
|
|
62
|
-
</Route>
|
|
63
|
-
</Routes>
|
|
64
|
-
</BrowserRouter>
|
|
65
|
-
<ToastContainer />
|
|
66
|
-
</AuthProvider>
|
|
67
|
-
</ConfirmDialogProvider>
|
|
68
|
-
</QueryClientProvider>
|
|
69
|
-
</ThemeProvider>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
@@ -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
|
-
}
|