mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-09 05:59:50 +00:00
Merge branch 'next' into feat/new-workflow
This commit is contained in:
commit
a6e9cc03c0
@ -1,3 +1,5 @@
|
|||||||
|
import type { WorkflowModel } from "./workflow";
|
||||||
|
|
||||||
export interface WorkflowRunModel extends BaseModel {
|
export interface WorkflowRunModel extends BaseModel {
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
status: string;
|
status: string;
|
||||||
@ -6,6 +8,9 @@ export interface WorkflowRunModel extends BaseModel {
|
|||||||
endedAt: ISO8601String;
|
endedAt: ISO8601String;
|
||||||
logs: WorkflowRunLog[];
|
logs: WorkflowRunLog[];
|
||||||
error: string;
|
error: string;
|
||||||
|
expand?: {
|
||||||
|
workflowId?: WorkflowModel;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowRunLog = {
|
export type WorkflowRunLog = {
|
||||||
|
@ -8,5 +8,11 @@
|
|||||||
"dashboard.statistics.enabled_workflows": "Enabled workflows",
|
"dashboard.statistics.enabled_workflows": "Enabled workflows",
|
||||||
"dashboard.statistics.unit": "",
|
"dashboard.statistics.unit": "",
|
||||||
|
|
||||||
"dashboard.latest_workflow_run": "Latest workflow run"
|
"dashboard.latest_workflow_run": "Latest workflow run",
|
||||||
|
|
||||||
|
"dashboard.quick_actions": "Quick actions",
|
||||||
|
"dashboard.quick_actions.create_workflow": "Create workflow",
|
||||||
|
"dashboard.quick_actions.change_login_password": "Change login password",
|
||||||
|
"dashboard.quick_actions.notification_settings": "Notification settings",
|
||||||
|
"dashboard.quick_actions.certificate_authority_configuration": "Certificate authority configuration"
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,12 @@
|
|||||||
"dashboard.statistics.enabled_workflows": "已启用工作流",
|
"dashboard.statistics.enabled_workflows": "已启用工作流",
|
||||||
"dashboard.statistics.unit": "个",
|
"dashboard.statistics.unit": "个",
|
||||||
|
|
||||||
"dashboard.latest_workflow_run": "最近执行的工作流"
|
"dashboard.latest_workflow_run": "最近执行的工作流",
|
||||||
|
|
||||||
|
"dashboard.quick_actions": "快捷操作",
|
||||||
|
"dashboard.quick_actions.create_workflow": "新建工作流",
|
||||||
|
"dashboard.quick_actions.change_login_password": "修改登录密码",
|
||||||
|
"dashboard.quick_actions.notification_settings": "消息推送设置",
|
||||||
|
"dashboard.quick_actions.certificate_authority_configuration": "证书颁发机构配置"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
ApiOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SelectOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import { Card, Col, Divider, Row, Space, Statistic, Typography, notification, theme } from "antd";
|
import type { TableProps } from "antd";
|
||||||
|
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, Tag, Typography, notification, theme } from "antd";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
CalendarClock as CalendarClockIcon,
|
CalendarClock as CalendarClockIcon,
|
||||||
CalendarX2 as CalendarX2Icon,
|
CalendarX2 as CalendarX2Icon,
|
||||||
@ -14,18 +27,158 @@ import {
|
|||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
import { get as getStatistics } from "@/api/statistics";
|
import { get as getStatistics } from "@/api/statistics";
|
||||||
|
import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
|
||||||
import { type Statistics } from "@/domain/statistics";
|
import { type Statistics } from "@/domain/statistics";
|
||||||
|
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||||
|
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||||
|
import { list as listWorkflowRuns } from "@/repository/workflowRun";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { token: themeToken } = theme.useToken();
|
const { token: themeToken } = theme.useToken();
|
||||||
|
|
||||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
|
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
|
||||||
|
{
|
||||||
|
key: "$index",
|
||||||
|
align: "center",
|
||||||
|
fixed: "left",
|
||||||
|
width: 50,
|
||||||
|
render: (_, __, index) => (page - 1) * pageSize + index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
title: t("workflow.props.name"),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space className="max-w-full" direction="vertical" size={4}>
|
||||||
|
<Typography.Text ellipsis>{record.expand?.workflowId?.name}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" ellipsis>
|
||||||
|
{record.expand?.workflowId?.description}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
title: t("workflow_run.props.status"),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
if (record.status === WORKFLOW_RUN_STATUSES.PENDING) {
|
||||||
|
return <Tag icon={<ClockCircleOutlined />}>{t("workflow_run.props.status.pending")}</Tag>;
|
||||||
|
} else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||||
|
return (
|
||||||
|
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||||
|
{t("workflow_run.props.status.running")}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
|
||||||
|
return (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
|
{t("workflow_run.props.status.succeeded")}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
|
||||||
|
return (
|
||||||
|
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||||
|
{t("workflow_run.props.status.failed")}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "trigger",
|
||||||
|
title: t("workflow_run.props.trigger"),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
if (record.trigger === WORKFLOW_TRIGGERS.AUTO) {
|
||||||
|
return t("workflow_run.props.trigger.auto");
|
||||||
|
} else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) {
|
||||||
|
return t("workflow_run.props.trigger.manual");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "startedAt",
|
||||||
|
title: t("workflow_run.props.started_at"),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
if (record.startedAt) {
|
||||||
|
return dayjs(record.startedAt).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "endedAt",
|
||||||
|
title: t("workflow_run.props.ended_at"),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
if (record.endedAt) {
|
||||||
|
return dayjs(record.endedAt).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "$action",
|
||||||
|
align: "end",
|
||||||
|
fixed: "right",
|
||||||
|
width: 120,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button.Group>
|
||||||
|
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlined />} variant="text" />} />
|
||||||
|
</Button.Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);
|
||||||
|
const [_tableTotal, setTableTotal] = useState<number>(0);
|
||||||
|
|
||||||
|
const [page, _setPage] = useState<number>(1);
|
||||||
|
const [pageSize, _setPageSize] = useState<number>(3);
|
||||||
|
|
||||||
|
const { loading: loadingWorkflowRun } = useRequest(
|
||||||
|
() => {
|
||||||
|
return listWorkflowRuns({
|
||||||
|
page: page,
|
||||||
|
perPage: pageSize,
|
||||||
|
expand: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refreshDeps: [page, pageSize],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setTableData(data.items);
|
||||||
|
setTableTotal(data.totalItems > 3 ? 3 : data.totalItems);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
if (err instanceof ClientResponseError && err.isAbort) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(err);
|
||||||
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const statisticsGridSpans = {
|
const statisticsGridSpans = {
|
||||||
xs: { flex: "50%" },
|
xs: { flex: "50%" },
|
||||||
md: { flex: "50%" },
|
md: { flex: "50%" },
|
||||||
@ -115,7 +268,39 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div>TODO: {t("dashboard.latest_workflow_run")}</div>
|
<Flex vertical={!screens.md} gap={16}>
|
||||||
|
<Card className="sm:h-full sm:w-[500px] sm:pb-32">
|
||||||
|
<div className="text-lg font-semibold">{t("dashboard.quick_actions")}</div>
|
||||||
|
<div className="mt-9">
|
||||||
|
<Button className="w-full" type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/workflows/new")}>
|
||||||
|
{t("dashboard.quick_actions.create_workflow")}
|
||||||
|
</Button>
|
||||||
|
<Button className="mt-5 w-full" size="large" icon={<LockOutlined />} onClick={() => navigate("/settings/password")}>
|
||||||
|
{t("dashboard.quick_actions.change_login_password")}
|
||||||
|
</Button>
|
||||||
|
<Button className="mt-5 w-full" size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}>
|
||||||
|
{t("dashboard.quick_actions.notification_settings")}
|
||||||
|
</Button>
|
||||||
|
<Button className="mt-5 w-full" size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}>
|
||||||
|
{t("dashboard.quick_actions.certificate_authority_configuration")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="size-full">
|
||||||
|
<div className="text-lg font-semibold">{t("dashboard.latest_workflow_run")} </div>
|
||||||
|
<Table<WorkflowRunModel>
|
||||||
|
className="mt-5"
|
||||||
|
columns={tableColumns}
|
||||||
|
dataSource={tableData}
|
||||||
|
loading={loadingWorkflowRun}
|
||||||
|
locale={{
|
||||||
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||||
|
}}
|
||||||
|
rowKey={(record: WorkflowRunModel) => record.id}
|
||||||
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,20 +5,29 @@ import { getPocketBase } from "./pocketbase";
|
|||||||
const COLLECTION_NAME = "workflow_run";
|
const COLLECTION_NAME = "workflow_run";
|
||||||
|
|
||||||
export type ListWorkflowRunsRequest = {
|
export type ListWorkflowRunsRequest = {
|
||||||
workflowId: string;
|
workflowId?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
expand?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const list = async (request: ListWorkflowRunsRequest) => {
|
export const list = async (request: ListWorkflowRunsRequest) => {
|
||||||
const page = request.page || 1;
|
const page = request.page || 1;
|
||||||
const perPage = request.perPage || 10;
|
const perPage = request.perPage || 10;
|
||||||
|
console.log("request.workflowId", request.workflowId);
|
||||||
|
let filter = "";
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (request.workflowId) {
|
||||||
|
filter = `workflowId={:workflowId}`;
|
||||||
|
params.workflowId = request.workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
return await getPocketBase()
|
return await getPocketBase()
|
||||||
.collection(COLLECTION_NAME)
|
.collection(COLLECTION_NAME)
|
||||||
.getList<WorkflowRunModel>(page, perPage, {
|
.getList<WorkflowRunModel>(page, perPage, {
|
||||||
filter: getPocketBase().filter("workflowId={:workflowId}", { workflowId: request.workflowId }),
|
filter: getPocketBase().filter(filter, params),
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
requestKey: null,
|
requestKey: null,
|
||||||
|
expand: request.expand ? "workflowId" : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user