import { useState } from 'react' import { Alert, Button, Form, Input, Modal, Popconfirm, Select, Space, Switch, Tag, Tabs, Table, } from 'antd' import { CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, DownloadOutlined, RadarChartOutlined, StopOutlined, } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAuthStore } from '../../stores/auth' import apiClient, { isEnvelope } from '../../api/client' import PageHeader from '../../components/PageHeader' import type { AddDecisionBody, Alert as CSAlert, Bouncer, CrowdSecStatus, Decision, HubItem, Machine, } from './types' // ---------- API helpers ----------------------------------------------------- async function fetchStatus(): Promise { const r = await apiClient.get('/crowdsec/status') if (isEnvelope(r.data)) return r.data.data as CrowdSecStatus return r.data as CrowdSecStatus } async function fetchDecisions(): Promise { const r = await apiClient.get('/crowdsec/decisions') if (!isEnvelope(r.data)) return [] return (r.data.data as { decisions: Decision[] }).decisions ?? [] } async function fetchAlerts(): Promise { const r = await apiClient.get('/crowdsec/alerts') if (!isEnvelope(r.data)) return [] return (r.data.data as { alerts: CSAlert[] }).alerts ?? [] } async function fetchBouncers(): Promise { const r = await apiClient.get('/crowdsec/bouncers') if (!isEnvelope(r.data)) return [] return (r.data.data as { bouncers: Bouncer[] }).bouncers ?? [] } async function fetchMachines(): Promise { const r = await apiClient.get('/crowdsec/machines') if (!isEnvelope(r.data)) return [] return (r.data.data as { machines: Machine[] }).machines ?? [] } async function fetchCollections(): Promise { const r = await apiClient.get('/crowdsec/collections') if (!isEnvelope(r.data)) return [] return (r.data.data as { collections: HubItem[] }).collections ?? [] } async function toggleService(service: string, enabled: boolean): Promise { await apiClient.post('/system/service-toggle', { service, enabled }) } // ---------- Status strip ---------------------------------------------------- function StatusStrip({ status, onToggle }: { status: CrowdSecStatus | undefined; onToggle: () => void }) { const { t } = useTranslation() const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const toggleAgent = useMutation({ mutationFn: (enabled: boolean) => toggleService('crowdsec', enabled), onSuccess: onToggle, }) const toggleBouncer = useMutation({ mutationFn: (enabled: boolean) => toggleService('crowdsec-firewall-bouncer', enabled), onSuccess: onToggle, }) return (
{t('cs.status.agent')}
: } color={status?.agent_running ? 'green' : 'red'} > {status?.agent_running ? t('cs.status.running') : t('cs.status.stopped')} toggleAgent.mutate(checked)} />
{status?.version && (
{status.version}
)}
{t('cs.status.bouncer')}
: } color={status?.bouncer_running ? 'green' : 'red'} > {status?.bouncer_running ? t('cs.status.running') : t('cs.status.stopped')} toggleBouncer.mutate(checked)} />
{t('cs.status.decisions')}
{status?.decision_count ?? '–'}
{t('cs.tabs.decisions')}
{t('cs.status.alerts')}
{status?.alert_count ?? '–'}
{t('cs.tabs.alerts')}
) } // ---------- Decisions tab --------------------------------------------------- function DecisionsTab() { const { t } = useTranslation() const queryClient = useQueryClient() const [banModalOpen, setBanModalOpen] = useState(false) const [form] = Form.useForm() const { data: decisions, isLoading } = useQuery({ queryKey: ['crowdsec', 'decisions'], queryFn: fetchDecisions, refetchInterval: 30_000, }) const unban = useMutation({ mutationFn: (id: number) => apiClient.delete(`/crowdsec/decisions?id=${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const addDecision = useMutation({ mutationFn: (body: AddDecisionBody) => apiClient.post('/crowdsec/decisions', body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['crowdsec'] }) setBanModalOpen(false) form.resetFields() }, }) const columns = [ { title: t('cs.decision.ip'), dataIndex: 'value', key: 'value' }, { title: t('cs.decision.reason'), dataIndex: 'reason', key: 'reason' }, { title: t('cs.decision.origin'), dataIndex: 'origin', key: 'origin' }, { title: t('cs.decision.duration'), dataIndex: 'duration', key: 'duration' }, { title: t('cs.decision.type'), dataIndex: 'type', key: 'type', render: (v: string) => {v} }, { title: t('cs.decision.country'), dataIndex: 'country', key: 'country' }, { title: t('cs.decision.as'), dataIndex: 'as', key: 'as' }, { title: '', key: 'actions', render: (_: unknown, row: Decision) => ( unban.mutate(row.id)} okText={t('cs.decision.unban')} okButtonProps={{ danger: true }} > ), }, ] return (
{ setBanModalOpen(false); form.resetFields() }} onOk={() => form.submit()} okButtonProps={{ danger: true, loading: addDecision.isPending }} okText={t('cs.decision.banBtn')} >
addDecision.mutate(vals)} >
) } // ---------- Alerts tab ------------------------------------------------------ function AlertsTab() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: alerts, isLoading } = useQuery({ queryKey: ['crowdsec', 'alerts'], queryFn: fetchAlerts, refetchInterval: 30_000, }) const deleteAlert = useMutation({ mutationFn: (id: number) => apiClient.delete(`/crowdsec/alerts/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const columns = [ { title: t('cs.alert.id'), dataIndex: 'id', key: 'id', width: 70 }, { title: t('cs.alert.scenario'), dataIndex: 'scenario', key: 'scenario' }, { title: t('cs.alert.events'), dataIndex: 'events_count', key: 'events_count', width: 80 }, { title: t('cs.alert.sourceIP'), key: 'sourceIP', render: (_: unknown, row: CSAlert) => row.source?.ip ?? row.source?.value ?? '–' }, { title: t('cs.alert.country'), key: 'country', render: (_: unknown, row: CSAlert) => row.source?.cn ?? '–' }, { title: t('cs.alert.start'), dataIndex: 'start_at', key: 'start_at' }, { title: t('cs.alert.stop'), dataIndex: 'stop_at', key: 'stop_at' }, { title: '', key: 'actions', render: (_: unknown, row: CSAlert) => ( deleteAlert.mutate(row.id)} okButtonProps={{ danger: true }} > ), }, ] return (
) } // ---------- Bouncers tab ---------------------------------------------------- function BouncersTab() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: bouncers, isLoading } = useQuery({ queryKey: ['crowdsec', 'bouncers'], queryFn: fetchBouncers, refetchInterval: 30_000, }) const deleteBouncer = useMutation({ mutationFn: (name: string) => apiClient.delete(`/crowdsec/bouncers/${encodeURIComponent(name)}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const columns = [ { title: t('cs.bouncer.name'), dataIndex: 'name', key: 'name' }, { title: t('cs.bouncer.ip'), dataIndex: 'ip_address', key: 'ip_address' }, { title: t('cs.bouncer.validKey'), dataIndex: 'revoked', key: 'revoked', render: (v: boolean) => !v ? OK : revoked }, { title: t('cs.bouncer.version'), dataIndex: 'version', key: 'version' }, { title: t('cs.bouncer.type'), dataIndex: 'type', key: 'type' }, { title: t('cs.bouncer.lastPull'), dataIndex: 'last_pull', key: 'last_pull' }, { title: '', key: 'actions', render: (_: unknown, row: Bouncer) => ( deleteBouncer.mutate(row.name)} okButtonProps={{ danger: true }} > ), }, ] return (
) } // ---------- Machines tab ---------------------------------------------------- function MachinesTab() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: machines, isLoading } = useQuery({ queryKey: ['crowdsec', 'machines'], queryFn: fetchMachines, refetchInterval: 30_000, }) const deleteMachine = useMutation({ mutationFn: (id: string) => apiClient.delete(`/crowdsec/machines/${encodeURIComponent(id)}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const columns = [ { title: t('cs.machine.id'), dataIndex: 'machineId', key: 'machineId' }, { title: t('cs.machine.created'), dataIndex: 'created_at', key: 'created_at' }, { title: t('cs.machine.lastPush'), dataIndex: 'last_push', key: 'last_push' }, { title: t('cs.machine.validated'), dataIndex: 'isValidated', key: 'isValidated', render: (v: boolean) => v ? ja : nein }, { title: t('cs.machine.version'), dataIndex: 'version', key: 'version' }, { title: '', key: 'actions', render: (_: unknown, row: Machine) => ( deleteMachine.mutate(row.machineId)} okButtonProps={{ danger: true }} > ), }, ] return (
) } // ---------- Collections tab ------------------------------------------------- function CollectionsTab() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: collections, isLoading } = useQuery({ queryKey: ['crowdsec', 'collections'], queryFn: fetchCollections, refetchInterval: 60_000, }) const installCollection = useMutation({ mutationFn: (name: string) => apiClient.post(`/crowdsec/collections/${encodeURIComponent(name)}/install`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const removeCollection = useMutation({ mutationFn: (name: string) => apiClient.delete(`/crowdsec/collections/${encodeURIComponent(name)}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['crowdsec'] }), }) const isEnabled = (status: string) => status === 'enabled' || status === 'downloaded' const columns = [ { title: t('cs.collection.name'), dataIndex: 'name', key: 'name' }, { title: t('cs.collection.status'), dataIndex: 'status', key: 'status', render: (v: string) => ( {isEnabled(v) ? t('cs.collection.enabled') : t('cs.collection.disabled')} ) }, { title: t('cs.collection.version'), dataIndex: 'local_version', key: 'local_version' }, { title: t('cs.collection.author'), dataIndex: 'author', key: 'author' }, { title: '', key: 'actions', render: (_: unknown, row: HubItem) => ( {!isEnabled(row.status) ? ( ) : ( removeCollection.mutate(row.name)} okButtonProps={{ danger: true }} > )} ), }, ] return (
) } // ---------- Page ------------------------------------------------------------ export default function CrowdSecPage() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: status } = useQuery({ queryKey: ['crowdsec', 'status'], queryFn: fetchStatus, refetchInterval: 10_000, }) const invalidateStatus = () => { void queryClient.invalidateQueries({ queryKey: ['crowdsec', 'status'] }) } const tabs = [ { key: 'decisions', label: t('cs.tabs.decisions'), children: }, { key: 'alerts', label: t('cs.tabs.alerts'), children: }, { key: 'bouncers', label: t('cs.tabs.bouncers'), children: }, { key: 'machines', label: t('cs.tabs.machines'), children: }, { key: 'collections', label: t('cs.tabs.collections'), children: }, ] return (
} title={t('cs.title')} subtitle={t('cs.intro')} /> {status && !status.installed && ( )}
) }