@camstack/addon-admin-ui 0.1.2 → 0.1.4

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-BoVZEQ1j.js +598 -0
  2. package/dist/assets/index-DwSc8ann.css +1 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,79 +0,0 @@
1
- import { useTranslation } from 'react-i18next'
2
- import { CompactDeviceCard } from './CompactDeviceCard'
3
- import { DiscoveredDeviceCard } from './DiscoveredDeviceCard'
4
-
5
- interface ActiveDevice {
6
- id: string
7
- name: string
8
- status: string
9
- snapshotUrl?: string | null
10
- }
11
-
12
- interface DiscoveredDevice {
13
- externalId: string
14
- name: string
15
- }
16
-
17
- interface DeviceGridProps {
18
- activeDevices: readonly ActiveDevice[]
19
- discoveredDevices?: readonly DiscoveredDevice[]
20
- onImport?: (externalId: string) => void
21
- importingIds?: ReadonlySet<string>
22
- }
23
-
24
- export function DeviceGrid({ activeDevices, discoveredDevices, onImport, importingIds }: DeviceGridProps) {
25
- const { t } = useTranslation()
26
- const hasDiscovered = discoveredDevices && discoveredDevices.length > 0
27
-
28
- return (
29
- <div className="space-y-4">
30
- {/* Active devices */}
31
- {activeDevices.length > 0 && (
32
- <div>
33
- <p className="text-[10px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2">
34
- {t('integrations.active')} ({activeDevices.length})
35
- </p>
36
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
37
- {activeDevices.map((device) => (
38
- <CompactDeviceCard
39
- key={device.id}
40
- id={device.id}
41
- name={device.name}
42
- status={device.status}
43
- snapshotUrl={device.snapshotUrl}
44
- />
45
- ))}
46
- </div>
47
- </div>
48
- )}
49
-
50
- {/* Empty state for active */}
51
- {activeDevices.length === 0 && !hasDiscovered && (
52
- <div className="flex flex-col items-center py-16 text-foreground-subtle">
53
- <p className="text-sm">{t('integrations.noDevices')}</p>
54
- <p className="text-xs mt-1">{t('integrations.addFirstDevice')}</p>
55
- </div>
56
- )}
57
-
58
- {/* Discovered devices */}
59
- {hasDiscovered && (
60
- <div>
61
- <p className="text-[10px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2">
62
- {t('integrations.available')} ({discoveredDevices.length})
63
- </p>
64
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
65
- {discoveredDevices.map((device) => (
66
- <DiscoveredDeviceCard
67
- key={device.externalId}
68
- name={device.name}
69
- externalId={device.externalId}
70
- onImport={onImport ?? (() => {})}
71
- importing={importingIds?.has(device.externalId)}
72
- />
73
- ))}
74
- </div>
75
- </div>
76
- )}
77
- </div>
78
- )
79
- }
@@ -1,17 +0,0 @@
1
- import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
2
-
3
- interface DeviceGroupHeaderProps {
4
- type: string
5
- name: string
6
- deviceCount: number
7
- }
8
-
9
- export function DeviceGroupHeader({ type, name, deviceCount }: DeviceGroupHeaderProps) {
10
- return (
11
- <div className="flex items-center gap-2.5 mb-2 mt-4 first:mt-0">
12
- <ProviderIcon type={type} size="sm" />
13
- <span className="text-xs font-semibold text-foreground">{name}</span>
14
- <span className="text-[10px] text-foreground-subtle">({deviceCount})</span>
15
- </div>
16
- )
17
- }
@@ -1,26 +0,0 @@
1
- import { useTranslation } from 'react-i18next'
2
- import { Camera } from 'lucide-react'
3
-
4
- interface DiscoveredDeviceCardProps {
5
- name: string
6
- externalId: string
7
- onImport: (externalId: string) => void
8
- importing?: boolean
9
- }
10
-
11
- export function DiscoveredDeviceCard({ name, externalId, onImport, importing }: DiscoveredDeviceCardProps) {
12
- const { t } = useTranslation()
13
- return (
14
- <div className="flex items-center gap-2 rounded-lg border border-dashed border-border bg-surface px-3 py-2.5 opacity-70">
15
- <Camera className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
16
- <span className="text-[11px] font-medium text-foreground flex-1 truncate">{name}</span>
17
- <button
18
- onClick={() => onImport(externalId)}
19
- disabled={importing}
20
- className="flex-shrink-0 rounded bg-primary/15 border border-primary/25 px-2 py-0.5 text-[9px] font-semibold text-primary hover:bg-primary/25 transition-colors disabled:opacity-50"
21
- >
22
- {importing ? '...' : t('integrations.import')}
23
- </button>
24
- </div>
25
- )
26
- }
@@ -1,40 +0,0 @@
1
- import { Play, Square } from 'lucide-react'
2
- import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
3
- import { StatusBadge } from '../shared/StatusBadge'
4
-
5
- interface IntegrationCardProps {
6
- id: string
7
- name: string
8
- type: string
9
- status: string
10
- deviceCount: number
11
- onStart: () => void
12
- onStop: () => void
13
- }
14
-
15
- export function IntegrationCard({ id, name, type, status, deviceCount, onStart, onStop }: IntegrationCardProps) {
16
- return (
17
- <div className="rounded-lg border border-border bg-surface p-4 hover:shadow-md hover:shadow-black/5 transition-all">
18
- <div className="flex items-start justify-between mb-3">
19
- <ProviderIcon type={type} size="lg" />
20
- <StatusBadge status={status} />
21
- </div>
22
- <h3 className="text-sm font-semibold text-foreground truncate">{name}</h3>
23
- <p className="text-[11px] text-foreground-subtle mt-0.5">{getProviderLabel(type)}</p>
24
- <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
25
- <span className="text-[11px] text-foreground-subtle">{deviceCount} device{deviceCount !== 1 ? 's' : ''}</span>
26
- <div className="flex items-center gap-1">
27
- {status === 'running' ? (
28
- <button onClick={onStop} className="p-1.5 rounded-md hover:bg-danger/10 text-foreground-subtle hover:text-danger transition-colors" title="Stop">
29
- <Square className="h-3.5 w-3.5" />
30
- </button>
31
- ) : (
32
- <button onClick={onStart} className="p-1.5 rounded-md hover:bg-success/10 text-foreground-subtle hover:text-success transition-colors" title="Start">
33
- <Play className="h-3.5 w-3.5" />
34
- </button>
35
- )}
36
- </div>
37
- </div>
38
- </div>
39
- )
40
- }
@@ -1,172 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
4
- import { useNavigate } from 'react-router-dom'
5
- import { X } from 'lucide-react'
6
- import { useBackendClient } from '../../hooks/useBackendClient'
7
- import { ProviderPicker } from './ProviderPicker'
8
- import { ProviderConfigForm } from './ProviderConfigForm'
9
- import { DeviceDiscoveryStep } from './DeviceDiscoveryStep'
10
- import type { ConfigUISchema } from '@camstack/types'
11
-
12
- type WizardStep = 'picker' | 'config' | 'discovery'
13
-
14
- interface IntegrationWizardProps {
15
- open: boolean
16
- onClose: () => void
17
- }
18
-
19
- export function IntegrationWizard({ open, onClose }: IntegrationWizardProps) {
20
- const { t } = useTranslation()
21
- const client = useBackendClient()
22
- const navigate = useNavigate()
23
- const queryClient = useQueryClient()
24
-
25
- const [step, setStep] = useState<WizardStep>('picker')
26
- const [selectedAddonId, setSelectedAddonId] = useState<string | null>(null)
27
- const [createdProviderId, setCreatedProviderId] = useState<string | null>(null)
28
- const [discoveryMode, setDiscoveryMode] = useState<string>('manual')
29
-
30
- const { data: configSchema } = useQuery({
31
- queryKey: ['addon-config-schema', selectedAddonId],
32
- queryFn: () => client.getAddonConfigSchema(selectedAddonId!),
33
- enabled: selectedAddonId != null,
34
- })
35
-
36
- const addProviderMutation = useMutation({
37
- mutationFn: (input: { id: string; type: string; name: string; config: Record<string, unknown> }) =>
38
- client.trpc.providerConfig.addProvider.mutate({
39
- id: input.id,
40
- type: input.type,
41
- name: input.name,
42
- ...input.config,
43
- }),
44
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
45
- })
46
-
47
- const adoptMutation = useMutation({
48
- mutationFn: (externalId: string) =>
49
- client.adoptDevice(createdProviderId!, externalId),
50
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['devices'] }),
51
- })
52
-
53
- async function handlePickerSelect(addonId: string, instanceMode: string, providerDiscoveryMode: string) {
54
- setSelectedAddonId(addonId)
55
- setDiscoveryMode(providerDiscoveryMode)
56
-
57
- if (instanceMode === 'unique') {
58
- const providerId = `${addonId}-${Date.now()}`
59
- try {
60
- await addProviderMutation.mutateAsync({
61
- id: providerId,
62
- type: addonId,
63
- name: addonId,
64
- config: {},
65
- })
66
- onClose()
67
- resetState()
68
- navigate(`/integrations/${providerId}`)
69
- } catch {
70
- // Error is handled by mutation state
71
- }
72
- return
73
- }
74
-
75
- setStep('config')
76
- }
77
-
78
- async function handleConfigSave(data: { name: string; config: Record<string, unknown> }) {
79
- if (!selectedAddonId) return
80
-
81
- const providerId = `${selectedAddonId}-${Date.now()}`
82
- try {
83
- await addProviderMutation.mutateAsync({
84
- id: providerId,
85
- type: selectedAddonId,
86
- name: data.name,
87
- config: data.config,
88
- })
89
- setCreatedProviderId(providerId)
90
- if (discoveryMode === 'auto' || discoveryMode === 'both') {
91
- setStep('discovery')
92
- } else {
93
- finish(providerId)
94
- }
95
- } catch {
96
- // Error is handled by mutation state
97
- }
98
- }
99
-
100
- async function handleImport(externalIds: readonly string[]) {
101
- for (const id of externalIds) {
102
- await adoptMutation.mutateAsync(id)
103
- }
104
- finish()
105
- }
106
-
107
- function finish(overrideProviderId?: string) {
108
- const targetId = overrideProviderId ?? createdProviderId ?? selectedAddonId
109
- onClose()
110
- resetState()
111
- if (targetId) {
112
- navigate(`/integrations/${targetId}`)
113
- }
114
- }
115
-
116
- function resetState() {
117
- setStep('picker')
118
- setSelectedAddonId(null)
119
- setCreatedProviderId(null)
120
- setDiscoveryMode('manual')
121
- }
122
-
123
- function handleClose() {
124
- onClose()
125
- resetState()
126
- }
127
-
128
- if (!open) return null
129
-
130
- const titles: Record<WizardStep, string> = {
131
- picker: t('integrations.newIntegration'),
132
- config: t('integrations.configureIntegration'),
133
- discovery: t('integrations.importDevices'),
134
- }
135
-
136
- return (
137
- <div className="fixed inset-0 z-50 flex items-center justify-center">
138
- <div className="absolute inset-0 bg-black/60" onClick={handleClose} />
139
- <div className="relative z-10 w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl">
140
- <div className="flex items-center justify-between border-b border-border px-5 py-4">
141
- <h2 className="text-sm font-semibold text-foreground">{titles[step]}</h2>
142
- <button
143
- onClick={handleClose}
144
- className="rounded-md p-1 text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
145
- >
146
- <X className="h-4 w-4" />
147
- </button>
148
- </div>
149
- <div className="px-5 py-4">
150
- {step === 'picker' && (
151
- <ProviderPicker onSelect={handlePickerSelect} onClose={handleClose} />
152
- )}
153
- {step === 'config' && selectedAddonId && (
154
- <ProviderConfigForm
155
- addonId={selectedAddonId}
156
- configSchema={(configSchema ?? null) as ConfigUISchema | null}
157
- onSave={handleConfigSave}
158
- onBack={() => setStep('picker')}
159
- />
160
- )}
161
- {step === 'discovery' && createdProviderId && (
162
- <DeviceDiscoveryStep
163
- providerId={createdProviderId}
164
- onImport={handleImport}
165
- onSkip={finish}
166
- />
167
- )}
168
- </div>
169
- </div>
170
- </div>
171
- )
172
- }
@@ -1,89 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { useMutation } from '@tanstack/react-query'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
- import { FormBuilder } from '../form-builder/FormBuilder'
6
- import type { ConfigUISchema } from '../../types/config-ui'
7
-
8
- interface ProviderConfigFormProps {
9
- addonId: string
10
- configSchema: ConfigUISchema | null
11
- onSave: (config: { name: string; config: Record<string, unknown> }) => void
12
- onBack: () => void
13
- }
14
-
15
- export function ProviderConfigForm({ addonId, configSchema, onSave, onBack }: ProviderConfigFormProps) {
16
- const { t } = useTranslation()
17
- const client = useBackendClient()
18
- const [name, setName] = useState('')
19
- const [config, setConfig] = useState<Record<string, unknown>>({})
20
- const [testResult, setTestResult] = useState<{ success: boolean; error?: string } | null>(null)
21
-
22
- const testMutation = useMutation({
23
- mutationFn: () =>
24
- client.trpc.providerConfig.testConnection.mutate({
25
- type: addonId,
26
- url: String(config.url ?? ''),
27
- username: config.username ? String(config.username) : undefined,
28
- password: config.password ? String(config.password) : undefined,
29
- }),
30
- onSuccess: (result) => setTestResult(result),
31
- onError: (err) => setTestResult({ success: false, error: String(err) }),
32
- })
33
-
34
- return (
35
- <div className="space-y-4">
36
- <div>
37
- <label className="block text-[11px] font-medium text-foreground mb-1">{t('integrations.integrationName')}</label>
38
- <input
39
- type="text"
40
- value={name}
41
- onChange={(e) => setName(e.target.value)}
42
- placeholder={t('integrations.integrationNamePlaceholder')}
43
- className="w-full rounded-lg border border-border bg-surface px-3 py-2 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-primary/50"
44
- />
45
- </div>
46
-
47
- {configSchema && (
48
- <FormBuilder schema={configSchema} values={config} onChange={setConfig} />
49
- )}
50
-
51
- {testResult && (
52
- <div
53
- className={`rounded-lg border px-3 py-2 text-xs ${
54
- testResult.success
55
- ? 'border-success/30 bg-success/5 text-success'
56
- : 'border-danger/30 bg-danger/5 text-danger'
57
- }`}
58
- >
59
- {testResult.success ? t('integrations.connectionSuccess') : `${t('integrations.errorPrefix')} ${testResult.error}`}
60
- </div>
61
- )}
62
-
63
- <div className="flex items-center justify-between pt-2">
64
- <button
65
- onClick={onBack}
66
- className="rounded-lg border border-border px-3 py-1.5 text-xs text-foreground-subtle hover:text-foreground transition-colors"
67
- >
68
- {t('integrations.back')}
69
- </button>
70
- <div className="flex items-center gap-2">
71
- <button
72
- onClick={() => testMutation.mutate()}
73
- disabled={testMutation.isPending}
74
- className="rounded-lg border border-border px-3 py-1.5 text-xs text-foreground hover:bg-surface-hover transition-colors disabled:opacity-50"
75
- >
76
- {testMutation.isPending ? t('integrations.testing') : t('integrations.testConnection')}
77
- </button>
78
- <button
79
- onClick={() => onSave({ name, config })}
80
- disabled={!name.trim()}
81
- className="rounded-lg bg-primary px-4 py-1.5 text-xs font-medium text-primary-foreground shadow-sm disabled:opacity-50"
82
- >
83
- {t('integrations.forward')}
84
- </button>
85
- </div>
86
- </div>
87
- </div>
88
- )
89
- }
@@ -1,91 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { useTranslation } from 'react-i18next'
3
- import { ChevronRight, Settings } from 'lucide-react'
4
- import { useNavigate } from 'react-router-dom'
5
- import { useBackendClient } from '../../hooks/useBackendClient'
6
-
7
- interface ProviderPickerProps {
8
- onSelect: (addonId: string, instanceMode: string, discoveryMode: string) => void
9
- onClose: () => void
10
- }
11
-
12
- export function ProviderPicker({ onSelect, onClose }: ProviderPickerProps) {
13
- const { t } = useTranslation()
14
- const client = useBackendClient()
15
- const navigate = useNavigate()
16
-
17
- const { data: providerTypes, isLoading } = useQuery({
18
- queryKey: ['provider-types'],
19
- queryFn: () => client.listProviderTypes(),
20
- })
21
-
22
- if (isLoading) {
23
- return (
24
- <div className="space-y-2 p-2">
25
- {[1, 2, 3].map((i) => (
26
- <div key={i} className="h-14 rounded-lg bg-surface animate-pulse" />
27
- ))}
28
- </div>
29
- )
30
- }
31
-
32
- return (
33
- <div className="space-y-2">
34
- {(providerTypes ?? []).map((provider) => {
35
- const isUniqueExisting = provider.instanceMode === 'unique' && provider.existingInstances.length > 0
36
-
37
- return (
38
- <button
39
- key={provider.addonId}
40
- onClick={() => {
41
- if (isUniqueExisting) {
42
- const existingId = provider.existingInstances[0]!.id
43
- onClose()
44
- navigate(`/integrations/${existingId}`)
45
- } else {
46
- onSelect(provider.addonId, provider.instanceMode, provider.discoveryMode)
47
- }
48
- }}
49
- className="flex w-full items-center gap-3 rounded-lg border border-border bg-surface p-3 text-left hover:border-foreground-subtle/30 transition-colors"
50
- >
51
- {provider.iconUrl ? (
52
- <div
53
- className="flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0"
54
- style={{ backgroundColor: `${provider.color}15` }}
55
- >
56
- <img src={provider.iconUrl} alt="" className="h-5 w-5" />
57
- </div>
58
- ) : (
59
- <div
60
- className="flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0"
61
- style={{ backgroundColor: `${provider.color}15` }}
62
- >
63
- <span className="text-sm" style={{ color: provider.color }}>◉</span>
64
- </div>
65
- )}
66
- <div className="flex-1 min-w-0">
67
- <p className="text-xs font-semibold text-foreground">{provider.name}</p>
68
- {provider.description && (
69
- <p className="text-[10px] text-foreground-subtle mt-0.5 truncate">{provider.description}</p>
70
- )}
71
- </div>
72
- {isUniqueExisting ? (
73
- <span className="flex items-center gap-1 text-[10px] font-medium text-foreground-subtle">
74
- <Settings className="h-3 w-3" />
75
- {t('integrations.manage')}
76
- </span>
77
- ) : (
78
- <ChevronRight className="h-4 w-4 text-foreground-subtle flex-shrink-0" />
79
- )}
80
- </button>
81
- )
82
- })}
83
-
84
- {(providerTypes ?? []).length === 0 && (
85
- <div className="py-8 text-center text-xs text-foreground-subtle">
86
- {t('integrations.noProviders')}
87
- </div>
88
- )}
89
- </div>
90
- )
91
- }
@@ -1,68 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { Eye } from 'lucide-react'
4
-
5
- interface SnapshotPopoverProps {
6
- deviceName: string
7
- snapshotUrl?: string | null
8
- status: string
9
- resolution?: string | null
10
- codec?: string | null
11
- fps?: number | null
12
- lastSnapshot?: string | null
13
- }
14
-
15
- export function SnapshotPopover({ deviceName, snapshotUrl, status, resolution, codec, fps, lastSnapshot }: SnapshotPopoverProps) {
16
- const { t } = useTranslation()
17
- const [showPopover, setShowPopover] = useState(false)
18
-
19
- return (
20
- <div
21
- className="relative flex-shrink-0"
22
- onMouseEnter={() => setShowPopover(true)}
23
- onMouseLeave={() => setShowPopover(false)}
24
- >
25
- <button
26
- className="flex h-6 w-6 items-center justify-center rounded text-foreground-subtle hover:bg-surface-hover hover:text-foreground transition-colors"
27
- title={t('integrations.preview')}
28
- >
29
- <Eye className="h-3.5 w-3.5" />
30
- </button>
31
-
32
- {showPopover && (
33
- <div className="absolute bottom-full right-0 mb-2 w-72 rounded-lg border border-border bg-surface shadow-lg shadow-black/30 z-50 overflow-hidden">
34
- <div className="h-40 bg-background flex items-center justify-center">
35
- {snapshotUrl && status !== 'offline' ? (
36
- <img
37
- src={snapshotUrl}
38
- alt={deviceName}
39
- className="h-full w-full object-cover"
40
- onError={(e) => {
41
- (e.target as HTMLImageElement).style.display = 'none'
42
- }}
43
- />
44
- ) : (
45
- <span className="text-xs text-foreground-subtle">
46
- {status === 'offline' ? t('integrations.snapshotUnavailable') : t('integrations.noSnapshot')}
47
- </span>
48
- )}
49
- </div>
50
- <div className="px-3 py-2 space-y-0.5">
51
- <p className="text-xs font-semibold text-foreground">{deviceName}</p>
52
- {(resolution || codec || fps) && (
53
- <p className="text-[10px] text-foreground-subtle">
54
- {[codec, resolution, fps ? `${fps}fps` : null].filter(Boolean).join(' · ')}
55
- </p>
56
- )}
57
- {lastSnapshot && (
58
- <p className="text-[10px] text-foreground-subtle">{t('integrations.lastSnapshot')} {lastSnapshot}</p>
59
- )}
60
- {status === 'offline' && (
61
- <p className="text-[10px] text-danger">{t('integrations.offline')}</p>
62
- )}
63
- </div>
64
- </div>
65
- )}
66
- </div>
67
- )
68
- }