feat: umfangreiches UI+API-Polish (v1.1.36–1.1.42)
Backend: - Audit-Log: Search-Endpoint mit ILIKE-Filter (actor/action/subject/date) - NTP: /ntp/status via chronyc tracking (Stratum, Offset, Quelle) - System: /service-restart mit Allowlist (haproxy/squid/unbound/chrony/scheduler) - Domain-Response-Headers + Rate-Limit (Migration 0024) - Join-Tokens (Migration 0025), Cluster-mTLS, Aggregator-Fan-Out - apt-Service für Update-Banner (apt-get update + Versionsprüfung) - Backup-Retry mit exponential backoff (retry_apt 3×) - publish.sh fail-fast + cleanup-old.sh (max 10 Versionen) Frontend: - Audit-Log-Page (/audit) mit Filter + Pagination - ErrorBoundary an React-Root + Vite build-target festgenagelt (iOS 15+) - Storage-Schema-Stamp: auto-wipe bei Versions-Mismatch (blank-page-Fix) - EmptyState-Komponente überall ausgerollt - SSL: Aggregate-Karte (total/expiring/expired/errors) - Backups: Aggregate-Karte (letzter Backup/Größe/Fehlschläge 24h) + Backup-Now - NTP: Sync-Status-Karte (chronyc tracking live) - Domains: Backend-UP/DOWN-Chip aus HAProxy-Stats - Backends: HAProxy-Status-Spalte (UP/DEGRADED/DOWN) - Settings: Service-Neustart-Karte (haproxy/squid/unbound/chrony/scheduler) - Settings: Upgrade-Status-Card, Wartungsmodus, Auto-Update, Retention - Dashboard: Recent-Alerts, Cluster-Health, License-Chip, Onboarding-Hint - System-Regeln im Firewall als eigener Tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GroupOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { AddressGroup, AddressObject } from './types'
|
||||
@@ -84,15 +87,30 @@ export default function AddressGroupsTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ member_ids: [] })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ member_ids: [] })
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.ag.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={groups ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={groups ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<GroupOutlined />}
|
||||
title={t('fw.ag.emptyTitle')}
|
||||
description={t('fw.ag.emptyDesc')}
|
||||
action={<Button type="primary" onClick={openCreate}>{t('fw.ag.add')}</Button>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.ag.edit') : t('fw.ag.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EnvironmentOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { AddressObject } from './types'
|
||||
@@ -77,15 +80,32 @@ export default function AddressObjectsTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ kind: 'host' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ kind: 'host' })
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.ao.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={data ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<EnvironmentOutlined />}
|
||||
title={t('fw.ao.emptyTitle')}
|
||||
description={t('fw.ao.emptyDesc')}
|
||||
action={
|
||||
<Button type="primary" onClick={openCreate}>{t('fw.ao.add')}</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.ao.edit') : t('fw.ao.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Swi
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BranchesOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { FwZone, NATRule } from './types'
|
||||
@@ -132,15 +135,30 @@ export default function NATRulesTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ priority: 100, enabled: true, kind: 'dnat' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ priority: 100, enabled: true, kind: 'dnat' })
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.nat.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={data ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<BranchesOutlined />}
|
||||
title={t('fw.nat.emptyTitle')}
|
||||
description={t('fw.nat.emptyDesc')}
|
||||
action={<Button type="primary" onClick={openCreate}>{t('fw.nat.add')}</Button>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.nat.edit') : t('fw.nat.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Swi
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FireOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import SystemRulesCard from './SystemRules'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { AddressGroup, AddressObject, FwRule, FwService, FwZone, ServiceGroup, Zone } from './types'
|
||||
@@ -197,20 +199,34 @@ export default function RulesTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({
|
||||
priority: 100, enabled: true, action: 'accept', log: false,
|
||||
src_zone: 'any', dst_zone: 'any',
|
||||
src_kind: 'any', dst_kind: 'any', service_kind: 'any',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SystemRulesCard />
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({
|
||||
priority: 100, enabled: true, action: 'accept', log: false,
|
||||
src_zone: 'any', dst_zone: 'any',
|
||||
src_kind: 'any', dst_kind: 'any', service_kind: 'any',
|
||||
})
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.rule.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={rules ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={rules ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<FireOutlined />}
|
||||
title={t('fw.rule.emptyTitle')}
|
||||
description={t('fw.rule.emptyDesc')}
|
||||
action={<Button type="primary" onClick={openCreate}>{t('fw.rule.add')}</Button>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.rule.edit') : t('fw.rule.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Form, Input, Modal, Popconfirm, Select, Space, Tag, message } f
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GroupOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { FwService, ServiceGroup } from './types'
|
||||
@@ -84,15 +87,30 @@ export default function ServiceGroupsTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ member_ids: [] })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ member_ids: [] })
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.sg.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={groups ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={groups ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<GroupOutlined />}
|
||||
title={t('fw.sg.emptyTitle')}
|
||||
description={t('fw.sg.emptyDesc')}
|
||||
action={<Button type="primary" onClick={openCreate}>{t('fw.sg.add')}</Button>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.sg.edit') : t('fw.sg.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Tag
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiOutlined } from '@ant-design/icons'
|
||||
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { FwService } from './types'
|
||||
@@ -88,15 +91,30 @@ export default function ServicesTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ proto: 'tcp' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
form.setFieldsValue({ proto: 'tcp' })
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.svc.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={data ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<ApiOutlined />}
|
||||
title={t('fw.svc.emptyTitle')}
|
||||
description={t('fw.svc.emptyDesc')}
|
||||
action={<Button type="primary" onClick={openCreate}>{t('fw.svc.add')}</Button>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? t('fw.svc.edit') : t('fw.svc.add')}
|
||||
open={editing !== null || creating}
|
||||
|
||||
@@ -4,8 +4,11 @@ import type { ColumnsType } from 'antd/es/table'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ApartmentOutlined } from '@ant-design/icons'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import DataTable from '../../components/DataTable'
|
||||
import EmptyState from '../../components/EmptyState'
|
||||
import type { FwZone } from './types'
|
||||
|
||||
interface FormValues {
|
||||
@@ -77,14 +80,29 @@ export default function ZonesTab() {
|
||||
},
|
||||
]
|
||||
|
||||
const openCreate = () => { setCreating(true); form.resetFields() }
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-16" onClick={() => {
|
||||
setCreating(true); form.resetFields()
|
||||
}}>
|
||||
<Button type="primary" className="mb-16" onClick={openCreate}>
|
||||
{t('fw.zone.add')}
|
||||
</Button>
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={data ?? []} columns={columns} />
|
||||
<DataTable
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
dataSource={data ?? []}
|
||||
columns={columns}
|
||||
emptyContent={
|
||||
<EmptyState
|
||||
icon={<ApartmentOutlined />}
|
||||
title={t('fw.zone.emptyTitle')}
|
||||
description={t('fw.zone.emptyDesc')}
|
||||
action={
|
||||
<Button type="primary" onClick={openCreate}>{t('fw.zone.add')}</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? t('fw.zone.edit') : t('fw.zone.add')}
|
||||
|
||||
@@ -9,6 +9,7 @@ import ServicesTab from './Services'
|
||||
import ServiceGroupsTab from './ServiceGroups'
|
||||
import RulesTab from './Rules'
|
||||
import NATRulesTab from './NATRules'
|
||||
import SystemRulesCard from './SystemRules'
|
||||
import ZonesTab from './Zones'
|
||||
|
||||
export default function FirewallPage() {
|
||||
@@ -22,6 +23,7 @@ export default function FirewallPage() {
|
||||
{ key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: <AddressGroupsTab /> },
|
||||
{ key: 'services', label: t('fw.tabs.services'), children: <ServicesTab /> },
|
||||
{ key: 'svcGrp', label: t('fw.tabs.svcGrp'), children: <ServiceGroupsTab /> },
|
||||
{ key: 'system', label: t('fw.tabs.system'), children: <SystemRulesCard /> },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user