mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-05-06 05:38:51 +00:00
Dashboard Redesign (#48)
* feat: add user_template setting * style: header * style: page padding * style: header * feat: header now time * style: login page * feat: nav indicator * style: button inset shadow * style: footer text size * feat: header show login_ip * fix: error toast * fix: frontend_templates setting * fix: lint * feat: pr auto format * chore: auto-fix linting and formatting issues --------- Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
+41
-35
@@ -1,5 +1,10 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { deleteAlertRules } from "@/api/alert-rule"
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { AlertRuleCard } from "@/components/alert-rule"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { NotificationTab } from "@/components/notification-tab"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,35 +12,29 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelAlertRule, triggerModes } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function AlertRulePage() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
|
||||
"/api/v1/alert-rule",
|
||||
swrFetcher
|
||||
);
|
||||
swrFetcher,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelAlertRule>[] = [
|
||||
{
|
||||
@@ -70,8 +69,8 @@ export default function AlertRulePage() {
|
||||
accessorKey: "name",
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -87,8 +86,12 @@ export default function AlertRulePage() {
|
||||
{
|
||||
header: t("Rules"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-48 whitespace-normal break-words">{JSON.stringify(s.rules)}</div>;
|
||||
const s = row.original
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{JSON.stringify(s.rules)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,7 +113,7 @@ export default function AlertRulePage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -122,25 +125,25 @@ export default function AlertRulePage() {
|
||||
>
|
||||
<AlertRuleCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup
|
||||
@@ -164,9 +167,12 @@ export default function AlertRulePage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -198,5 +204,5 @@ export default function AlertRulePage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+66
-58
@@ -1,5 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteCron, runCron } from "@/api/cron"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { CronCard } from "@/components/cron"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,32 +11,29 @@ import {
|
||||
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, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { ModelCron } from "@/types"
|
||||
import { cronTypes } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function CronPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelCron>[] = [
|
||||
{
|
||||
@@ -41,7 +42,7 @@ export default function CronPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -66,8 +67,8 @@ export default function CronPage() {
|
||||
header: t("Name"),
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,8 +85,8 @@ export default function CronPage() {
|
||||
header: t("Command"),
|
||||
accessorKey: "command",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,24 +104,24 @@ export default function CronPage() {
|
||||
accessorKey: "cover",
|
||||
accessorFn: (row) => row.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
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>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -133,8 +134,12 @@ export default function CronPage() {
|
||||
accessorKey: "lastExecution",
|
||||
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>;
|
||||
const s = row.original
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.last_executed_at}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,7 +151,7 @@ export default function CronPage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -158,43 +163,43 @@ export default function CronPage() {
|
||||
icon="play"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await runCron(s.id);
|
||||
await runCron(s.id)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.UnExpectedError"),
|
||||
});
|
||||
await mutate();
|
||||
return;
|
||||
})
|
||||
await mutate()
|
||||
return
|
||||
}
|
||||
toast(t("Success"), {
|
||||
description: t("Results.TaskTriggeredSuccessfully"),
|
||||
});
|
||||
await mutate();
|
||||
})
|
||||
await mutate()
|
||||
}}
|
||||
/>
|
||||
<CronCard mutate={mutate} data={s} />
|
||||
</>
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1>
|
||||
<HeaderButtonGroup
|
||||
@@ -218,9 +223,12 @@ export default function CronPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -252,5 +260,5 @@ export default function CronPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+55
-44
@@ -1,6 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DDNSCard } from "@/components/ddns";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { DDNSCard } from "@/components/ddns"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,38 +11,39 @@ import {
|
||||
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, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelDDNSProfile } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function DDNSPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
|
||||
const [providers, setProviders] = useState<string[]>([]);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>(
|
||||
"/api/v1/ddns",
|
||||
swrFetcher,
|
||||
)
|
||||
const [providers, setProviders] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
const fetchedProviders = await getDDNSProviders();
|
||||
setProviders(fetchedProviders);
|
||||
};
|
||||
fetchProviders();
|
||||
}, []);
|
||||
const fetchedProviders = await getDDNSProviders()
|
||||
setProviders(fetchedProviders)
|
||||
}
|
||||
fetchProviders()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelDDNSProfile>[] = [
|
||||
{
|
||||
@@ -48,7 +52,7 @@ export default function DDNSPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -74,8 +78,8 @@ export default function DDNSPage() {
|
||||
accessorKey: "name",
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -94,12 +98,12 @@ export default function DDNSPage() {
|
||||
accessorFn: (row) => row.provider,
|
||||
},
|
||||
{
|
||||
header: t('Domains'),
|
||||
header: t("Domains"),
|
||||
accessorKey: "domains",
|
||||
accessorFn: (row) => row.domains,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,33 +115,37 @@ export default function DDNSPage() {
|
||||
id: "actions",
|
||||
header: t("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 }}
|
||||
delete={{
|
||||
fn: deleteDDNSProfiles,
|
||||
id: s.id,
|
||||
mutate: mutate,
|
||||
}}
|
||||
>
|
||||
<DDNSCard mutate={mutate} data={s} providers={providers} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
|
||||
<HeaderButtonGroup
|
||||
@@ -161,9 +169,12 @@ export default function DDNSPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -195,5 +206,5 @@ export default function DDNSPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+14
-12
@@ -1,7 +1,3 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
@@ -13,9 +9,11 @@ import {
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import i18next from "i18next"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import i18next from "i18next";
|
||||
import { z } from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
@@ -23,10 +21,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: i18next.t("Results.PasswordRequired"),
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
function Login() {
|
||||
const { login } = useAuth()
|
||||
|
||||
@@ -42,10 +39,10 @@ function Login() {
|
||||
login(values.username, values.password)
|
||||
}
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="my-8 max-w-xl m-auto">
|
||||
<div className="mt-28 max-w-sm m-auto">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
@@ -68,7 +65,12 @@ function Login() {
|
||||
<FormItem>
|
||||
<FormLabel>{t("Password")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="admin" autoComplete="current-password" {...field} />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="admin"
|
||||
autoComplete="current-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -81,4 +83,4 @@ function Login() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
export default Login
|
||||
|
||||
+43
-39
@@ -1,6 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { NATCard } from "@/components/nat";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteNAT } from "@/api/nat"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { NATCard } from "@/components/nat"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,29 +11,27 @@ import {
|
||||
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, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelNAT } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function NATPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelNAT>[] = [
|
||||
{
|
||||
@@ -39,7 +40,7 @@ export default function NATPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -65,12 +66,12 @@ export default function NATPage() {
|
||||
accessorKey: "name",
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t("Server")+" ID",
|
||||
header: t("Server") + " ID",
|
||||
accessorKey: "serverID",
|
||||
accessorFn: (row) => row.server_id,
|
||||
},
|
||||
@@ -79,8 +80,8 @@ export default function NATPage() {
|
||||
accessorKey: "host",
|
||||
accessorFn: (row) => row.host,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -88,15 +89,15 @@ export default function NATPage() {
|
||||
accessorKey: "domain",
|
||||
accessorFn: (row) => row.domain,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -104,25 +105,25 @@ export default function NATPage() {
|
||||
>
|
||||
<NATCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
|
||||
<HeaderButtonGroup
|
||||
@@ -146,9 +147,12 @@ export default function NATPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -180,5 +184,5 @@ export default function NATPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteNotificationGroups } from "@/api/notification-group"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { GroupTab } from "@/components/group-tab"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { NotificationGroupCard } from "@/components/notification-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,34 +12,30 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelNotificationGroupResponseItem } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function NotificationGroupPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
|
||||
"/api/v1/notification-group",
|
||||
swrFetcher
|
||||
);
|
||||
swrFetcher,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
|
||||
{
|
||||
@@ -43,7 +44,7 @@ export default function NotificationGroupPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -69,12 +70,12 @@ export default function NotificationGroupPage() {
|
||||
accessorKey: "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>;
|
||||
const s = row.original
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t("Notifier")+"(ID)",
|
||||
header: t("Notifier") + "(ID)",
|
||||
accessorKey: "notifications",
|
||||
accessorFn: (row) => row.notifications,
|
||||
},
|
||||
@@ -82,7 +83,7 @@ export default function NotificationGroupPage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -94,25 +95,25 @@ export default function NotificationGroupPage() {
|
||||
>
|
||||
<NotificationGroupCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup
|
||||
@@ -136,9 +137,12 @@ export default function NotificationGroupPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -170,5 +174,5 @@ export default function NotificationGroupPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+45
-42
@@ -1,5 +1,10 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteNotification } from "@/api/notification"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { NotificationTab } from "@/components/notification-tab"
|
||||
import { NotifierCard } from "@/components/notifier"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,37 +12,32 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
} from "@/components/ui/table"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
import { ModelNotification } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function NotificationPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
|
||||
"/api/v1/notification",
|
||||
swrFetcher
|
||||
);
|
||||
const { notifierGroup } = useNotification();
|
||||
swrFetcher,
|
||||
)
|
||||
const { notifierGroup } = useNotification()
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelNotification>[] = [
|
||||
{
|
||||
@@ -46,7 +46,7 @@ export default function NotificationPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -72,8 +72,8 @@ export default function NotificationPage() {
|
||||
accessorKey: "name",
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,7 +84,7 @@ export default function NotificationPage() {
|
||||
notifierGroup
|
||||
?.filter((ng) => ng.notifications?.includes(row.id))
|
||||
.map((ng) => ng.group.id) || []
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -92,8 +92,8 @@ export default function NotificationPage() {
|
||||
accessorKey: "url",
|
||||
accessorFn: (row) => row.url,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -105,7 +105,7 @@ export default function NotificationPage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -117,25 +117,25 @@ export default function NotificationPage() {
|
||||
>
|
||||
<NotifierCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup
|
||||
@@ -159,9 +159,12 @@ export default function NotificationPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -193,5 +196,5 @@ export default function NotificationPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,40 +1,45 @@
|
||||
import { ProfileCard } from "@/components/profile"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useMainStore } from "@/hooks/useMainStore"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
import { Server, Boxes } from "lucide-react";
|
||||
import { useServer } from "@/hooks/useServer";
|
||||
import { ProfileCard } from "@/components/profile";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
||||
import { useServer } from "@/hooks/useServer"
|
||||
import { Boxes, Server } from "lucide-react"
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { profile } = useMainStore();
|
||||
const { servers, serverGroups } = useServer();
|
||||
const { profile } = useMainStore()
|
||||
const { servers, serverGroups } = useServer()
|
||||
const isDesktop = useMediaQuery("(min-width: 890px)")
|
||||
|
||||
return (
|
||||
profile && (
|
||||
<div className={`flex p-8 gap-4 ${isDesktop ? 'ml-6' : 'flex-col'}`}>
|
||||
<div className={`flex ${isDesktop ? 'flex-col mr-6' : 'gap-4 w-full items-center'}`}>
|
||||
<Avatar className={`${isDesktop ? 'h-[300px] w-[300px]' : 'h-[150px] w-[150px]'} border-foreground border-[1px]`}>
|
||||
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
|
||||
<div className={`flex p-8 gap-4 ${isDesktop ? "ml-6" : "flex-col"}`}>
|
||||
<div
|
||||
className={`flex ${isDesktop ? "flex-col mr-6" : "gap-4 w-full items-center"}`}
|
||||
>
|
||||
<Avatar
|
||||
className={`${isDesktop ? "h-[300px] w-[300px]" : "h-[150px] w-[150px]"} border-foreground border-[1px]`}
|
||||
>
|
||||
<AvatarImage
|
||||
src={
|
||||
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
|
||||
profile.username
|
||||
}
|
||||
alt={profile.username}
|
||||
/>
|
||||
<AvatarFallback>{profile.username}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="justify-center text-3xl font-semibold">{profile.username}</p>
|
||||
<p className="text-gray-400">IP: {profile.login_ip || 'Unknown'}</p>
|
||||
<p className="text-gray-400">IP: {profile.login_ip || "Unknown"}</p>
|
||||
</div>
|
||||
{isDesktop &&
|
||||
{isDesktop && (
|
||||
<ProfileCard className="flex mt-4 justify-center items-center max-w-[300px] rounded-lg" />
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{!isDesktop &&
|
||||
{!isDesktop && (
|
||||
<ProfileCard className="flex justify-center items-center max-w-full rounded-lg" />
|
||||
}
|
||||
)}
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="w-full">
|
||||
@@ -61,5 +66,5 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+13
-10
@@ -1,16 +1,19 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { Navigate } from "react-router-dom"
|
||||
|
||||
export const ProtectedRoute = ({ children }: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { profile } = useAuth();
|
||||
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const { profile } = useAuth()
|
||||
|
||||
if (!profile && window.location.pathname !== "/dashboard/login") {
|
||||
return <><Navigate to="/dashboard/login" />{children}</>;
|
||||
return (
|
||||
<>
|
||||
<Navigate to="/dashboard/login" />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
return children
|
||||
}
|
||||
|
||||
export default ProtectedRoute;
|
||||
export default ProtectedRoute
|
||||
|
||||
+19
-20
@@ -1,34 +1,33 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import Header from "@/components/header";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSetting from "@/hooks/useSetting";
|
||||
import Header from "@/components/header"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import useSetting from "@/hooks/useSetting"
|
||||
import { useEffect } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
export default function Root() {
|
||||
const { t } = useTranslation();
|
||||
const settings = useSetting();
|
||||
const { t } = useTranslation()
|
||||
const settings = useSetting()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = settings?.site_name || "哪吒监控 Nezha Monitoring";
|
||||
}, [settings]);
|
||||
document.title = settings?.site_name || "哪吒监控 Nezha Monitoring"
|
||||
}, [settings])
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<Card className="text-sm max-w-7xl mx-auto mt-5 min-h-[90%] flex flex-col justify-between">
|
||||
<section className="text-sm mx-auto h-full flex flex-col justify-between">
|
||||
<div>
|
||||
<Header />
|
||||
<Outlet />
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<footer className="mx-5 pb-5 text-foreground/60 font-thin text-center">
|
||||
© 2019-2024 {t('nezha')} {settings?.version}
|
||||
<footer className="mx-5 pb-5 text-foreground/50 font-light text-xs text-center">
|
||||
© 2019-2024 {t("nezha")} {settings?.version}
|
||||
</footer>
|
||||
</Card>
|
||||
</section>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+41
-37
@@ -1,5 +1,10 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteServerGroups } from "@/api/server-group"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { GroupTab } from "@/components/group-tab"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { ServerGroupCard } from "@/components/server-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,34 +12,30 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelServerGroupResponseItem } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function ServerGroupPage() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>(
|
||||
"/api/v1/server-group",
|
||||
swrFetcher
|
||||
);
|
||||
swrFetcher,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
|
||||
{
|
||||
@@ -43,7 +44,7 @@ export default function ServerGroupPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -69,12 +70,12 @@ export default function ServerGroupPage() {
|
||||
accessorKey: "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>;
|
||||
const s = row.original
|
||||
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t("Server")+"(ID)",
|
||||
header: t("Server") + "(ID)",
|
||||
accessorKey: "servers",
|
||||
accessorFn: (row) => row.servers,
|
||||
},
|
||||
@@ -82,7 +83,7 @@ export default function ServerGroupPage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -94,25 +95,25 @@ export default function ServerGroupPage() {
|
||||
>
|
||||
<ServerGroupCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup
|
||||
@@ -135,9 +136,12 @@ export default function ServerGroupPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -169,5 +173,5 @@ export default function ServerGroupPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+73
-58
@@ -1,5 +1,12 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteServer, forceUpdateServer } from "@/api/server"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { InstallCommandsMenu } from "@/components/install-commands"
|
||||
import { NoteMenu } from "@/components/note-menu"
|
||||
import { ServerCard } from "@/components/server"
|
||||
import { TerminalButton } from "@/components/terminal"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,37 +14,29 @@ import {
|
||||
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, useMemo } 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 { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { useServer } from "@/hooks/useServer"
|
||||
import { joinIP } from "@/lib/utils"
|
||||
import { ModelForceUpdateResponse, ModelServer as Server } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function ServerPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher);
|
||||
const { serverGroups } = useServer();
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher)
|
||||
const { serverGroups } = useServer()
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<Server>[] = [
|
||||
{
|
||||
@@ -72,8 +71,8 @@ export default function ServerPage() {
|
||||
accessorKey: "name",
|
||||
accessorFn: (row) => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -81,16 +80,22 @@ export default function ServerPage() {
|
||||
accessorKey: "groups",
|
||||
accessorFn: (row) => {
|
||||
return (
|
||||
serverGroups?.filter((sg) => sg.servers?.includes(row.id)).map((sg) => sg.group.id) || []
|
||||
);
|
||||
serverGroups
|
||||
?.filter((sg) => sg.servers?.includes(row.id))
|
||||
.map((sg) => sg.group.id) || []
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ip",
|
||||
header: "IP",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{joinIP(s.geoip?.ip)}</div>;
|
||||
const s = row.original
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{joinIP(s.geoip?.ip)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -112,15 +117,15 @@ export default function ServerPage() {
|
||||
id: "note",
|
||||
header: t("Note"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <NoteMenu note={{ private: s.note, public: s.public_note }} />;
|
||||
const s = row.original
|
||||
return <NoteMenu note={{ private: s.note, public: s.public_note }} />
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -131,25 +136,25 @@ export default function ServerPage() {
|
||||
<ServerCard mutate={mutate} data={s} />
|
||||
</>
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1>
|
||||
<HeaderButtonGroup
|
||||
@@ -163,33 +168,40 @@ export default function ServerPage() {
|
||||
<IconButton
|
||||
icon="update"
|
||||
onClick={async () => {
|
||||
const id = selectedRows.map((r) => r.original.id);
|
||||
const id = selectedRows.map((r) => r.original.id)
|
||||
if (id.length < 1) {
|
||||
toast(t("Error"), {
|
||||
description: t("Results.SelectAtLeastOneServer"),
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let resp: ModelForceUpdateResponse = {};
|
||||
let resp: ModelForceUpdateResponse = {}
|
||||
try {
|
||||
resp = await forceUpdateServer(id);
|
||||
resp = await forceUpdateServer(id)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.UnExpectedError"),
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
toast(t("Done"), {
|
||||
description: t("Results.ForceUpdate")
|
||||
+ (resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "")
|
||||
+ (resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "")
|
||||
+ (resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : "")
|
||||
});
|
||||
description:
|
||||
t("Results.ForceUpdate") +
|
||||
(resp.success?.length
|
||||
? t(`Success`) + ` [${resp.success.join(",")}]`
|
||||
: "") +
|
||||
(resp.failure?.length
|
||||
? t(`Failure`) + ` [${resp.failure.join(",")}]`
|
||||
: "") +
|
||||
(resp.offline?.length
|
||||
? t(`Offline`) + ` [${resp.offline.join(",")}]`
|
||||
: ""),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<InstallCommandsMenu className="bg-blue-700" />
|
||||
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
|
||||
</HeaderButtonGroup>
|
||||
</div>
|
||||
<Table>
|
||||
@@ -201,9 +213,12 @@ export default function ServerPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -235,5 +250,5 @@ export default function ServerPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+50
-45
@@ -1,6 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ServiceCard } from "@/components/service";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteService } from "@/api/service"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { ServiceCard } from "@/components/service"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,33 +11,28 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ModelService as Service } from "@/types";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ModelService as Service } from "@/types"
|
||||
import { serviceTypes } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function ServicePage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<Service[]>(
|
||||
"/api/v1/service/list",
|
||||
swrFetcher
|
||||
);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<Service[]>("/api/v1/service/list", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<Service>[] = [
|
||||
{
|
||||
@@ -69,8 +67,8 @@ export default function ServicePage() {
|
||||
accessorFn: (row) => row.name,
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,8 +76,8 @@ export default function ServicePage() {
|
||||
accessorFn: (row) => row.target,
|
||||
accessorKey: "target",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.target}</div>;
|
||||
const s = row.original
|
||||
return <div className="max-w-24 whitespace-normal break-words">{s.target}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -87,29 +85,33 @@ export default function ServicePage() {
|
||||
accessorKey: "cover",
|
||||
accessorFn: (row) => row.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>{t("CoverAll")}</span>;
|
||||
return <span>{t("CoverAll")}</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>{t("IgnoreAll")}</span>;
|
||||
return <span>{t("IgnoreAll")}</span>
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t("SpecificServers"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return <div className="max-w-32 whitespace-normal break-words">{Object.keys(s.skip_servers ?? {}).join(',')}</div>;
|
||||
}
|
||||
const s = row.original
|
||||
return (
|
||||
<div className="max-w-32 whitespace-normal break-words">
|
||||
{Object.keys(s.skip_servers ?? {}).join(",")}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t("Type"),
|
||||
@@ -146,7 +148,7 @@ export default function ServicePage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -154,25 +156,25 @@ export default function ServicePage() {
|
||||
>
|
||||
<ServiceCard mutate={mutate} data={s} />
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<div className="flex mt-6 mb-4">
|
||||
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1>
|
||||
<HeaderButtonGroup
|
||||
@@ -196,9 +198,12 @@ export default function ServicePage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -230,5 +235,5 @@ export default function ServicePage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+230
-87
@@ -1,11 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ModelSettingResponse, 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 { getSettings, updateSettings } from "@/api/settings"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -13,23 +10,25 @@ 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 { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { asOptionalField } from "@/lib/utils"
|
||||
import { ModelSettingResponse, nezhaLang, settingCoverageTypes } from "@/types"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
const settingFormSchema = z.object({
|
||||
dns_servers: asOptionalField(z.string()),
|
||||
@@ -47,77 +46,82 @@ const settingFormSchema = z.object({
|
||||
tls: asOptionalField(z.boolean()),
|
||||
enable_ip_change_notification: asOptionalField(z.boolean()),
|
||||
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
|
||||
});
|
||||
})
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [config, setConfig] = useState<ModelSettingResponse>();
|
||||
const [error, setError] = useState<Error>();
|
||||
const { t, i18n } = useTranslation()
|
||||
const [config, setConfig] = useState<ModelSettingResponse>()
|
||||
const [error, setError] = useState<Error>()
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.ErrorFetchingResource", { error: error.message }),
|
||||
});
|
||||
description: t("Results.ErrorFetchingResource", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
}, [error])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
;(async () => {
|
||||
try {
|
||||
const c = await getSettings();
|
||||
setConfig(c);
|
||||
const c = await getSettings()
|
||||
setConfig(c)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e);
|
||||
if (e instanceof Error) setError(e)
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const form = useForm<z.infer<typeof settingFormSchema>>({
|
||||
resolver: zodResolver(settingFormSchema),
|
||||
defaultValues: config
|
||||
? {
|
||||
...config,
|
||||
site_name: config.site_name || "",
|
||||
user_template: config.user_template || Object.keys(config.frontend_templates || {})[0] || "user-dist",
|
||||
}
|
||||
...config,
|
||||
site_name: config.site_name || "",
|
||||
user_template:
|
||||
config.user_template ||
|
||||
Object.keys(config.frontend_templates.filter((t) => !t.is_admin) || {})[0] ||
|
||||
"user-dist",
|
||||
}
|
||||
: {
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
user_template: "user-dist",
|
||||
},
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
user_template: "user-dist",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
form.reset(config);
|
||||
form.reset(config)
|
||||
}
|
||||
}, [config, form]);
|
||||
}, [config, form])
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => {
|
||||
try {
|
||||
await updateSettings(values);
|
||||
const newConfig = await getSettings();
|
||||
setConfig(newConfig);
|
||||
form.reset();
|
||||
await updateSettings(values)
|
||||
const newConfig = await getSettings()
|
||||
setConfig(newConfig)
|
||||
form.reset()
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e);
|
||||
return;
|
||||
if (e instanceof Error) setError(e)
|
||||
return
|
||||
} finally {
|
||||
if (values.language != i18n.language) {
|
||||
i18n.changeLanguage(values.language);
|
||||
i18n.changeLanguage(values.language)
|
||||
}
|
||||
toast(t("Success"));
|
||||
toast(t("Success"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<SettingsTab className="mt-6 mb-4 w-full" />
|
||||
<div>
|
||||
<Form {...form}>
|
||||
@@ -171,9 +175,11 @@ export default function SettingsPage() {
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
const template = config?.frontend_templates?.find(t => t.path === value);
|
||||
const template = config?.frontend_templates?.find(
|
||||
(t) => t.path === value,
|
||||
)
|
||||
if (template) {
|
||||
form.setValue("user_template", template.path ?? '');
|
||||
form.setValue("user_template", template.path)
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -183,13 +189,22 @@ export default function SettingsPage() {
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{(config?.frontend_templates?.filter(t => !t.is_admin) || []).map((template) => (
|
||||
{(
|
||||
config?.frontend_templates.filter(
|
||||
(t) => !t.is_admin,
|
||||
) || []
|
||||
).map((template) => (
|
||||
<div key={template.path}>
|
||||
<SelectItem value={template.path!}>
|
||||
<SelectItem value={template.path}>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="font-medium">
|
||||
{template.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{t("Author")}: {template.author}</span>
|
||||
<span>
|
||||
{t("Author")}:{" "}
|
||||
{template.author}
|
||||
</span>
|
||||
{!template.is_official ? (
|
||||
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
|
||||
{t("Community")}
|
||||
@@ -218,10 +233,102 @@ export default function SettingsPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{!config?.frontend_templates?.find(t => t.path === field.value)?.is_official && (
|
||||
{!config?.frontend_templates?.find(
|
||||
(t) => t.path === field.value,
|
||||
)?.is_official && (
|
||||
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
|
||||
<div className="font-medium text-lg mb-1">{t("CommunityThemeWarning")}</div>
|
||||
<div className="text-yellow-700 dark:text-yellow-200">{t("CommunityThemeDescription")}</div>
|
||||
<div className="font-medium text-lg mb-1">
|
||||
{t("CommunityThemeWarning")}
|
||||
</div>
|
||||
<div className="text-yellow-700 dark:text-yellow-200">
|
||||
{t("CommunityThemeDescription")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user_template"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Theme")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
const template = config?.frontend_templates?.find(
|
||||
(t) => t.path === value,
|
||||
)
|
||||
if (template) {
|
||||
form.setValue(
|
||||
"user_template",
|
||||
template.path ?? "",
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("SelectTheme")} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{(
|
||||
config?.frontend_templates?.filter(
|
||||
(t) => !t.is_admin,
|
||||
) || []
|
||||
).map((template) => (
|
||||
<div key={template.path}>
|
||||
<SelectItem value={template.path!}>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="font-medium">
|
||||
{template.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>
|
||||
{t("Author")}:{" "}
|
||||
{template.author}
|
||||
</span>
|
||||
{!template.is_official ? (
|
||||
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
|
||||
{t("Community")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-1.5 py-0.5 rounded-md bg-blue-100 text-blue-800 text-xs">
|
||||
{t("Official")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<div className="px-8 py-1">
|
||||
<a
|
||||
href={template.repository}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
||||
>
|
||||
{template.repository}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{!config?.frontend_templates?.find(
|
||||
(t) => t.path === field.value,
|
||||
)?.is_official && (
|
||||
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
|
||||
<div className="font-medium text-lg mb-1">
|
||||
{t("CommunityThemeWarning")}
|
||||
</div>
|
||||
<div className="text-yellow-700 dark:text-yellow-200">
|
||||
{t("CommunityThemeDescription")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
@@ -273,10 +380,11 @@ 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">
|
||||
{t("ConfigTLS")}
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">{t("ConfigTLS")}</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -289,7 +397,9 @@ export default function SettingsPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("CustomPublicDNSNameserversforDDNS") + " " + t("SeparateWithComma")}
|
||||
{t("CustomPublicDNSNameserversforDDNS") +
|
||||
" " +
|
||||
t("SeparateWithComma")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -306,16 +416,28 @@ export default function SettingsPage() {
|
||||
<FormLabel>{t("RealIPHeader")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center">
|
||||
<Input disabled={field.value == 'NZ::Use-Peer-IP'} className="w-1/2" placeholder="CF-Connecting-IP" {...field} />
|
||||
<Checkbox checked={field.value == 'NZ::Use-Peer-IP'} className="ml-2" onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.disabled = true;
|
||||
form.setValue("real_ip_header", "NZ::Use-Peer-IP");
|
||||
} else {
|
||||
field.disabled = false;
|
||||
form.setValue("real_ip_header", "");
|
||||
}
|
||||
}} />
|
||||
<Input
|
||||
disabled={field.value == "NZ::Use-Peer-IP"}
|
||||
className="w-1/2"
|
||||
placeholder="CF-Connecting-IP"
|
||||
{...field}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={field.value == "NZ::Use-Peer-IP"}
|
||||
className="ml-2"
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.disabled = true
|
||||
form.setValue(
|
||||
"real_ip_header",
|
||||
"NZ::Use-Peer-IP",
|
||||
)
|
||||
} else {
|
||||
field.disabled = false
|
||||
form.setValue("real_ip_header", "")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormLabel className="font-normal ml-2">
|
||||
{t("UseDirectConnectingIP")}
|
||||
</FormLabel>
|
||||
@@ -336,14 +458,19 @@ export default function SettingsPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Coverage")}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={`${field.value}`}>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={`${field.value}`}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.entries(settingCoverageTypes).map(([k, v]) => (
|
||||
{Object.entries(
|
||||
settingCoverageTypes,
|
||||
).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
{v}
|
||||
</SelectItem>
|
||||
@@ -359,7 +486,11 @@ export default function SettingsPage() {
|
||||
name="ignored_ip_notification"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("SpecificServers") + " " + t("SeparateWithComma")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("SpecificServers") +
|
||||
" " +
|
||||
t("SeparateWithComma")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="1,2,3" {...field} />
|
||||
</FormControl>
|
||||
@@ -374,7 +505,11 @@ export default function SettingsPage() {
|
||||
<FormItem>
|
||||
<FormLabel>{t("NotifierGroupID")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="0" type="number" {...field} />
|
||||
<Input
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -387,8 +522,13 @@ 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">{t("Enable")}</Label>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
{t("Enable")}
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -406,7 +546,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} />
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
{t("FullIPNotification")}
|
||||
</Label>
|
||||
@@ -421,5 +564,5 @@ export default function SettingsPage() {
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+37
-33
@@ -1,5 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteUser } from "@/api/user"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,31 +11,28 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } 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";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { UserCard } from "@/components/user"
|
||||
import { ModelUser } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function UserPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.UnExpectedError", { error: error.message }),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
description: t("Results.UnExpectedError", {
|
||||
error: error.message,
|
||||
}),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelUser>[] = [
|
||||
{
|
||||
@@ -40,7 +41,7 @@ export default function UserPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -70,7 +71,7 @@ export default function UserPage() {
|
||||
id: "actions",
|
||||
header: t("Actions"),
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const s = row.original
|
||||
return (
|
||||
<ActionButtonGroup
|
||||
className="flex gap-2"
|
||||
@@ -82,25 +83,25 @@ export default function UserPage() {
|
||||
>
|
||||
<></>
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<SettingsTab className="mt-6 w-full" />
|
||||
<div className="flex mt-4 mb-4">
|
||||
<HeaderButtonGroup
|
||||
@@ -124,9 +125,12 @@ export default function UserPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -158,5 +162,5 @@ export default function UserPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
+37
-35
@@ -1,5 +1,9 @@
|
||||
import { swrFetcher } from "@/api/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { deleteWAF } from "@/api/waf"
|
||||
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||
import { SettingsTab } from "@/components/settings-tab"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,31 +11,26 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
||||
import { toast } from "sonner";
|
||||
import { ModelWAFApiMock, wafBlockReasons } from "@/types";
|
||||
import { deleteWAF } from "@/api/waf";
|
||||
import { ip16Str } from "@/lib/utils";
|
||||
import { SettingsTab } from "@/components/settings-tab";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/table"
|
||||
import { ip16Str } from "@/lib/utils"
|
||||
import { ModelWAFApiMock, wafBlockReasons } from "@/types"
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import useSWR from "swr"
|
||||
|
||||
export default function WAFPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock[]>("/api/v1/waf", swrFetcher);
|
||||
const { t } = useTranslation()
|
||||
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock[]>("/api/v1/waf", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
toast(t("Error"), {
|
||||
description: t(`Error fetching resource: ${error.message}.`),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error])
|
||||
|
||||
const columns: ColumnDef<ModelWAFApiMock>[] = [
|
||||
{
|
||||
@@ -40,7 +39,7 @@ export default function WAFPage() {
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
@@ -77,16 +76,16 @@ export default function WAFPage() {
|
||||
accessorKey: "lastBlockTime",
|
||||
accessorFn: (row) => row.last_block_timestamp,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
const date = new Date((s.last_block_timestamp || 0)*1000);
|
||||
return <span>{date.toISOString()}</span>;
|
||||
const s = row.original
|
||||
const date = new Date((s.last_block_timestamp || 0) * 1000)
|
||||
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"
|
||||
@@ -98,25 +97,25 @@ export default function WAFPage() {
|
||||
>
|
||||
<></>
|
||||
</ActionButtonGroup>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const dataCache = useMemo(() => {
|
||||
return data ?? [];
|
||||
}, [data]);
|
||||
return data ?? []
|
||||
}, [data])
|
||||
|
||||
const table = useReactTable({
|
||||
data: dataCache,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
})
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="px-3">
|
||||
<SettingsTab className="mt-6 w-full" />
|
||||
<div className="flex mt-4 mb-4">
|
||||
<HeaderButtonGroup
|
||||
@@ -139,9 +138,12 @@ export default function WAFPage() {
|
||||
<TableHead key={header.id} className="text-sm">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -173,5 +175,5 @@ export default function WAFPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user