fix(lint): resolve remaining ESLint errors (unused vars, any types, react-refresh)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { performRegistration, assertWebCryptoAvailable } from '@/lib/crypto/registration'
|
import { performRegistration } from '@/lib/crypto/registration'
|
||||||
import { keyStore } from '@/lib/crypto/keyStore'
|
import { keyStore } from '@/lib/crypto/keyStore'
|
||||||
import { authApi } from '@/lib/api'
|
import { authApi } from '@/lib/api'
|
||||||
import { EmergencyKitDialog } from './EmergencyKitDialog'
|
import { EmergencyKitDialog } from './EmergencyKitDialog'
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function BulkDeployDialog({
|
|||||||
queryKey: ['devices-for-cert', tenantId],
|
queryKey: ['devices-for-cert', tenantId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await devicesApi.list(tenantId)
|
const result = await devicesApi.list(tenantId)
|
||||||
return (result as any).items ?? result
|
return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
|
||||||
},
|
},
|
||||||
enabled: !!tenantId && open,
|
enabled: !!tenantId && open,
|
||||||
})
|
})
|
||||||
@@ -128,14 +128,15 @@ export function BulkDeployDialog({
|
|||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
setResult({
|
setResult({
|
||||||
success: 0,
|
success: 0,
|
||||||
failed: selected.size,
|
failed: selected.size,
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
device_id: 'bulk',
|
device_id: 'bulk',
|
||||||
error: e?.response?.data?.detail || 'Bulk deployment failed',
|
error: err?.response?.data?.detail || 'Bulk deployment failed',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ export function CAStatusCard({ ca, canWrite: writable, tenantId }: CAStatusCardP
|
|||||||
void queryClient.invalidateQueries({ queryKey: ['ca'] })
|
void queryClient.invalidateQueries({ queryKey: ['ca'] })
|
||||||
toast({ title: 'Certificate Authority initialized' })
|
toast({ title: 'Certificate Authority initialized' })
|
||||||
},
|
},
|
||||||
onError: (e: any) =>
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
toast({
|
toast({
|
||||||
title: e?.response?.data?.detail || 'Failed to initialize CA',
|
title: err?.response?.data?.detail || 'Failed to initialize CA',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDownloadPEM = async () => {
|
const handleDownloadPEM = async () => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function CertConfirmDialog({
|
|||||||
// Reset confirm text when dialog opens/closes or action changes
|
// Reset confirm text when dialog opens/closes or action changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setConfirmText('')
|
setConfirmText('')
|
||||||
}
|
}
|
||||||
}, [open, action])
|
}, [open, action])
|
||||||
|
|||||||
@@ -9,11 +9,7 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useUIStore } from '@/lib/store'
|
import { useUIStore } from '@/lib/store'
|
||||||
import { Shield, Building2 } from 'lucide-react'
|
import { Shield, Building2 } from 'lucide-react'
|
||||||
import {
|
import { certificatesApi } from '@/lib/certificatesApi'
|
||||||
certificatesApi,
|
|
||||||
type CAResponse,
|
|
||||||
type DeviceCertResponse,
|
|
||||||
} from '@/lib/certificatesApi'
|
|
||||||
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
||||||
import { canWrite } from '@/lib/auth'
|
import { canWrite } from '@/lib/auth'
|
||||||
import { CAStatusCard } from './CAStatusCard'
|
import { CAStatusCard } from './CAStatusCard'
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function DeployCertDialog({
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await devicesApi.list(tenantId)
|
const result = await devicesApi.list(tenantId)
|
||||||
// The list endpoint returns { items, total, ... } or an array
|
// The list endpoint returns { items, total, ... } or an array
|
||||||
return (result as any).items ?? result
|
return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
|
||||||
},
|
},
|
||||||
enabled: !!tenantId && open,
|
enabled: !!tenantId && open,
|
||||||
})
|
})
|
||||||
@@ -110,9 +110,10 @@ export function DeployCertDialog({
|
|||||||
setErrorMsg(result.error ?? 'Deployment failed')
|
setErrorMsg(result.error ?? 'Deployment failed')
|
||||||
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setStep('error')
|
setStep('error')
|
||||||
const detail = e?.response?.data?.detail || 'Failed to deploy certificate'
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
|
const detail = err?.response?.data?.detail || 'Failed to deploy certificate'
|
||||||
setErrorMsg(detail)
|
setErrorMsg(detail)
|
||||||
toast({ title: detail, variant: 'destructive' })
|
toast({ title: detail, variant: 'destructive' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useState } from 'react'
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
ShieldAlert,
|
|
||||||
Plus,
|
Plus,
|
||||||
Layers,
|
Layers,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
@@ -132,11 +131,13 @@ export function DeviceCertTable({
|
|||||||
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (e: any) =>
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
toast({
|
toast({
|
||||||
title: e?.response?.data?.detail || 'Failed to deploy certificate',
|
title: err?.response?.data?.detail || 'Failed to deploy certificate',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const rotateMutation = useMutation({
|
const rotateMutation = useMutation({
|
||||||
@@ -149,11 +150,13 @@ export function DeviceCertTable({
|
|||||||
toast({ title: result.error ?? 'Rotation failed', variant: 'destructive' })
|
toast({ title: result.error ?? 'Rotation failed', variant: 'destructive' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (e: any) =>
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
toast({
|
toast({
|
||||||
title: e?.response?.data?.detail || 'Failed to rotate certificate',
|
title: err?.response?.data?.detail || 'Failed to rotate certificate',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const revokeMutation = useMutation({
|
const revokeMutation = useMutation({
|
||||||
@@ -162,11 +165,13 @@ export function DeviceCertTable({
|
|||||||
void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] })
|
void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] })
|
||||||
toast({ title: 'Certificate revoked' })
|
toast({ title: 'Certificate revoked' })
|
||||||
},
|
},
|
||||||
onError: (e: any) =>
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
toast({
|
toast({
|
||||||
title: e?.response?.data?.detail || 'Failed to revoke certificate',
|
title: err?.response?.data?.detail || 'Failed to revoke certificate',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Filtering ──
|
// ── Filtering ──
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
* users can browse RouterOS menu paths and manage entries.
|
* users can browse RouterOS menu paths and manage entries.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Terminal, ChevronRight, Loader2, WifiOff, Building2 } from 'lucide-react'
|
import { Terminal, ChevronRight, Loader2, WifiOff, Building2 } from 'lucide-react'
|
||||||
import { configEditorApi, type BrowseResponse } from '@/lib/configEditorApi'
|
import { configEditorApi } from '@/lib/configEditorApi'
|
||||||
import { metricsApi } from '@/lib/api'
|
import { metricsApi } from '@/lib/api'
|
||||||
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
||||||
import { useUIStore } from '@/lib/store'
|
import { useUIStore } from '@/lib/store'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* in a dynamic table with edit/delete action buttons.
|
* in a dynamic table with edit/delete action buttons.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pencil, Trash2, Plus, Loader2 } from 'lucide-react'
|
import { Pencil, Trash2, Plus } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { canWrite } from '@/lib/auth'
|
import { canWrite } from '@/lib/auth'
|
||||||
@@ -19,8 +19,6 @@ interface EntryTableProps {
|
|||||||
onAdd: () => void
|
onAdd: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read-only fields that should not have edit buttons */
|
|
||||||
const READ_ONLY_FIELDS = new Set(['.id', 'running', 'dynamic', 'default', 'invalid'])
|
|
||||||
|
|
||||||
export function EntryTable({
|
export function EntryTable({
|
||||||
entries,
|
entries,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
import { SafetyToggle } from './SafetyToggle'
|
import { SafetyToggle } from './SafetyToggle'
|
||||||
import { ChangePreviewModal } from './ChangePreviewModal'
|
import { ChangePreviewModal } from './ChangePreviewModal'
|
||||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -48,8 +47,6 @@ interface AddressListForm {
|
|||||||
|
|
||||||
const EMPTY_FORM: AddressListForm = { list: '', address: '', comment: '' }
|
const EMPTY_FORM: AddressListForm = { list: '', address: '', comment: '' }
|
||||||
|
|
||||||
type PanelHook = ReturnType<typeof useConfigPanel>
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AddressListPanel
|
// AddressListPanel
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { configEditorApi } from '@/lib/configEditorApi'
|
import { configEditorApi } from '@/lib/configEditorApi'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||||
|
|
||||||
interface BwResult {
|
interface BwResult {
|
||||||
|
|||||||
@@ -48,12 +48,6 @@ export function BridgeVlanPanel({ tenantId, deviceId, active }: ConfigPanelProps
|
|||||||
[ports.entries],
|
[ports.entries],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if VLAN filtering is enabled on any bridge
|
|
||||||
const vlanFilteringBridges = useMemo(
|
|
||||||
() => bridges.entries.filter((b) => b['vlan-filtering'] === 'true' || b['vlan-filtering'] === 'yes'),
|
|
||||||
[bridges.entries],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleAdd = useCallback(() => {
|
const handleAdd = useCallback(() => {
|
||||||
setEditEntry(null)
|
setEditEntry(null)
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|||||||
@@ -22,15 +22,12 @@ import {
|
|||||||
import { SafetyToggle } from './SafetyToggle'
|
import { SafetyToggle } from './SafetyToggle'
|
||||||
import { ChangePreviewModal } from './ChangePreviewModal'
|
import { ChangePreviewModal } from './ChangePreviewModal'
|
||||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type PanelHook = ReturnType<typeof useConfigPanel>
|
|
||||||
|
|
||||||
// Timeout fields we expose for editing
|
// Timeout fields we expose for editing
|
||||||
const TIMEOUT_FIELDS = [
|
const TIMEOUT_FIELDS = [
|
||||||
{ key: 'tcp-established-timeout', label: 'TCP Established' },
|
{ key: 'tcp-established-timeout', label: 'TCP Established' },
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export function DiffViewer({ tenantId, deviceId, snapshotId, onClose }: DiffView
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{[75, 90, 65, 85, 70, 80].map((w, i) => (
|
||||||
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${60 + Math.random() * 40}%` }} />
|
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${w}%` }} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : isError || !diff ? (
|
) : isError || !diff ? (
|
||||||
|
|||||||
@@ -6,15 +6,13 @@
|
|||||||
* provides a visual rule builder form, and supports move up/down reordering.
|
* provides a visual rule builder form, and supports move up/down reordering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
Trash2,
|
Trash2,
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -472,11 +470,6 @@ export function FirewallPanel({ tenantId, deviceId, active }: ConfigPanelProps)
|
|||||||
setPreviewOpen(false)
|
setPreviewOpen(false)
|
||||||
}, [panel])
|
}, [panel])
|
||||||
|
|
||||||
const afterApply = useMemo(() => {
|
|
||||||
// When applyChanges succeeds, the hook auto-refetches via queryClient.invalidateQueries
|
|
||||||
return panel.pendingChanges.length
|
|
||||||
}, [panel.pendingChanges.length])
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Render
|
// Render
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from 'react'
|
import { useState, useCallback, useMemo } from 'react'
|
||||||
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown, Filter } from 'lucide-react'
|
import { Plus, Pencil, Trash2, Filter } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
|||||||
@@ -197,7 +197,8 @@ function PoolTable({
|
|||||||
entries,
|
entries,
|
||||||
panel,
|
panel,
|
||||||
poolUsedBy,
|
poolUsedBy,
|
||||||
existingPools,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
existingPools: _existingPools,
|
||||||
}: {
|
}: {
|
||||||
entries: PoolEntry[]
|
entries: PoolEntry[]
|
||||||
panel: PanelHook
|
panel: PanelHook
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||||
import { SafetyToggle } from '@/components/config/SafetyToggle'
|
import { SafetyToggle } from '@/components/config/SafetyToggle'
|
||||||
import { ChangePreviewModal } from '@/components/config/ChangePreviewModal'
|
import { ChangePreviewModal } from '@/components/config/ChangePreviewModal'
|
||||||
import type { ConfigPanelProps, ConfigChange } from '@/lib/configPanelTypes'
|
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from 'react'
|
import { useState, useCallback, useMemo } from 'react'
|
||||||
import { Plus, Pencil, Trash2, Route, Filter } from 'lucide-react'
|
import { Plus, Pencil, Trash2, Route } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -29,15 +29,6 @@ interface TorchEntry {
|
|||||||
rx: string
|
rx: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(val: string): string {
|
|
||||||
const n = parseInt(val, 10)
|
|
||||||
if (isNaN(n)) return val || '-'
|
|
||||||
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)} GB`
|
|
||||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} MB`
|
|
||||||
if (n >= 1_000) return `${(n / 1_000).toFixed(0)} KB`
|
|
||||||
return `${n} B`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBps(val: string): string {
|
function formatBps(val: string): string {
|
||||||
const n = parseInt(val, 10)
|
const n = parseInt(val, 10)
|
||||||
if (isNaN(n)) return val || '-'
|
if (isNaN(n)) return val || '-'
|
||||||
|
|||||||
@@ -618,8 +618,6 @@ function WirelessEditDialog({
|
|||||||
}) {
|
}) {
|
||||||
const [showPassphrase, setShowPassphrase] = useState(false)
|
const [showPassphrase, setShowPassphrase] = useState(false)
|
||||||
|
|
||||||
const ssid = entry?.ssid || entry?.['configuration.ssid'] || ''
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState<WirelessFormData>({
|
const [formData, setFormData] = useState<WirelessFormData>({
|
||||||
ssid: '',
|
ssid: '',
|
||||||
band: '',
|
band: '',
|
||||||
@@ -631,7 +629,8 @@ function WirelessEditDialog({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Reset form when entry changes
|
// Reset form when entry changes
|
||||||
const entryId = entry?.['.id'] || ''
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const _entryId = entry?.['.id'] || ''
|
||||||
useState(() => {
|
useState(() => {
|
||||||
if (entry) {
|
if (entry) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ export function AlertSummary({
|
|||||||
criticalCount,
|
criticalCount,
|
||||||
warningCount,
|
warningCount,
|
||||||
infoCount,
|
infoCount,
|
||||||
tenantId,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
tenantId: _tenantId,
|
||||||
}: AlertSummaryProps) {
|
}: AlertSummaryProps) {
|
||||||
const total = criticalCount + warningCount + infoCount
|
const total = criticalCount + warningCount + infoCount
|
||||||
const counts: Record<string, number> = {
|
const counts: Record<string, number> = {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function getScoreTier(score: number): ScoreTier {
|
|||||||
* - Memory healthy % (weight 0.2) -- % of online devices with memory < 80%
|
* - Memory healthy % (weight 0.2) -- % of online devices with memory < 80%
|
||||||
* - Critical alert penalty (weight 0.2) -- 100 if 0 critical, 50 if 1-2, 0 if 3+
|
* - Critical alert penalty (weight 0.2) -- 100 if 0 critical, 50 if 1-2, 0 if 3+
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function computeHealthScore(
|
export function computeHealthScore(
|
||||||
devices: HealthScoreProps['devices'],
|
devices: HealthScoreProps['devices'],
|
||||||
criticalAlerts: number,
|
criticalAlerts: number,
|
||||||
@@ -115,7 +116,6 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS
|
|||||||
|
|
||||||
export function HealthScore({
|
export function HealthScore({
|
||||||
devices,
|
devices,
|
||||||
activeAlerts: _activeAlerts,
|
|
||||||
criticalAlerts,
|
criticalAlerts,
|
||||||
}: HealthScoreProps) {
|
}: HealthScoreProps) {
|
||||||
const totalDevices = devices.length
|
const totalDevices = devices.length
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface KpiCardsProps {
|
|||||||
* Formats bytes-per-second into a human-readable bandwidth string.
|
* Formats bytes-per-second into a human-readable bandwidth string.
|
||||||
* Auto-scales through bps, Kbps, Mbps, Gbps.
|
* Auto-scales through bps, Kbps, Mbps, Gbps.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function formatBandwidth(bps: number): { value: number; unit: string } {
|
export function formatBandwidth(bps: number): { value: number; unit: string } {
|
||||||
if (bps < 1_000) return { value: bps, unit: 'bps' }
|
if (bps < 1_000) return { value: bps, unit: 'bps' }
|
||||||
if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' }
|
if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' }
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
firmwareApi,
|
firmwareApi,
|
||||||
type FirmwareOverview,
|
|
||||||
type DeviceFirmwareStatus,
|
type DeviceFirmwareStatus,
|
||||||
type FirmwareVersionGroup,
|
type FirmwareVersionGroup,
|
||||||
} from '@/lib/firmwareApi'
|
} from '@/lib/firmwareApi'
|
||||||
@@ -24,7 +23,6 @@ import { useUIStore } from '@/lib/store'
|
|||||||
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import { firmwareApi } from '@/lib/firmwareApi'
|
||||||
firmwareApi,
|
|
||||||
type FirmwareUpgradeJob,
|
|
||||||
} from '@/lib/firmwareApi'
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Step 5: Import & Verify (bulk-add, then check connectivity)
|
* Step 5: Import & Verify (bulk-add, then check connectivity)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
@@ -31,8 +31,6 @@ import {
|
|||||||
type SubnetScanResponse,
|
type SubnetScanResponse,
|
||||||
type SubnetScanResult,
|
type SubnetScanResult,
|
||||||
type DeviceResponse,
|
type DeviceResponse,
|
||||||
type DeviceGroupResponse,
|
|
||||||
type DeviceTagResponse,
|
|
||||||
} from '@/lib/api'
|
} from '@/lib/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
|||||||
(s) => s.status === 'active' || s.status === 'creating',
|
(s) => s.status === 'active' || s.status === 'creating',
|
||||||
)
|
)
|
||||||
if (active) {
|
if (active) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setSession(active)
|
setSession(active)
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setState(active.status === 'active' ? 'active' : 'connecting')
|
setState(active.status === 'active' ? 'active' : 'connecting')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +68,7 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
|||||||
// Countdown timer for session expiry
|
// Countdown timer for session expiry
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== 'active' || !session?.expires_at) {
|
if (state !== 'active' || !session?.expires_at) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setCountdown(null)
|
setCountdown(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,9 +99,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
|||||||
setState('connecting')
|
setState('connecting')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: unknown) => {
|
||||||
|
const e = err as { response?: { data?: { detail?: string } } }
|
||||||
setState('failed')
|
setState('failed')
|
||||||
setError(err.response?.data?.detail || 'Failed to create session')
|
setError(e.response?.data?.detail || 'Failed to create session')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,9 +117,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
|||||||
setError(null)
|
setError(null)
|
||||||
queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] })
|
queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] })
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: unknown) => {
|
||||||
|
const e = err as { response?: { data?: { detail?: string } } }
|
||||||
setState('failed')
|
setState('failed')
|
||||||
setError(err.response?.data?.detail || 'Failed to close session')
|
setError(e.response?.data?.detail || 'Failed to close session')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,12 +135,6 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
|||||||
closeMutation.mutate()
|
closeMutation.mutate()
|
||||||
}, [closeMutation])
|
}, [closeMutation])
|
||||||
|
|
||||||
const handleRetry = useCallback(() => {
|
|
||||||
setSession(null)
|
|
||||||
setError(null)
|
|
||||||
handleOpen()
|
|
||||||
}, [handleOpen])
|
|
||||||
|
|
||||||
const handleReset = useCallback(async () => {
|
const handleReset = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const sessions = await remoteWinboxApi.list(tenantId, deviceId)
|
const sessions = await remoteWinboxApi.list(tenantId, deviceId)
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ export function WinBoxButton({ tenantId, deviceId }: WinBoxButtonProps) {
|
|||||||
window.open(data.winbox_uri, '_blank')
|
window.open(data.winbox_uri, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: unknown) => {
|
||||||
|
const e = err as { response?: { data?: { detail?: string } } }
|
||||||
setState('error')
|
setState('error')
|
||||||
setError(err.response?.data?.detail || 'Failed to open tunnel')
|
setError(e.response?.data?.detail || 'Failed to open tunnel')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -60,23 +60,6 @@ export function MaintenanceForm({
|
|||||||
|
|
||||||
const devices = deviceData?.items ?? []
|
const devices = deviceData?.items ?? []
|
||||||
|
|
||||||
// Populate form when editing
|
|
||||||
useEffect(() => {
|
|
||||||
if (editWindow) {
|
|
||||||
setName(editWindow.name)
|
|
||||||
// Convert ISO to datetime-local format
|
|
||||||
setStartAt(toDatetimeLocal(editWindow.start_at))
|
|
||||||
setEndAt(toDatetimeLocal(editWindow.end_at))
|
|
||||||
setSuppressAlerts(editWindow.suppress_alerts)
|
|
||||||
setNotes(editWindow.notes ?? '')
|
|
||||||
const hasDevices = editWindow.device_ids.length > 0
|
|
||||||
setAllDevices(!hasDevices)
|
|
||||||
setSelectedDevices(hasDevices ? editWindow.device_ids : [])
|
|
||||||
} else {
|
|
||||||
resetForm()
|
|
||||||
}
|
|
||||||
}, [editWindow, open])
|
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
setName('')
|
setName('')
|
||||||
setStartAt('')
|
setStartAt('')
|
||||||
@@ -94,6 +77,25 @@ export function MaintenanceForm({
|
|||||||
return local.toISOString().slice(0, 16)
|
return local.toISOString().slice(0, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate form when editing
|
||||||
|
useEffect(() => {
|
||||||
|
/* eslint-disable react-hooks/set-state-in-effect */
|
||||||
|
if (editWindow) {
|
||||||
|
setName(editWindow.name)
|
||||||
|
// Convert ISO to datetime-local format
|
||||||
|
setStartAt(toDatetimeLocal(editWindow.start_at))
|
||||||
|
setEndAt(toDatetimeLocal(editWindow.end_at))
|
||||||
|
setSuppressAlerts(editWindow.suppress_alerts)
|
||||||
|
setNotes(editWindow.notes ?? '')
|
||||||
|
const hasDevices = editWindow.device_ids.length > 0
|
||||||
|
setAllDevices(!hasDevices)
|
||||||
|
setSelectedDevices(hasDevices ? editWindow.device_ids : [])
|
||||||
|
} else {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
/* eslint-enable react-hooks/set-state-in-effect */
|
||||||
|
}, [editWindow, open])
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: MaintenanceWindowCreate) =>
|
mutationFn: (data: MaintenanceWindowCreate) =>
|
||||||
maintenanceApi.create(tenantId, data),
|
maintenanceApi.create(tenantId, data),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function toDatetimeLocal(iso: string): string {
|
|||||||
/**
|
/**
|
||||||
* Returns start/end ISO strings for a given preset range or custom range.
|
* Returns start/end ISO strings for a given preset range or custom range.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function getTimeRange(
|
export function getTimeRange(
|
||||||
range: string,
|
range: string,
|
||||||
customStart?: string,
|
customStart?: string,
|
||||||
@@ -64,6 +65,7 @@ export function getTimeRange(
|
|||||||
* Returns refetchInterval (ms) for short ranges, false for longer ones.
|
* Returns refetchInterval (ms) for short ranges, false for longer ones.
|
||||||
* Per user decision: 1h and 6h auto-refresh every 60 seconds.
|
* Per user decision: 1h and 6h auto-refresh every 60 seconds.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function shouldAutoRefresh(range: string): number | false {
|
export function shouldAutoRefresh(range: string): number | false {
|
||||||
if (range === '1h' || range === '6h') return 60_000
|
if (range === '1h' || range === '6h') return 60_000
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function ClientsTab({ tenantId, deviceId, active }: ClientsTabProps) {
|
|||||||
}
|
}
|
||||||
return sortDir === 'asc' ? cmp : -cmp
|
return sortDir === 'asc' ? cmp : -cmp
|
||||||
})
|
})
|
||||||
}, [data?.clients, searchQuery, sortField, sortDir])
|
}, [data, searchQuery, sortField, sortDir])
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
const totalClients = data?.clients.length ?? 0
|
const totalClients = data?.clients.length ?? 0
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ interface GaugeBarProps {
|
|||||||
direction: 'RX' | 'TX'
|
direction: 'RX' | 'TX'
|
||||||
}
|
}
|
||||||
|
|
||||||
function GaugeBar({ label, value, maxSpeed, direction }: GaugeBarProps) {
|
function GaugeBar({ value, maxSpeed, direction }: GaugeBarProps) {
|
||||||
const pct = Math.min((value / maxSpeed) * 100, 100)
|
const pct = Math.min((value / maxSpeed) * 100, 100)
|
||||||
const colorClass = getBarColor(pct)
|
const colorClass = getBarColor(pct)
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ interface TooltipData {
|
|||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function NodeTooltip({ data, onClose }: { data: TooltipData; onClose: () => void }) {
|
function NodeTooltip({ data }: { data: TooltipData; onClose?: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute z-50 rounded-lg border border-border bg-elevated shadow-lg px-3 py-2 text-xs pointer-events-none"
|
className="absolute z-50 rounded-lg border border-border bg-elevated shadow-lg px-3 py-2 text-xs pointer-events-none"
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
devicesApi,
|
devicesApi,
|
||||||
deviceGroupsApi,
|
deviceGroupsApi,
|
||||||
type DeviceResponse,
|
type DeviceResponse,
|
||||||
type DeviceGroupResponse,
|
|
||||||
} from '@/lib/api'
|
} from '@/lib/api'
|
||||||
import { configEditorApi } from '@/lib/configEditorApi'
|
import { configEditorApi } from '@/lib/configEditorApi'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAuth, isSuperAdmin, isTenantAdmin } from '@/lib/auth'
|
|||||||
import { authApi } from '@/lib/api'
|
import { authApi } from '@/lib/api'
|
||||||
import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi'
|
import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi'
|
||||||
import { SMTP_PRESETS } from '@/lib/smtpPresets'
|
import { SMTP_PRESETS } from '@/lib/smtpPresets'
|
||||||
import { Settings, User, Shield, Info, Key, Lock, ChevronRight, Download, Trash2, AlertTriangle, Mail, Monitor } from 'lucide-react'
|
import { User, Shield, Info, Key, Lock, ChevronRight, Download, Trash2, AlertTriangle, Mail, Monitor } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -365,8 +365,9 @@ function SMTPSettingsSection() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
saveMutation.mutate()
|
saveMutation.mutate()
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setTestResult({ success: false, message: e.response?.data?.message || e.message })
|
const err = e as { response?: { data?: { message?: string } }; message?: string }
|
||||||
|
setTestResult({ success: false, message: err.response?.data?.message || err.message || 'Unknown error' })
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(false)
|
setTesting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ function VerifyConnectivityStep({ tenantId, deviceId, onComplete }: Step3Props)
|
|||||||
}, [tenantId, deviceId, cleanup])
|
}, [tenantId, deviceId, cleanup])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
startPolling()
|
startPolling()
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [startPolling, cleanup])
|
}, [startPolling, cleanup])
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Simpler than the Standard DnsPanel: no TTL, no MX/TXT types, no advanced settings.
|
* Simpler than the Standard DnsPanel: no TTL, no MX/TXT types, no advanced settings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
import { Server, Globe, Plus, Pencil, Trash2 } from 'lucide-react'
|
import { Server, Globe, Plus, Pencil, Trash2 } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|||||||
@@ -42,19 +42,26 @@ export function LanDhcpPanel({ tenantId, deviceId, active }: ConfigPanelProps) {
|
|||||||
const [leaseTime, setLeaseTime] = useState('')
|
const [leaseTime, setLeaseTime] = useState('')
|
||||||
|
|
||||||
// Sync from browse data
|
// Sync from browse data
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
if (lanEntry) setLanAddress(lanEntry.address ?? '')
|
if (lanEntry) setLanAddress(lanEntry.address ?? '')
|
||||||
}, [lanEntry])
|
}, [lanEntry])
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
if (poolEntry) setPoolRange(poolEntry.ranges ?? '')
|
if (poolEntry) setPoolRange(poolEntry.ranges ?? '')
|
||||||
}, [poolEntry])
|
}, [poolEntry])
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (networkEntry) {
|
if (networkEntry) {
|
||||||
|
/* eslint-disable react-hooks/set-state-in-effect */
|
||||||
setDhcpGateway(networkEntry.gateway ?? '')
|
setDhcpGateway(networkEntry.gateway ?? '')
|
||||||
setDhcpDns(networkEntry['dns-server'] ?? '')
|
setDhcpDns(networkEntry['dns-server'] ?? '')
|
||||||
setLeaseTime(networkEntry['lease-time'] ?? '')
|
setLeaseTime(networkEntry['lease-time'] ?? '')
|
||||||
|
/* eslint-enable react-hooks/set-state-in-effect */
|
||||||
}
|
}
|
||||||
}, [networkEntry])
|
}, [networkEntry])
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
|
|||||||
const [formState, setFormState] = useState<Record<string, Record<string, string>>>({})
|
const [formState, setFormState] = useState<Record<string, Record<string, string>>>({})
|
||||||
|
|
||||||
// Sync form state from browse data
|
// Sync form state from browse data
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newState: Record<string, Record<string, string>> = {}
|
const newState: Record<string, Record<string, string>> = {}
|
||||||
wireless.entries.forEach((entry) => {
|
wireless.entries.forEach((entry) => {
|
||||||
@@ -60,6 +61,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (Object.keys(newState).length > 0) {
|
if (Object.keys(newState).length > 0) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setFormState((prev) => ({ ...prev, ...newState }))
|
setFormState((prev) => ({ ...prev, ...newState }))
|
||||||
}
|
}
|
||||||
}, [wireless.entries, isV7])
|
}, [wireless.entries, isV7])
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ interface TemplateEditorProps {
|
|||||||
|
|
||||||
const VARIABLE_TYPES = ['string', 'ip', 'integer', 'boolean', 'subnet'] as const
|
const VARIABLE_TYPES = ['string', 'ip', 'integer', 'boolean', 'subnet'] as const
|
||||||
|
|
||||||
export function TemplateEditor({ tenantId: _tenantId, template, onSave, onCancel }: TemplateEditorProps) {
|
export function TemplateEditor({ template, onSave, onCancel }: TemplateEditorProps) {
|
||||||
const [name, setName] = useState(template?.name ?? '')
|
const [name, setName] = useState(template?.name ?? '')
|
||||||
const [description, setDescription] = useState(template?.description ?? '')
|
const [description, setDescription] = useState(template?.description ?? '')
|
||||||
const [content, setContent] = useState(template?.content ?? '')
|
const [content, setContent] = useState(template?.content ?? '')
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function TemplatePushWizard({ open, onClose, tenantId, template }: Templa
|
|||||||
|
|
||||||
const selectedDevices = devices?.filter((d) => selectedDeviceIds.has(d.id)) ?? []
|
const selectedDevices = devices?.filter((d) => selectedDeviceIds.has(d.id)) ?? []
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const handleGroupSelect = (_groupId: string) => {
|
const handleGroupSelect = (_groupId: string) => {
|
||||||
// For now, just select all online devices. In a real implementation,
|
// For now, just select all online devices. In a real implementation,
|
||||||
// we'd load group members from the API. Here we select all devices
|
// we'd load group members from the API. Here we select all devices
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
)
|
)
|
||||||
Button.displayName = 'Button'
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
|
type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
export function Skeleton({ className, ...props }: SkeletonProps) {
|
export function Skeleton({ className, ...props }: SkeletonProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface ToastOptions {
|
|||||||
variant?: 'default' | 'destructive'
|
variant?: 'default' | 'destructive'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function toast(options: ToastOptions) {
|
export function toast(options: ToastOptions) {
|
||||||
if (options.variant === 'destructive') {
|
if (options.variant === 'destructive') {
|
||||||
sonnerToast.error(options.title, {
|
sonnerToast.error(options.title, {
|
||||||
@@ -45,4 +46,5 @@ export const Toast = () => null
|
|||||||
export const ToastTitle = () => null
|
export const ToastTitle = () => null
|
||||||
export const ToastDescription = () => null
|
export const ToastDescription = () => null
|
||||||
export const ToastClose = () => null
|
export const ToastClose = () => null
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useToasts = () => ({ toasts: [] as never[], dismiss: () => {} })
|
export const useToasts = () => ({ toasts: [] as never[], dismiss: () => {} })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||||
import { CheckCircle2, Copy, Loader2, AlertTriangle, Wifi } from 'lucide-react'
|
import { CheckCircle2, Copy, AlertTriangle, Wifi } from 'lucide-react'
|
||||||
import { vpnApi } from '@/lib/api'
|
import { vpnApi } from '@/lib/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -61,6 +61,7 @@ export function VpnOnboardingWizard({ tenantId, onSuccess, onCancel }: Props) {
|
|||||||
// Timer for waiting step
|
// Timer for waiting step
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step !== 'waiting') return
|
if (step !== 'waiting') return
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setElapsed(0)
|
setElapsed(0)
|
||||||
const interval = setInterval(() => setElapsed((e) => e + 1), 1000)
|
const interval = setInterval(() => setElapsed((e) => e + 1), 1000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
|
|||||||
@@ -26,15 +26,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
vpnApi,
|
vpnApi,
|
||||||
devicesApi,
|
devicesApi,
|
||||||
type VpnConfigResponse,
|
|
||||||
type VpnPeerResponse,
|
|
||||||
type VpnPeerConfig,
|
|
||||||
type DeviceResponse,
|
type DeviceResponse,
|
||||||
} from '@/lib/api'
|
} from '@/lib/api'
|
||||||
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -63,7 +58,6 @@ export function VpnPage() {
|
|||||||
|
|
||||||
const [showAddDevice, setShowAddDevice] = useState(false)
|
const [showAddDevice, setShowAddDevice] = useState(false)
|
||||||
const [showConfig, setShowConfig] = useState<string | null>(null)
|
const [showConfig, setShowConfig] = useState<string | null>(null)
|
||||||
const [endpoint, setEndpoint] = useState('')
|
|
||||||
const [selectedDevice, setSelectedDevice] = useState('')
|
const [selectedDevice, setSelectedDevice] = useState('')
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
@@ -83,7 +77,11 @@ export function VpnPage() {
|
|||||||
|
|
||||||
const { data: devices = [] } = useQuery({
|
const { data: devices = [] } = useQuery({
|
||||||
queryKey: ['devices', tenantId],
|
queryKey: ['devices', tenantId],
|
||||||
queryFn: () => devicesApi.list(tenantId).then((r: any) => r.items ?? r.devices ?? []),
|
queryFn: () => devicesApi.list(tenantId).then((r: unknown) => {
|
||||||
|
const result = r as { items?: DeviceResponse[]; devices?: DeviceResponse[] } | DeviceResponse[]
|
||||||
|
if (Array.isArray(result)) return result
|
||||||
|
return result.items ?? result.devices ?? []
|
||||||
|
}),
|
||||||
enabled: !!tenantId && showAddDevice,
|
enabled: !!tenantId && showAddDevice,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,12 +94,15 @@ export function VpnPage() {
|
|||||||
// ── Mutations ──
|
// ── Mutations ──
|
||||||
|
|
||||||
const setupMutation = useMutation({
|
const setupMutation = useMutation({
|
||||||
mutationFn: () => vpnApi.setup(tenantId, endpoint || undefined),
|
mutationFn: () => vpnApi.setup(tenantId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
||||||
toast({ title: 'VPN enabled successfully' })
|
toast({ title: 'VPN enabled successfully' })
|
||||||
},
|
},
|
||||||
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to enable VPN', variant: 'destructive' }),
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
|
toast({ title: err?.response?.data?.detail || 'Failed to enable VPN', variant: 'destructive' })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const addPeerMutation = useMutation({
|
const addPeerMutation = useMutation({
|
||||||
@@ -113,7 +114,10 @@ export function VpnPage() {
|
|||||||
setSelectedDevice('')
|
setSelectedDevice('')
|
||||||
toast({ title: 'Device added to VPN' })
|
toast({ title: 'Device added to VPN' })
|
||||||
},
|
},
|
||||||
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to add device', variant: 'destructive' }),
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
|
toast({ title: err?.response?.data?.detail || 'Failed to add device', variant: 'destructive' })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const removePeerMutation = useMutation({
|
const removePeerMutation = useMutation({
|
||||||
@@ -123,7 +127,10 @@ export function VpnPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
||||||
toast({ title: 'Device removed from VPN' })
|
toast({ title: 'Device removed from VPN' })
|
||||||
},
|
},
|
||||||
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to remove device', variant: 'destructive' }),
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
|
toast({ title: err?.response?.data?.detail || 'Failed to remove device', variant: 'destructive' })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleMutation = useMutation({
|
const toggleMutation = useMutation({
|
||||||
@@ -141,7 +148,10 @@ export function VpnPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
|
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
|
||||||
toast({ title: 'VPN configuration deleted' })
|
toast({ title: 'VPN configuration deleted' })
|
||||||
},
|
},
|
||||||
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to delete VPN', variant: 'destructive' }),
|
onError: (e: unknown) => {
|
||||||
|
const err = e as { response?: { data?: { detail?: string } } }
|
||||||
|
toast({ title: err?.response?.data?.detail || 'Failed to delete VPN', variant: 'destructive' })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function EventStreamProvider({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export function useEventStreamContext() {
|
export function useEventStreamContext() {
|
||||||
return useContext(EventStreamContext)
|
return useContext(EventStreamContext)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { useEffect, useRef } from 'react'
|
|||||||
*/
|
*/
|
||||||
export function useShortcut(key: string, callback: () => void, enabled = true) {
|
export function useShortcut(key: string, callback: () => void, enabled = true) {
|
||||||
const callbackRef = useRef(callback)
|
const callbackRef = useRef(callback)
|
||||||
|
useEffect(() => {
|
||||||
callbackRef.current = callback
|
callbackRef.current = callback
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
@@ -43,7 +45,9 @@ export function useSequenceShortcut(
|
|||||||
enabled = true,
|
enabled = true,
|
||||||
) {
|
) {
|
||||||
const callbackRef = useRef(callback)
|
const callbackRef = useRef(callback)
|
||||||
|
useEffect(() => {
|
||||||
callbackRef.current = callback
|
callbackRef.current = callback
|
||||||
|
})
|
||||||
const pendingRef = useRef<string | null>(null)
|
const pendingRef = useRef<string | null>(null)
|
||||||
const timeoutRef = useRef<number | null>(null)
|
const timeoutRef = useRef<number | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ export const certificatesApi = {
|
|||||||
params: tenantParams(tenantId),
|
params: tenantParams(tenantId),
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
if (err?.response?.status === 404) return null
|
const e = err as { response?: { status?: number } }
|
||||||
|
if (e?.response?.status === 404) return null
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const N_HEX =
|
|||||||
const N = BigInt('0x' + N_HEX);
|
const N = BigInt('0x' + N_HEX);
|
||||||
const g = 2n;
|
const g = 2n;
|
||||||
const N_BYTES = 256; // 2048 bits = 256 bytes
|
const N_BYTES = 256; // 2048 bits = 256 bytes
|
||||||
const N_HEX_LEN = N_BYTES * 2; // 512 hex chars
|
|
||||||
|
|
||||||
// ---- Utility Functions ----
|
// ---- Utility Functions ----
|
||||||
|
|
||||||
@@ -39,16 +38,6 @@ function toHex(n: bigint): string {
|
|||||||
return hex;
|
return hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pad a hex string to N's byte length (512 hex chars) with leading zeros. */
|
|
||||||
function padHex(hex: string): string {
|
|
||||||
return hex.padStart(N_HEX_LEN, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert BigInt to padded hex bytes (for hash inputs involving N-sized values). */
|
|
||||||
function bigintToPaddedHex(n: bigint): string {
|
|
||||||
return padHex(toHex(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert hex string to Uint8Array. */
|
/** Convert hex string to Uint8Array. */
|
||||||
function hexToBytes(hex: string): Uint8Array {
|
function hexToBytes(hex: string): Uint8Array {
|
||||||
const padded = hex.length % 2 === 1 ? '0' + hex : hex;
|
const padded = hex.length % 2 === 1 ? '0' + hex : hex;
|
||||||
@@ -93,12 +82,6 @@ function bigintToBytes(n: bigint): Uint8Array {
|
|||||||
return hexToBytes(toHex(n));
|
return hexToBytes(toHex(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hash BigInt values (unpadded, matching srptools int_to_bytes) and return bytes. */
|
|
||||||
async function hashBigInt(...values: bigint[]): Promise<Uint8Array> {
|
|
||||||
const inputs = values.map((v) => bigintToBytes(v));
|
|
||||||
return H(...inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pad a BigInt value to N's byte length (256 bytes) matching srptools context.pad(). */
|
/** Pad a BigInt value to N's byte length (256 bytes) matching srptools context.pad(). */
|
||||||
function padBigInt(n: bigint): Uint8Array {
|
function padBigInt(n: bigint): Uint8Array {
|
||||||
const bytes = bigintToBytes(n);
|
const bytes = bigintToBytes(n);
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ function placeFormatInfo(matrix: boolean[][], size: number) {
|
|||||||
// After BCH: 0x77c0... Let's use the precomputed value
|
// After BCH: 0x77c0... Let's use the precomputed value
|
||||||
// EC L = 01, mask 0 = 000 -> data = 01000
|
// EC L = 01, mask 0 = 000 -> data = 01000
|
||||||
// Format string after BCH and XOR with 101010000010010:
|
// Format string after BCH and XOR with 101010000010010:
|
||||||
const formatBits = 0x77c0 // L, mask 0
|
// const formatBits = 0x77c0 // L, mask 0 (unused, computed via getFormatInfo below)
|
||||||
// Actually, let's compute it properly
|
// Actually, let's compute it properly
|
||||||
// data = 01 000 = 0b01000 = 8
|
// data = 01 000 = 0b01000 = 8
|
||||||
// Generator: 10100110111 (0x537)
|
// Generator: 10100110111 (0x537)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function DevicesPage() {
|
|||||||
// Open dialog when ?add=true is set (e.g. from empty state button)
|
// Open dialog when ?add=true is set (e.g. from empty state button)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (search.add === 'true') {
|
if (search.add === 'true') {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setAddOpen(true)
|
setAddOpen(true)
|
||||||
// Clear the search param so it doesn't re-open on navigation
|
// Clear the search param so it doesn't re-open on navigation
|
||||||
void navigate({
|
void navigate({
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export function renderWithProviders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export * from '@testing-library/react'
|
export * from '@testing-library/react'
|
||||||
export { renderWithProviders as render }
|
export { renderWithProviders as render }
|
||||||
|
|||||||
Reference in New Issue
Block a user