@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.
- package/dist/assets/index-BoVZEQ1j.js +598 -0
- package/dist/assets/index-DwSc8ann.css +1 -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 +4 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -355
- package/src/components/addons/AddonUploadZone.tsx +0 -69
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -108
- 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 -172
- 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 -105
- package/src/components/metrics/IntegrationUsage.tsx +0 -73
- package/src/components/metrics/PipelineStatus.tsx +0 -74
- package/src/components/metrics/ProcessResources.tsx +0 -123
- 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 -254
- 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 -222
- package/src/pages/Integrations.tsx +0 -333
- 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 -396
- 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 -28
- 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,354 +0,0 @@
|
|
|
1
|
-
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { Stage, Layer, Line, Circle, Text, Group, Arrow } from 'react-konva'
|
|
3
|
-
import type Konva from 'konva'
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Types
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
export interface Zone {
|
|
10
|
-
id: string
|
|
11
|
-
name: string
|
|
12
|
-
type: 'intrusion' | 'package-watch' | 'tripwire'
|
|
13
|
-
color: string
|
|
14
|
-
points: { x: number; y: number }[] // normalized 0-1
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ZoneCanvasProps {
|
|
18
|
-
zones: Zone[]
|
|
19
|
-
selectedZoneId: string | null
|
|
20
|
-
onSelectZone: (id: string | null) => void
|
|
21
|
-
onZonePointsChange: (id: string, points: { x: number; y: number }[]) => void
|
|
22
|
-
onZoneComplete: (zone: Zone) => void
|
|
23
|
-
drawingType: 'intrusion' | 'package-watch' | 'tripwire' | null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Helpers
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
function toCanvas(p: { x: number; y: number }, w: number, h: number) {
|
|
31
|
-
return { x: p.x * w, y: p.y * h }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function toNorm(p: { x: number; y: number }, w: number, h: number) {
|
|
35
|
-
return { x: p.x / w, y: p.y / h }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function flatPoints(points: { x: number; y: number }[], w: number, h: number): number[] {
|
|
39
|
-
return points.flatMap((p) => [p.x * w, p.y * h])
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function zoneColor(zone: Zone, selected: boolean): string {
|
|
43
|
-
return selected ? '#6366f1' : zone.color
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const GRID_COLS = 16
|
|
47
|
-
const GRID_ROWS = 9
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Component
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
export function ZoneCanvas({
|
|
54
|
-
zones,
|
|
55
|
-
selectedZoneId,
|
|
56
|
-
onSelectZone,
|
|
57
|
-
onZonePointsChange,
|
|
58
|
-
onZoneComplete,
|
|
59
|
-
drawingType,
|
|
60
|
-
}: ZoneCanvasProps) {
|
|
61
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
62
|
-
const [size, setSize] = useState({ w: 800, h: 450 })
|
|
63
|
-
const [drawPoints, setDrawPoints] = useState<{ x: number; y: number }[]>([])
|
|
64
|
-
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
|
|
65
|
-
|
|
66
|
-
// Resize observer
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
const el = containerRef.current
|
|
69
|
-
if (!el) return
|
|
70
|
-
const ro = new ResizeObserver((entries) => {
|
|
71
|
-
const entry = entries[0]
|
|
72
|
-
if (entry) {
|
|
73
|
-
const { width } = entry.contentRect
|
|
74
|
-
setSize({ w: width, h: (width * 9) / 16 })
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
ro.observe(el)
|
|
78
|
-
return () => ro.disconnect()
|
|
79
|
-
}, [])
|
|
80
|
-
|
|
81
|
-
const isDrawing = drawingType !== null
|
|
82
|
-
|
|
83
|
-
const handleStageClick = useCallback(
|
|
84
|
-
(e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
85
|
-
if (!isDrawing) {
|
|
86
|
-
onSelectZone(null)
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
const stage = e.target.getStage()
|
|
90
|
-
if (!stage) return
|
|
91
|
-
const pos = stage.getPointerPosition()
|
|
92
|
-
if (!pos) return
|
|
93
|
-
|
|
94
|
-
const norm = toNorm(pos, size.w, size.h)
|
|
95
|
-
const isTripwire = drawingType === 'tripwire'
|
|
96
|
-
|
|
97
|
-
if (drawPoints.length === 0) {
|
|
98
|
-
setDrawPoints([norm])
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// For tripwire: complete after 2 points
|
|
103
|
-
if (isTripwire && drawPoints.length === 1) {
|
|
104
|
-
const newPoints = [...drawPoints, norm]
|
|
105
|
-
const zone: Zone = {
|
|
106
|
-
id: crypto.randomUUID(),
|
|
107
|
-
name: 'Tripwire',
|
|
108
|
-
type: 'tripwire',
|
|
109
|
-
color: '#f59e0b',
|
|
110
|
-
points: newPoints,
|
|
111
|
-
}
|
|
112
|
-
onZoneComplete(zone)
|
|
113
|
-
setDrawPoints([])
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check if clicking near first point to close polygon
|
|
118
|
-
if (drawPoints.length >= 3 && !isTripwire) {
|
|
119
|
-
const first = toCanvas(drawPoints[0]!, size.w, size.h)
|
|
120
|
-
const dist = Math.hypot(pos.x - first.x, pos.y - first.y)
|
|
121
|
-
if (dist < 12) {
|
|
122
|
-
const zone: Zone = {
|
|
123
|
-
id: crypto.randomUUID(),
|
|
124
|
-
name: drawingType === 'package-watch' ? 'Package Watch' : 'Zone',
|
|
125
|
-
type: drawingType,
|
|
126
|
-
color: drawingType === 'package-watch' ? '#10b981' : '#3b82f6',
|
|
127
|
-
points: drawPoints,
|
|
128
|
-
}
|
|
129
|
-
onZoneComplete(zone)
|
|
130
|
-
setDrawPoints([])
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
setDrawPoints((prev) => [...prev, norm])
|
|
136
|
-
},
|
|
137
|
-
[isDrawing, drawPoints, size, drawingType, onZoneComplete, onSelectZone],
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
const handleDblClick = useCallback(
|
|
141
|
-
(e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
142
|
-
if (!isDrawing || drawingType === 'tripwire') return
|
|
143
|
-
if (drawPoints.length < 3) return
|
|
144
|
-
e.cancelBubble = true
|
|
145
|
-
const zone: Zone = {
|
|
146
|
-
id: crypto.randomUUID(),
|
|
147
|
-
name: drawingType === 'package-watch' ? 'Package Watch' : 'Zone',
|
|
148
|
-
type: drawingType,
|
|
149
|
-
color: drawingType === 'package-watch' ? '#10b981' : '#3b82f6',
|
|
150
|
-
points: drawPoints,
|
|
151
|
-
}
|
|
152
|
-
onZoneComplete(zone)
|
|
153
|
-
setDrawPoints([])
|
|
154
|
-
},
|
|
155
|
-
[isDrawing, drawingType, drawPoints, onZoneComplete],
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
const handleMouseMove = useCallback(
|
|
159
|
-
(e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
160
|
-
const stage = e.target.getStage()
|
|
161
|
-
if (!stage) return
|
|
162
|
-
const pos = stage.getPointerPosition()
|
|
163
|
-
setCursorPos(pos ?? null)
|
|
164
|
-
},
|
|
165
|
-
[],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
// Grid lines
|
|
169
|
-
const gridLines: React.ReactNode[] = []
|
|
170
|
-
for (let i = 1; i < GRID_COLS; i++) {
|
|
171
|
-
const x = (size.w / GRID_COLS) * i
|
|
172
|
-
gridLines.push(
|
|
173
|
-
<Line key={`gv-${i}`} points={[x, 0, x, size.h]} stroke="#ffffff" strokeWidth={0.5} opacity={0.08} />,
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
for (let i = 1; i < GRID_ROWS; i++) {
|
|
177
|
-
const y = (size.h / GRID_ROWS) * i
|
|
178
|
-
gridLines.push(
|
|
179
|
-
<Line key={`gh-${i}`} points={[0, y, size.w, y]} stroke="#ffffff" strokeWidth={0.5} opacity={0.08} />,
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return (
|
|
184
|
-
<div
|
|
185
|
-
ref={containerRef}
|
|
186
|
-
className="w-full bg-zinc-900 rounded-lg overflow-hidden"
|
|
187
|
-
style={{ cursor: isDrawing ? 'crosshair' : 'default' }}
|
|
188
|
-
>
|
|
189
|
-
<Stage
|
|
190
|
-
width={size.w}
|
|
191
|
-
height={size.h}
|
|
192
|
-
onClick={handleStageClick}
|
|
193
|
-
onDblClick={handleDblClick}
|
|
194
|
-
onMouseMove={handleMouseMove}
|
|
195
|
-
>
|
|
196
|
-
{/* Background + grid */}
|
|
197
|
-
<Layer>
|
|
198
|
-
<Line points={[0, 0, size.w, 0, size.w, size.h, 0, size.h, 0, 0]} stroke="transparent" strokeWidth={0} />
|
|
199
|
-
{gridLines}
|
|
200
|
-
</Layer>
|
|
201
|
-
|
|
202
|
-
{/* Existing zones */}
|
|
203
|
-
<Layer>
|
|
204
|
-
{zones.map((zone) => {
|
|
205
|
-
const selected = zone.id === selectedZoneId
|
|
206
|
-
const color = zoneColor(zone, selected)
|
|
207
|
-
const pts = flatPoints(zone.points, size.w, size.h)
|
|
208
|
-
const isTripwire = zone.type === 'tripwire'
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<Group
|
|
212
|
-
key={zone.id}
|
|
213
|
-
onClick={(e) => {
|
|
214
|
-
e.cancelBubble = true
|
|
215
|
-
if (!isDrawing) onSelectZone(zone.id)
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
{isTripwire ? (
|
|
219
|
-
<Arrow
|
|
220
|
-
points={pts}
|
|
221
|
-
stroke={color}
|
|
222
|
-
strokeWidth={selected ? 3 : 2}
|
|
223
|
-
fill={color}
|
|
224
|
-
pointerLength={10}
|
|
225
|
-
pointerWidth={8}
|
|
226
|
-
opacity={selected ? 1 : 0.8}
|
|
227
|
-
/>
|
|
228
|
-
) : (
|
|
229
|
-
<Line
|
|
230
|
-
points={pts}
|
|
231
|
-
closed
|
|
232
|
-
fill={color + '33'}
|
|
233
|
-
stroke={color}
|
|
234
|
-
strokeWidth={selected ? 2.5 : 1.5}
|
|
235
|
-
opacity={selected ? 1 : 0.85}
|
|
236
|
-
/>
|
|
237
|
-
)}
|
|
238
|
-
|
|
239
|
-
{/* Zone label */}
|
|
240
|
-
{zone.points.length > 0 && (
|
|
241
|
-
<Text
|
|
242
|
-
x={toCanvas(zone.points[0]!, size.w, size.h).x + 4}
|
|
243
|
-
y={toCanvas(zone.points[0]!, size.w, size.h).y + 4}
|
|
244
|
-
text={zone.name}
|
|
245
|
-
fontSize={11}
|
|
246
|
-
fill={color}
|
|
247
|
-
fontStyle="bold"
|
|
248
|
-
/>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
|
-
{/* Drag handles for selected zone */}
|
|
252
|
-
{selected &&
|
|
253
|
-
zone.points.map((p, idx) => {
|
|
254
|
-
const cp = toCanvas(p, size.w, size.h)
|
|
255
|
-
return (
|
|
256
|
-
<Circle
|
|
257
|
-
key={idx}
|
|
258
|
-
x={cp.x}
|
|
259
|
-
y={cp.y}
|
|
260
|
-
radius={5}
|
|
261
|
-
fill={color}
|
|
262
|
-
stroke="#ffffff"
|
|
263
|
-
strokeWidth={1.5}
|
|
264
|
-
draggable
|
|
265
|
-
onDragMove={(e) => {
|
|
266
|
-
const newPt = toNorm({ x: e.target.x(), y: e.target.y() }, size.w, size.h)
|
|
267
|
-
const updated = zone.points.map((pp, i) => (i === idx ? newPt : pp))
|
|
268
|
-
onZonePointsChange(zone.id, updated)
|
|
269
|
-
}}
|
|
270
|
-
/>
|
|
271
|
-
)
|
|
272
|
-
})}
|
|
273
|
-
</Group>
|
|
274
|
-
)
|
|
275
|
-
})}
|
|
276
|
-
</Layer>
|
|
277
|
-
|
|
278
|
-
{/* Drawing preview */}
|
|
279
|
-
<Layer>
|
|
280
|
-
{isDrawing && drawPoints.length > 0 && (
|
|
281
|
-
<Group>
|
|
282
|
-
{drawPoints.length >= 2 && drawingType !== 'tripwire' && (
|
|
283
|
-
<Line
|
|
284
|
-
points={flatPoints(drawPoints, size.w, size.h)}
|
|
285
|
-
stroke="#6366f1"
|
|
286
|
-
strokeWidth={1.5}
|
|
287
|
-
dash={[6, 3]}
|
|
288
|
-
opacity={0.8}
|
|
289
|
-
/>
|
|
290
|
-
)}
|
|
291
|
-
{drawPoints.length === 1 && drawingType === 'tripwire' && cursorPos && (
|
|
292
|
-
<Line
|
|
293
|
-
points={[
|
|
294
|
-
drawPoints[0]!.x * size.w,
|
|
295
|
-
drawPoints[0]!.y * size.h,
|
|
296
|
-
cursorPos.x,
|
|
297
|
-
cursorPos.y,
|
|
298
|
-
]}
|
|
299
|
-
stroke="#f59e0b"
|
|
300
|
-
strokeWidth={1.5}
|
|
301
|
-
dash={[6, 3]}
|
|
302
|
-
opacity={0.7}
|
|
303
|
-
/>
|
|
304
|
-
)}
|
|
305
|
-
{/* Ghost line to cursor */}
|
|
306
|
-
{cursorPos && drawPoints.length >= 1 && drawingType !== 'tripwire' && (
|
|
307
|
-
<Line
|
|
308
|
-
points={[
|
|
309
|
-
drawPoints[drawPoints.length - 1]!.x * size.w,
|
|
310
|
-
drawPoints[drawPoints.length - 1]!.y * size.h,
|
|
311
|
-
cursorPos.x,
|
|
312
|
-
cursorPos.y,
|
|
313
|
-
]}
|
|
314
|
-
stroke="#6366f1"
|
|
315
|
-
strokeWidth={1}
|
|
316
|
-
dash={[4, 4]}
|
|
317
|
-
opacity={0.5}
|
|
318
|
-
/>
|
|
319
|
-
)}
|
|
320
|
-
{/* Points */}
|
|
321
|
-
{drawPoints.map((p, idx) => {
|
|
322
|
-
const cp = toCanvas(p, size.w, size.h)
|
|
323
|
-
const isFirst = idx === 0
|
|
324
|
-
return (
|
|
325
|
-
<Circle
|
|
326
|
-
key={idx}
|
|
327
|
-
x={cp.x}
|
|
328
|
-
y={cp.y}
|
|
329
|
-
radius={isFirst && drawPoints.length >= 3 ? 7 : 4}
|
|
330
|
-
fill={isFirst && drawPoints.length >= 3 ? '#6366f1' : '#ffffff'}
|
|
331
|
-
stroke="#6366f1"
|
|
332
|
-
strokeWidth={1.5}
|
|
333
|
-
opacity={0.9}
|
|
334
|
-
/>
|
|
335
|
-
)
|
|
336
|
-
})}
|
|
337
|
-
</Group>
|
|
338
|
-
)}
|
|
339
|
-
</Layer>
|
|
340
|
-
</Stage>
|
|
341
|
-
|
|
342
|
-
{/* Hint overlay */}
|
|
343
|
-
{isDrawing && (
|
|
344
|
-
<div className="px-3 py-1.5 bg-zinc-800/80 text-[11px] text-zinc-400 border-t border-zinc-700">
|
|
345
|
-
{drawingType === 'tripwire'
|
|
346
|
-
? 'Click to set start point, click again to complete the tripwire'
|
|
347
|
-
: drawPoints.length < 3
|
|
348
|
-
? 'Click to add points (min 3). Double-click or click first point to close.'
|
|
349
|
-
: 'Continue adding points. Double-click or click first point to close.'}
|
|
350
|
-
</div>
|
|
351
|
-
)}
|
|
352
|
-
</div>
|
|
353
|
-
)
|
|
354
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { Trash2 } from 'lucide-react'
|
|
2
|
-
import type { Zone } from './ZoneCanvas'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Constants
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const PRESET_COLORS = [
|
|
9
|
-
'#3b82f6', // blue
|
|
10
|
-
'#10b981', // green
|
|
11
|
-
'#f59e0b', // amber
|
|
12
|
-
'#ef4444', // red
|
|
13
|
-
'#8b5cf6', // purple
|
|
14
|
-
'#06b6d4', // cyan
|
|
15
|
-
'#f97316', // orange
|
|
16
|
-
'#ec4899', // pink
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
const TYPE_OPTIONS: { value: Zone['type']; label: string }[] = [
|
|
20
|
-
{ value: 'intrusion', label: 'Intrusion' },
|
|
21
|
-
{ value: 'package-watch', label: 'Package Watch' },
|
|
22
|
-
{ value: 'tripwire', label: 'Tripwire' },
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
const INPUT_CLASS =
|
|
26
|
-
'w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none'
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Props
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
interface ZoneFormProps {
|
|
33
|
-
zone: Zone
|
|
34
|
-
onChange: (updated: Zone) => void
|
|
35
|
-
onDelete: () => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Component
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
export function ZoneForm({ zone, onChange, onDelete }: ZoneFormProps) {
|
|
43
|
-
return (
|
|
44
|
-
<div className="flex flex-col gap-3">
|
|
45
|
-
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider">Edit Zone</h3>
|
|
46
|
-
|
|
47
|
-
{/* Name */}
|
|
48
|
-
<div>
|
|
49
|
-
<label className="block text-xs font-medium text-foreground mb-1">Name</label>
|
|
50
|
-
<input
|
|
51
|
-
type="text"
|
|
52
|
-
className={INPUT_CLASS}
|
|
53
|
-
value={zone.name}
|
|
54
|
-
onChange={(e) => onChange({ ...zone, name: e.target.value })}
|
|
55
|
-
placeholder="Zone name"
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{/* Type */}
|
|
60
|
-
<div>
|
|
61
|
-
<label className="block text-xs font-medium text-foreground mb-1">Type</label>
|
|
62
|
-
<select
|
|
63
|
-
className={INPUT_CLASS}
|
|
64
|
-
value={zone.type}
|
|
65
|
-
onChange={(e) => onChange({ ...zone, type: e.target.value as Zone['type'] })}
|
|
66
|
-
>
|
|
67
|
-
{TYPE_OPTIONS.map((opt) => (
|
|
68
|
-
<option key={opt.value} value={opt.value}>
|
|
69
|
-
{opt.label}
|
|
70
|
-
</option>
|
|
71
|
-
))}
|
|
72
|
-
</select>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
{/* Color */}
|
|
76
|
-
<div>
|
|
77
|
-
<label className="block text-xs font-medium text-foreground mb-1.5">Color</label>
|
|
78
|
-
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
79
|
-
{PRESET_COLORS.map((color) => (
|
|
80
|
-
<button
|
|
81
|
-
key={color}
|
|
82
|
-
type="button"
|
|
83
|
-
title={color}
|
|
84
|
-
className={[
|
|
85
|
-
'h-5 w-5 rounded-md border-2 transition-transform hover:scale-110',
|
|
86
|
-
zone.color === color ? 'border-white scale-110' : 'border-transparent',
|
|
87
|
-
].join(' ')}
|
|
88
|
-
style={{ backgroundColor: color }}
|
|
89
|
-
onClick={() => onChange({ ...zone, color })}
|
|
90
|
-
/>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
{/* Custom color input */}
|
|
94
|
-
<div className="flex items-center gap-2">
|
|
95
|
-
<input
|
|
96
|
-
type="color"
|
|
97
|
-
className="h-7 w-7 rounded border border-border bg-background cursor-pointer p-0.5"
|
|
98
|
-
value={zone.color}
|
|
99
|
-
onChange={(e) => onChange({ ...zone, color: e.target.value })}
|
|
100
|
-
/>
|
|
101
|
-
<input
|
|
102
|
-
type="text"
|
|
103
|
-
className={INPUT_CLASS + ' font-mono flex-1'}
|
|
104
|
-
value={zone.color}
|
|
105
|
-
placeholder="#000000"
|
|
106
|
-
onChange={(e) => onChange({ ...zone, color: e.target.value })}
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
{/* Point count info */}
|
|
112
|
-
<p className="text-[10px] text-foreground-subtle">
|
|
113
|
-
{zone.type === 'tripwire' ? 'Tripwire: 2 points' : `Polygon: ${zone.points.length} points`}
|
|
114
|
-
{zone.points.length > 0 && ' — drag handles on canvas to reshape'}
|
|
115
|
-
</p>
|
|
116
|
-
|
|
117
|
-
{/* Delete */}
|
|
118
|
-
<button
|
|
119
|
-
type="button"
|
|
120
|
-
onClick={onDelete}
|
|
121
|
-
className="mt-1 flex items-center justify-center gap-1.5 w-full rounded-md border border-danger/40 bg-danger/10 px-3 py-1.5 text-xs font-medium text-danger hover:bg-danger/20 transition-colors"
|
|
122
|
-
>
|
|
123
|
-
<Trash2 className="h-3 w-3" />
|
|
124
|
-
Delete zone
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
127
|
-
)
|
|
128
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { Plus, Pencil, Trash2, Minus } from 'lucide-react'
|
|
2
|
-
import type { Zone } from './ZoneCanvas'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Type badge helper
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const TYPE_LABELS: Record<Zone['type'], string> = {
|
|
9
|
-
intrusion: 'Intrusion',
|
|
10
|
-
'package-watch': 'Package',
|
|
11
|
-
tripwire: 'Tripwire',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const TYPE_COLORS: Record<Zone['type'], string> = {
|
|
15
|
-
intrusion: 'bg-danger/10 text-danger',
|
|
16
|
-
'package-watch': 'bg-success/10 text-success',
|
|
17
|
-
tripwire: 'bg-warning/10 text-warning',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Props
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
interface ZoneListProps {
|
|
25
|
-
zones: Zone[]
|
|
26
|
-
selectedZoneId: string | null
|
|
27
|
-
drawingType: 'intrusion' | 'package-watch' | 'tripwire' | null
|
|
28
|
-
onSelectZone: (id: string | null) => void
|
|
29
|
-
onDeleteZone: (id: string) => void
|
|
30
|
-
onStartDraw: (type: 'intrusion' | 'package-watch' | 'tripwire') => void
|
|
31
|
-
onCancelDraw: () => void
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Component
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
export function ZoneList({
|
|
39
|
-
zones,
|
|
40
|
-
selectedZoneId,
|
|
41
|
-
drawingType,
|
|
42
|
-
onSelectZone,
|
|
43
|
-
onDeleteZone,
|
|
44
|
-
onStartDraw,
|
|
45
|
-
onCancelDraw,
|
|
46
|
-
}: ZoneListProps) {
|
|
47
|
-
return (
|
|
48
|
-
<div className="flex flex-col gap-3">
|
|
49
|
-
{/* Add buttons */}
|
|
50
|
-
<div className="flex flex-col gap-1.5">
|
|
51
|
-
{drawingType ? (
|
|
52
|
-
<button
|
|
53
|
-
type="button"
|
|
54
|
-
onClick={onCancelDraw}
|
|
55
|
-
className="w-full flex items-center justify-center gap-1.5 rounded-md border border-warning/40 bg-warning/10 px-3 py-1.5 text-xs font-medium text-warning hover:bg-warning/20 transition-colors"
|
|
56
|
-
>
|
|
57
|
-
<Minus className="h-3 w-3" />
|
|
58
|
-
Cancel drawing
|
|
59
|
-
</button>
|
|
60
|
-
) : (
|
|
61
|
-
<>
|
|
62
|
-
<button
|
|
63
|
-
type="button"
|
|
64
|
-
onClick={() => onStartDraw('intrusion')}
|
|
65
|
-
className="w-full flex items-center justify-center gap-1.5 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-foreground hover:bg-surface-hover transition-colors"
|
|
66
|
-
>
|
|
67
|
-
<Plus className="h-3 w-3" />
|
|
68
|
-
Add Zone
|
|
69
|
-
</button>
|
|
70
|
-
<button
|
|
71
|
-
type="button"
|
|
72
|
-
onClick={() => onStartDraw('tripwire')}
|
|
73
|
-
className="w-full flex items-center justify-center gap-1.5 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-foreground hover:bg-surface-hover transition-colors"
|
|
74
|
-
>
|
|
75
|
-
<Plus className="h-3 w-3" />
|
|
76
|
-
Add Tripwire
|
|
77
|
-
</button>
|
|
78
|
-
</>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{/* Zone list */}
|
|
83
|
-
{zones.length === 0 ? (
|
|
84
|
-
<p className="text-center text-[11px] text-foreground-subtle py-4">
|
|
85
|
-
No zones yet. Click "Add Zone" or "Add Tripwire" to start.
|
|
86
|
-
</p>
|
|
87
|
-
) : (
|
|
88
|
-
<div className="flex flex-col gap-1">
|
|
89
|
-
{zones.map((zone) => {
|
|
90
|
-
const isSelected = zone.id === selectedZoneId
|
|
91
|
-
return (
|
|
92
|
-
<div
|
|
93
|
-
key={zone.id}
|
|
94
|
-
className={[
|
|
95
|
-
'flex items-center gap-2 rounded-md border px-2 py-1.5 cursor-pointer transition-colors',
|
|
96
|
-
isSelected
|
|
97
|
-
? 'border-primary/50 bg-primary/10'
|
|
98
|
-
: 'border-border bg-surface hover:bg-surface-hover',
|
|
99
|
-
].join(' ')}
|
|
100
|
-
onClick={() => onSelectZone(isSelected ? null : zone.id)}
|
|
101
|
-
>
|
|
102
|
-
{/* Color swatch */}
|
|
103
|
-
<div
|
|
104
|
-
className="h-3 w-3 rounded-sm flex-shrink-0"
|
|
105
|
-
style={{ backgroundColor: zone.color }}
|
|
106
|
-
/>
|
|
107
|
-
|
|
108
|
-
{/* Name */}
|
|
109
|
-
<span className="flex-1 truncate text-xs font-medium text-foreground">{zone.name}</span>
|
|
110
|
-
|
|
111
|
-
{/* Type badge */}
|
|
112
|
-
<span
|
|
113
|
-
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-[9px] font-medium flex-shrink-0 ${TYPE_COLORS[zone.type]}`}
|
|
114
|
-
>
|
|
115
|
-
{TYPE_LABELS[zone.type]}
|
|
116
|
-
</span>
|
|
117
|
-
|
|
118
|
-
{/* Actions */}
|
|
119
|
-
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
120
|
-
<button
|
|
121
|
-
type="button"
|
|
122
|
-
title="Select"
|
|
123
|
-
className="p-0.5 text-foreground-subtle hover:text-primary rounded transition-colors"
|
|
124
|
-
onClick={(e) => {
|
|
125
|
-
e.stopPropagation()
|
|
126
|
-
onSelectZone(zone.id)
|
|
127
|
-
}}
|
|
128
|
-
>
|
|
129
|
-
<Pencil className="h-3 w-3" />
|
|
130
|
-
</button>
|
|
131
|
-
<button
|
|
132
|
-
type="button"
|
|
133
|
-
title="Delete"
|
|
134
|
-
className="p-0.5 text-foreground-subtle hover:text-danger rounded transition-colors"
|
|
135
|
-
onClick={(e) => {
|
|
136
|
-
e.stopPropagation()
|
|
137
|
-
onDeleteZone(zone.id)
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
|
-
<Trash2 className="h-3 w-3" />
|
|
141
|
-
</button>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
)
|
|
145
|
-
})}
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
)
|
|
150
|
-
}
|