chore(frontend): fix lint errors and update biome schema
Fix exhaustive deps, non-null assertion, a11y roles, and error boundary type. Migrate biome config to 2.4.4.
This commit is contained in:
parent
cda830a4e5
commit
65e01d055c
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@
|
|||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './useThreats'
|
||||
export * from './useStats'
|
||||
export * from './useModels'
|
||||
export * from './useAlerts'
|
||||
export * from './useModels'
|
||||
export * from './useStats'
|
||||
export * from './useThreats'
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import { type WebSocketAlert, WebSocketAlertSchema } from '@/api/types'
|
||||
import { ALERTS, WS_ENDPOINTS } from '@/config'
|
||||
import { WebSocketAlertSchema, type WebSocketAlert } from '@/api/types'
|
||||
|
||||
interface AlertState {
|
||||
alerts: WebSocketAlert[]
|
||||
|
|
@ -31,11 +31,9 @@ const useAlertStore = create<AlertState>()((set) => ({
|
|||
setConnected: (connected) =>
|
||||
set({ isConnected: connected, connectionError: null }),
|
||||
|
||||
setError: (error) =>
|
||||
set({ isConnected: false, connectionError: error }),
|
||||
setError: (error) => set({ isConnected: false, connectionError: error }),
|
||||
|
||||
clear: () =>
|
||||
set({ alerts: [], isConnected: false, connectionError: null }),
|
||||
clear: () => set({ alerts: [], isConnected: false, connectionError: null }),
|
||||
}))
|
||||
|
||||
function getWsUrl(): string {
|
||||
|
|
@ -82,7 +80,7 @@ export function useAlerts() {
|
|||
function scheduleReconnect() {
|
||||
const delay = Math.min(
|
||||
ALERTS.RECONNECT_BASE_MS * 2 ** retryCountRef.current,
|
||||
ALERTS.RECONNECT_MAX_MS,
|
||||
ALERTS.RECONNECT_MAX_MS
|
||||
)
|
||||
retryCountRef.current += 1
|
||||
retryTimerRef.current = setTimeout(connect, delay)
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { QUERY_KEYS, API_ENDPOINTS } from '@/config'
|
||||
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
|
||||
import type { ModelStatus, RetrainResponse } from '@/api/types'
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from '@/config'
|
||||
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
|
||||
|
||||
export function useModelStatus() {
|
||||
return useQuery<ModelStatus>({
|
||||
queryKey: QUERY_KEYS.MODELS.STATUS(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ModelStatus>(
|
||||
API_ENDPOINTS.MODELS.STATUS,
|
||||
API_ENDPOINTS.MODELS.STATUS
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
|
@ -28,7 +28,7 @@ export function useRetrain() {
|
|||
return useMutation<RetrainResponse>({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post<RetrainResponse>(
|
||||
API_ENDPOINTS.MODELS.RETRAIN,
|
||||
API_ENDPOINTS.MODELS.RETRAIN
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@
|
|||
// ===================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QUERY_KEYS, API_ENDPOINTS } from '@/config'
|
||||
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
|
||||
import type { StatsResponse } from '@/api/types'
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from '@/config'
|
||||
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
|
||||
|
||||
export function useStats(range = '24h') {
|
||||
return useQuery<StatsResponse>({
|
||||
queryKey: QUERY_KEYS.STATS.BY_RANGE(range),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<StatsResponse>(
|
||||
API_ENDPOINTS.STATS,
|
||||
{ params: { range } },
|
||||
)
|
||||
const { data } = await apiClient.get<StatsResponse>(API_ENDPOINTS.STATS, {
|
||||
params: { range },
|
||||
})
|
||||
return data
|
||||
},
|
||||
...QUERY_STRATEGIES.frequent,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
// ===================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QUERY_KEYS, API_ENDPOINTS, PAGINATION } from '@/config'
|
||||
import type { ThreatEvent, ThreatList } from '@/api/types'
|
||||
import { API_ENDPOINTS, PAGINATION, QUERY_KEYS } from '@/config'
|
||||
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
|
||||
import type { ThreatList, ThreatEvent } from '@/api/types'
|
||||
|
||||
interface ThreatParams {
|
||||
limit?: number
|
||||
|
|
@ -29,7 +29,7 @@ export function useThreats(params: ThreatParams = {}) {
|
|||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ThreatList>(
|
||||
API_ENDPOINTS.THREATS.LIST,
|
||||
{ params: queryParams },
|
||||
{ params: queryParams }
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
|
@ -42,7 +42,7 @@ export function useThreat(id: string | null) {
|
|||
queryKey: QUERY_KEYS.THREATS.BY_ID(id ?? ''),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ThreatEvent>(
|
||||
API_ENDPOINTS.THREATS.BY_ID(id!),
|
||||
API_ENDPOINTS.THREATS.BY_ID(id as string)
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './threats.types'
|
||||
export * from './stats.types'
|
||||
export * from './models.types'
|
||||
export * from './stats.types'
|
||||
export * from './threats.types'
|
||||
export * from './websocket.types'
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { WebSocketAlert } from '@/api/types'
|
||||
import { SeverityBadge } from './severity-badge'
|
||||
import styles from './alert-feed.module.scss'
|
||||
import { SeverityBadge } from './severity-badge'
|
||||
|
||||
interface AlertFeedProps {
|
||||
alerts: WebSocketAlert[]
|
||||
|
|
@ -25,11 +25,13 @@ export function AlertFeed({
|
|||
}: AlertFeedProps): React.ReactElement {
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const alertCount = alerts.length
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll on new alerts
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = 0
|
||||
}
|
||||
}, [alerts.length])
|
||||
}, [alertCount])
|
||||
|
||||
return (
|
||||
<div className={styles.feed}>
|
||||
|
|
@ -50,9 +52,7 @@ export function AlertFeed({
|
|||
) : (
|
||||
alerts.map((alert, i) => (
|
||||
<div key={`${alert.timestamp}-${i}`} className={styles.row}>
|
||||
<span className={styles.time}>
|
||||
{formatTime(alert.timestamp)}
|
||||
</span>
|
||||
<span className={styles.time}>{formatTime(alert.timestamp)}</span>
|
||||
<span className={styles.ip}>{alert.source_ip}</span>
|
||||
<span className={styles.path}>{alert.request_path}</span>
|
||||
<SeverityBadge
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// index.tsx
|
||||
// ===================
|
||||
|
||||
export { AlertFeed } from './alert-feed'
|
||||
export { SeverityBadge } from './severity-badge'
|
||||
export { StatCard } from './stat-card'
|
||||
export { AlertFeed } from './alert-feed'
|
||||
export { ThreatDetail } from './threat-detail'
|
||||
|
|
|
|||
|
|
@ -24,8 +24,15 @@ export function ThreatDetail({
|
|||
if (!threat) return null
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onClick={onClose} onKeyDown={() => {}}>
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: modal overlay dismiss
|
||||
<div
|
||||
role="presentation"
|
||||
className={styles.overlay}
|
||||
onClick={onClose}
|
||||
onKeyDown={() => {}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
className={styles.panel}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={() => {}}
|
||||
|
|
@ -80,9 +87,7 @@ export function ThreatDetail({
|
|||
</div>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>Method</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{threat.request_method}
|
||||
</span>
|
||||
<span className={styles.fieldValue}>{threat.request_method}</span>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>Path</span>
|
||||
|
|
@ -90,9 +95,7 @@ export function ThreatDetail({
|
|||
</div>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>Status</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{threat.status_code}
|
||||
</span>
|
||||
<span className={styles.fieldValue}>{threat.status_code}</span>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>Response Size</span>
|
||||
|
|
@ -131,16 +134,12 @@ export function ThreatDetail({
|
|||
<div className={styles.grid}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>Country</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{threat.geo.country}
|
||||
</span>
|
||||
<span className={styles.fieldValue}>{threat.geo.country}</span>
|
||||
</div>
|
||||
{threat.geo.city && (
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldLabel}>City</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{threat.geo.city}
|
||||
</span>
|
||||
<span className={styles.fieldValue}>{threat.geo.city}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,11 @@ export const QUERY_KEYS = {
|
|||
ALL: ['threats'] as const,
|
||||
LIST: (params: Record<string, unknown>) =>
|
||||
[...QUERY_KEYS.THREATS.ALL, 'list', params] as const,
|
||||
BY_ID: (id: string) =>
|
||||
[...QUERY_KEYS.THREATS.ALL, 'detail', id] as const,
|
||||
BY_ID: (id: string) => [...QUERY_KEYS.THREATS.ALL, 'detail', id] as const,
|
||||
},
|
||||
STATS: {
|
||||
ALL: ['stats'] as const,
|
||||
BY_RANGE: (range: string) =>
|
||||
[...QUERY_KEYS.STATS.ALL, range] as const,
|
||||
BY_RANGE: (range: string) => [...QUERY_KEYS.STATS.ALL, range] as const,
|
||||
},
|
||||
MODELS: {
|
||||
ALL: ['models'] as const,
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ apiClient.interceptors.response.use(
|
|||
(response) => response,
|
||||
(error: AxiosError): Promise<never> => {
|
||||
return Promise.reject(transformAxiosError(error))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class ApiError extends Error {
|
|||
message: string,
|
||||
code: ApiErrorCode,
|
||||
statusCode: number,
|
||||
details?: Record<string, string[]>,
|
||||
details?: Record<string, string[]>
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const calculateRetryDelay = (attemptIndex: number): number => {
|
|||
|
||||
const handleQueryCacheError = (
|
||||
error: Error,
|
||||
query: { state: { data: unknown } },
|
||||
query: { state: { data: unknown } }
|
||||
): void => {
|
||||
if (query.state.data !== undefined) {
|
||||
const message =
|
||||
|
|
@ -45,7 +45,7 @@ const handleMutationCacheError = (
|
|||
error: Error,
|
||||
_variables: unknown,
|
||||
_context: unknown,
|
||||
mutation: { options: { onError?: unknown } },
|
||||
mutation: { options: { onError?: unknown } }
|
||||
): void => {
|
||||
if (mutation.options.onError === undefined) {
|
||||
const message =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// routers.tsx
|
||||
// ===================
|
||||
|
||||
import { Navigate, createBrowserRouter, type RouteObject } from 'react-router-dom'
|
||||
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router-dom'
|
||||
import { ROUTES } from '@/config'
|
||||
import { Shell } from './shell'
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@
|
|||
|
||||
import { Suspense } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { LuChevronLeft, LuChevronRight, LuCpu, LuLayoutDashboard, LuMenu, LuShield } from 'react-icons/lu'
|
||||
import {
|
||||
LuChevronLeft,
|
||||
LuChevronRight,
|
||||
LuCpu,
|
||||
LuLayoutDashboard,
|
||||
LuMenu,
|
||||
LuShield,
|
||||
} from 'react-icons/lu'
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom'
|
||||
import { ROUTES } from '@/config'
|
||||
import { useUIStore } from '@/core/lib'
|
||||
|
|
@ -17,11 +24,12 @@ const NAV_ITEMS = [
|
|||
{ path: ROUTES.MODELS, label: 'Models', icon: LuCpu },
|
||||
]
|
||||
|
||||
function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
|
||||
function ShellErrorFallback({ error }: { error: unknown }): React.ReactElement {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<h2>Something went wrong</h2>
|
||||
<pre>{error.message}</pre>
|
||||
<pre>{message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
// index.tsx
|
||||
// ===================
|
||||
|
||||
import { useStats, useModelStatus, useAlerts } from '@/api/hooks'
|
||||
import { StatCard, AlertFeed } from '@/components'
|
||||
import { useAlerts, useModelStatus, useStats } from '@/api/hooks'
|
||||
import { AlertFeed, StatCard } from '@/components'
|
||||
import styles from './dashboard.module.scss'
|
||||
|
||||
function SeverityBar({
|
||||
|
|
@ -106,14 +106,8 @@ export function Component(): React.ReactElement {
|
|||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.statRow}>
|
||||
<StatCard
|
||||
label="Threats Detected"
|
||||
value={stats.threats_detected}
|
||||
/>
|
||||
<StatCard
|
||||
label="Threats Stored"
|
||||
value={stats.threats_stored}
|
||||
/>
|
||||
<StatCard label="Threats Detected" value={stats.threats_detected} />
|
||||
<StatCard label="Threats Stored" value={stats.threats_stored} />
|
||||
<StatCard
|
||||
label="High Severity"
|
||||
value={sb.high}
|
||||
|
|
@ -122,9 +116,7 @@ export function Component(): React.ReactElement {
|
|||
<StatCard
|
||||
label="Detection Mode"
|
||||
value={modelStatus?.detection_mode ?? '...'}
|
||||
sublabel={
|
||||
modelStatus?.models_loaded ? 'Models loaded' : 'Rules only'
|
||||
}
|
||||
sublabel={modelStatus?.models_loaded ? 'Models loaded' : 'Rules only'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -134,11 +126,7 @@ export function Component(): React.ReactElement {
|
|||
</div>
|
||||
|
||||
<div className={styles.bottomRow}>
|
||||
<AlertFeed
|
||||
alerts={alerts}
|
||||
isConnected={isConnected}
|
||||
maxHeight="360px"
|
||||
/>
|
||||
<AlertFeed alerts={alerts} isConnected={isConnected} maxHeight="360px" />
|
||||
|
||||
<div className={styles.lists}>
|
||||
<RankedList
|
||||
|
|
@ -158,9 +146,7 @@ export function Component(): React.ReactElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{connectionError && (
|
||||
<div className={styles.wsError}>{connectionError}</div>
|
||||
)}
|
||||
{connectionError && <div className={styles.wsError}>{connectionError}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ function ModelCard({ model }: { model: ActiveModel }): React.ReactElement {
|
|||
{model.threshold !== null && (
|
||||
<div className={styles.stat}>
|
||||
<span className={styles.statLabel}>Threshold</span>
|
||||
<span className={styles.statValue}>
|
||||
{model.threshold.toFixed(4)}
|
||||
</span>
|
||||
<span className={styles.statValue}>{model.threshold.toFixed(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -77,9 +75,7 @@ export function Component(): React.ReactElement {
|
|||
disabled={retrain.isPending}
|
||||
onClick={() => retrain.mutate()}
|
||||
>
|
||||
<LuRefreshCw
|
||||
className={retrain.isPending ? styles.spinning : ''}
|
||||
/>
|
||||
<LuRefreshCw className={retrain.isPending ? styles.spinning : ''} />
|
||||
{retrain.isPending ? 'Retraining...' : 'Retrain Models'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,9 +121,7 @@ export function Component(): React.ReactElement {
|
|||
|
||||
<div className={styles.pagination}>
|
||||
<span className={styles.paginationInfo}>
|
||||
{total === 0
|
||||
? 'No results'
|
||||
: `${offset + 1}–${showing} of ${total}`}
|
||||
{total === 0 ? 'No results' : `${offset + 1}–${showing} of ${total}`}
|
||||
</span>
|
||||
<div className={styles.paginationButtons}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ export default defineConfig(({ mode }) => {
|
|||
const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
|
||||
const isDev = mode === 'development'
|
||||
const apiTarget =
|
||||
process.env.VITE_API_TARGET ||
|
||||
env.VITE_API_TARGET ||
|
||||
'http://localhost:8000'
|
||||
process.env.VITE_API_TARGET || env.VITE_API_TARGET || 'http://localhost:8000'
|
||||
|
||||
return {
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
|
|
|
|||
Loading…
Reference in New Issue