@camstack/addon-admin-ui 0.1.1 → 0.1.2
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/package.json +2 -1
- package/src/components/addons/AddonCard.tsx +36 -20
- package/src/components/addons/AddonUploadZone.tsx +26 -264
- package/src/components/addons/UpdatesList.tsx +9 -20
- package/src/components/integrations/DeviceDiscoveryStep.tsx +2 -2
- package/src/components/integrations/IntegrationWizard.tsx +2 -1
- package/src/components/metrics/AgentLoad.tsx +4 -12
- package/src/components/metrics/IntegrationUsage.tsx +4 -21
- package/src/components/metrics/PipelineStatus.tsx +25 -56
- package/src/components/metrics/ProcessResources.tsx +3 -19
- package/src/layouts/AppLayout.tsx +17 -1
- package/src/pages/Cameras.tsx +14 -14
- package/src/pages/IntegrationDetail.tsx +10 -12
- package/src/pages/Integrations.tsx +21 -18
- package/src/pages/system/Addons.tsx +89 -218
- package/src/types/config-ui.ts +28 -210
|
@@ -1,44 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { AddonCard } from '../../components/addons/AddonCard'
|
|
1
|
+
import type { BackendClient } from '@camstack/sdk'
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { Check, ChevronDown, ChevronUp, Download, Loader2, Package, RefreshCw, Search } from 'lucide-react'
|
|
4
|
+
import { useDeferredValue, useMemo, useState } from 'react'
|
|
6
5
|
import type { AddonListItem, AgentInfo } from '../../components/addons/AddonCard'
|
|
6
|
+
import { AddonCard } from '../../components/addons/AddonCard'
|
|
7
|
+
import { VersionBadge } from '@camstack/ui'
|
|
7
8
|
import { AddonUploadZone } from '../../components/addons/AddonUploadZone'
|
|
8
|
-
import {
|
|
9
|
-
import type { UpdateEntry } from '../../components/addons/UpdatesList'
|
|
10
|
-
import { CapabilityMap } from '../../components/addons/CapabilityMap'
|
|
11
|
-
import type { CapabilityEntry } from '../../components/addons/CapabilityMap'
|
|
9
|
+
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
12
10
|
|
|
13
11
|
// ---------------------------------------------------------------------------
|
|
14
|
-
// Types
|
|
12
|
+
// Types — inferred from tRPC addons.list return type
|
|
15
13
|
// ---------------------------------------------------------------------------
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
}
|
|
15
|
+
type RawAddonListItem = Awaited<ReturnType<BackendClient['listAddons']>>[number]
|
|
16
|
+
type RawAddonManifest = RawAddonListItem['manifest']
|
|
35
17
|
|
|
36
18
|
// ---------------------------------------------------------------------------
|
|
37
19
|
// Normalise raw addon to typed AddonListItem, deriving group from ID
|
|
38
20
|
// ---------------------------------------------------------------------------
|
|
39
21
|
|
|
40
22
|
function deriveGroup(manifest: RawAddonManifest, source?: string): AddonListItem['group'] {
|
|
41
|
-
if (manifest.group) return manifest.group
|
|
42
23
|
if (source === 'core' || manifest.packageName === '@camstack/core') return 'core'
|
|
43
24
|
const pkg = manifest.packageName.toLowerCase()
|
|
44
25
|
if (pkg.includes('provider')) return 'provider'
|
|
@@ -53,92 +34,19 @@ function normaliseAddon(raw: RawAddonListItem): AddonListItem | null {
|
|
|
53
34
|
...raw.manifest,
|
|
54
35
|
capabilities: raw.manifest.capabilities ?? [],
|
|
55
36
|
},
|
|
56
|
-
enabled:
|
|
37
|
+
enabled: true,
|
|
57
38
|
hasConfigSchema: raw.hasConfigSchema ?? false,
|
|
58
|
-
group: deriveGroup(raw.manifest,
|
|
59
|
-
source:
|
|
39
|
+
group: deriveGroup(raw.manifest, raw.source),
|
|
40
|
+
source: raw.source,
|
|
60
41
|
installedOn: [],
|
|
61
42
|
}
|
|
62
43
|
}
|
|
63
44
|
|
|
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
45
|
// ---------------------------------------------------------------------------
|
|
98
46
|
// Group definitions
|
|
99
47
|
// ---------------------------------------------------------------------------
|
|
100
48
|
|
|
101
|
-
|
|
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
|
-
}
|
|
49
|
+
// Groups removed — flat list with package grouping only
|
|
142
50
|
|
|
143
51
|
// ---------------------------------------------------------------------------
|
|
144
52
|
// Installed tab
|
|
@@ -150,10 +58,12 @@ function TabBar({ active, onChange }: { active: Tab; onChange: (tab: Tab) => voi
|
|
|
150
58
|
|
|
151
59
|
interface PackageGroup {
|
|
152
60
|
packageName: string
|
|
61
|
+
displayName: string
|
|
153
62
|
version: string
|
|
154
63
|
addons: AddonListItem[]
|
|
155
64
|
}
|
|
156
65
|
|
|
66
|
+
|
|
157
67
|
function groupByPackage(addons: AddonListItem[]): Array<PackageGroup | AddonListItem> {
|
|
158
68
|
const packageMap = new Map<string, PackageGroup>()
|
|
159
69
|
const ungrouped: AddonListItem[] = []
|
|
@@ -170,6 +80,7 @@ function groupByPackage(addons: AddonListItem[]): Array<PackageGroup | AddonList
|
|
|
170
80
|
} else {
|
|
171
81
|
packageMap.set(pkgName, {
|
|
172
82
|
packageName: pkgName,
|
|
83
|
+
displayName: (addon.manifest as any).packageDisplayName ?? addon.manifest.name,
|
|
173
84
|
version: addon.manifest.packageVersion,
|
|
174
85
|
addons: [addon],
|
|
175
86
|
})
|
|
@@ -198,27 +109,48 @@ function PackageGroupHeader({ group, agents }: { group: PackageGroup; agents: Ag
|
|
|
198
109
|
|
|
199
110
|
return (
|
|
200
111
|
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
112
|
+
{/* Header — same layout as AddonCard */}
|
|
113
|
+
<div className="flex items-start gap-3 px-4 py-3">
|
|
114
|
+
<div className="flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center bg-purple-500/20 text-purple-300">
|
|
115
|
+
<Package className="h-4 w-4" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex-1 min-w-0">
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<span className="text-sm font-semibold text-foreground truncate">{group.displayName}</span>
|
|
121
|
+
<span className="text-[10px] text-foreground-subtle font-mono shrink-0">({group.packageName})</span>
|
|
122
|
+
<span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium shrink-0 inline-flex items-center gap-1">
|
|
123
|
+
<Package className="h-3 w-3" />
|
|
124
|
+
{group.addons.length}
|
|
125
|
+
</span>
|
|
126
|
+
<span className="ml-auto shrink-0">
|
|
127
|
+
<VersionBadge version={group.version} />
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
{group.addons[0]?.manifest.description && (
|
|
131
|
+
<p className="text-[10px] text-foreground-subtle mt-0.5 line-clamp-1">{group.addons[0].manifest.description}</p>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Expand handle — bottom edge tab */}
|
|
201
137
|
<button
|
|
202
138
|
type="button"
|
|
203
139
|
onClick={() => setExpanded((e) => !e)}
|
|
204
|
-
className="w-full flex items-center
|
|
140
|
+
className="w-full flex items-center justify-center border-t border-border py-1 hover:bg-surface-hover/50 transition-colors"
|
|
205
141
|
>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
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" />}
|
|
142
|
+
{expanded ? (
|
|
143
|
+
<ChevronUp className="h-3.5 w-3.5 text-foreground-subtle" />
|
|
144
|
+
) : (
|
|
145
|
+
<ChevronDown className="h-3.5 w-3.5 text-foreground-subtle" />
|
|
146
|
+
)}
|
|
217
147
|
</button>
|
|
148
|
+
|
|
149
|
+
{/* Expanded content */}
|
|
218
150
|
{expanded && (
|
|
219
151
|
<div className="border-t border-border px-3 py-2 space-y-2">
|
|
220
152
|
{group.addons.map((addon) => (
|
|
221
|
-
<AddonCard key={addon.manifest.id} addon={addon} agents={agents} />
|
|
153
|
+
<AddonCard key={addon.manifest.id} addon={addon} agents={agents} hideVersion />
|
|
222
154
|
))}
|
|
223
155
|
</div>
|
|
224
156
|
)}
|
|
@@ -267,14 +199,10 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
267
199
|
},
|
|
268
200
|
})
|
|
269
201
|
|
|
270
|
-
const
|
|
271
|
-
key,
|
|
272
|
-
label,
|
|
273
|
-
items: addons.filter((a) => a.group === key),
|
|
274
|
-
})).filter((g) => g.items.length > 0)
|
|
202
|
+
const packageGrouped = groupByPackage(addons)
|
|
275
203
|
|
|
276
204
|
// Filter search results: hide already installed
|
|
277
|
-
const availableAddons = (searchResults ?? []).filter((r
|
|
205
|
+
const availableAddons = (searchResults ?? []).filter((r) => !r.installed)
|
|
278
206
|
|
|
279
207
|
return (
|
|
280
208
|
<div className="space-y-6">
|
|
@@ -294,6 +222,12 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
294
222
|
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle animate-spin" />
|
|
295
223
|
)}
|
|
296
224
|
</div>
|
|
225
|
+
<AddonUploadZone
|
|
226
|
+
onUploadSuccess={() => {
|
|
227
|
+
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
228
|
+
onRefresh()
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
297
231
|
<button
|
|
298
232
|
type="button"
|
|
299
233
|
onClick={onRefresh}
|
|
@@ -315,7 +249,7 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
315
249
|
</button>
|
|
316
250
|
</div>
|
|
317
251
|
<div className="max-h-64 overflow-auto divide-y divide-border">
|
|
318
|
-
{availableAddons.map((addon
|
|
252
|
+
{availableAddons.map((addon) => (
|
|
319
253
|
<div key={addon.name} className="flex items-center gap-3 px-3 py-2.5 hover:bg-surface-hover transition-colors">
|
|
320
254
|
<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
255
|
{addon.name.replace('@camstack/addon-', '').charAt(0).toUpperCase()}
|
|
@@ -326,9 +260,9 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
326
260
|
<span className="text-[10px] text-foreground-subtle">v{addon.version}</span>
|
|
327
261
|
</div>
|
|
328
262
|
<div className="text-[10px] text-foreground-subtle truncate">{addon.description}</div>
|
|
329
|
-
{addon.keywords
|
|
263
|
+
{addon.keywords && addon.keywords.length > 0 && (
|
|
330
264
|
<div className="flex gap-1 mt-0.5 flex-wrap">
|
|
331
|
-
{addon.keywords.filter((k
|
|
265
|
+
{addon.keywords.filter((k) => k !== 'camstack' && k !== 'addon' && k !== 'camstack-addon').slice(0, 5).map((k) => (
|
|
332
266
|
<span key={k} className="text-[9px] px-1.5 py-0 rounded bg-primary/5 text-foreground-subtle">{k}</span>
|
|
333
267
|
))}
|
|
334
268
|
</div>
|
|
@@ -361,39 +295,20 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
361
295
|
</div>
|
|
362
296
|
)}
|
|
363
297
|
|
|
364
|
-
{/* Upload zone — below search results */}
|
|
365
|
-
<AddonUploadZone
|
|
366
|
-
onUploadSuccess={() => {
|
|
367
|
-
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
368
|
-
onRefresh()
|
|
369
|
-
}}
|
|
370
|
-
/>
|
|
371
|
-
|
|
372
298
|
{/* Installed addons grouped */}
|
|
373
299
|
{addons.length === 0 && (
|
|
374
300
|
<div className="text-xs text-foreground-subtle">No addons installed</div>
|
|
375
301
|
)}
|
|
376
302
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
})}
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
{packageGrouped.map((item) =>
|
|
305
|
+
isPackageGroup(item) ? (
|
|
306
|
+
<PackageGroupHeader key={item.packageName} group={item} agents={agents} />
|
|
307
|
+
) : (
|
|
308
|
+
<AddonCard key={item.manifest.id} addon={item} agents={agents} />
|
|
309
|
+
),
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
397
312
|
</div>
|
|
398
313
|
)
|
|
399
314
|
}
|
|
@@ -405,11 +320,10 @@ function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabP
|
|
|
405
320
|
export function AddonsPage() {
|
|
406
321
|
const client = useBackendClient()
|
|
407
322
|
const queryClient = useQueryClient()
|
|
408
|
-
const [activeTab, setActiveTab] = useState<Tab>('installed')
|
|
409
323
|
|
|
410
|
-
const { data: rawAddons, isLoading, isError } = useQuery
|
|
324
|
+
const { data: rawAddons, isLoading, isError } = useQuery({
|
|
411
325
|
queryKey: ['addons', 'list'],
|
|
412
|
-
queryFn: () => client.listAddons()
|
|
326
|
+
queryFn: () => client.listAddons(),
|
|
413
327
|
})
|
|
414
328
|
|
|
415
329
|
// Fetch agents for the "Add agent" dropdown in AddonCard
|
|
@@ -421,62 +335,36 @@ export function AddonsPage() {
|
|
|
421
335
|
|
|
422
336
|
const agents: AgentInfo[] = useMemo(
|
|
423
337
|
() =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
isHub: Boolean(raw.isHub ?? raw.hub ?? false),
|
|
430
|
-
}
|
|
431
|
-
}),
|
|
338
|
+
rawAgents.map((raw) => ({
|
|
339
|
+
id: raw.info.id,
|
|
340
|
+
name: raw.info.name ?? raw.info.id,
|
|
341
|
+
isHub: raw.isHub,
|
|
342
|
+
})),
|
|
432
343
|
[rawAgents],
|
|
433
344
|
)
|
|
434
345
|
|
|
435
|
-
const { data: updates = [], isFetching: isChecking } = useQuery({
|
|
436
|
-
queryKey: ['updates'],
|
|
437
|
-
queryFn: () => client.trpc.update.listUpdates.query(),
|
|
438
|
-
staleTime: 60_000,
|
|
439
|
-
})
|
|
440
|
-
|
|
441
346
|
const reloadMutation = useMutation({
|
|
442
|
-
mutationFn: () =>
|
|
443
|
-
|
|
444
|
-
|
|
347
|
+
mutationFn: async () => {
|
|
348
|
+
// Force refresh update cache from npm + reload addon packages
|
|
349
|
+
await client.trpc.update.forceRefresh.mutate()
|
|
350
|
+
await client.trpc.bridgeAddons.reloadPackages.mutate()
|
|
445
351
|
},
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
const updatePackageMutation = useMutation({
|
|
449
|
-
mutationFn: (name: string) => client.trpc.update.updatePackage.mutate({ name }),
|
|
450
352
|
onSuccess: () => {
|
|
451
|
-
queryClient.invalidateQueries({ queryKey: ['updates'] })
|
|
452
353
|
queryClient.invalidateQueries({ queryKey: ['addons'] })
|
|
354
|
+
queryClient.invalidateQueries({ queryKey: ['updates'] })
|
|
453
355
|
},
|
|
454
356
|
})
|
|
455
357
|
|
|
456
358
|
const addonList: AddonListItem[] = (rawAddons ?? []).map(normaliseAddon).filter((a): a is AddonListItem => a !== null)
|
|
457
359
|
|
|
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
360
|
return (
|
|
473
361
|
<div className="p-6 space-y-5">
|
|
474
362
|
{/* Header */}
|
|
475
363
|
<div className="flex items-center justify-between">
|
|
476
364
|
<div>
|
|
477
|
-
<h1 className="text-lg font-semibold text-foreground">Addons
|
|
365
|
+
<h1 className="text-lg font-semibold text-foreground">Addons</h1>
|
|
478
366
|
<p className="text-xs text-foreground-subtle mt-0.5">
|
|
479
|
-
Manage installed addons
|
|
367
|
+
Manage installed addons and explore capability providers.
|
|
480
368
|
</p>
|
|
481
369
|
</div>
|
|
482
370
|
{!isLoading && !isError && (
|
|
@@ -486,19 +374,16 @@ export function AddonsPage() {
|
|
|
486
374
|
)}
|
|
487
375
|
</div>
|
|
488
376
|
|
|
489
|
-
{/* Tabs */}
|
|
490
|
-
<TabBar active={activeTab} onChange={setActiveTab} />
|
|
491
|
-
|
|
492
377
|
{/* Loading / error states */}
|
|
493
|
-
{isLoading &&
|
|
378
|
+
{isLoading && (
|
|
494
379
|
<div className="text-xs text-foreground-subtle animate-pulse">Loading…</div>
|
|
495
380
|
)}
|
|
496
|
-
{isError &&
|
|
381
|
+
{isError && (
|
|
497
382
|
<div className="text-xs text-danger">Failed to load addons</div>
|
|
498
383
|
)}
|
|
499
384
|
|
|
500
|
-
{/*
|
|
501
|
-
{
|
|
385
|
+
{/* Installed addons */}
|
|
386
|
+
{!isLoading && !isError && (
|
|
502
387
|
<InstalledTab
|
|
503
388
|
addons={addonList}
|
|
504
389
|
agents={agents}
|
|
@@ -506,20 +391,6 @@ export function AddonsPage() {
|
|
|
506
391
|
isRefreshing={reloadMutation.isPending}
|
|
507
392
|
/>
|
|
508
393
|
)}
|
|
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
394
|
</div>
|
|
524
395
|
)
|
|
525
396
|
}
|