diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts index 36920de2..95b44546 100644 --- a/ui/src/domain/workflowRun.ts +++ b/ui/src/domain/workflowRun.ts @@ -1,3 +1,5 @@ +import type { WorkflowModel } from "./workflow"; + export interface WorkflowRunModel extends BaseModel { workflowId: string; status: string; @@ -6,6 +8,9 @@ export interface WorkflowRunModel extends BaseModel { endedAt: ISO8601String; logs: WorkflowRunLog[]; error: string; + expand?: { + workflowId?: WorkflowModel; + }; } export type WorkflowRunLog = { diff --git a/ui/src/i18n/locales/en/nls.dashboard.json b/ui/src/i18n/locales/en/nls.dashboard.json index 865e11ec..e179f20f 100644 --- a/ui/src/i18n/locales/en/nls.dashboard.json +++ b/ui/src/i18n/locales/en/nls.dashboard.json @@ -8,5 +8,11 @@ "dashboard.statistics.enabled_workflows": "Enabled workflows", "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" } diff --git a/ui/src/i18n/locales/zh/nls.dashboard.json b/ui/src/i18n/locales/zh/nls.dashboard.json index 72086011..471ebfce 100644 --- a/ui/src/i18n/locales/zh/nls.dashboard.json +++ b/ui/src/i18n/locales/zh/nls.dashboard.json @@ -8,5 +8,12 @@ "dashboard.statistics.enabled_workflows": "已启用工作流", "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": "证书颁发机构配置" } + diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index d5a12e19..82e0acea 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -1,9 +1,22 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; 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 { 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 { CalendarClock as CalendarClockIcon, CalendarX2 as CalendarX2Icon, @@ -14,18 +27,158 @@ import { import { ClientResponseError } from "pocketbase"; import { get as getStatistics } from "@/api/statistics"; +import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer"; 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"; +const { useBreakpoint } = Grid; + const Dashboard = () => { const navigate = useNavigate(); + const screens = useBreakpoint(); + const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); 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 = { xs: { flex: "50%" }, md: { flex: "50%" }, @@ -115,7 +268,39 @@ const Dashboard = () => { <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> ); }; diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 7dac78a6..fd99de98 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -5,20 +5,29 @@ import { getPocketBase } from "./pocketbase"; const COLLECTION_NAME = "workflow_run"; export type ListWorkflowRunsRequest = { - workflowId: string; + workflowId?: string; page?: number; perPage?: number; + expand?: boolean; }; export const list = async (request: ListWorkflowRunsRequest) => { const page = request.page || 1; 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() .collection(COLLECTION_NAME) .getList<WorkflowRunModel>(page, perPage, { - filter: getPocketBase().filter("workflowId={:workflowId}", { workflowId: request.workflowId }), + filter: getPocketBase().filter(filter, params), sort: "-created", requestKey: null, + expand: request.expand ? "workflowId" : undefined, }); };