@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
@@ -1,224 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { useParams } from 'react-router-dom'
4
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
5
- import { RefreshCw, Settings, Plus, Play, Square } from 'lucide-react'
6
- import { useBackendClient } from '../hooks/useBackendClient'
7
- import { StatusBadge } from '../components/shared/StatusBadge'
8
- import { DeviceGrid } from '../components/integrations/DeviceGrid'
9
- import { AddDeviceDialog } from '../components/integrations/AddDeviceDialog'
10
-
11
- export function IntegrationDetailPage() {
12
- const { t } = useTranslation()
13
- const { integrationId } = useParams<{ integrationId: string }>()
14
- const client = useBackendClient()
15
- const queryClient = useQueryClient()
16
- const [showAddDevice, setShowAddDevice] = useState(false)
17
- const [showConfig, setShowConfig] = useState(false)
18
-
19
- const { data: provider, isLoading: providerLoading } = useQuery({
20
- queryKey: ['provider', integrationId],
21
- queryFn: () => client.getProvider(integrationId!),
22
- enabled: !!integrationId,
23
- })
24
-
25
- const { data: allDevices } = useQuery({
26
- queryKey: ['devices'],
27
- queryFn: () => client.listDevices(),
28
- refetchInterval: 5_000,
29
- })
30
-
31
- const { data: discovered, refetch: refetchDiscovery } = useQuery({
32
- queryKey: ['discover-devices', integrationId],
33
- queryFn: () => client.discoverDevices(integrationId!),
34
- enabled: !!integrationId && provider?.discoveryMode !== 'manual',
35
- })
36
-
37
- const startMutation = useMutation({
38
- mutationFn: () => client.startProvider(integrationId!),
39
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['provider', integrationId] }),
40
- })
41
-
42
- const stopMutation = useMutation({
43
- mutationFn: () => client.stopProvider(integrationId!),
44
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['provider', integrationId] }),
45
- })
46
-
47
- const adoptMutation = useMutation({
48
- mutationFn: (externalId: string) => client.adoptDevice(integrationId!, externalId),
49
- onSuccess: () => {
50
- queryClient.invalidateQueries({ queryKey: ['devices'] })
51
- queryClient.invalidateQueries({ queryKey: ['discover-devices', integrationId] })
52
- },
53
- })
54
-
55
- const [importingIds, setImportingIds] = useState<Set<string>>(new Set())
56
-
57
- async function handleImport(externalId: string) {
58
- setImportingIds((prev) => new Set([...prev, externalId]))
59
- try {
60
- await adoptMutation.mutateAsync(externalId)
61
- } finally {
62
- setImportingIds((prev) => {
63
- const next = new Set(prev)
64
- next.delete(externalId)
65
- return next
66
- })
67
- }
68
- }
69
-
70
- if (providerLoading) {
71
- return (
72
- <div className="p-6">
73
- <div className="h-16 rounded-lg bg-surface animate-pulse mb-4" />
74
- <div className="h-40 rounded-lg bg-surface animate-pulse" />
75
- </div>
76
- )
77
- }
78
-
79
- if (!provider) {
80
- return (
81
- <div className="flex items-center justify-center py-20 text-foreground-subtle text-sm">
82
- {t('integrations.integrationNotFound')}
83
- </div>
84
- )
85
- }
86
-
87
- const devices = ((allDevices ?? []) as unknown as Array<Record<string, unknown>>)
88
- .filter((d) => String(d.providerId) === integrationId)
89
-
90
- const activeDevices = devices.map((d) => ({
91
- id: String(d.id),
92
- name: String(d.name ?? d.id),
93
- status: String(d.status ?? 'offline'),
94
- snapshotUrl: d.snapshotUrl ? String(d.snapshotUrl) : null,
95
- }))
96
-
97
- const discoveredDevices = (discovered ?? []).map((d: any) => ({
98
- externalId: String(d.externalId ?? d.id),
99
- name: String(d.name ?? d.externalId ?? d.id),
100
- }))
101
-
102
- const providerStatus = typeof provider.status === 'object'
103
- ? (provider.status as any).connected ? 'running' : 'stopped'
104
- : String(provider.status ?? 'stopped')
105
-
106
- const isAutoDiscovery = provider.discoveryMode !== 'manual'
107
- const isManual = provider.discoveryMode === 'manual'
108
-
109
- return (
110
- <div className="p-6 space-y-5">
111
- {/* Header */}
112
- <div className="flex items-center justify-between">
113
- <div className="flex items-center gap-3">
114
- <img
115
- src={client.getAddonAssetUrl(provider.type, 'assets/icon.svg')}
116
- alt=""
117
- className="h-8 w-8 rounded-lg"
118
- onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
119
- />
120
- <div>
121
- <h1 className="text-lg font-semibold text-foreground">{provider.name}</h1>
122
- <p className="text-[11px] text-foreground-subtle mt-0.5">
123
- {provider.type} · {activeDevices.length} {t('integrations.devices')}
124
- </p>
125
- </div>
126
- <StatusBadge status={providerStatus} />
127
- </div>
128
- <div className="flex items-center gap-2">
129
- {isAutoDiscovery && (
130
- <button
131
- onClick={() => refetchDiscovery()}
132
- className="flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-[11px] text-foreground hover:bg-surface-hover transition-colors"
133
- >
134
- <RefreshCw className="h-3 w-3" />
135
- {t('integrations.rediscover')}
136
- </button>
137
- )}
138
- {/* TODO: open provider config dialog */}
139
- <button
140
- onClick={() => setShowConfig(true)}
141
- className="flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-[11px] text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
142
- >
143
- <Settings className="h-3 w-3" />
144
- {t('integrations.config')}
145
- </button>
146
- {providerStatus === 'running' ? (
147
- <button
148
- onClick={() => stopMutation.mutate()}
149
- className="rounded-lg border border-border bg-surface p-1.5 text-foreground-subtle hover:text-danger hover:bg-danger/10 transition-colors"
150
- title="Stop"
151
- >
152
- <Square className="h-3.5 w-3.5" />
153
- </button>
154
- ) : (
155
- <button
156
- onClick={() => startMutation.mutate()}
157
- className="rounded-lg border border-border bg-surface p-1.5 text-foreground-subtle hover:text-success hover:bg-success/10 transition-colors"
158
- title="Start"
159
- >
160
- <Play className="h-3.5 w-3.5" />
161
- </button>
162
- )}
163
- </div>
164
- </div>
165
-
166
- {/* Device grid */}
167
- <DeviceGrid
168
- activeDevices={activeDevices}
169
- discoveredDevices={isAutoDiscovery ? discoveredDevices : undefined}
170
- onImport={handleImport}
171
- importingIds={importingIds}
172
- />
173
-
174
- {/* FAB */}
175
- <button
176
- onClick={() => {
177
- if (isManual) {
178
- setShowAddDevice(true)
179
- } else {
180
- refetchDiscovery()
181
- }
182
- }}
183
- className="fixed bottom-6 right-6 flex h-12 w-12 items-center justify-center rounded-full bg-primary shadow-lg shadow-primary/30 hover:shadow-primary/40 transition-shadow"
184
- title={isManual ? t('integrations.addDevice') : t('integrations.rediscover')}
185
- >
186
- {isManual ? (
187
- <Plus className="h-5 w-5 text-primary-foreground" />
188
- ) : (
189
- <RefreshCw className="h-5 w-5 text-primary-foreground" />
190
- )}
191
- </button>
192
-
193
- {showAddDevice && integrationId && (
194
- <AddDeviceDialog
195
- open={showAddDevice}
196
- providerId={integrationId}
197
- onClose={() => setShowAddDevice(false)}
198
- />
199
- )}
200
-
201
- {/* TODO: replace with a real FormBuilder config dialog */}
202
- {showConfig && (
203
- <div
204
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
205
- onClick={() => setShowConfig(false)}
206
- >
207
- <div
208
- className="rounded-xl border border-border bg-surface p-6 shadow-xl w-80"
209
- onClick={(e) => e.stopPropagation()}
210
- >
211
- <h2 className="text-sm font-semibold text-foreground mb-2">{t('integrations.providerConfig')}</h2>
212
- <p className="text-[11px] text-foreground-subtle">{t('integrations.configComingSoon')}</p>
213
- <button
214
- onClick={() => setShowConfig(false)}
215
- className="mt-4 w-full rounded-lg bg-primary px-3 py-1.5 text-[11px] text-primary-foreground hover:bg-primary/90 transition-colors"
216
- >
217
- Close
218
- </button>
219
- </div>
220
- </div>
221
- )}
222
- </div>
223
- )
224
- }
@@ -1,330 +0,0 @@
1
- import { useState, useMemo } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { useNavigate } from 'react-router-dom'
4
- import {
5
- Plus,
6
- Search,
7
- ChevronDown,
8
- ChevronRight,
9
- Settings,
10
- Camera,
11
- Lightbulb,
12
- ToggleLeft,
13
- Radio,
14
- HelpCircle,
15
- } from 'lucide-react'
16
- import { useBackendClient } from '../hooks/useBackendClient'
17
- import { ProviderIcon, getProviderLabel } from '../components/shared/ProviderIcon'
18
- import { StatusBadge } from '../components/shared/StatusBadge'
19
- import { IntegrationWizard } from '../components/integrations/IntegrationWizard'
20
-
21
- type DeviceTypeFilter = 'all' | 'camera' | 'sensor' | 'light' | 'switch' | 'other'
22
- type GroupBy = 'provider' | 'type' | 'status'
23
-
24
- const TYPE_FILTERS: Array<{ id: DeviceTypeFilter; label: string; icon: React.ComponentType<{ className?: string }> }> = [
25
- { id: 'all', label: 'All', icon: HelpCircle },
26
- { id: 'camera', label: 'Camera', icon: Camera },
27
- { id: 'sensor', label: 'Sensor', icon: Radio },
28
- { id: 'light', label: 'Light', icon: Lightbulb },
29
- { id: 'switch', label: 'Switch', icon: ToggleLeft },
30
- { id: 'other', label: 'Other', icon: HelpCircle },
31
- ]
32
-
33
- function getDeviceType(device: Record<string, unknown>): string {
34
- const type = String(device.type ?? '').toLowerCase()
35
- if (type.includes('camera') || type === 'camera') return 'camera'
36
- if (type.includes('sensor')) return 'sensor'
37
- if (type.includes('light')) return 'light'
38
- if (type.includes('switch')) return 'switch'
39
- if (type) return 'other'
40
- return 'camera' // default for untyped devices
41
- }
42
-
43
- export function IntegrationsPage() {
44
- const client = useBackendClient()
45
- const queryClient = useQueryClient()
46
- const navigate = useNavigate()
47
-
48
- const [wizardOpen, setWizardOpen] = useState(false)
49
- const [search, setSearch] = useState('')
50
- const [typeFilter, setTypeFilter] = useState<DeviceTypeFilter>('all')
51
- const [groupBy, setGroupBy] = useState<GroupBy>('provider')
52
- const [expandedSections, setExpandedSections] = useState<ReadonlySet<string>>(new Set(['__all__']))
53
-
54
- const { data: providers, isLoading: providersLoading } = useQuery({
55
- queryKey: ['providers'],
56
- queryFn: () => client.listProviders(),
57
- refetchInterval: 5_000,
58
- })
59
-
60
- const { data: devices, isLoading: devicesLoading } = useQuery({
61
- queryKey: ['devices'],
62
- queryFn: () => client.listDevices(),
63
- refetchInterval: 5_000,
64
- })
65
-
66
- const startMutation = useMutation({
67
- mutationFn: (id: string) => client.startProvider(id),
68
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
69
- })
70
-
71
- const stopMutation = useMutation({
72
- mutationFn: (id: string) => client.stopProvider(id),
73
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
74
- })
75
-
76
- const providerList = (providers ?? []) as unknown as Array<Record<string, unknown>>
77
- const deviceList = (devices ?? []) as unknown as Array<Record<string, unknown>>
78
-
79
- // Filter devices
80
- const filteredDevices = useMemo(() => {
81
- const searchLower = search.toLowerCase()
82
- return deviceList.filter((d) => {
83
- if (typeFilter !== 'all' && getDeviceType(d) !== typeFilter) return false
84
- if (searchLower) {
85
- const name = String(d.name ?? d.id ?? '').toLowerCase()
86
- if (!name.includes(searchLower)) return false
87
- }
88
- return true
89
- })
90
- }, [deviceList, typeFilter, search])
91
-
92
- // Group devices
93
- const grouped = useMemo(() => {
94
- const groups = new Map<string, Array<Record<string, unknown>>>()
95
- for (const device of filteredDevices) {
96
- let key: string
97
- switch (groupBy) {
98
- case 'provider':
99
- key = String(device.providerId ?? 'unknown')
100
- break
101
- case 'type':
102
- key = getDeviceType(device)
103
- break
104
- case 'status':
105
- key = String(device.status ?? 'offline')
106
- break
107
- }
108
- if (!groups.has(key)) {
109
- groups.set(key, [])
110
- }
111
- groups.get(key)!.push(device)
112
- }
113
- return groups
114
- }, [filteredDevices, groupBy])
115
-
116
- function toggleSection(sectionId: string) {
117
- setExpandedSections((prev) => {
118
- const next = new Set(prev)
119
- if (next.has(sectionId)) {
120
- next.delete(sectionId)
121
- } else {
122
- next.add(sectionId)
123
- }
124
- return next
125
- })
126
- }
127
-
128
- function getGroupLabel(key: string): string {
129
- if (groupBy === 'provider') {
130
- const provider = providerList.find((p) => String(p.id) === key)
131
- return provider ? String(provider.name ?? key) : key
132
- }
133
- return key.charAt(0).toUpperCase() + key.slice(1)
134
- }
135
-
136
- function getGroupProviderType(key: string): string {
137
- if (groupBy === 'provider') {
138
- const provider = providerList.find((p) => String(p.id) === key)
139
- return String(provider?.type ?? 'rtsp')
140
- }
141
- return 'rtsp'
142
- }
143
-
144
- function getGroupProviderStatus(key: string): string {
145
- if (groupBy === 'provider') {
146
- const provider = providerList.find((p) => String(p.id) === key)
147
- return String(provider?.status ?? 'stopped')
148
- }
149
- return ''
150
- }
151
-
152
- const isLoading = providersLoading || devicesLoading
153
-
154
- return (
155
- <div className="p-6 space-y-4">
156
- {/* Header */}
157
- <div className="flex items-center justify-between gap-4">
158
- <h1 className="text-lg font-semibold text-foreground">Integrations</h1>
159
- <button
160
- onClick={() => setWizardOpen(true)}
161
- className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm"
162
- >
163
- <Plus className="h-3.5 w-3.5" />
164
- New Integration
165
- </button>
166
- </div>
167
-
168
- {/* Toolbar: search + filters + group by */}
169
- <div className="flex items-center gap-3 flex-wrap">
170
- {/* Search */}
171
- <div className="relative">
172
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle" />
173
- <input
174
- type="text"
175
- value={search}
176
- onChange={(e) => setSearch(e.target.value)}
177
- placeholder="Search devices..."
178
- className="rounded-lg border border-border bg-surface pl-8 pr-3 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-primary/50 w-48"
179
- />
180
- </div>
181
-
182
- {/* Type filter chips */}
183
- <div className="flex items-center gap-1">
184
- {TYPE_FILTERS.map(({ id, label }) => (
185
- <button
186
- key={id}
187
- onClick={() => setTypeFilter(id)}
188
- className={`rounded-full px-2.5 py-1 text-[10px] font-medium transition-colors ${
189
- typeFilter === id
190
- ? 'bg-primary/15 text-primary border border-primary/30'
191
- : 'bg-surface border border-border text-foreground-subtle hover:text-foreground'
192
- }`}
193
- >
194
- {label}
195
- </button>
196
- ))}
197
- </div>
198
-
199
- {/* Group by selector */}
200
- <div className="flex items-center gap-1.5 ml-auto">
201
- <span className="text-[10px] text-foreground-subtle">Group by:</span>
202
- <select
203
- value={groupBy}
204
- onChange={(e) => setGroupBy(e.target.value as GroupBy)}
205
- className="rounded-lg border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:outline-none focus:border-primary/50"
206
- >
207
- <option value="provider">Provider</option>
208
- <option value="type">Type</option>
209
- <option value="status">Status</option>
210
- </select>
211
- </div>
212
- </div>
213
-
214
- {/* Loading state */}
215
- {isLoading && (
216
- <div className="space-y-2">
217
- {[1, 2, 3].map((i) => (
218
- <div key={i} className="h-14 rounded-lg border border-border bg-surface animate-pulse" />
219
- ))}
220
- </div>
221
- )}
222
-
223
- {/* Empty state */}
224
- {!isLoading && filteredDevices.length === 0 && (
225
- <div className="flex flex-col items-center py-20 text-foreground-subtle">
226
- <p className="text-sm">No devices found.</p>
227
- <p className="text-xs mt-1">
228
- {search || typeFilter !== 'all'
229
- ? 'Try adjusting your filters'
230
- : 'Start an integration to discover devices.'}
231
- </p>
232
- </div>
233
- )}
234
-
235
- {/* Collapsible sections */}
236
- {!isLoading && Array.from(grouped.entries()).map(([key, groupDevices]) => {
237
- const isExpanded = expandedSections.has(key) || expandedSections.has('__all__')
238
- const providerType = getGroupProviderType(key)
239
- const providerStatus = getGroupProviderStatus(key)
240
- const providerId = groupBy === 'provider' ? key : null
241
-
242
- return (
243
- <div key={key} className="rounded-lg border border-border bg-surface overflow-hidden">
244
- {/* Section header */}
245
- <button
246
- onClick={() => toggleSection(key)}
247
- className="w-full flex items-center gap-3 px-4 py-3 hover:bg-surface-hover transition-colors"
248
- >
249
- {isExpanded ? (
250
- <ChevronDown className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
251
- ) : (
252
- <ChevronRight className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
253
- )}
254
-
255
- {groupBy === 'provider' && <ProviderIcon type={providerType} size="sm" />}
256
-
257
- {groupBy === 'provider' ? (
258
- <span
259
- className="text-xs font-semibold text-foreground hover:text-primary hover:underline cursor-pointer"
260
- onClick={(e) => { e.stopPropagation(); navigate(`/integrations/${key}`) }}
261
- >
262
- {getGroupLabel(key)}
263
- </span>
264
- ) : (
265
- <span className="text-xs font-semibold text-foreground">{getGroupLabel(key)}</span>
266
- )}
267
- <span className="text-[10px] text-foreground-subtle">
268
- {groupDevices.length} device{groupDevices.length !== 1 ? 's' : ''}
269
- </span>
270
-
271
- {providerStatus && <StatusBadge status={providerStatus} />}
272
-
273
- <div className="ml-auto flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
274
- {providerId && (
275
- <button
276
- className="p-1.5 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
277
- title="Provider settings"
278
- >
279
- <Settings className="h-3.5 w-3.5" />
280
- </button>
281
- )}
282
- </div>
283
- </button>
284
-
285
- {/* Device table */}
286
- {isExpanded && (
287
- <div className="border-t border-border">
288
- {/* Table header */}
289
- <div className="grid grid-cols-[1fr_80px_80px_80px_100px_100px] gap-2 px-4 py-2 text-[10px] font-medium text-foreground-subtle uppercase tracking-wider border-b border-border bg-background">
290
- <span>Name</span>
291
- <span>Type</span>
292
- <span>Status</span>
293
- <span>Phase</span>
294
- <span>Today</span>
295
- <span>Last Event</span>
296
- </div>
297
-
298
- {/* Device rows */}
299
- {groupDevices.map((device) => {
300
- const deviceId = String(device.id)
301
- const deviceName = String(device.name ?? device.id)
302
- const deviceType = getDeviceType(device)
303
- const deviceStatus = String(device.status ?? 'offline')
304
- const phase = String(device.phase ?? '—')
305
-
306
- return (
307
- <button
308
- key={deviceId}
309
- onClick={() => navigate(`/devices/${deviceId}`)}
310
- className="w-full grid grid-cols-[1fr_80px_80px_80px_100px_100px] gap-2 px-4 py-2.5 text-left hover:bg-surface-hover transition-colors border-b border-border last:border-b-0"
311
- >
312
- <span className="text-xs font-medium text-foreground truncate">{deviceName}</span>
313
- <span className="text-[11px] text-foreground-subtle capitalize">{deviceType}</span>
314
- <span><StatusBadge status={deviceStatus} /></span>
315
- <span className="text-[11px] text-foreground-subtle">{phase}</span>
316
- <span className="text-[11px] text-foreground-subtle">—</span>
317
- <span className="text-[11px] text-foreground-subtle">—</span>
318
- </button>
319
- )
320
- })}
321
- </div>
322
- )}
323
- </div>
324
- )
325
- })}
326
-
327
- <IntegrationWizard open={wizardOpen} onClose={() => setWizardOpen(false)} />
328
- </div>
329
- )
330
- }
@@ -1,106 +0,0 @@
1
- import { useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
- import { Eye, EyeOff } from 'lucide-react'
4
- import { useThemeMode } from '@camstack/ui'
5
- import { useAuth } from '../contexts/auth-context'
6
-
7
- export function LoginPage() {
8
- const { login } = useAuth()
9
- const theme = useThemeMode()
10
- const navigate = useNavigate()
11
- const [username, setUsername] = useState('')
12
- const [password, setPassword] = useState('')
13
- const [showPassword, setShowPassword] = useState(false)
14
- const [error, setError] = useState('')
15
- const [submitting, setSubmitting] = useState(false)
16
-
17
- const handleSubmit = async (e: React.FormEvent) => {
18
- e.preventDefault()
19
- setError('')
20
- setSubmitting(true)
21
- try {
22
- await login(username, password)
23
- navigate('/dashboard', { replace: true })
24
- } catch (err) {
25
- setError(err instanceof Error ? err.message : 'Login failed')
26
- } finally {
27
- setSubmitting(false)
28
- }
29
- }
30
-
31
- return (
32
- <div className="flex min-h-screen items-center justify-center bg-background p-4">
33
- <div className="w-full max-w-sm">
34
- {/* Logo */}
35
- <div className="flex justify-center mb-8">
36
- <img
37
- src={theme?.resolvedMode === 'light' ? '/brand/logo-horizontal-light.svg' : '/brand/logo-horizontal-dark.svg'}
38
- alt="CamStack Admin"
39
- className="h-12"
40
- />
41
- </div>
42
-
43
- {/* Form */}
44
- <form onSubmit={handleSubmit} className="space-y-4 rounded-xl border border-border bg-surface p-6 shadow-xl shadow-black/10">
45
- {error && (
46
- <div className="rounded-md bg-danger/10 border border-danger/20 px-3 py-2 text-xs text-danger">
47
- {error}
48
- </div>
49
- )}
50
-
51
- <div className="space-y-1.5">
52
- <label className="text-xs font-medium text-foreground-subtle">Username</label>
53
- <input
54
- type="text"
55
- placeholder="admin"
56
- value={username}
57
- onChange={(e) => setUsername(e.target.value)}
58
- className="w-full rounded-lg border border-border bg-background px-3 py-2.5 text-sm text-foreground placeholder:text-foreground-disabled focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-all"
59
- autoFocus
60
- />
61
- </div>
62
-
63
- <div className="space-y-1.5">
64
- <label className="text-xs font-medium text-foreground-subtle">Password</label>
65
- <div className="relative">
66
- <input
67
- type={showPassword ? 'text' : 'password'}
68
- placeholder="Enter password"
69
- value={password}
70
- onChange={(e) => setPassword(e.target.value)}
71
- className="w-full rounded-lg border border-border bg-background px-3 py-2.5 pr-10 text-sm text-foreground placeholder:text-foreground-disabled focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-all"
72
- />
73
- <button
74
- type="button"
75
- onClick={() => setShowPassword(!showPassword)}
76
- className="absolute right-2.5 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground transition-colors"
77
- tabIndex={-1}
78
- >
79
- {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
80
- </button>
81
- </div>
82
- </div>
83
-
84
- <button
85
- type="submit"
86
- disabled={submitting || !username || !password}
87
- className="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground shadow-md shadow-primary/20 hover:shadow-lg hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none transition-all"
88
- >
89
- {submitting ? (
90
- <span className="flex items-center justify-center gap-2">
91
- <span className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground" />
92
- Logging in...
93
- </span>
94
- ) : (
95
- 'Log in'
96
- )}
97
- </button>
98
- </form>
99
-
100
- <p className="mt-6 text-center text-[10px] text-foreground-subtle/50">
101
- CamStack v0.1.0
102
- </p>
103
- </div>
104
- </div>
105
- )
106
- }
@@ -1,18 +0,0 @@
1
- import { PipelineStatus } from '../components/metrics/PipelineStatus'
2
- import { IntegrationUsage } from '../components/metrics/IntegrationUsage'
3
- import { ProcessResources } from '../components/metrics/ProcessResources'
4
- import { AgentLoad } from '../components/metrics/AgentLoad'
5
-
6
- export function MetricsPage() {
7
- return (
8
- <div className="p-6 space-y-4">
9
- <h1 className="text-lg font-semibold text-foreground">Metrics</h1>
10
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
11
- <PipelineStatus />
12
- <IntegrationUsage />
13
- <ProcessResources />
14
- <AgentLoad />
15
- </div>
16
- </div>
17
- )
18
- }