@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,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,171 +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
|
-
|
|
11
|
-
type WizardStep = 'picker' | 'config' | 'discovery'
|
|
12
|
-
|
|
13
|
-
interface IntegrationWizardProps {
|
|
14
|
-
open: boolean
|
|
15
|
-
onClose: () => void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function IntegrationWizard({ open, onClose }: IntegrationWizardProps) {
|
|
19
|
-
const { t } = useTranslation()
|
|
20
|
-
const client = useBackendClient()
|
|
21
|
-
const navigate = useNavigate()
|
|
22
|
-
const queryClient = useQueryClient()
|
|
23
|
-
|
|
24
|
-
const [step, setStep] = useState<WizardStep>('picker')
|
|
25
|
-
const [selectedAddonId, setSelectedAddonId] = useState<string | null>(null)
|
|
26
|
-
const [createdProviderId, setCreatedProviderId] = useState<string | null>(null)
|
|
27
|
-
const [discoveryMode, setDiscoveryMode] = useState<string>('manual')
|
|
28
|
-
|
|
29
|
-
const { data: configSchema } = useQuery({
|
|
30
|
-
queryKey: ['addon-config-schema', selectedAddonId],
|
|
31
|
-
queryFn: () => client.getAddonConfigSchema(selectedAddonId!),
|
|
32
|
-
enabled: selectedAddonId != null,
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const addProviderMutation = useMutation({
|
|
36
|
-
mutationFn: (input: { id: string; type: string; name: string; config: Record<string, unknown> }) =>
|
|
37
|
-
client.trpc.providerConfig.addProvider.mutate({
|
|
38
|
-
id: input.id,
|
|
39
|
-
type: input.type,
|
|
40
|
-
name: input.name,
|
|
41
|
-
...input.config,
|
|
42
|
-
}),
|
|
43
|
-
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const adoptMutation = useMutation({
|
|
47
|
-
mutationFn: (externalId: string) =>
|
|
48
|
-
client.adoptDevice(createdProviderId!, externalId),
|
|
49
|
-
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['devices'] }),
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
async function handlePickerSelect(addonId: string, instanceMode: string, providerDiscoveryMode: string) {
|
|
53
|
-
setSelectedAddonId(addonId)
|
|
54
|
-
setDiscoveryMode(providerDiscoveryMode)
|
|
55
|
-
|
|
56
|
-
if (instanceMode === 'unique') {
|
|
57
|
-
const providerId = `${addonId}-${Date.now()}`
|
|
58
|
-
try {
|
|
59
|
-
await addProviderMutation.mutateAsync({
|
|
60
|
-
id: providerId,
|
|
61
|
-
type: addonId,
|
|
62
|
-
name: addonId,
|
|
63
|
-
config: {},
|
|
64
|
-
})
|
|
65
|
-
onClose()
|
|
66
|
-
resetState()
|
|
67
|
-
navigate(`/integrations/${providerId}`)
|
|
68
|
-
} catch {
|
|
69
|
-
// Error is handled by mutation state
|
|
70
|
-
}
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
setStep('config')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function handleConfigSave(data: { name: string; config: Record<string, unknown> }) {
|
|
78
|
-
if (!selectedAddonId) return
|
|
79
|
-
|
|
80
|
-
const providerId = `${selectedAddonId}-${Date.now()}`
|
|
81
|
-
try {
|
|
82
|
-
await addProviderMutation.mutateAsync({
|
|
83
|
-
id: providerId,
|
|
84
|
-
type: selectedAddonId,
|
|
85
|
-
name: data.name,
|
|
86
|
-
config: data.config,
|
|
87
|
-
})
|
|
88
|
-
setCreatedProviderId(providerId)
|
|
89
|
-
if (discoveryMode === 'auto' || discoveryMode === 'both') {
|
|
90
|
-
setStep('discovery')
|
|
91
|
-
} else {
|
|
92
|
-
finish(providerId)
|
|
93
|
-
}
|
|
94
|
-
} catch {
|
|
95
|
-
// Error is handled by mutation state
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function handleImport(externalIds: readonly string[]) {
|
|
100
|
-
for (const id of externalIds) {
|
|
101
|
-
await adoptMutation.mutateAsync(id)
|
|
102
|
-
}
|
|
103
|
-
finish()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function finish(overrideProviderId?: string) {
|
|
107
|
-
const targetId = overrideProviderId ?? createdProviderId ?? selectedAddonId
|
|
108
|
-
onClose()
|
|
109
|
-
resetState()
|
|
110
|
-
if (targetId) {
|
|
111
|
-
navigate(`/integrations/${targetId}`)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resetState() {
|
|
116
|
-
setStep('picker')
|
|
117
|
-
setSelectedAddonId(null)
|
|
118
|
-
setCreatedProviderId(null)
|
|
119
|
-
setDiscoveryMode('manual')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function handleClose() {
|
|
123
|
-
onClose()
|
|
124
|
-
resetState()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!open) return null
|
|
128
|
-
|
|
129
|
-
const titles: Record<WizardStep, string> = {
|
|
130
|
-
picker: t('integrations.newIntegration'),
|
|
131
|
-
config: t('integrations.configureIntegration'),
|
|
132
|
-
discovery: t('integrations.importDevices'),
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
137
|
-
<div className="absolute inset-0 bg-black/60" onClick={handleClose} />
|
|
138
|
-
<div className="relative z-10 w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl">
|
|
139
|
-
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
|
140
|
-
<h2 className="text-sm font-semibold text-foreground">{titles[step]}</h2>
|
|
141
|
-
<button
|
|
142
|
-
onClick={handleClose}
|
|
143
|
-
className="rounded-md p-1 text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
|
|
144
|
-
>
|
|
145
|
-
<X className="h-4 w-4" />
|
|
146
|
-
</button>
|
|
147
|
-
</div>
|
|
148
|
-
<div className="px-5 py-4">
|
|
149
|
-
{step === 'picker' && (
|
|
150
|
-
<ProviderPicker onSelect={handlePickerSelect} onClose={handleClose} />
|
|
151
|
-
)}
|
|
152
|
-
{step === 'config' && selectedAddonId && (
|
|
153
|
-
<ProviderConfigForm
|
|
154
|
-
addonId={selectedAddonId}
|
|
155
|
-
configSchema={(configSchema ?? null) as any}
|
|
156
|
-
onSave={handleConfigSave}
|
|
157
|
-
onBack={() => setStep('picker')}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
{step === 'discovery' && createdProviderId && (
|
|
161
|
-
<DeviceDiscoveryStep
|
|
162
|
-
providerId={createdProviderId}
|
|
163
|
-
onImport={handleImport}
|
|
164
|
-
onSkip={finish}
|
|
165
|
-
/>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
@@ -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
|
-
}
|