@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.
Files changed (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /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.2",
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
- }