feat(ui): new WorkflowList UI using antd
This commit is contained in:
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getPb } from "@/repository/api";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
import { ConfigProvider } from "@/providers/config";
|
||||
|
||||
import Version from "@/components/certimate/Version";
|
||||
@@ -18,7 +18,7 @@ export default function Dashboard() {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) {
|
||||
if (!getPocketBase().authStore.isValid || !getPocketBase().authStore.isAdmin) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
getPb().authStore.clear();
|
||||
getPocketBase().authStore.clear();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
@@ -57,7 +57,10 @@ export default function Dashboard() {
|
||||
<Home className="h-4 w-4" />
|
||||
{t("dashboard.page.title")}
|
||||
</Link>
|
||||
<Link to="/workflow" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/workflow"))}>
|
||||
<Link
|
||||
to="/workflows"
|
||||
className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/workflows"))}
|
||||
>
|
||||
<Workflow className="h-4 w-4" />
|
||||
{t("workflow.page.title")}
|
||||
</Link>
|
||||
@@ -103,8 +106,8 @@ export default function Dashboard() {
|
||||
{t("dashboard.page.title")}
|
||||
</Link>
|
||||
<Link
|
||||
to="/workflow"
|
||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/workflow"))}
|
||||
to="/workflows"
|
||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/workflows"))}
|
||||
>
|
||||
<Workflow className="h-5 w-5" />
|
||||
{t("workflow.page.title")}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
import Version from "@/components/certimate/Version";
|
||||
import { getPb } from "@/repository/api";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
|
||||
const LoginLayout = () => {
|
||||
if (getPb().authStore.isValid && getPb().authStore.isAdmin) {
|
||||
if (getPocketBase().authStore.isValid && getPocketBase().authStore.isAdmin) {
|
||||
return <Navigate to="/" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ const Dashboard = () => {
|
||||
<div className="flex items-baseline">
|
||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||
{statistic?.workflowTotal ? (
|
||||
<Link to="/workflow" className="hover:underline">
|
||||
<Link to="/workflows" className="hover:underline">
|
||||
{statistic?.workflowTotal}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -122,7 +122,7 @@ const Dashboard = () => {
|
||||
<div className="flex items-baseline">
|
||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||
{statistic?.workflowEnabled ? (
|
||||
<Link to="/workflow?state=enabled" className="hover:underline">
|
||||
<Link to="/workflows?state=enabled" className="hover:underline">
|
||||
{statistic?.workflowEnabled}
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getErrMessage } from "@/lib/error";
|
||||
import { getPb } from "@/repository/api";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().email({
|
||||
@@ -33,7 +33,7 @@ const Login = () => {
|
||||
// 2. Define a submit handler.
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await getPb().admins.authWithPassword(values.username, values.password);
|
||||
await getPocketBase().admins.authWithPassword(values.username, values.password);
|
||||
navigage("/");
|
||||
} catch (e) {
|
||||
const message = getErrMessage(e);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { getErrMessage } from "@/lib/error";
|
||||
import { getPb } from "@/repository/api";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email("settings.account.email.errmsg.invalid"),
|
||||
@@ -26,17 +26,17 @@ const Account = () => {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: getPb().authStore.model?.email,
|
||||
email: getPocketBase().authStore.model?.email,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await getPb().admins.update(getPb().authStore.model?.id, {
|
||||
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
|
||||
email: values.email,
|
||||
});
|
||||
|
||||
getPb().authStore.clear();
|
||||
getPocketBase().authStore.clear();
|
||||
toast({
|
||||
title: t("settings.account.email.changed.message"),
|
||||
description: t("settings.account.relogin.message"),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { getErrMessage } from "@/lib/error";
|
||||
import { getPb } from "@/repository/api";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -44,19 +44,19 @@ const Password = () => {
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
await getPb().admins.authWithPassword(getPb().authStore.model?.email, values.oldPassword);
|
||||
await getPocketBase().admins.authWithPassword(getPocketBase().authStore.model?.email, values.oldPassword);
|
||||
} catch (e) {
|
||||
const message = getErrMessage(e);
|
||||
form.setError("oldPassword", { message });
|
||||
}
|
||||
|
||||
try {
|
||||
await getPb().admins.update(getPb().authStore.model?.id, {
|
||||
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
|
||||
password: values.newPassword,
|
||||
passwordConfirm: values.confirmPassword,
|
||||
});
|
||||
|
||||
getPb().authStore.clear();
|
||||
getPocketBase().authStore.clear();
|
||||
toast({
|
||||
title: t("settings.password.changed.message"),
|
||||
description: t("settings.account.relogin.message"),
|
||||
|
||||
248
ui/src/pages/workflow/WorkflowList.tsx
Normal file
248
ui/src/pages/workflow/WorkflowList.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, notification, Space, Switch, Table, Tooltip, Typography, type TableProps } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { Pencil as PencilIcon, Plus as PlusIcon, Trash2 as Trash2Icon } from "lucide-react";
|
||||
|
||||
import { Workflow as WorkflowType } from "@/domain/workflow";
|
||||
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow, type WorkflowListReq } from "@/repository/workflow";
|
||||
|
||||
const WorkflowList = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const tableColumns: TableProps<WorkflowType>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
align: "center",
|
||||
title: "",
|
||||
width: 50,
|
||||
render: (_, __, index) => (page - 1) * pageSize + index + 1,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
title: t("common.text.name"),
|
||||
render: (_, record) => (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<Typography.Text ellipsis>{record.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
{record.description}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
title: t("workflow.props.executionMethod"),
|
||||
render: (_, record) => {
|
||||
const method = record.type;
|
||||
if (!method) {
|
||||
return "-";
|
||||
} else if (method === "manual") {
|
||||
return <Typography.Text>{t("workflow.node.start.form.executionMethod.options.manual")}</Typography.Text>;
|
||||
} else if (method === "auto") {
|
||||
return (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<Typography.Text>{t("workflow.node.start.form.executionMethod.options.auto")}</Typography.Text>
|
||||
<Typography.Text type="secondary">{record.crontab ?? ""}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "enabled",
|
||||
title: t("workflow.props.enabled"),
|
||||
render: (_, record) => {
|
||||
const enabled = record.enabled;
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
handleEnabledChange(record.id);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "lastExecutedAt",
|
||||
title: "最近执行状态",
|
||||
render: () => {
|
||||
// TODO: 最近执行状态
|
||||
return <>TODO</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
title: t("common.text.created_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return new Date(record.created!).toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
title: t("common.text.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return new Date(record.updated!).toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "$operations",
|
||||
align: "end",
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Tooltip title={t("common.edit")}>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<PencilIcon size={16} />}
|
||||
onClick={() => {
|
||||
navigate(`/workflow/detail?id=${record.id}`);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("common.delete")}>
|
||||
<Button
|
||||
type="link"
|
||||
danger={true}
|
||||
icon={<Trash2Icon size={16} />}
|
||||
onClick={() => {
|
||||
handleDeleteClick(record.id);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<WorkflowType[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
// TODO: 表头筛选
|
||||
const fetchTableData = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
const state = searchParams.get("state");
|
||||
const req: WorkflowListReq = { page: page, perPage: pageSize };
|
||||
if (state == "enabled") {
|
||||
req.enabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await listWorkflow(req);
|
||||
|
||||
setTableData(resp.items);
|
||||
setTableTotal(resp.totalItems);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTableData();
|
||||
}, [page, pageSize]);
|
||||
|
||||
const handleEnabledChange = async (id: string) => {
|
||||
try {
|
||||
const resp = await saveWorkflow({ id, enabled: !tableData.find((item) => item.id === id)?.enabled });
|
||||
if (resp) {
|
||||
setTableData((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
return resp;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: <>{String(err)}</> });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
modalApi.confirm({
|
||||
title: t("workflow.action.delete.alert.title"),
|
||||
content: t("workflow.action.delete.alert.content"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const resp = await removeWorkflow(id);
|
||||
if (resp) {
|
||||
setTableData((prev) => prev.filter((item) => item.id !== id));
|
||||
}
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: <>{String(err)}</> });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflow/detail");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={t("workflow.page.title")}
|
||||
extra={[
|
||||
<Button
|
||||
key="create"
|
||||
type="primary"
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={() => {
|
||||
handleCreateClick();
|
||||
}}
|
||||
>
|
||||
{t("workflow.action.create")}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Table<WorkflowType>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
rowKey={(record) => record.id}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
onChange: (page, pageSize) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page, pageSize) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{ModelContextHolder}
|
||||
{NotificationContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowList;
|
||||
@@ -1,224 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoreHorizontal, Plus } from "lucide-react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Workflow as WorkflowType } from "@/domain/workflow";
|
||||
import { DataTable } from "@/components/workflow/DataTable";
|
||||
import { useState } from "react";
|
||||
import { list, remove, save, WorkflowListReq } from "@/repository/workflow";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CustomAlertDialog from "@/components/workflow/CustomAlertDialog";
|
||||
|
||||
const Workflow = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [data, setData] = useState<WorkflowType[]>([]);
|
||||
const [pageCount, setPageCount] = useState<number>(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
|
||||
const [alertProps, setAlertProps] = useState<{
|
||||
title: string;
|
||||
description: string;
|
||||
onConfirm: () => void;
|
||||
}>();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const fetchData = async (page: number, pageSize?: number) => {
|
||||
const state = searchParams.get("state");
|
||||
const req: WorkflowListReq = { page: page, perPage: pageSize };
|
||||
if (state && state == "enabled") {
|
||||
req.enabled = true;
|
||||
}
|
||||
const resp = await list(req);
|
||||
setData(resp.items);
|
||||
setPageCount(resp.totalPages);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<WorkflowType>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("workflow.props.name"),
|
||||
cell: ({ row }) => {
|
||||
let name: string = row.getValue("name");
|
||||
if (!name) {
|
||||
name = t("workflow.props.name.default");
|
||||
}
|
||||
return <div className="max-w-[150px] truncate">{name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: t("workflow.props.description"),
|
||||
cell: ({ row }) => {
|
||||
let description: string = row.getValue("description");
|
||||
if (!description) {
|
||||
description = "-";
|
||||
}
|
||||
return <div className="max-w-[200px] truncate">{description}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: t("workflow.props.executionMethod"),
|
||||
cell: ({ row }) => {
|
||||
const method = row.getValue("type");
|
||||
if (!method) {
|
||||
return "-";
|
||||
} else if (method === "manual") {
|
||||
return t("workflow.node.start.form.executionMethod.options.manual");
|
||||
} else if (method === "auto") {
|
||||
const crontab: string = row.original.crontab ?? "";
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div>{t("workflow.node.start.form.executionMethod.options.auto")}</div>
|
||||
<div className="text-muted-foreground text-xs">{crontab}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: t("workflow.props.enabled"),
|
||||
cell: ({ row }) => {
|
||||
const enabled: boolean = row.getValue("enabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={() => {
|
||||
handleCheckedChange(row.original.id ?? "");
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created",
|
||||
header: t("workflow.props.created"),
|
||||
cell: ({ row }) => {
|
||||
const date: string = row.getValue("created");
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "updated",
|
||||
header: t("workflow.props.updated"),
|
||||
cell: ({ row }) => {
|
||||
const date: string = row.getValue("updated");
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const workflow = row.original;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("workflow.action")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/workflow/detail?id=${workflow.id}`);
|
||||
}}
|
||||
>
|
||||
{t("workflow.action.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(workflow.id ?? "");
|
||||
}}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleCheckedChange = async (id: string) => {
|
||||
const resp = await save({ id, enabled: !data.find((item) => item.id === id)?.enabled });
|
||||
if (resp) {
|
||||
setData((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
return resp;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
setAlertProps({
|
||||
title: t("workflow.action.delete.alert.title"),
|
||||
description: t("workflow.action.delete.alert.description"),
|
||||
onConfirm: async () => {
|
||||
const resp = await remove(id);
|
||||
if (resp) {
|
||||
setData((prev) => {
|
||||
return prev.filter((item) => item.id !== id);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
setAlertOpen(true);
|
||||
};
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflow/detail");
|
||||
};
|
||||
|
||||
const handleRowClick = (id: string) => {
|
||||
navigate(`/workflow/detail?id=${id}`);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-muted-foreground">{t("workflow.page.title")}</div>
|
||||
<Button onClick={handleCreateClick}>
|
||||
<Plus size={16} />
|
||||
{t("workflow.action.create")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DataTable columns={columns} data={data} onPageChange={fetchData} pageCount={pageCount} onRowClick={handleRowClick} withPagination />
|
||||
</div>
|
||||
|
||||
<CustomAlertDialog
|
||||
open={alertOpen}
|
||||
onOpenChange={setAlertOpen}
|
||||
title={alertProps?.title}
|
||||
description={alertProps?.description}
|
||||
confirm={alertProps?.onConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Workflow;
|
||||
Reference in New Issue
Block a user