mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +00:00
Optimize loading and style (#2)
This commit is contained in:
158
src/main.tsx
158
src/main.tsx
@@ -1,8 +1,8 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
|
||||
import './index.css'
|
||||
@@ -29,85 +29,85 @@ import UserPage from './routes/user';
|
||||
import WAFPage from './routes/waf';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: <AuthProvider><ProtectedRoute><Root /></ProtectedRoute></AuthProvider>,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: <ServerProvider withServerGroup><ServerPage /></ServerProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/service",
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<ServicePage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/cron",
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<CronPage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/notification",
|
||||
element: <NotificationProvider withNotifierGroup><NotificationPage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/alert-rule",
|
||||
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/ddns",
|
||||
element: <DDNSPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/nat",
|
||||
element: <NATPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/server-group",
|
||||
element: <ServerProvider withServer><ServerGroupPage /></ServerProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/notification-group",
|
||||
element: <NotificationProvider withNotifier><NotificationGroupPage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/terminal/:id",
|
||||
element: <TerminalPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings",
|
||||
element: <SettingsPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings/user",
|
||||
element: <UserPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings/waf",
|
||||
element: <WAFPage />,
|
||||
},
|
||||
]
|
||||
},
|
||||
element: <AuthProvider><ProtectedRoute><Root /></ProtectedRoute></AuthProvider>,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: <ServerProvider withServerGroup><ServerPage /></ServerProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/service",
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<ServicePage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/cron",
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<CronPage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/notification",
|
||||
element: <NotificationProvider withNotifierGroup><NotificationPage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/alert-rule",
|
||||
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/ddns",
|
||||
element: <DDNSPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/nat",
|
||||
element: <NATPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/server-group",
|
||||
element: <ServerProvider withServer><ServerGroupPage /></ServerProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/notification-group",
|
||||
element: <NotificationProvider withNotifier><NotificationGroupPage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/terminal/:id",
|
||||
element: <TerminalPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings",
|
||||
element: <SettingsPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings/user",
|
||||
element: <UserPage />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings/waf",
|
||||
element: <WAFPage />,
|
||||
},
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelAlertRule, triggerModes } from "@/types"
|
||||
import { deleteAlertRules } from "@/api/alert-rule"
|
||||
import { NotificationTab } from "@/components/notification-tab"
|
||||
import { AlertRuleCard } from "@/components/alert-rule"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelAlertRule, triggerModes } from "@/types";
|
||||
import { deleteAlertRules } from "@/api/alert-rule";
|
||||
import { NotificationTab } from "@/components/notification-tab";
|
||||
import { AlertRuleCard } from "@/components/alert-rule";
|
||||
|
||||
export default function AlertRulePage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>("/api/v1/alert-rule", swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
|
||||
"/api/v1/alert-rule",
|
||||
swrFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelAlertRule>[] = [
|
||||
{
|
||||
@@ -30,7 +39,7 @@ export default function AlertRulePage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -49,74 +58,73 @@ export default function AlertRulePage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Notifier Group",
|
||||
accessorKey: "ngroup",
|
||||
accessorFn: row => row.notification_group_id,
|
||||
accessorFn: (row) => row.notification_group_id,
|
||||
},
|
||||
{
|
||||
header: "Trigger Mode",
|
||||
accessorKey: "trigger Mode",
|
||||
accessorFn: row => triggerModes[row.trigger_mode] || '',
|
||||
accessorFn: (row) => triggerModes[row.trigger_mode] || "",
|
||||
},
|
||||
{
|
||||
header: "Rules",
|
||||
accessorKey: "rules",
|
||||
accessorFn: row => JSON.stringify(row.rules),
|
||||
accessorFn: (row) => JSON.stringify(row.rules),
|
||||
},
|
||||
{
|
||||
header: "Tasks to trigger on alert",
|
||||
accessorKey: "failTriggerTasks",
|
||||
accessorFn: row => row.fail_trigger_tasks,
|
||||
accessorFn: (row) => row.fail_trigger_tasks,
|
||||
},
|
||||
{
|
||||
header: "Tasks to trigger after recovery",
|
||||
accessorKey: "recoverTriggerTasks",
|
||||
accessorFn: row => row.recover_trigger_tasks,
|
||||
accessorFn: (row) => row.recover_trigger_tasks,
|
||||
},
|
||||
{
|
||||
header: "Enable",
|
||||
accessorKey: "enable",
|
||||
accessorFn: row => row.enable,
|
||||
accessorFn: (row) => row.enable,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteAlertRules,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteAlertRules,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<AlertRuleCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -124,64 +132,60 @@ export default function AlertRulePage() {
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteAlertRules,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex gap-2 ml-auto"
|
||||
delete={{
|
||||
fn: deleteAlertRules,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<AlertRuleCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ModelCron } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { deleteCron, runCron } from "@/api/cron"
|
||||
import { CronCard } from "@/components/cron"
|
||||
import { cronTypes } from "@/types"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelCron } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { deleteCron, runCron } from "@/api/cron";
|
||||
import { CronCard } from "@/components/cron";
|
||||
import { cronTypes } from "@/types";
|
||||
import { IconButton } from "@/components/xui/icon-button";
|
||||
|
||||
export default function CronPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>('/api/v1/cron', swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelCron>[] = [
|
||||
{
|
||||
@@ -31,7 +37,7 @@ export default function CronPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -50,205 +56,193 @@ export default function CronPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Task Type",
|
||||
accessorKey: "taskType",
|
||||
accessorFn: row => cronTypes[row.task_type] || '',
|
||||
accessorFn: (row) => cronTypes[row.task_type] || "",
|
||||
},
|
||||
{
|
||||
header: "Cron Expression",
|
||||
accessorKey: "scheduler",
|
||||
accessorFn: row => row.scheduler,
|
||||
accessorFn: (row) => row.scheduler,
|
||||
},
|
||||
{
|
||||
header: "Command",
|
||||
accessorKey: "command",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{s.command}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Notifier Group",
|
||||
accessorKey: "ngroup",
|
||||
accessorFn: row => row.notification_group_id,
|
||||
accessorFn: (row) => row.notification_group_id,
|
||||
},
|
||||
{
|
||||
header: "Send Success Notification",
|
||||
accessorKey: "pushSuccessful",
|
||||
accessorFn: row => row.push_successful ?? false,
|
||||
accessorFn: (row) => row.push_successful ?? false,
|
||||
},
|
||||
{
|
||||
header: "Coverage",
|
||||
accessorKey: "cover",
|
||||
accessorFn: row => row.cover,
|
||||
accessorFn: (row) => row.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>Ignore All</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>Cover All</span>
|
||||
}
|
||||
case 2: {
|
||||
return <span>On alert</span>
|
||||
}
|
||||
case 0: {
|
||||
return <span>Ignore All</span>;
|
||||
}
|
||||
case 1: {
|
||||
return <span>Cover All</span>;
|
||||
}
|
||||
case 2: {
|
||||
return <span>On alert</span>;
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Specific Servers",
|
||||
accessorKey: "servers",
|
||||
accessorFn: row => row.servers,
|
||||
accessorFn: (row) => row.servers,
|
||||
},
|
||||
{
|
||||
header: "Last Execution",
|
||||
accessorKey: "lastExecution",
|
||||
accessorFn: row => row.last_executed_at,
|
||||
accessorFn: (row) => row.last_executed_at,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.last_executed_at}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.last_executed_at}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Last Result",
|
||||
accessorKey: "lastResult",
|
||||
accessorFn: row => row.last_result ?? false,
|
||||
accessorFn: (row) => row.last_result ?? false,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteCron, id: s.id, mutate: mutate }}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{ fn: deleteCron, id: s.id, mutate: mutate }}
|
||||
>
|
||||
<>
|
||||
<IconButton variant="outline" icon="play" onClick={
|
||||
async () => {
|
||||
<IconButton
|
||||
variant="outline"
|
||||
icon="play"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await runCron(s.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast("Error executing task", {
|
||||
description: "Please see the console for details.",
|
||||
})
|
||||
});
|
||||
await mutate();
|
||||
return;
|
||||
}
|
||||
toast("Success", {
|
||||
description: "The task triggered successfully.",
|
||||
})
|
||||
});
|
||||
await mutate();
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
<CronCard mutate={mutate} data={s} />
|
||||
</>
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">
|
||||
Task
|
||||
</h1>
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteCron,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">Task</h1>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteCron,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<CronCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { DDNSCard } from "@/components/ddns"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ModelDDNSProfile } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DDNSCard } from "@/components/ddns";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelDDNSProfile } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns";
|
||||
|
||||
export default function DDNSPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>('/api/v1/ddns', swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
|
||||
const [providers, setProviders] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,8 +34,8 @@ export default function DDNSPage() {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelDDNSProfile>[] = [
|
||||
{
|
||||
@@ -38,7 +44,7 @@ export default function DDNSPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -57,140 +63,129 @@ export default function DDNSPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "IPv4 Enabled",
|
||||
accessorKey: "enableIPv4",
|
||||
accessorFn: row => row.enable_ipv4 ?? false,
|
||||
accessorFn: (row) => row.enable_ipv4 ?? false,
|
||||
},
|
||||
{
|
||||
header: "IPv6 Enabled",
|
||||
accessorKey: "enableIPv6",
|
||||
accessorFn: row => row.enable_ipv6 ?? false,
|
||||
accessorFn: (row) => row.enable_ipv6 ?? false,
|
||||
},
|
||||
{
|
||||
header: "DDNS Provider",
|
||||
accessorKey: "provider",
|
||||
accessorFn: row => row.provider,
|
||||
accessorFn: (row) => row.provider,
|
||||
},
|
||||
{
|
||||
header: "Domains",
|
||||
accessorKey: "domains",
|
||||
accessorFn: row => row.domains,
|
||||
accessorFn: (row) => row.domains,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.domains}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Maximum retry attempts",
|
||||
accessorKey: "maxRetries",
|
||||
accessorFn: row => row.max_retries,
|
||||
accessorFn: (row) => row.max_retries,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}
|
||||
>
|
||||
<DDNSCard mutate={mutate} data={s} providers={providers} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">
|
||||
Dynamic DNS
|
||||
</h1>
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteDDNSProfiles,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">Dynamic DNS</h1>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteDDNSProfiles,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<DDNSCard mutate={mutate} providers={providers} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { NATCard } from "@/components/nat"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ModelNAT } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { deleteNAT } from "@/api/nat"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { NATCard } from "@/components/nat";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelNAT } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { deleteNAT } from "@/api/nat";
|
||||
|
||||
export default function NATPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>('/api/v1/nat', swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelNAT>[] = [
|
||||
{
|
||||
@@ -29,7 +35,7 @@ export default function NATPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -48,138 +54,123 @@ export default function NATPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Server ID",
|
||||
accessorKey: "serverID",
|
||||
accessorFn: row => row.server_id,
|
||||
accessorFn: (row) => row.server_id,
|
||||
},
|
||||
{
|
||||
header: "Local service",
|
||||
accessorKey: "host",
|
||||
accessorFn: row => row.host,
|
||||
accessorFn: (row) => row.host,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.host}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Bind hostname",
|
||||
accessorKey: "domain",
|
||||
accessorFn: row => row.domain,
|
||||
accessorFn: (row) => row.domain,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.domain}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}
|
||||
>
|
||||
<NATCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">
|
||||
NAT Traversal
|
||||
</h1>
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteNAT,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">NAT Traversal</h1>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteNAT,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<NATCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelNotificationGroupResponseItem } from "@/types"
|
||||
import { deleteNotificationGroups } from "@/api/notification-group"
|
||||
import { GroupTab } from "@/components/group-tab"
|
||||
import { NotificationGroupCard } from "@/components/notification-group"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelNotificationGroupResponseItem } from "@/types";
|
||||
import { deleteNotificationGroups } from "@/api/notification-group";
|
||||
import { GroupTab } from "@/components/group-tab";
|
||||
import { NotificationGroupCard } from "@/components/notification-group";
|
||||
|
||||
export default function NotificationGroupPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>("/api/v1/notification-group", swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
|
||||
"/api/v1/notification-group",
|
||||
swrFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
|
||||
{
|
||||
@@ -30,7 +39,7 @@ export default function NotificationGroupPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -49,49 +58,48 @@ export default function NotificationGroupPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.group.id,
|
||||
accessorFn: (row) => row.group.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.group.name,
|
||||
accessorFn: (row) => row.group.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{s.group.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Notifiers (ID)",
|
||||
accessorKey: "notifications",
|
||||
accessorFn: row => row.notifications,
|
||||
accessorFn: (row) => row.notifications,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteNotificationGroups,
|
||||
id: s.group.id,
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteNotificationGroups,
|
||||
id: s.group.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<NotificationGroupCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -99,64 +107,60 @@ export default function NotificationGroupPage() {
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteNotificationGroups,
|
||||
id: selectedRows.map(r => r.original.group.id),
|
||||
mutate: mutate
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex gap-2 ml-auto"
|
||||
delete={{
|
||||
fn: deleteNotificationGroups,
|
||||
id: selectedRows.map((r) => r.original.group.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<NotificationGroupCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelNotification } from "@/types"
|
||||
import { deleteNotification } from "@/api/notification"
|
||||
import { NotificationTab } from "@/components/notification-tab"
|
||||
import { NotifierCard } from "@/components/notifier"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelNotification } from "@/types";
|
||||
import { deleteNotification } from "@/api/notification";
|
||||
import { NotificationTab } from "@/components/notification-tab";
|
||||
import { NotifierCard } from "@/components/notifier";
|
||||
import { useNotification } from "@/hooks/useNotfication";
|
||||
|
||||
export default function NotificationPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>("/api/v1/notification", swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
|
||||
"/api/v1/notification",
|
||||
swrFetcher
|
||||
);
|
||||
const { notifierGroup } = useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelNotification>[] = [
|
||||
{
|
||||
@@ -32,7 +41,7 @@ export default function NotificationPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -51,71 +60,68 @@ export default function NotificationPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Groups",
|
||||
accessorKey: "groups",
|
||||
accessorFn: row => {
|
||||
return notifierGroup?.filter(ng => ng.notifications?.includes(row.id))
|
||||
.map(ng => ng.group.id)
|
||||
|| [];
|
||||
accessorFn: (row) => {
|
||||
return (
|
||||
notifierGroup
|
||||
?.filter((ng) => ng.notifications?.includes(row.id))
|
||||
.map((ng) => ng.group.id) || []
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "URL",
|
||||
accessorKey: "url",
|
||||
accessorFn: row => row.url,
|
||||
accessorFn: (row) => row.url,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-64 whitespace-normal break-words">
|
||||
{s.url}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Verify TLS",
|
||||
accessorKey: "verify_tls",
|
||||
accessorFn: row => row.verify_tls,
|
||||
accessorFn: (row) => row.verify_tls,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteNotification,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteNotification,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<NotifierCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -123,64 +129,60 @@ export default function NotificationPage() {
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteNotification,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex gap-2 ml-auto"
|
||||
delete={{
|
||||
fn: deleteNotification,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<NotifierCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelServerGroupResponseItem } from "@/types"
|
||||
import { deleteServerGroups } from "@/api/server-group"
|
||||
import { GroupTab } from "@/components/group-tab"
|
||||
import { ServerGroupCard } from "@/components/server-group"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelServerGroupResponseItem } from "@/types";
|
||||
import { deleteServerGroups } from "@/api/server-group";
|
||||
import { GroupTab } from "@/components/group-tab";
|
||||
import { ServerGroupCard } from "@/components/server-group";
|
||||
|
||||
export default function ServerGroupPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>("/api/v1/server-group", swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>(
|
||||
"/api/v1/server-group",
|
||||
swrFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
|
||||
{
|
||||
@@ -30,7 +39,7 @@ export default function ServerGroupPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -49,49 +58,48 @@ export default function ServerGroupPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.group.id,
|
||||
accessorFn: (row) => row.group.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.group.name,
|
||||
accessorFn: (row) => row.group.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{s.group.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Servers (ID)",
|
||||
accessorKey: "servers",
|
||||
accessorFn: row => row.servers,
|
||||
accessorFn: (row) => row.servers,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteServerGroups,
|
||||
id: s.group.id,
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteServerGroups,
|
||||
id: s.group.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<ServerGroupCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -99,64 +107,59 @@ export default function ServerGroupPage() {
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteServerGroups,
|
||||
id: selectedRows.map(r => r.original.group.id),
|
||||
mutate: mutate
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteServerGroups,
|
||||
id: selectedRows.map((r) => r.original.group.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<ServerGroupCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ModelServer as Server, ModelForceUpdateResponse } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { deleteServer, forceUpdateServer } from "@/api/server"
|
||||
import { ServerCard } from "@/components/server"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { InstallCommandsMenu } from "@/components/install-commands"
|
||||
import { NoteMenu } from "@/components/note-menu"
|
||||
import { TerminalButton } from "@/components/terminal"
|
||||
import { useServer } from "@/hooks/useServer"
|
||||
import { joinIP } from "@/lib/utils"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelServer as Server, ModelForceUpdateResponse } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { deleteServer, forceUpdateServer } from "@/api/server";
|
||||
import { ServerCard } from "@/components/server";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { IconButton } from "@/components/xui/icon-button";
|
||||
import { InstallCommandsMenu } from "@/components/install-commands";
|
||||
import { NoteMenu } from "@/components/note-menu";
|
||||
import { TerminalButton } from "@/components/terminal";
|
||||
import { useServer } from "@/hooks/useServer";
|
||||
import { joinIP } from "@/lib/utils";
|
||||
|
||||
export default function ServerPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<Server[]>('/api/v1/server', swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher);
|
||||
const { serverGroups } = useServer();
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<Server>[] = [
|
||||
{
|
||||
@@ -36,7 +42,7 @@ export default function ServerPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -55,28 +61,24 @@ export default function ServerPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => `${row.id}(${row.display_index})`,
|
||||
accessorFn: (row) => `${row.id}(${row.display_index})`,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Groups",
|
||||
accessorKey: "groups",
|
||||
accessorFn: row => {
|
||||
return serverGroups?.filter(sg => sg.servers?.includes(row.id))
|
||||
.map(sg => sg.group.id)
|
||||
|| [];
|
||||
accessorFn: (row) => {
|
||||
return (
|
||||
serverGroups?.filter((sg) => sg.servers?.includes(row.id)).map((sg) => sg.group.id) || []
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,27 +86,23 @@ export default function ServerPage() {
|
||||
header: "IP",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{joinIP(s.geoip?.ip)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{joinIP(s.geoip?.ip)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "host.version",
|
||||
accessorFn: row => row.host.version || "Unknown",
|
||||
accessorFn: (row) => row.host.version || "Unknown",
|
||||
},
|
||||
{
|
||||
header: "Enable DDNS",
|
||||
accessorKey: "enableDDNS",
|
||||
accessorFn: row => row.enable_ddns ?? false,
|
||||
accessorFn: (row) => row.enable_ddns ?? false,
|
||||
},
|
||||
{
|
||||
header: "Hide from Guest",
|
||||
accessorKey: "hideForGuest",
|
||||
accessorFn: row => row.hide_for_guest ?? false,
|
||||
accessorFn: (row) => row.hide_for_guest ?? false,
|
||||
},
|
||||
{
|
||||
id: "installCommands",
|
||||
@@ -125,42 +123,47 @@ export default function ServerPage() {
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteServer, id: s.id, mutate: mutate }}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{ fn: deleteServer, id: s.id, mutate: mutate }}
|
||||
>
|
||||
<>
|
||||
<TerminalButton id={s.id} />
|
||||
<ServerCard mutate={mutate} data={s} />
|
||||
</>
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Server
|
||||
</h1>
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteServer,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<IconButton icon="update" onClick={
|
||||
async () => {
|
||||
const id = selectedRows.map(r => r.original.id);
|
||||
<h1 className="text-3xl font-bold tracking-tight">Server</h1>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteServer,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
icon="update"
|
||||
onClick={async () => {
|
||||
const id = selectedRows.map((r) => r.original.id);
|
||||
if (id.length < 1) {
|
||||
toast("Error", {
|
||||
description: "No rows are selected."
|
||||
description: "No rows are selected.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -172,69 +175,63 @@ export default function ServerPage() {
|
||||
console.error(e);
|
||||
toast("Error executing task", {
|
||||
description: "Please see the console for details.",
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast("Task executed successfully", {
|
||||
description: `Result (Server ID):
|
||||
${resp.success?.length ? `Success: ${resp.success.join(",")}, ` : ''}
|
||||
${resp.failure?.length ? `Failure: ${resp.failure.join(",")}, ` : ''}
|
||||
${resp.offline?.length ? `Offline: ${resp.offline.join(",")}` : ''}
|
||||
`
|
||||
})
|
||||
}} />
|
||||
${resp.success?.length ? `Success: ${resp.success.join(",")}, ` : ""}
|
||||
${resp.failure?.length ? `Failure: ${resp.failure.join(",")}, ` : ""}
|
||||
${resp.offline?.length ? `Offline: ${resp.offline.join(",")}` : ""}
|
||||
`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { ServiceCard } from "@/components/service"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { conv } from "@/lib/utils"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { serviceTypes } from "@/types"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { deleteService } from "@/api/service"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ServiceCard } from "@/components/service";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { conv } from "@/lib/utils";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { serviceTypes } from "@/types";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { deleteService } from "@/api/service";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ServicePage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelServiceResponse>('/api/v1/service', swrFetcher);
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelServiceResponse>(
|
||||
"/api/v1/service",
|
||||
swrFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<Service>[] = [
|
||||
{
|
||||
@@ -31,7 +40,7 @@ export default function ServicePage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -50,105 +59,100 @@ export default function ServicePage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "service.id",
|
||||
accessorFn: row => row.service.id,
|
||||
accessorFn: (row) => row.service.id,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorFn: row => row.service.name,
|
||||
accessorFn: (row) => row.service.name,
|
||||
accessorKey: "service.name",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.service.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.service.name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Target",
|
||||
accessorFn: row => row.service.target,
|
||||
accessorFn: (row) => row.service.target,
|
||||
accessorKey: "service.target",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.service.target}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.service.target}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Coverage",
|
||||
accessorKey: "service.cover",
|
||||
accessorFn: row => row.service.cover,
|
||||
accessorFn: (row) => row.service.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original.service;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>Cover All</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>Ignore All</span>
|
||||
}
|
||||
case 0: {
|
||||
return <span>Cover All</span>;
|
||||
}
|
||||
case 1: {
|
||||
return <span>Ignore All</span>;
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Specific Servers",
|
||||
accessorKey: "service.skipServers",
|
||||
accessorFn: row => Object.keys(row.service.skip_servers ?? {}),
|
||||
accessorFn: (row) => Object.keys(row.service.skip_servers ?? {}),
|
||||
},
|
||||
{
|
||||
header: "Type",
|
||||
accessorKey: "service.type",
|
||||
accessorFn: row => row.service.type,
|
||||
cell: ({ row }) => serviceTypes[row.original.service.type] || '',
|
||||
accessorFn: (row) => row.service.type,
|
||||
cell: ({ row }) => serviceTypes[row.original.service.type] || "",
|
||||
},
|
||||
{
|
||||
header: "Interval",
|
||||
accessorKey: "service.duration",
|
||||
accessorFn: row => row.service.duration,
|
||||
accessorFn: (row) => row.service.duration,
|
||||
},
|
||||
{
|
||||
header: "Notifier Group ID",
|
||||
accessorKey: "service.ngroup",
|
||||
accessorFn: row => row.service.notification_group_id,
|
||||
accessorFn: (row) => row.service.notification_group_id,
|
||||
},
|
||||
{
|
||||
header: "On Trigger",
|
||||
accessorKey: "service.triggerTask",
|
||||
accessorFn: row => row.service.enable_trigger_task ?? false,
|
||||
accessorFn: (row) => row.service.enable_trigger_task ?? false,
|
||||
},
|
||||
{
|
||||
header: "Tasks to trigger on alert",
|
||||
accessorKey: "service.failTriggerTasks",
|
||||
accessorFn: row => row.service.fail_trigger_tasks,
|
||||
accessorFn: (row) => row.service.fail_trigger_tasks,
|
||||
},
|
||||
{
|
||||
header: "Tasks to trigger after recovery",
|
||||
accessorKey: "service.recoverTriggerTasks",
|
||||
accessorFn: row => row.service.recover_trigger_tasks,
|
||||
accessorFn: (row) => row.service.recover_trigger_tasks,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteService, id: s.service.id, mutate: mutate }}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{ fn: deleteService, id: s.service.id, mutate: mutate }}
|
||||
>
|
||||
<ServiceCard mutate={mutate} data={s.service} />
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const dataArr = useMemo(() => {
|
||||
return conv.recordToArr(data?.services ?? {});
|
||||
@@ -158,74 +162,68 @@ export default function ServicePage() {
|
||||
data: dataArr,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">
|
||||
Service
|
||||
</h1>
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteService,
|
||||
id: selectedRows.map(r => r.original.service.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">Service</h1>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex ml-auto gap-2"
|
||||
delete={{
|
||||
fn: deleteService,
|
||||
id: selectedRows.map((r) => r.original.service.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<ServiceCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { z } from "zod"
|
||||
import { asOptionalField } from "@/lib/utils"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types";
|
||||
import { SettingsTab } from "@/components/settings-tab";
|
||||
import { z } from "zod";
|
||||
import { asOptionalField } from "@/lib/utils";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -13,24 +13,21 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { getSettings, updateSettings } from "@/api/settings"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
} from "@/components/ui/card"
|
||||
} from "@/components/ui/form";
|
||||
import { getSettings, updateSettings } from "@/api/settings";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const settingFormSchema = z.object({
|
||||
custom_nameservers: asOptionalField(z.string()),
|
||||
@@ -56,8 +53,8 @@ export default function SettingsPage() {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -67,21 +64,23 @@ export default function SettingsPage() {
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e);
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const form = useForm<z.infer<typeof settingFormSchema>>({
|
||||
resolver: zodResolver(settingFormSchema),
|
||||
defaultValues: config ? config : {
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
},
|
||||
defaultValues: config
|
||||
? config
|
||||
: {
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
@@ -101,9 +100,9 @@ export default function SettingsPage() {
|
||||
} finally {
|
||||
toast("Success", {
|
||||
description: "Config updated successfully.",
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
@@ -118,9 +117,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Site Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -141,7 +138,9 @@ export default function SettingsPage() {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.entries(nezhaLang).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>{v}</SelectItem>
|
||||
<SelectItem key={k} value={k}>
|
||||
{v}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -157,10 +156,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Custom Codes (Style and Script)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="resize-y min-h-48"
|
||||
{...field}
|
||||
/>
|
||||
<Textarea className="resize-y min-h-48" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -173,10 +169,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Custom Codes for Dashboard</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="resize-y min-h-48"
|
||||
{...field}
|
||||
/>
|
||||
<Textarea className="resize-y min-h-48" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -189,9 +182,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Dashboard Server Domain/IP without CDN</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -202,11 +193,11 @@ export default function SettingsPage() {
|
||||
name="custom_nameservers"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Custom Public DNS Nameservers for DDNS (separate with comma)</FormLabel>
|
||||
<FormLabel>
|
||||
Custom Public DNS Nameservers for DDNS (separate with comma)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -219,10 +210,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Real IP Header</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="NZ::Use-Peer-IP"
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder="NZ::Use-Peer-IP" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -247,7 +235,9 @@ export default function SettingsPage() {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.entries(settingCoverageTypes).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>{v}</SelectItem>
|
||||
<SelectItem key={k} value={k}>
|
||||
{v}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -262,10 +252,7 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>Specific Servers (separate with comma)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1,2,3"
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder="1,2,3" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -278,10 +265,7 @@ export default function SettingsPage() {
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Label className="text-sm">Enable</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
@@ -300,11 +284,10 @@ export default function SettingsPage() {
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">Show Full IP Address in Notification Messages</Label>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Label className="text-sm">
|
||||
Show Full IP Address in Notification Messages
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -316,5 +299,5 @@ export default function SettingsPage() {
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelUser } from "@/types"
|
||||
import { deleteUser } from "@/api/user"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { UserCard } from "@/components/user"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelUser } from "@/types";
|
||||
import { deleteUser } from "@/api/user";
|
||||
import { SettingsTab } from "@/components/settings-tab";
|
||||
import { UserCard } from "@/components/user";
|
||||
|
||||
export default function UserPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher);
|
||||
@@ -20,8 +26,8 @@ export default function UserPage() {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelUser>[] = [
|
||||
{
|
||||
@@ -30,7 +36,7 @@ export default function UserPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -49,36 +55,39 @@ export default function UserPage() {
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "id",
|
||||
accessorFn: row => row.id,
|
||||
accessorFn: (row) => row.id,
|
||||
},
|
||||
{
|
||||
header: "Username",
|
||||
accessorKey: "username",
|
||||
accessorFn: row => row.username,
|
||||
accessorFn: (row) => row.username,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteUser,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteUser,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<></>
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -86,64 +95,60 @@ export default function UserPage() {
|
||||
<div className="px-8">
|
||||
<SettingsTab className="mt-6 w-full" />
|
||||
<div className="flex mt-4 mb-4">
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteUser,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex gap-2 ml-auto"
|
||||
delete={{
|
||||
fn: deleteUser,
|
||||
id: selectedRows.map((r) => r.original.id),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<UserCard mutate={mutate} />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import useSWR from "swr"
|
||||
import { useEffect } from "react"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "sonner"
|
||||
import { ModelWAF, wafBlockReasons } from "@/types"
|
||||
import { deleteWAF } from "@/api/waf"
|
||||
import { ip16Str } from "@/lib/utils"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelWAF, wafBlockReasons } from "@/types";
|
||||
import { deleteWAF } from "@/api/waf";
|
||||
import { ip16Str } from "@/lib/utils";
|
||||
import { SettingsTab } from "@/components/settings-tab";
|
||||
|
||||
export default function WAFPage() {
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelWAF[]>("/api/v1/waf", swrFetcher);
|
||||
@@ -20,8 +26,8 @@ export default function WAFPage() {
|
||||
if (error)
|
||||
toast("Error", {
|
||||
description: `Error fetching resource: ${error.message}.`,
|
||||
})
|
||||
}, [error])
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
const columns: ColumnDef<ModelWAF>[] = [
|
||||
{
|
||||
@@ -30,7 +36,7 @@ export default function WAFPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -49,54 +55,55 @@ export default function WAFPage() {
|
||||
{
|
||||
header: "IP",
|
||||
accessorKey: "ip",
|
||||
accessorFn: row => ip16Str(row.ip ?? []),
|
||||
accessorFn: (row) => ip16Str(row.ip ?? []),
|
||||
},
|
||||
{
|
||||
header: "Count",
|
||||
accessorKey: "count",
|
||||
accessorFn: row => row.count,
|
||||
accessorFn: (row) => row.count,
|
||||
},
|
||||
{
|
||||
header: "Last Block Reason",
|
||||
accessorKey: "lastBlockReason",
|
||||
accessorFn: row => row.last_block_reason,
|
||||
cell: ({ row }) => (
|
||||
<span>{wafBlockReasons[row.original.last_block_reason] || ''}</span>
|
||||
)
|
||||
accessorFn: (row) => row.last_block_reason,
|
||||
cell: ({ row }) => <span>{wafBlockReasons[row.original.last_block_reason] || ""}</span>,
|
||||
},
|
||||
{
|
||||
header: "Last Block Time",
|
||||
accessorKey: "lastBlockTime",
|
||||
accessorFn: row => row.last_block_timestamp,
|
||||
accessorFn: (row) => row.last_block_timestamp,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const date = new Date(s.last_block_timestamp || 0);
|
||||
return <span>{date.toISOString()}</span>
|
||||
}
|
||||
return <span>{date.toISOString()}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original
|
||||
const s = row.original;
|
||||
return (
|
||||
<ActionButtonGroup className="flex gap-2" delete={{
|
||||
fn: deleteWAF,
|
||||
id: ip16Str(s.ip ?? []),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
delete={{
|
||||
fn: deleteWAF,
|
||||
id: ip16Str(s.ip ?? []),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<></>
|
||||
</ActionButtonGroup>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
|
||||
@@ -104,64 +111,59 @@ export default function WAFPage() {
|
||||
<div className="px-8">
|
||||
<SettingsTab className="mt-6 w-full" />
|
||||
<div className="flex mt-4 mb-4">
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteWAF,
|
||||
id: selectedRows.map(r => ip16Str(r.original.ip ?? [])),
|
||||
mutate: mutate,
|
||||
}}>
|
||||
<HeaderButtonGroup
|
||||
className="flex-2 flex gap-2 ml-auto"
|
||||
delete={{
|
||||
fn: deleteWAF,
|
||||
id: selectedRows.map((r) => ip16Str(r.original.ip ?? [])),
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<></>
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading ...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="text-xsm">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user