diff --git a/VERSION b/VERSION
index c0ab82c..3eefcb9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.1-dev
+1.0.0
diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go
index 6082605..86ef771 100644
--- a/cmd/edgeguard-api/main.go
+++ b/cmd/edgeguard-api/main.go
@@ -35,7 +35,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
-var version = "0.0.1-dev"
+var version = "1.0.0"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")
diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go
index e1fcee5..21b73ce 100644
--- a/cmd/edgeguard-ctl/main.go
+++ b/cmd/edgeguard-ctl/main.go
@@ -9,7 +9,7 @@ import (
"os"
)
-var version = "0.0.1-dev"
+var version = "1.0.0"
const usage = `edgeguard-ctl — EdgeGuard CLI
diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go
index e880fa0..126e79d 100644
--- a/cmd/edgeguard-scheduler/main.go
+++ b/cmd/edgeguard-scheduler/main.go
@@ -5,7 +5,7 @@ import (
"time"
)
-var version = "0.0.1-dev"
+var version = "1.0.0"
func main() {
log.Printf("edgeguard-scheduler %s starting", version)
diff --git a/management-ui/package.json b/management-ui/package.json
index c18e52a..9e0a650 100644
--- a/management-ui/package.json
+++ b/management-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "edgeguard-management-ui",
"private": true,
- "version": "0.0.1-dev",
+ "version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/management-ui/src/components/DataTable.tsx b/management-ui/src/components/DataTable.tsx
new file mode 100644
index 0000000..9a5a3e4
--- /dev/null
+++ b/management-ui/src/components/DataTable.tsx
@@ -0,0 +1,123 @@
+import { useMemo, useState } from 'react'
+import { Input, Space, Table } from 'antd'
+import type { ColumnsType, TableProps } from 'antd/es/table'
+import { SearchOutlined } from '@ant-design/icons'
+import { useTranslation } from 'react-i18next'
+
+// DataTable wraps AntD's Table and gives every CRUD page the same
+// baseline UX:
+//
+// * Search input above the table — filters across every string
+// field of every row, case-insensitive.
+// * Pagination on by default (25 / page, size-changer enabled).
+// * Sortable columns out of the box — any column with `dataIndex`
+// gets a default sorter inferred from the value type (string →
+// localeCompare, number → subtract). Columns that already
+// define `sorter` keep their custom one.
+//
+// Drop-in replacement: callers pass the same `columns` + `dataSource`
+// props they would to `
`. Search visibility and page-size
+// can be customised via the props.
+interface DataTableProps extends Omit, 'pagination'> {
+ pageSize?: number
+ searchPlaceholder?: string
+ searchable?: boolean
+ // toolbar renders to the right of the search input, useful for
+ // "Add" buttons.
+ toolbar?: React.ReactNode
+}
+
+function inferSorter(dataIndex: string | string[] | undefined) {
+ if (dataIndex == null) return undefined
+ const path = Array.isArray(dataIndex) ? dataIndex : [dataIndex]
+ const get = (row: T): unknown => path.reduce(
+ (acc, k) => (acc != null && typeof acc === 'object' ? (acc as Record)[k] : undefined),
+ row as unknown,
+ )
+ return (a: T, b: T) => {
+ const va = get(a), vb = get(b)
+ if (typeof va === 'number' && typeof vb === 'number') return va - vb
+ if (typeof va === 'boolean' && typeof vb === 'boolean') return Number(va) - Number(vb)
+ const sa = String(va ?? '')
+ const sb = String(vb ?? '')
+ return sa.localeCompare(sb, undefined, { numeric: true, sensitivity: 'base' })
+ }
+}
+
+function flatStringValues(row: unknown): string[] {
+ if (row == null) return []
+ if (typeof row === 'string') return [row]
+ if (typeof row === 'number' || typeof row === 'boolean') return [String(row)]
+ if (Array.isArray(row)) return row.flatMap(flatStringValues)
+ if (typeof row === 'object') {
+ return Object.values(row as Record).flatMap(flatStringValues)
+ }
+ return []
+}
+
+export default function DataTable(
+ props: DataTableProps,
+) {
+ const { t } = useTranslation()
+ const {
+ dataSource,
+ columns,
+ pageSize = 25,
+ searchPlaceholder,
+ searchable = true,
+ toolbar,
+ ...rest
+ } = props
+
+ const [search, setSearch] = useState('')
+
+ const filtered = useMemo(() => {
+ if (!search || !dataSource) return dataSource
+ const q = search.toLowerCase()
+ return (dataSource as readonly T[]).filter((row) =>
+ flatStringValues(row).some((s) => s.toLowerCase().includes(q)),
+ )
+ }, [dataSource, search])
+
+ const enhancedCols: ColumnsType = useMemo(() => {
+ if (!columns) return []
+ return (columns as ColumnsType).map((c) => {
+ // Skip if user already declared a sorter or this column has
+ // no dataIndex (e.g. action columns).
+ const col = c as Record
+ if (col.sorter || col.dataIndex == null) return c
+ const sorter = inferSorter(col.dataIndex as string | string[])
+ if (!sorter) return c
+ return { ...c, sorter }
+ })
+ }, [columns])
+
+ return (
+
+ {(searchable || toolbar) && (
+
+ {searchable && (
+
}
+ placeholder={searchPlaceholder ?? t('common.search')}
+ allowClear
+ onChange={(e) => setSearch(e.target.value)}
+ style={{ maxWidth: 320 }}
+ />
+ )}
+ {toolbar &&
{toolbar}
}
+
+ )}
+
+ {...rest}
+ dataSource={filtered}
+ columns={enhancedCols}
+ pagination={{
+ pageSize,
+ showSizeChanger: true,
+ showTotal: (total) => t('common.totalRows', { count: total }),
+ }}
+ />
+
+ )
+}
diff --git a/management-ui/src/components/Layout/Sidebar.tsx b/management-ui/src/components/Layout/Sidebar.tsx
index 5623ae3..c384b7e 100644
--- a/management-ui/src/components/Layout/Sidebar.tsx
+++ b/management-ui/src/components/Layout/Sidebar.tsx
@@ -68,7 +68,7 @@ const NAV: NavSection[] = [
},
]
-const VERSION = '0.0.1-dev'
+const VERSION = '1.0.0'
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
const { t } = useTranslation()
diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json
index 678dced..72ccfe0 100644
--- a/management-ui/src/i18n/locales/de/common.json
+++ b/management-ui/src/i18n/locales/de/common.json
@@ -248,6 +248,8 @@
"loading": "Lädt …",
"error": "Fehler",
"edit": "Bearbeiten",
- "delete": "Löschen"
+ "delete": "Löschen",
+ "search": "Suchen …",
+ "totalRows": "{{count}} Einträge"
}
}
diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json
index 20ab488..a862453 100644
--- a/management-ui/src/i18n/locales/en/common.json
+++ b/management-ui/src/i18n/locales/en/common.json
@@ -249,6 +249,8 @@
"loading": "Loading …",
"error": "Error",
"edit": "Edit",
- "delete": "Delete"
+ "delete": "Delete",
+ "search": "Search …",
+ "totalRows": "{{count}} rows"
}
}
diff --git a/management-ui/src/pages/Backends/index.tsx b/management-ui/src/pages/Backends/index.tsx
index 43a1a5d..e2d94bd 100644
--- a/management-ui/src/pages/Backends/index.tsx
+++ b/management-ui/src/pages/Backends/index.tsx
@@ -1,8 +1,9 @@
import { useState } from 'react'
-import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Typography, message } from 'antd'
+import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
+import DataTable from '../../components/DataTable'
import apiClient, { isEnvelope } from '../../api/client'
@@ -118,7 +119,7 @@ export default function BackendsPage() {
}}>
{t('backends.addBackend')}
-
+
-
diff --git a/management-ui/src/pages/Domains/index.tsx b/management-ui/src/pages/Domains/index.tsx
index 27304e3..0a112b5 100644
--- a/management-ui/src/pages/Domains/index.tsx
+++ b/management-ui/src/pages/Domains/index.tsx
@@ -1,8 +1,9 @@
import { useState } from 'react'
-import { Button, Form, Input, Modal, Popconfirm, Space, Switch, Table, Typography, message } from 'antd'
+import { Button, Form, Input, Modal, Popconfirm, Space, Switch, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
+import DataTable from '../../components/DataTable'
import apiClient, { isEnvelope } from '../../api/client'
@@ -124,12 +125,12 @@ export default function DomainsPage() {
}}>
{t('domains.addDomain')}
-
{t('fw.ag.add')}
-
+
{t('fw.ao.add')}
-
+
{t('fw.nat.add')}
-
+
{t('fw.rule.add')}
-
+
{t('fw.sg.add')}
-
+
{t('fw.svc.add')}
-
+
—
: (
- `${r.ifname}-${r.address}`}
dataSource={sysAddrs ?? []}
- pagination={false}
+
columns={[
{ title: t('ips.interface'), dataIndex: 'ifname', key: 'ifname', render: (s: string) => {s} },
{ title: t('ips.address'), key: 'addr', render: (_, row: SystemAddress) => {row.address}/{row.prefix} },
@@ -189,7 +190,7 @@ export default function IPAddressesPage() {
}}>
{t('ips.addAddress')}
-
+
-
+
{t('routing.addRule')}
-
+
{t('ssl.installedTitle')}
-
+
)
}