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,
|
||||
} from '@/components/ui/dialog'
|
||||
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 { authApi } from '@/lib/api'
|
||||
import { EmergencyKitDialog } from './EmergencyKitDialog'
|
||||
|
||||
@@ -56,7 +56,7 @@ export function BulkDeployDialog({
|
||||
queryKey: ['devices-for-cert', tenantId],
|
||||
queryFn: async () => {
|
||||
const result = await devicesApi.list(tenantId)
|
||||
return (result as any).items ?? result
|
||||
return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
|
||||
},
|
||||
enabled: !!tenantId && open,
|
||||
})
|
||||
@@ -128,14 +128,15 @@ export function BulkDeployDialog({
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { detail?: string } } }
|
||||
setResult({
|
||||
success: 0,
|
||||
failed: selected.size,
|
||||
errors: [
|
||||
{
|
||||
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'] })
|
||||
toast({ title: 'Certificate Authority initialized' })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
onError: (e: unknown) => {
|
||||
const err = e as { response?: { data?: { detail?: string } } }
|
||||
toast({
|
||||
title: e?.response?.data?.detail || 'Failed to initialize CA',
|
||||
title: err?.response?.data?.detail || 'Failed to initialize CA',
|
||||
variant: 'destructive',
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleDownloadPEM = async () => {
|
||||
|
||||
@@ -43,6 +43,7 @@ export function CertConfirmDialog({
|
||||
// Reset confirm text when dialog opens/closes or action changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setConfirmText('')
|
||||
}
|
||||
}, [open, action])
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useUIStore } from '@/lib/store'
|
||||
import { Shield, Building2 } from 'lucide-react'
|
||||
import {
|
||||
certificatesApi,
|
||||
type CAResponse,
|
||||
type DeviceCertResponse,
|
||||
} from '@/lib/certificatesApi'
|
||||
import { certificatesApi } from '@/lib/certificatesApi'
|
||||
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
||||
import { canWrite } from '@/lib/auth'
|
||||
import { CAStatusCard } from './CAStatusCard'
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DeployCertDialog({
|
||||
queryFn: async () => {
|
||||
const result = await devicesApi.list(tenantId)
|
||||
// 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,
|
||||
})
|
||||
@@ -110,9 +110,10 @@ export function DeployCertDialog({
|
||||
setErrorMsg(result.error ?? 'Deployment failed')
|
||||
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
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)
|
||||
toast({ title: detail, variant: 'destructive' })
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
ShieldCheck,
|
||||
ShieldAlert,
|
||||
Plus,
|
||||
Layers,
|
||||
MoreHorizontal,
|
||||
@@ -132,11 +131,13 @@ export function DeviceCertTable({
|
||||
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
|
||||
}
|
||||
},
|
||||
onError: (e: any) =>
|
||||
onError: (e: unknown) => {
|
||||
const err = e as { response?: { data?: { detail?: string } } }
|
||||
toast({
|
||||
title: e?.response?.data?.detail || 'Failed to deploy certificate',
|
||||
title: err?.response?.data?.detail || 'Failed to deploy certificate',
|
||||
variant: 'destructive',
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const rotateMutation = useMutation({
|
||||
@@ -149,11 +150,13 @@ export function DeviceCertTable({
|
||||
toast({ title: result.error ?? 'Rotation failed', variant: 'destructive' })
|
||||
}
|
||||
},
|
||||
onError: (e: any) =>
|
||||
onError: (e: unknown) => {
|
||||
const err = e as { response?: { data?: { detail?: string } } }
|
||||
toast({
|
||||
title: e?.response?.data?.detail || 'Failed to rotate certificate',
|
||||
title: err?.response?.data?.detail || 'Failed to rotate certificate',
|
||||
variant: 'destructive',
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const revokeMutation = useMutation({
|
||||
@@ -162,11 +165,13 @@ export function DeviceCertTable({
|
||||
void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] })
|
||||
toast({ title: 'Certificate revoked' })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
onError: (e: unknown) => {
|
||||
const err = e as { response?: { data?: { detail?: string } } }
|
||||
toast({
|
||||
title: e?.response?.data?.detail || 'Failed to revoke certificate',
|
||||
title: err?.response?.data?.detail || 'Failed to revoke certificate',
|
||||
variant: 'destructive',
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// ── Filtering ──
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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 { 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 { useAuth, isSuperAdmin } from '@/lib/auth'
|
||||
import { useUIStore } from '@/lib/store'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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 { cn } from '@/lib/utils'
|
||||
import { canWrite } from '@/lib/auth'
|
||||
@@ -19,8 +19,6 @@ interface EntryTableProps {
|
||||
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({
|
||||
entries,
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { SafetyToggle } from './SafetyToggle'
|
||||
import { ChangePreviewModal } from './ChangePreviewModal'
|
||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -48,8 +47,6 @@ interface AddressListForm {
|
||||
|
||||
const EMPTY_FORM: AddressListForm = { list: '', address: '', comment: '' }
|
||||
|
||||
type PanelHook = ReturnType<typeof useConfigPanel>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AddressListPanel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { configEditorApi } from '@/lib/configEditorApi'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||
|
||||
interface BwResult {
|
||||
|
||||
@@ -48,12 +48,6 @@ export function BridgeVlanPanel({ tenantId, deviceId, active }: ConfigPanelProps
|
||||
[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(() => {
|
||||
setEditEntry(null)
|
||||
setFormData({
|
||||
|
||||
@@ -22,15 +22,12 @@ import {
|
||||
import { SafetyToggle } from './SafetyToggle'
|
||||
import { ChangePreviewModal } from './ChangePreviewModal'
|
||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type PanelHook = ReturnType<typeof useConfigPanel>
|
||||
|
||||
// Timeout fields we expose for editing
|
||||
const TIMEOUT_FIELDS = [
|
||||
{ key: 'tcp-established-timeout', label: 'TCP Established' },
|
||||
|
||||
@@ -48,8 +48,8 @@ export function DiffViewer({ tenantId, deviceId, snapshotId, onClose }: DiffView
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${60 + Math.random() * 40}%` }} />
|
||||
{[75, 90, 65, 85, 70, 80].map((w, i) => (
|
||||
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${w}%` }} />
|
||||
))}
|
||||
</div>
|
||||
) : isError || !diff ? (
|
||||
|
||||
@@ -6,15 +6,13 @@
|
||||
* 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 {
|
||||
Plus,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Trash2,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Shield,
|
||||
@@ -472,11 +470,6 @@ export function FirewallPanel({ tenantId, deviceId, active }: ConfigPanelProps)
|
||||
setPreviewOpen(false)
|
||||
}, [panel])
|
||||
|
||||
const afterApply = useMemo(() => {
|
||||
// When applyChanges succeeds, the hook auto-refetches via queryClient.invalidateQueries
|
||||
return panel.pendingChanges.length
|
||||
}, [panel.pendingChanges.length])
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Render
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -197,7 +197,8 @@ function PoolTable({
|
||||
entries,
|
||||
panel,
|
||||
poolUsedBy,
|
||||
existingPools,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
existingPools: _existingPools,
|
||||
}: {
|
||||
entries: PoolEntry[]
|
||||
panel: PanelHook
|
||||
|
||||
@@ -44,7 +44,7 @@ import { cn } from '@/lib/utils'
|
||||
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
|
||||
import { SafetyToggle } from '@/components/config/SafetyToggle'
|
||||
import { ChangePreviewModal } from '@/components/config/ChangePreviewModal'
|
||||
import type { ConfigPanelProps, ConfigChange } from '@/lib/configPanelTypes'
|
||||
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
*/
|
||||
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -29,15 +29,6 @@ interface TorchEntry {
|
||||
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 {
|
||||
const n = parseInt(val, 10)
|
||||
if (isNaN(n)) return val || '-'
|
||||
|
||||
@@ -618,8 +618,6 @@ function WirelessEditDialog({
|
||||
}) {
|
||||
const [showPassphrase, setShowPassphrase] = useState(false)
|
||||
|
||||
const ssid = entry?.ssid || entry?.['configuration.ssid'] || ''
|
||||
|
||||
const [formData, setFormData] = useState<WirelessFormData>({
|
||||
ssid: '',
|
||||
band: '',
|
||||
@@ -631,7 +629,8 @@ function WirelessEditDialog({
|
||||
})
|
||||
|
||||
// Reset form when entry changes
|
||||
const entryId = entry?.['.id'] || ''
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _entryId = entry?.['.id'] || ''
|
||||
useState(() => {
|
||||
if (entry) {
|
||||
setFormData({
|
||||
|
||||
@@ -51,7 +51,8 @@ export function AlertSummary({
|
||||
criticalCount,
|
||||
warningCount,
|
||||
infoCount,
|
||||
tenantId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
tenantId: _tenantId,
|
||||
}: AlertSummaryProps) {
|
||||
const total = criticalCount + warningCount + infoCount
|
||||
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%
|
||||
* - 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(
|
||||
devices: HealthScoreProps['devices'],
|
||||
criticalAlerts: number,
|
||||
@@ -115,7 +116,6 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS
|
||||
|
||||
export function HealthScore({
|
||||
devices,
|
||||
activeAlerts: _activeAlerts,
|
||||
criticalAlerts,
|
||||
}: HealthScoreProps) {
|
||||
const totalDevices = devices.length
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface KpiCardsProps {
|
||||
* Formats bytes-per-second into a human-readable bandwidth string.
|
||||
* 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 } {
|
||||
if (bps < 1_000) return { value: bps, unit: 'bps' }
|
||||
if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' }
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
firmwareApi,
|
||||
type FirmwareOverview,
|
||||
type DeviceFirmwareStatus,
|
||||
type FirmwareVersionGroup,
|
||||
} from '@/lib/firmwareApi'
|
||||
@@ -24,7 +23,6 @@ import { useUIStore } from '@/lib/store'
|
||||
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -17,10 +17,7 @@ import {
|
||||
XCircle,
|
||||
PauseCircle,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
firmwareApi,
|
||||
type FirmwareUpgradeJob,
|
||||
} from '@/lib/firmwareApi'
|
||||
import { firmwareApi } from '@/lib/firmwareApi'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* 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 { useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
type SubnetScanResponse,
|
||||
type SubnetScanResult,
|
||||
type DeviceResponse,
|
||||
type DeviceGroupResponse,
|
||||
type DeviceTagResponse,
|
||||
} from '@/lib/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -32,7 +32,9 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
||||
(s) => s.status === 'active' || s.status === 'creating',
|
||||
)
|
||||
if (active) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSession(active)
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setState(active.status === 'active' ? 'active' : 'connecting')
|
||||
}
|
||||
}
|
||||
@@ -66,6 +68,7 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
||||
// Countdown timer for session expiry
|
||||
useEffect(() => {
|
||||
if (state !== 'active' || !session?.expires_at) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setCountdown(null)
|
||||
return
|
||||
}
|
||||
@@ -96,9 +99,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
|
||||
setState('connecting')
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
onError: (err: unknown) => {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
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)
|
||||
queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] })
|
||||
},
|
||||
onError: (err: any) => {
|
||||
onError: (err: unknown) => {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
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])
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setSession(null)
|
||||
setError(null)
|
||||
handleOpen()
|
||||
}, [handleOpen])
|
||||
|
||||
const handleReset = useCallback(async () => {
|
||||
try {
|
||||
const sessions = await remoteWinboxApi.list(tenantId, deviceId)
|
||||
|
||||
@@ -32,9 +32,10 @@ export function WinBoxButton({ tenantId, deviceId }: WinBoxButtonProps) {
|
||||
window.open(data.winbox_uri, '_blank')
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
onError: (err: unknown) => {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
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 ?? []
|
||||
|
||||
// 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() {
|
||||
setName('')
|
||||
setStartAt('')
|
||||
@@ -94,6 +77,25 @@ export function MaintenanceForm({
|
||||
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({
|
||||
mutationFn: (data: MaintenanceWindowCreate) =>
|
||||
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.
|
||||
*/
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function getTimeRange(
|
||||
range: string,
|
||||
customStart?: string,
|
||||
@@ -64,6 +65,7 @@ export function getTimeRange(
|
||||
* Returns refetchInterval (ms) for short ranges, false for longer ones.
|
||||
* 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 {
|
||||
if (range === '1h' || range === '6h') return 60_000
|
||||
return false
|
||||
|
||||
@@ -162,7 +162,7 @@ export function ClientsTab({ tenantId, deviceId, active }: ClientsTabProps) {
|
||||
}
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [data?.clients, searchQuery, sortField, sortDir])
|
||||
}, [data, searchQuery, sortField, sortDir])
|
||||
|
||||
// Stats
|
||||
const totalClients = data?.clients.length ?? 0
|
||||
|
||||
@@ -53,7 +53,7 @@ interface GaugeBarProps {
|
||||
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 colorClass = getBarColor(pct)
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ interface TooltipData {
|
||||
y: number
|
||||
}
|
||||
|
||||
function NodeTooltip({ data, onClose }: { data: TooltipData; onClose: () => void }) {
|
||||
function NodeTooltip({ data }: { data: TooltipData; onClose?: () => void }) {
|
||||
return (
|
||||
<div
|
||||
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,
|
||||
deviceGroupsApi,
|
||||
type DeviceResponse,
|
||||
type DeviceGroupResponse,
|
||||
} from '@/lib/api'
|
||||
import { configEditorApi } from '@/lib/configEditorApi'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuth, isSuperAdmin, isTenantAdmin } from '@/lib/auth'
|
||||
import { authApi } from '@/lib/api'
|
||||
import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi'
|
||||
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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -365,8 +365,9 @@ function SMTPSettingsSection() {
|
||||
if (result.success) {
|
||||
saveMutation.mutate()
|
||||
}
|
||||
} catch (e: any) {
|
||||
setTestResult({ success: false, message: e.response?.data?.message || e.message })
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { message?: string } }; message?: string }
|
||||
setTestResult({ success: false, message: err.response?.data?.message || err.message || 'Unknown error' })
|
||||
} finally {
|
||||
setTesting(false)
|
||||
}
|
||||
|
||||
@@ -353,6 +353,7 @@ function VerifyConnectivityStep({ tenantId, deviceId, onComplete }: Step3Props)
|
||||
}, [tenantId, deviceId, cleanup])
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
startPolling()
|
||||
return cleanup
|
||||
}, [startPolling, cleanup])
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -42,19 +42,26 @@ export function LanDhcpPanel({ tenantId, deviceId, active }: ConfigPanelProps) {
|
||||
const [leaseTime, setLeaseTime] = useState('')
|
||||
|
||||
// Sync from browse data
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (lanEntry) setLanAddress(lanEntry.address ?? '')
|
||||
}, [lanEntry])
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (poolEntry) setPoolRange(poolEntry.ranges ?? '')
|
||||
}, [poolEntry])
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => {
|
||||
if (networkEntry) {
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
setDhcpGateway(networkEntry.gateway ?? '')
|
||||
setDhcpDns(networkEntry['dns-server'] ?? '')
|
||||
setLeaseTime(networkEntry['lease-time'] ?? '')
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
}
|
||||
}, [networkEntry])
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
|
||||
const [formState, setFormState] = useState<Record<string, Record<string, string>>>({})
|
||||
|
||||
// Sync form state from browse data
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => {
|
||||
const newState: Record<string, Record<string, string>> = {}
|
||||
wireless.entries.forEach((entry) => {
|
||||
@@ -60,6 +61,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
|
||||
}
|
||||
})
|
||||
if (Object.keys(newState).length > 0) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setFormState((prev) => ({ ...prev, ...newState }))
|
||||
}
|
||||
}, [wireless.entries, isV7])
|
||||
|
||||
@@ -31,7 +31,7 @@ interface TemplateEditorProps {
|
||||
|
||||
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 [description, setDescription] = useState(template?.description ?? '')
|
||||
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)) ?? []
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleGroupSelect = (_groupId: string) => {
|
||||
// For now, just select all online devices. In a real implementation,
|
||||
// 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'
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export function Skeleton({ className, ...props }: SkeletonProps) {
|
||||
return (
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ToastOptions {
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function toast(options: ToastOptions) {
|
||||
if (options.variant === 'destructive') {
|
||||
sonnerToast.error(options.title, {
|
||||
@@ -45,4 +46,5 @@ export const Toast = () => null
|
||||
export const ToastTitle = () => null
|
||||
export const ToastDescription = () => null
|
||||
export const ToastClose = () => null
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useToasts = () => ({ toasts: [] as never[], dismiss: () => {} })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -61,6 +61,7 @@ export function VpnOnboardingWizard({ tenantId, onSuccess, onCancel }: Props) {
|
||||
// Timer for waiting step
|
||||
useEffect(() => {
|
||||
if (step !== 'waiting') return
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setElapsed(0)
|
||||
const interval = setInterval(() => setElapsed((e) => e + 1), 1000)
|
||||
return () => clearInterval(interval)
|
||||
|
||||
@@ -26,15 +26,10 @@ import {
|
||||
import {
|
||||
vpnApi,
|
||||
devicesApi,
|
||||
type VpnConfigResponse,
|
||||
type VpnPeerResponse,
|
||||
type VpnPeerConfig,
|
||||
type DeviceResponse,
|
||||
} from '@/lib/api'
|
||||
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -63,7 +58,6 @@ export function VpnPage() {
|
||||
|
||||
const [showAddDevice, setShowAddDevice] = useState(false)
|
||||
const [showConfig, setShowConfig] = useState<string | null>(null)
|
||||
const [endpoint, setEndpoint] = useState('')
|
||||
const [selectedDevice, setSelectedDevice] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -83,7 +77,11 @@ export function VpnPage() {
|
||||
|
||||
const { data: devices = [] } = useQuery({
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -96,12 +94,15 @@ export function VpnPage() {
|
||||
// ── Mutations ──
|
||||
|
||||
const setupMutation = useMutation({
|
||||
mutationFn: () => vpnApi.setup(tenantId, endpoint || undefined),
|
||||
mutationFn: () => vpnApi.setup(tenantId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
||||
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({
|
||||
@@ -113,7 +114,10 @@ export function VpnPage() {
|
||||
setSelectedDevice('')
|
||||
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({
|
||||
@@ -123,7 +127,10 @@ export function VpnPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
|
||||
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({
|
||||
@@ -141,7 +148,10 @@ export function VpnPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
|
||||
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 ──
|
||||
|
||||
@@ -27,6 +27,7 @@ export function EventStreamProvider({
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useEventStreamContext() {
|
||||
return useContext(EventStreamContext)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { useEffect, useRef } from 'react'
|
||||
*/
|
||||
export function useShortcut(key: string, callback: () => void, enabled = true) {
|
||||
const callbackRef = useRef(callback)
|
||||
callbackRef.current = callback
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
@@ -43,7 +45,9 @@ export function useSequenceShortcut(
|
||||
enabled = true,
|
||||
) {
|
||||
const callbackRef = useRef(callback)
|
||||
callbackRef.current = callback
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback
|
||||
})
|
||||
const pendingRef = useRef<string | null>(null)
|
||||
const timeoutRef = useRef<number | null>(null)
|
||||
|
||||
|
||||
@@ -78,8 +78,9 @@ export const certificatesApi = {
|
||||
params: tenantParams(tenantId),
|
||||
})
|
||||
return data
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 404) return null
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { status?: number } }
|
||||
if (e?.response?.status === 404) return null
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,7 +29,6 @@ const N_HEX =
|
||||
const N = BigInt('0x' + N_HEX);
|
||||
const g = 2n;
|
||||
const N_BYTES = 256; // 2048 bits = 256 bytes
|
||||
const N_HEX_LEN = N_BYTES * 2; // 512 hex chars
|
||||
|
||||
// ---- Utility Functions ----
|
||||
|
||||
@@ -39,16 +38,6 @@ function toHex(n: bigint): string {
|
||||
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. */
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const padded = hex.length % 2 === 1 ? '0' + hex : hex;
|
||||
@@ -93,12 +82,6 @@ function bigintToBytes(n: bigint): Uint8Array {
|
||||
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(). */
|
||||
function padBigInt(n: bigint): Uint8Array {
|
||||
const bytes = bigintToBytes(n);
|
||||
|
||||
@@ -336,7 +336,7 @@ function placeFormatInfo(matrix: boolean[][], size: number) {
|
||||
// After BCH: 0x77c0... Let's use the precomputed value
|
||||
// EC L = 01, mask 0 = 000 -> data = 01000
|
||||
// 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
|
||||
// data = 01 000 = 0b01000 = 8
|
||||
// Generator: 10100110111 (0x537)
|
||||
|
||||
@@ -35,6 +35,7 @@ function DevicesPage() {
|
||||
// Open dialog when ?add=true is set (e.g. from empty state button)
|
||||
useEffect(() => {
|
||||
if (search.add === 'true') {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setAddOpen(true)
|
||||
// Clear the search param so it doesn't re-open on navigation
|
||||
void navigate({
|
||||
|
||||
@@ -35,5 +35,6 @@ export function renderWithProviders(
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from '@testing-library/react'
|
||||
export { renderWithProviders as render }
|
||||
|
||||
Reference in New Issue
Block a user