@camstack/addon-admin-ui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -339
  10. package/src/components/addons/AddonUploadZone.tsx +0 -307
  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 -119
  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 -171
  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 -113
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -90
  69. package/src/components/metrics/PipelineStatus.tsx +0 -105
  70. package/src/components/metrics/ProcessResources.tsx +0 -139
  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 -238
  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 -224
  98. package/src/pages/Integrations.tsx +0 -330
  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 -525
  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 -210
  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.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",
@@ -13,6 +16,7 @@
13
16
  "./package.json": "./package.json"
14
17
  },
15
18
  "camstack": {
19
+ "displayName": "Admin UI",
16
20
  "addons": [
17
21
  {
18
22
  "id": "admin-ui",
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,339 +0,0 @@
1
- import { useState, useEffect } from 'react'
2
- import { ChevronDown, ChevronUp, Plus, Trash2, Save, Loader2 } from 'lucide-react'
3
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4
- import { CapabilityBadge } from './CapabilityBadge'
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
- components?: string[]
23
- packageName: string
24
- packageVersion: string
25
- }
26
-
27
- export interface AddonListItem {
28
- manifest: AddonManifest
29
- enabled: boolean
30
- hasConfigSchema: boolean
31
- group?: 'core' | 'addon' | 'provider' | 'page'
32
- source?: 'core' | 'installed' | 'workspace'
33
- installedOn?: string[]
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Icon letter
38
- // ---------------------------------------------------------------------------
39
-
40
- const GROUP_COLORS: Record<string, string> = {
41
- core: 'bg-blue-500/20 text-blue-300',
42
- addon: 'bg-purple-500/20 text-purple-300',
43
- provider: 'bg-green-500/20 text-green-300',
44
- page: 'bg-orange-500/20 text-orange-300',
45
- }
46
-
47
- function AddonIcon({ name, group }: { name: string; group?: string }) {
48
- const color = GROUP_COLORS[group ?? 'addon'] ?? GROUP_COLORS['addon']!
49
- return (
50
- <div className={`flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center font-bold text-sm uppercase ${color}`}>
51
- {name.charAt(0)}
52
- </div>
53
- )
54
- }
55
-
56
- // ---------------------------------------------------------------------------
57
- // AddonCard
58
- // ---------------------------------------------------------------------------
59
-
60
- export interface AgentInfo {
61
- id: string
62
- name: string
63
- isHub: boolean
64
- }
65
-
66
- interface AddonCardProps {
67
- addon: AddonListItem
68
- agents?: AgentInfo[]
69
- }
70
-
71
- export function AddonCard({ addon, agents = [] }: AddonCardProps) {
72
- const [expanded, setExpanded] = useState(false)
73
- const [agentDropdownOpen, setAgentDropdownOpen] = useState(false)
74
- const client = useBackendClient()
75
- const queryClient = useQueryClient()
76
- const confirm = useConfirm()
77
-
78
- const { manifest } = addon
79
- const installedOn = addon.installedOn ?? []
80
- const removable = manifest.removable !== false && addon.source !== 'core'
81
- // Only expandable if addon has config, components, or agents to show
82
- const hasExpandableContent = addon.hasConfigSchema || (manifest.components && manifest.components.length > 0) || (agents.length >= 2)
83
-
84
- // Load config schema when expanded and addon has config
85
- const { data: configSchema, isLoading: schemaLoading } = useQuery({
86
- queryKey: ['addon-config-schema', manifest.id],
87
- queryFn: () => client.trpc.addons.getConfigSchema.query({ addonId: manifest.id }) as Promise<ConfigUISchema | null>,
88
- enabled: expanded && addon.hasConfigSchema,
89
- staleTime: 60_000,
90
- })
91
-
92
- // Load current config values
93
- const { data: configValues } = useQuery({
94
- queryKey: ['addon-config', manifest.id],
95
- queryFn: () => client.trpc.addons.getConfig.query({ addonId: manifest.id }) as Promise<Record<string, unknown> | null>,
96
- enabled: expanded && addon.hasConfigSchema,
97
- staleTime: 30_000,
98
- })
99
-
100
- // Local state for editing
101
- const [editValues, setEditValues] = useState<Record<string, unknown>>({})
102
- const [dirty, setDirty] = useState(false)
103
-
104
- useEffect(() => {
105
- if (configValues) {
106
- setEditValues(configValues)
107
- setDirty(false)
108
- }
109
- }, [configValues])
110
-
111
- const handleChange = (values: Record<string, unknown>) => {
112
- setEditValues(values)
113
- setDirty(true)
114
- }
115
-
116
- // Save config
117
- const saveMutation = useMutation({
118
- mutationFn: () => client.trpc.addons.updateConfig.mutate({ addonId: manifest.id, config: editValues }),
119
- onSuccess: () => {
120
- setDirty(false)
121
- queryClient.invalidateQueries({ queryKey: ['addon-config', manifest.id] })
122
- },
123
- })
124
-
125
- // Uninstall
126
- const uninstallMutation = useMutation({
127
- mutationFn: () => {
128
- console.log(`[AddonCard] Uninstalling ${manifest.packageName}`)
129
- return client.trpc.bridgeAddons.uninstallPackage.mutate({ packageName: manifest.packageName })
130
- },
131
- onSuccess: () => {
132
- console.log(`[AddonCard] Uninstalled ${manifest.packageName}`)
133
- queryClient.invalidateQueries({ queryKey: ['addons'] })
134
- queryClient.invalidateQueries({ queryKey: ['addon-pages'] })
135
- queryClient.invalidateQueries({ queryKey: ['capabilities'] })
136
- },
137
- onError: (err) => {
138
- console.error(`[AddonCard] Uninstall failed:`, err)
139
- },
140
- })
141
-
142
- return (
143
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
144
- {/* Header row */}
145
- <div
146
- className={[
147
- 'flex items-start gap-3 px-4 py-3 transition-colors',
148
- hasExpandableContent ? 'cursor-pointer hover:bg-surface-hover/50' : '',
149
- ].join(' ')}
150
- onClick={() => hasExpandableContent && setExpanded((e) => !e)}
151
- >
152
- <AddonIcon name={manifest.name} group={addon.group} />
153
-
154
- <div className="flex-1 min-w-0">
155
- <div className="flex items-center gap-2 flex-wrap">
156
- <span className="text-sm font-semibold text-foreground truncate">{manifest.name}</span>
157
- <span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium shrink-0">
158
- v{manifest.version}
159
- </span>
160
- {addon.source === 'workspace' && (
161
- <span className="text-[10px] rounded-full bg-orange-500/15 text-orange-400 px-2 py-0.5 font-medium shrink-0">
162
- DEV
163
- </span>
164
- )}
165
- </div>
166
-
167
- {manifest.description && (
168
- <p className="text-[10px] text-foreground-subtle mt-0.5 line-clamp-1">{manifest.description}</p>
169
- )}
170
-
171
- {/* Capability badges */}
172
- {manifest.capabilities.length > 0 && (
173
- <div className="flex flex-wrap gap-1 mt-1.5">
174
- {manifest.capabilities.map((cap, i) => (
175
- <CapabilityBadge key={i} capability={cap} />
176
- ))}
177
- </div>
178
- )}
179
- </div>
180
-
181
- {/* Actions: uninstall + expand chevron */}
182
- <div className="flex items-center gap-2 shrink-0 pt-1">
183
- {removable && (
184
- <button
185
- type="button"
186
- onClick={async (e) => {
187
- e.stopPropagation()
188
- console.log('[AddonCard] Confirm dialog opening...')
189
- const confirmed = await confirm({
190
- title: `Uninstall ${manifest.name}?`,
191
- message: `This will remove the addon package and all its data. This action cannot be undone.`,
192
- confirmLabel: 'Uninstall',
193
- variant: 'danger',
194
- })
195
- if (confirmed) uninstallMutation.mutate()
196
- }}
197
- disabled={uninstallMutation.isPending}
198
- 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"
199
- title={`Uninstall ${manifest.name}`}
200
- >
201
- <Trash2 className="h-3 w-3" />
202
- {uninstallMutation.isPending && 'Removing...'}
203
- </button>
204
- )}
205
- {hasExpandableContent && (
206
- expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle" />
207
- )}
208
- </div>
209
- </div>
210
-
211
- {/* Expanded: settings inline */}
212
- {expanded && (
213
- <div className="border-t border-border px-4 py-3 space-y-4">
214
-
215
- {/* Config form — loaded from backend */}
216
- {addon.hasConfigSchema && (
217
- <div>
218
- {schemaLoading && (
219
- <div className="flex items-center gap-2 text-xs text-foreground-subtle py-2">
220
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
221
- Loading configuration...
222
- </div>
223
- )}
224
-
225
- {configSchema && configSchema.sections?.some((s: any) => s.fields?.length > 0) && (
226
- <div className="space-y-3">
227
- <FormBuilder
228
- schema={configSchema}
229
- values={editValues}
230
- onChange={handleChange}
231
- />
232
- {dirty && (
233
- <div className="flex items-center justify-between">
234
- <span className="text-[10px] text-orange-400">Unsaved changes</span>
235
- <button
236
- type="button"
237
- onClick={(e) => { e.stopPropagation(); saveMutation.mutate() }}
238
- disabled={saveMutation.isPending}
239
- 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"
240
- >
241
- {saveMutation.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <Save className="h-3 w-3" />}
242
- {saveMutation.isPending ? 'Saving...' : 'Save'}
243
- </button>
244
- </div>
245
- )}
246
- {saveMutation.isError && (
247
- <div className="text-[10px] text-red-400">
248
- Save failed: {saveMutation.error instanceof Error ? saveMutation.error.message : 'Unknown error'}
249
- </div>
250
- )}
251
- </div>
252
- )}
253
-
254
- {/* No fallback message — addons without config don't show the expander */}
255
- </div>
256
- )}
257
-
258
- {/* No message for addons without config — just show other sections */}
259
-
260
- {/* Components list (for core) */}
261
- {manifest.components && manifest.components.length > 0 && (
262
- <div>
263
- <div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-2">
264
- Components ({manifest.components.length})
265
- </div>
266
- <div className="flex flex-wrap gap-1">
267
- {manifest.components.map((comp) => (
268
- <span key={comp} className="text-[10px] rounded bg-background border border-border px-1.5 py-0.5 text-foreground-subtle font-mono">
269
- {comp}
270
- </span>
271
- ))}
272
- </div>
273
- </div>
274
- )}
275
-
276
- {/* Installed on agents — only show when remote agents exist (2+ total) */}
277
- {agents.length >= 2 && (
278
- <div>
279
- <div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-2">
280
- Installed on
281
- </div>
282
- <div className="flex items-center gap-1.5 flex-wrap">
283
- {installedOn.length === 0 && (
284
- <span className="text-[10px] text-foreground-subtle">Hub (local)</span>
285
- )}
286
- {installedOn.map((agentName) => (
287
- <span
288
- key={agentName}
289
- className="rounded-full bg-surface-hover border border-border px-2 py-0.5 text-[10px] text-foreground-subtle"
290
- >
291
- {agentName}
292
- </span>
293
- ))}
294
- <div className="relative">
295
- <button
296
- type="button"
297
- onClick={(e) => { e.stopPropagation(); setAgentDropdownOpen((o) => !o) }}
298
- 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"
299
- title="Install on more agents"
300
- >
301
- <Plus className="h-3 w-3" />
302
- Add agent
303
- </button>
304
- {agentDropdownOpen && (
305
- <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">
306
- {agents
307
- .filter((a) => !a.isHub && !installedOn.includes(a.name))
308
- .map((agent) => (
309
- <button
310
- key={agent.id}
311
- type="button"
312
- onClick={(e) => {
313
- e.stopPropagation()
314
- setAgentDropdownOpen(false)
315
- // TODO: wire up install-on-agent mutation
316
- }}
317
- className="w-full text-left px-3 py-1.5 text-[10px] text-foreground hover:bg-surface-hover transition-colors"
318
- >
319
- {agent.name}
320
- </button>
321
- ))}
322
- {agents.filter((a) => !a.isHub && !installedOn.includes(a.name)).length === 0 && (
323
- <div className="px-3 py-1.5 text-[10px] text-foreground-subtle">
324
- No available agents
325
- </div>
326
- )}
327
- </div>
328
- )}
329
- </div>
330
- </div>
331
- </div>
332
- )}
333
-
334
- {/* Uninstall button is now inline in the header */}
335
- </div>
336
- )}
337
- </div>
338
- )
339
- }