diff --git a/management-ui/src/pages/DNS/index.tsx b/management-ui/src/pages/DNS/index.tsx index 364244f..87993aa 100644 --- a/management-ui/src/pages/DNS/index.tsx +++ b/management-ui/src/pages/DNS/index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { Alert, Button, Drawer, Form, Input, InputNumber, Modal, Select, Space, Switch, Tabs, Tag, Typography, message } from 'antd' import type { ColumnsType } from 'antd/es/table' -import { GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' +import { CheckCircleOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' @@ -71,8 +71,21 @@ async function listSystemInterfaces(): Promise { return (r.data.data as { interfaces?: SystemIface[] }).interfaces ?? [] } +interface ServiceStatus { label: string; unit: string; active: boolean; state: string } + export default function DNSPage() { const { t } = useTranslation() + + const { data: services } = useQuery({ + queryKey: ['system', 'services'], + queryFn: async () => { + const r = await apiClient.get('/system/services') + return isEnvelope(r.data) ? (r.data.data as { services: ServiceStatus[] }).services : [] + }, + refetchInterval: 30_000, + }) + const unbound = services?.find(s => s.unit === 'unbound.service' || s.unit === 'unbound') + return (
: } + color={unbound.active ? 'green' : 'red'} + > + + unbound + {unbound.state} + + + )} items={[ { key: 'zones', label: {t('dns.tabs.zones')}, children: }, { key: 'settings', label: {t('dns.tabs.settings')}, children: }, diff --git a/management-ui/src/pages/Firewall/index.tsx b/management-ui/src/pages/Firewall/index.tsx index cbb8288..151a651 100644 --- a/management-ui/src/pages/Firewall/index.tsx +++ b/management-ui/src/pages/Firewall/index.tsx @@ -1,7 +1,9 @@ -import { Tabs } from 'antd' -import { FireOutlined } from '@ant-design/icons' +import { Space, Tag, Tabs } from 'antd' +import { CheckCircleOutlined, CloseCircleOutlined, FireOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' +import { useQuery } from '@tanstack/react-query' +import apiClient, { isEnvelope } from '../../api/client' import PageHeader from '../../components/PageHeader' import AddressObjectsTab from './AddressObjects' import AddressGroupsTab from './AddressGroups' @@ -12,9 +14,21 @@ import NATRulesTab from './NATRules' import SystemRulesCard from './SystemRules' import ZonesTab from './Zones' +interface ServiceStatus { label: string; unit: string; active: boolean; state: string } + export default function FirewallPage() { const { t } = useTranslation() + const { data: services } = useQuery({ + queryKey: ['system', 'services'], + queryFn: async () => { + const r = await apiClient.get('/system/services') + return isEnvelope(r.data) ? (r.data.data as { services: ServiceStatus[] }).services : [] + }, + refetchInterval: 30_000, + }) + const nftables = services?.find(s => s.unit === 'nftables.service' || s.unit === 'nftables') + const tabs = [ { key: 'rules', label: t('fw.tabs.rules'), children: }, { key: 'nat', label: t('fw.tabs.nat'), children: }, @@ -33,7 +47,21 @@ export default function FirewallPage() { title={t('fw.title')} subtitle={t('fw.intro')} /> - + : } + color={nftables.active ? 'green' : 'red'} + > + + nftables + {nftables.state} + + + )} + />
) } diff --git a/management-ui/src/pages/Networks/Routes.tsx b/management-ui/src/pages/Networks/Routes.tsx index d2a692a..b937e6a 100644 --- a/management-ui/src/pages/Networks/Routes.tsx +++ b/management-ui/src/pages/Networks/Routes.tsx @@ -99,6 +99,14 @@ export default function RoutesTab() { onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes'] }) }, onError: (e: Error) => message.error(e.message), }) + const quickToggle = useMutation({ + mutationFn: async ({ id, row, checked }: { id: number; row: ManagedRoute; checked: boolean }) => { + const { id: _id, ...body } = row + await apiClient.put(`/routes/${id}`, { ...body, active: checked }) + }, + onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes'] }) }, + onError: (e: Error) => message.error(e.message), + }) const managedColumns: ColumnsType = [ { @@ -115,8 +123,17 @@ export default function RoutesTab() { render: (v?: string) => v ? {v} : '—' }, { title: t('routes.col.metric'), dataIndex: 'metric', width: 80 }, { title: t('routes.col.table'), dataIndex: 'table_name', width: 90 }, - { title: t('routes.col.active'), dataIndex: 'active', width: 80, - render: (v: boolean) => v ? on : off }, + { + title: t('routes.col.active'), dataIndex: 'active', width: 80, + render: (v: boolean, row: ManagedRoute) => ( + quickToggle.mutate({ id: row.id, row, checked })} + /> + ), + }, { title: t('routes.col.comment'), dataIndex: 'comment', render: (v?: string) => v || }, {