@camstack/addon-admin-ui 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -0
- package/{index.html → dist/index.html} +3 -1
- package/dist/server/addon.d.ts +11 -0
- package/dist/server/addon.js +50 -0
- package/dist/server/addon.js.map +1 -0
- package/package.json +5 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -339
- package/src/components/addons/AddonUploadZone.tsx +0 -307
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -119
- package/src/components/agents/AgentCard.tsx +0 -281
- package/src/components/agents/AgentLogs.tsx +0 -231
- package/src/components/agents/ProcessList.tsx +0 -127
- package/src/components/agents/ProcessTree.tsx +0 -369
- package/src/components/agents/TaskList.tsx +0 -68
- package/src/components/cameras/CameraCard.tsx +0 -60
- package/src/components/cameras/LiveEventsPanel.tsx +0 -91
- package/src/components/cameras/ProviderSection.tsx +0 -50
- package/src/components/cameras/StreamArea.tsx +0 -107
- package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
- package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
- package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
- package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
- package/src/components/dashboard/BlockPicker.tsx +0 -54
- package/src/components/dashboard/BlockWrapper.tsx +0 -97
- package/src/components/dashboard/DashboardGrid.tsx +0 -160
- package/src/components/dashboard/block-registry.ts +0 -15
- package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
- package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
- package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
- package/src/components/dashboard/blocks/index.ts +0 -32
- package/src/components/device/DeviceHeader.tsx +0 -116
- package/src/components/device/FloatingPanel.tsx +0 -132
- package/src/components/device/FloatingPanelManager.tsx +0 -167
- package/src/components/device/PanelContent.tsx +0 -196
- package/src/components/device/QuickConfigWizard.tsx +0 -507
- package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
- package/src/components/device/tabs/EventsTab.tsx +0 -19
- package/src/components/device/tabs/LogsTab.tsx +0 -22
- package/src/components/device/tabs/OverviewTab.tsx +0 -104
- package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
- package/src/components/device/tabs/RecordingTab.tsx +0 -47
- package/src/components/device/tabs/ReplTab.tsx +0 -153
- package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
- package/src/components/device/tabs/ZonesTab.tsx +0 -98
- package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
- package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
- package/src/components/device/zone-editor/ZoneList.tsx +0 -150
- package/src/components/form-builder/FormBuilder.tsx +0 -135
- package/src/components/form-builder/FormField.tsx +0 -732
- package/src/components/form-builder/ModelSelector.tsx +0 -239
- package/src/components/integrations/AddDeviceDialog.tsx +0 -205
- package/src/components/integrations/CompactDeviceCard.tsx +0 -35
- package/src/components/integrations/DeviceCard.tsx +0 -29
- package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
- package/src/components/integrations/DeviceGrid.tsx +0 -79
- package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
- package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
- package/src/components/integrations/IntegrationCard.tsx +0 -40
- package/src/components/integrations/IntegrationWizard.tsx +0 -171
- package/src/components/integrations/ProviderConfigForm.tsx +0 -89
- package/src/components/integrations/ProviderPicker.tsx +0 -91
- package/src/components/integrations/SnapshotPopover.tsx +0 -68
- package/src/components/metrics/AgentLoad.tsx +0 -113
- package/src/components/metrics/IntegrationUsage.tsx +0 -90
- package/src/components/metrics/PipelineStatus.tsx +0 -105
- package/src/components/metrics/ProcessResources.tsx +0 -139
- package/src/components/pipeline/PhaseSettings.tsx +0 -131
- package/src/components/shared/CapabilityBadges.tsx +0 -30
- package/src/components/shared/ProviderIcon.tsx +0 -42
- package/src/components/shared/StatusBadge.tsx +0 -23
- package/src/components/shared/WebRtcPlayer.tsx +0 -211
- package/src/components/timeline/EventMarker.tsx +0 -32
- package/src/components/timeline/TimelineBar.tsx +0 -131
- package/src/components/ui/ConfirmDialog.tsx +0 -115
- package/src/components/ui/ToastContainer.tsx +0 -92
- package/src/contexts/auth-context.tsx +0 -91
- package/src/hooks/useBackendClient.ts +0 -6
- package/src/hooks/useTheme.ts +0 -1
- package/src/i18n/en.json +0 -164
- package/src/i18n/index.ts +0 -29
- package/src/i18n/it.json +0 -164
- package/src/index.css +0 -63
- package/src/layouts/AddonPageLoader.tsx +0 -120
- package/src/layouts/AppLayout.tsx +0 -238
- package/src/layouts/ProtectedRoute.tsx +0 -25
- package/src/lib/addon-page-context.ts +0 -29
- package/src/lib/backend.ts +0 -16
- package/src/main.tsx +0 -21
- package/src/pages/AccessDenied.tsx +0 -22
- package/src/pages/Cameras.tsx +0 -127
- package/src/pages/Dashboard.tsx +0 -6
- package/src/pages/DeviceDetail.tsx +0 -175
- package/src/pages/IntegrationDetail.tsx +0 -224
- package/src/pages/Integrations.tsx +0 -330
- package/src/pages/Login.tsx +0 -106
- package/src/pages/Metrics.tsx +0 -18
- package/src/pages/PipelineConfig.tsx +0 -282
- package/src/pages/Showroom.tsx +0 -351
- package/src/pages/Timeline.tsx +0 -269
- package/src/pages/system/Addons.tsx +0 -525
- package/src/pages/system/Agents.tsx +0 -362
- package/src/pages/system/Logs.tsx +0 -131
- package/src/pages/system/Models.tsx +0 -102
- package/src/pages/system/Processes.tsx +0 -129
- package/src/pages/system/Repl.tsx +0 -148
- package/src/pages/system/Settings.tsx +0 -168
- package/src/pages/system/Users.tsx +0 -174
- package/src/server/addon.ts +0 -54
- package/src/types/config-ui.ts +0 -210
- package/src/types/dashboard.ts +0 -39
- package/tsconfig.json +0 -29
- package/tsconfig.server.json +0 -16
- package/tsup.config.ts +0 -20
- package/vite.config.ts +0 -68
- /package/{public → dist}/brand/logo-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
- /package/{public → dist}/brand/logo-light.svg +0 -0
- /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
- /package/{public → dist}/brand/logo-wide-light.svg +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
- /package/{public → dist}/vendor/react.mjs +0 -0
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
import { useState, useDeferredValue, useMemo } from 'react'
|
|
2
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import { Package, Download, RefreshCw, Map as MapIcon, Search, Check, Loader2, ChevronDown, ChevronUp } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
import { AddonCard } from '../../components/addons/AddonCard'
|
|
6
|
-
import type { AddonListItem, AgentInfo } from '../../components/addons/AddonCard'
|
|
7
|
-
import { AddonUploadZone } from '../../components/addons/AddonUploadZone'
|
|
8
|
-
import { UpdatesList } from '../../components/addons/UpdatesList'
|
|
9
|
-
import type { UpdateEntry } from '../../components/addons/UpdatesList'
|
|
10
|
-
import { CapabilityMap } from '../../components/addons/CapabilityMap'
|
|
11
|
-
import type { CapabilityEntry } from '../../components/addons/CapabilityMap'
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Types
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
interface RawAddonManifest {
|
|
18
|
-
id: string
|
|
19
|
-
name: string
|
|
20
|
-
version: string
|
|
21
|
-
description?: string
|
|
22
|
-
capabilities: readonly string[]
|
|
23
|
-
requiredFeatures?: readonly string[]
|
|
24
|
-
group?: 'core' | 'addon' | 'provider' | 'page'
|
|
25
|
-
packageName: string
|
|
26
|
-
packageVersion: string
|
|
27
|
-
removable?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface RawAddonListItem {
|
|
31
|
-
manifest: RawAddonManifest
|
|
32
|
-
enabled: boolean
|
|
33
|
-
hasConfigSchema: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Normalise raw addon to typed AddonListItem, deriving group from ID
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
function deriveGroup(manifest: RawAddonManifest, source?: string): AddonListItem['group'] {
|
|
41
|
-
if (manifest.group) return manifest.group
|
|
42
|
-
if (source === 'core' || manifest.packageName === '@camstack/core') return 'core'
|
|
43
|
-
const pkg = manifest.packageName.toLowerCase()
|
|
44
|
-
if (pkg.includes('provider')) return 'provider'
|
|
45
|
-
if (pkg.includes('page') || pkg.includes('benchmark')) return 'page'
|
|
46
|
-
return 'addon'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normaliseAddon(raw: RawAddonListItem): AddonListItem | null {
|
|
50
|
-
if (!raw?.manifest) return null
|
|
51
|
-
return {
|
|
52
|
-
manifest: {
|
|
53
|
-
...raw.manifest,
|
|
54
|
-
capabilities: raw.manifest.capabilities ?? [],
|
|
55
|
-
},
|
|
56
|
-
enabled: raw.enabled ?? false,
|
|
57
|
-
hasConfigSchema: raw.hasConfigSchema ?? false,
|
|
58
|
-
group: deriveGroup(raw.manifest, (raw as any).source),
|
|
59
|
-
source: (raw as any).source,
|
|
60
|
-
installedOn: [],
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Derive capabilities from addon manifests
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
function deriveCapabilities(addons: AddonListItem[]): CapabilityEntry[] {
|
|
69
|
-
const capMap = new Map<string, CapabilityEntry>()
|
|
70
|
-
|
|
71
|
-
for (const addon of addons) {
|
|
72
|
-
const capabilities = addon.manifest.capabilities ?? []
|
|
73
|
-
for (const cap of capabilities) {
|
|
74
|
-
const capName = typeof cap === 'string' ? cap : (cap as { name: string }).name
|
|
75
|
-
const capMode = typeof cap === 'string' ? 'collection' : ((cap as { mode?: string }).mode ?? 'collection')
|
|
76
|
-
|
|
77
|
-
if (!capMap.has(capName)) {
|
|
78
|
-
capMap.set(capName, {
|
|
79
|
-
name: capName,
|
|
80
|
-
mode: capMode as CapabilityEntry['mode'],
|
|
81
|
-
providers: [],
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const entry = capMap.get(capName)!
|
|
86
|
-
entry.providers.push({
|
|
87
|
-
addonId: addon.manifest.id,
|
|
88
|
-
addonName: addon.manifest.name,
|
|
89
|
-
active: addon.enabled,
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Array.from(capMap.values())
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Group definitions
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
const GROUPS: { key: AddonListItem['group']; label: string }[] = [
|
|
102
|
-
{ key: 'core', label: 'Core (Builtins)' },
|
|
103
|
-
{ key: 'addon', label: 'Addon Packages' },
|
|
104
|
-
{ key: 'provider', label: 'Providers' },
|
|
105
|
-
{ key: 'page', label: 'Addon Pages' },
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Tab bar
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
type Tab = 'installed' | 'updates' | 'map'
|
|
113
|
-
|
|
114
|
-
const TABS: { id: Tab; label: string; icon: React.ElementType }[] = [
|
|
115
|
-
{ id: 'installed', label: 'Installed', icon: Package },
|
|
116
|
-
{ id: 'updates', label: 'Updates', icon: RefreshCw },
|
|
117
|
-
{ id: 'map', label: 'Capability Map', icon: MapIcon },
|
|
118
|
-
]
|
|
119
|
-
|
|
120
|
-
function TabBar({ active, onChange }: { active: Tab; onChange: (tab: Tab) => void }) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="flex gap-1 border-b border-border pb-0">
|
|
123
|
-
{TABS.map(({ id, label, icon: Icon }) => (
|
|
124
|
-
<button
|
|
125
|
-
key={id}
|
|
126
|
-
type="button"
|
|
127
|
-
onClick={() => onChange(id)}
|
|
128
|
-
className={[
|
|
129
|
-
'flex items-center gap-1.5 px-4 py-2 text-xs font-medium border-b-2 -mb-px transition-colors',
|
|
130
|
-
active === id
|
|
131
|
-
? 'border-primary text-primary'
|
|
132
|
-
: 'border-transparent text-foreground-subtle hover:text-foreground',
|
|
133
|
-
].join(' ')}
|
|
134
|
-
>
|
|
135
|
-
<Icon className="h-3.5 w-3.5" />
|
|
136
|
-
{label}
|
|
137
|
-
</button>
|
|
138
|
-
))}
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// Installed tab
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Package group — collapsible header for addons from the same npm package
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
interface PackageGroup {
|
|
152
|
-
packageName: string
|
|
153
|
-
version: string
|
|
154
|
-
addons: AddonListItem[]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function groupByPackage(addons: AddonListItem[]): Array<PackageGroup | AddonListItem> {
|
|
158
|
-
const packageMap = new Map<string, PackageGroup>()
|
|
159
|
-
const ungrouped: AddonListItem[] = []
|
|
160
|
-
|
|
161
|
-
for (const addon of addons) {
|
|
162
|
-
const pkgName = addon.manifest.packageName
|
|
163
|
-
if (!pkgName) {
|
|
164
|
-
ungrouped.push(addon)
|
|
165
|
-
continue
|
|
166
|
-
}
|
|
167
|
-
const existing = packageMap.get(pkgName)
|
|
168
|
-
if (existing) {
|
|
169
|
-
existing.addons.push(addon)
|
|
170
|
-
} else {
|
|
171
|
-
packageMap.set(pkgName, {
|
|
172
|
-
packageName: pkgName,
|
|
173
|
-
version: addon.manifest.packageVersion,
|
|
174
|
-
addons: [addon],
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const result: Array<PackageGroup | AddonListItem> = []
|
|
180
|
-
for (const group of packageMap.values()) {
|
|
181
|
-
// Only create a collapsible group if the package contains 2+ addons
|
|
182
|
-
if (group.addons.length >= 2) {
|
|
183
|
-
result.push(group)
|
|
184
|
-
} else {
|
|
185
|
-
result.push(group.addons[0]!)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
result.push(...ungrouped)
|
|
189
|
-
return result
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function isPackageGroup(item: PackageGroup | AddonListItem): item is PackageGroup {
|
|
193
|
-
return 'packageName' in item && 'addons' in item
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function PackageGroupHeader({ group, agents }: { group: PackageGroup; agents: AgentInfo[] }) {
|
|
197
|
-
const [expanded, setExpanded] = useState(false)
|
|
198
|
-
|
|
199
|
-
return (
|
|
200
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
onClick={() => setExpanded((e) => !e)}
|
|
204
|
-
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-hover/50 transition-colors text-left"
|
|
205
|
-
>
|
|
206
|
-
<Package className="h-4 w-4 text-primary shrink-0" />
|
|
207
|
-
<div className="flex-1 min-w-0 flex items-center gap-2 flex-wrap">
|
|
208
|
-
<span className="text-xs font-semibold text-foreground truncate">{group.packageName}</span>
|
|
209
|
-
<span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium shrink-0">
|
|
210
|
-
v{group.version}
|
|
211
|
-
</span>
|
|
212
|
-
<span className="text-[10px] rounded-full bg-surface-hover border border-border px-2 py-0.5 font-medium shrink-0">
|
|
213
|
-
{group.addons.length} addons
|
|
214
|
-
</span>
|
|
215
|
-
</div>
|
|
216
|
-
{expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle shrink-0" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle shrink-0" />}
|
|
217
|
-
</button>
|
|
218
|
-
{expanded && (
|
|
219
|
-
<div className="border-t border-border px-3 py-2 space-y-2">
|
|
220
|
-
{group.addons.map((addon) => (
|
|
221
|
-
<AddonCard key={addon.manifest.id} addon={addon} agents={agents} />
|
|
222
|
-
))}
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Installed tab
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
interface InstalledTabProps {
|
|
234
|
-
addons: AddonListItem[]
|
|
235
|
-
agents: AgentInfo[]
|
|
236
|
-
onRefresh: () => void
|
|
237
|
-
isRefreshing: boolean
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabProps) {
|
|
241
|
-
const client = useBackendClient()
|
|
242
|
-
const queryClient = useQueryClient()
|
|
243
|
-
const [searchInput, setSearchInput] = useState('')
|
|
244
|
-
const deferredSearch = useDeferredValue(searchInput)
|
|
245
|
-
const [showSearch, setShowSearch] = useState(false)
|
|
246
|
-
|
|
247
|
-
// Search npm for available addons — uses deferred (debounced) value
|
|
248
|
-
const { data: searchResults, isFetching: isSearching } = useQuery({
|
|
249
|
-
queryKey: ['addon-search', deferredSearch],
|
|
250
|
-
queryFn: () => client.trpc.bridgeAddons.searchAvailable.query({ query: deferredSearch || undefined }),
|
|
251
|
-
enabled: showSearch,
|
|
252
|
-
staleTime: 60_000,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
// Install addon mutation
|
|
256
|
-
const installMutation = useMutation({
|
|
257
|
-
mutationFn: (packageName: string) => client.trpc.bridgeAddons.installPackage.mutate({ packageName }),
|
|
258
|
-
onSuccess: (_data, packageName) => {
|
|
259
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
260
|
-
queryClient.invalidateQueries({ queryKey: ['addon-search'] })
|
|
261
|
-
queryClient.invalidateQueries({ queryKey: ['addon-pages'] })
|
|
262
|
-
queryClient.invalidateQueries({ queryKey: ['capabilities'] })
|
|
263
|
-
console.log(`[Addons] Installed ${packageName}`)
|
|
264
|
-
},
|
|
265
|
-
onError: (err, packageName) => {
|
|
266
|
-
console.error(`[Addons] Install failed for ${packageName}:`, err)
|
|
267
|
-
},
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
const grouped = GROUPS.map(({ key, label }) => ({
|
|
271
|
-
key,
|
|
272
|
-
label,
|
|
273
|
-
items: addons.filter((a) => a.group === key),
|
|
274
|
-
})).filter((g) => g.items.length > 0)
|
|
275
|
-
|
|
276
|
-
// Filter search results: hide already installed
|
|
277
|
-
const availableAddons = (searchResults ?? []).filter((r: any) => !r.installed)
|
|
278
|
-
|
|
279
|
-
return (
|
|
280
|
-
<div className="space-y-6">
|
|
281
|
-
{/* Search bar + actions */}
|
|
282
|
-
<div className="flex gap-2">
|
|
283
|
-
<div className="relative flex-1">
|
|
284
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle" />
|
|
285
|
-
<input
|
|
286
|
-
type="text"
|
|
287
|
-
value={searchInput}
|
|
288
|
-
onChange={(e) => { setSearchInput(e.target.value); setShowSearch(true) }}
|
|
289
|
-
onFocus={() => setShowSearch(true)}
|
|
290
|
-
placeholder="Search addons by name or capability..."
|
|
291
|
-
className="w-full rounded-md border border-border bg-background pl-9 pr-3 py-2 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
|
|
292
|
-
/>
|
|
293
|
-
{isSearching && (
|
|
294
|
-
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle animate-spin" />
|
|
295
|
-
)}
|
|
296
|
-
</div>
|
|
297
|
-
<button
|
|
298
|
-
type="button"
|
|
299
|
-
onClick={onRefresh}
|
|
300
|
-
disabled={isRefreshing}
|
|
301
|
-
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"
|
|
302
|
-
>
|
|
303
|
-
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
304
|
-
Refresh
|
|
305
|
-
</button>
|
|
306
|
-
</div>
|
|
307
|
-
|
|
308
|
-
{/* Search results dropdown */}
|
|
309
|
-
{showSearch && availableAddons.length > 0 && (
|
|
310
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
311
|
-
<div className="px-3 py-2 text-[10px] uppercase tracking-wide text-foreground-subtle border-b border-border flex justify-between">
|
|
312
|
-
<span>Available on npm ({availableAddons.length})</span>
|
|
313
|
-
<button type="button" onClick={() => setShowSearch(false)} className="text-foreground-subtle hover:text-foreground">
|
|
314
|
-
close
|
|
315
|
-
</button>
|
|
316
|
-
</div>
|
|
317
|
-
<div className="max-h-64 overflow-auto divide-y divide-border">
|
|
318
|
-
{availableAddons.map((addon: any) => (
|
|
319
|
-
<div key={addon.name} className="flex items-center gap-3 px-3 py-2.5 hover:bg-surface-hover transition-colors">
|
|
320
|
-
<div className="w-7 h-7 rounded-md bg-primary/10 flex items-center justify-center text-primary text-[10px] font-bold shrink-0">
|
|
321
|
-
{addon.name.replace('@camstack/addon-', '').charAt(0).toUpperCase()}
|
|
322
|
-
</div>
|
|
323
|
-
<div className="flex-1 min-w-0">
|
|
324
|
-
<div className="flex items-center gap-2">
|
|
325
|
-
<span className="text-xs font-semibold truncate">{addon.name}</span>
|
|
326
|
-
<span className="text-[10px] text-foreground-subtle">v{addon.version}</span>
|
|
327
|
-
</div>
|
|
328
|
-
<div className="text-[10px] text-foreground-subtle truncate">{addon.description}</div>
|
|
329
|
-
{addon.keywords?.length > 0 && (
|
|
330
|
-
<div className="flex gap-1 mt-0.5 flex-wrap">
|
|
331
|
-
{addon.keywords.filter((k: string) => k !== 'camstack' && k !== 'addon' && k !== 'camstack-addon').slice(0, 5).map((k: string) => (
|
|
332
|
-
<span key={k} className="text-[9px] px-1.5 py-0 rounded bg-primary/5 text-foreground-subtle">{k}</span>
|
|
333
|
-
))}
|
|
334
|
-
</div>
|
|
335
|
-
)}
|
|
336
|
-
</div>
|
|
337
|
-
<button
|
|
338
|
-
type="button"
|
|
339
|
-
onClick={() => installMutation.mutate(addon.name)}
|
|
340
|
-
disabled={installMutation.isPending}
|
|
341
|
-
className="flex items-center gap-1 px-2.5 py-1 text-[10px] font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors shrink-0"
|
|
342
|
-
>
|
|
343
|
-
{installMutation.isPending ? (
|
|
344
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
345
|
-
) : (
|
|
346
|
-
<Download className="h-3 w-3" />
|
|
347
|
-
)}
|
|
348
|
-
Install
|
|
349
|
-
</button>
|
|
350
|
-
</div>
|
|
351
|
-
))}
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
)}
|
|
355
|
-
|
|
356
|
-
{/* Show "all installed" if search returned only installed results */}
|
|
357
|
-
{showSearch && searchResults && availableAddons.length === 0 && searchResults.length > 0 && (
|
|
358
|
-
<div className="text-xs text-foreground-subtle flex items-center gap-1.5 px-1">
|
|
359
|
-
<Check className="h-3.5 w-3.5 text-green-500" />
|
|
360
|
-
All matching addons are already installed
|
|
361
|
-
</div>
|
|
362
|
-
)}
|
|
363
|
-
|
|
364
|
-
{/* Upload zone — below search results */}
|
|
365
|
-
<AddonUploadZone
|
|
366
|
-
onUploadSuccess={() => {
|
|
367
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
368
|
-
onRefresh()
|
|
369
|
-
}}
|
|
370
|
-
/>
|
|
371
|
-
|
|
372
|
-
{/* Installed addons grouped */}
|
|
373
|
-
{addons.length === 0 && (
|
|
374
|
-
<div className="text-xs text-foreground-subtle">No addons installed</div>
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
|
-
{grouped.map(({ key, label, items }) => {
|
|
378
|
-
const packageGrouped = groupByPackage(items)
|
|
379
|
-
return (
|
|
380
|
-
<div key={key} className="space-y-2">
|
|
381
|
-
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wide flex items-center gap-2">
|
|
382
|
-
{label}
|
|
383
|
-
<span className="rounded-full bg-surface-hover border border-border px-2 py-0.5 text-[10px] normal-case font-normal">
|
|
384
|
-
{items.length}
|
|
385
|
-
</span>
|
|
386
|
-
</h3>
|
|
387
|
-
{packageGrouped.map((item) =>
|
|
388
|
-
isPackageGroup(item) ? (
|
|
389
|
-
<PackageGroupHeader key={item.packageName} group={item} agents={agents} />
|
|
390
|
-
) : (
|
|
391
|
-
<AddonCard key={item.manifest.id} addon={item} agents={agents} />
|
|
392
|
-
),
|
|
393
|
-
)}
|
|
394
|
-
</div>
|
|
395
|
-
)
|
|
396
|
-
})}
|
|
397
|
-
</div>
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
// Main page
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
|
|
405
|
-
export function AddonsPage() {
|
|
406
|
-
const client = useBackendClient()
|
|
407
|
-
const queryClient = useQueryClient()
|
|
408
|
-
const [activeTab, setActiveTab] = useState<Tab>('installed')
|
|
409
|
-
|
|
410
|
-
const { data: rawAddons, isLoading, isError } = useQuery<RawAddonListItem[]>({
|
|
411
|
-
queryKey: ['addons', 'list'],
|
|
412
|
-
queryFn: () => client.listAddons() as unknown as Promise<RawAddonListItem[]>,
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
// Fetch agents for the "Add agent" dropdown in AddonCard
|
|
416
|
-
const { data: rawAgents = [] } = useQuery({
|
|
417
|
-
queryKey: ['agents', 'list'],
|
|
418
|
-
queryFn: () => client.listAgents(),
|
|
419
|
-
staleTime: 30_000,
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
const agents: AgentInfo[] = useMemo(
|
|
423
|
-
() =>
|
|
424
|
-
(rawAgents as unknown as Record<string, unknown>[]).map((raw) => {
|
|
425
|
-
const info = (raw.info ?? raw) as Record<string, unknown>
|
|
426
|
-
return {
|
|
427
|
-
id: String(info.id ?? ''),
|
|
428
|
-
name: String(info.name ?? info.id ?? 'Unknown'),
|
|
429
|
-
isHub: Boolean(raw.isHub ?? raw.hub ?? false),
|
|
430
|
-
}
|
|
431
|
-
}),
|
|
432
|
-
[rawAgents],
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
const { data: updates = [], isFetching: isChecking } = useQuery({
|
|
436
|
-
queryKey: ['updates'],
|
|
437
|
-
queryFn: () => client.trpc.update.listUpdates.query(),
|
|
438
|
-
staleTime: 60_000,
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
const reloadMutation = useMutation({
|
|
442
|
-
mutationFn: () => client.trpc.bridgeAddons.reloadPackages.mutate(),
|
|
443
|
-
onSuccess: () => {
|
|
444
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
445
|
-
},
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
const updatePackageMutation = useMutation({
|
|
449
|
-
mutationFn: (name: string) => client.trpc.update.updatePackage.mutate({ name }),
|
|
450
|
-
onSuccess: () => {
|
|
451
|
-
queryClient.invalidateQueries({ queryKey: ['updates'] })
|
|
452
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
453
|
-
},
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
const addonList: AddonListItem[] = (rawAddons ?? []).map(normaliseAddon).filter((a): a is AddonListItem => a !== null)
|
|
457
|
-
|
|
458
|
-
function handleCheckUpdates() {
|
|
459
|
-
queryClient.invalidateQueries({ queryKey: ['updates'] })
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function handleUpdate(id: string) {
|
|
463
|
-
updatePackageMutation.mutate(id)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function handleUpdateAll() {
|
|
467
|
-
for (const u of updates) {
|
|
468
|
-
updatePackageMutation.mutate((u as unknown as UpdateEntry).id)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return (
|
|
473
|
-
<div className="p-6 space-y-5">
|
|
474
|
-
{/* Header */}
|
|
475
|
-
<div className="flex items-center justify-between">
|
|
476
|
-
<div>
|
|
477
|
-
<h1 className="text-lg font-semibold text-foreground">Addons & Updates</h1>
|
|
478
|
-
<p className="text-xs text-foreground-subtle mt-0.5">
|
|
479
|
-
Manage installed addons, check for updates, and explore capability providers.
|
|
480
|
-
</p>
|
|
481
|
-
</div>
|
|
482
|
-
{!isLoading && !isError && (
|
|
483
|
-
<span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium">
|
|
484
|
-
{addonList.length} installed
|
|
485
|
-
</span>
|
|
486
|
-
)}
|
|
487
|
-
</div>
|
|
488
|
-
|
|
489
|
-
{/* Tabs */}
|
|
490
|
-
<TabBar active={activeTab} onChange={setActiveTab} />
|
|
491
|
-
|
|
492
|
-
{/* Loading / error states */}
|
|
493
|
-
{isLoading && activeTab === 'installed' && (
|
|
494
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading…</div>
|
|
495
|
-
)}
|
|
496
|
-
{isError && activeTab === 'installed' && (
|
|
497
|
-
<div className="text-xs text-danger">Failed to load addons</div>
|
|
498
|
-
)}
|
|
499
|
-
|
|
500
|
-
{/* Tab content */}
|
|
501
|
-
{activeTab === 'installed' && !isLoading && !isError && (
|
|
502
|
-
<InstalledTab
|
|
503
|
-
addons={addonList}
|
|
504
|
-
agents={agents}
|
|
505
|
-
onRefresh={() => reloadMutation.mutate()}
|
|
506
|
-
isRefreshing={reloadMutation.isPending}
|
|
507
|
-
/>
|
|
508
|
-
)}
|
|
509
|
-
|
|
510
|
-
{activeTab === 'updates' && (
|
|
511
|
-
<UpdatesList
|
|
512
|
-
updates={updates as unknown as UpdateEntry[]}
|
|
513
|
-
onUpdate={handleUpdate}
|
|
514
|
-
onUpdateAll={handleUpdateAll}
|
|
515
|
-
onCheckUpdates={handleCheckUpdates}
|
|
516
|
-
isChecking={isChecking}
|
|
517
|
-
/>
|
|
518
|
-
)}
|
|
519
|
-
|
|
520
|
-
{activeTab === 'map' && (
|
|
521
|
-
<CapabilityMap capabilities={deriveCapabilities(addonList)} />
|
|
522
|
-
)}
|
|
523
|
-
</div>
|
|
524
|
-
)
|
|
525
|
-
}
|