feat(ui): new WorkflowDetail page

This commit is contained in:
Fu Diwei 2025-07-24 12:58:20 +08:00
parent a37c42cb0b
commit 488960397a
12 changed files with 230 additions and 68 deletions

View File

@ -125,7 +125,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
{Object.entries(record.data).map(([key, value]) => (
<div key={key} className="flex space-x-2" style={{ wordBreak: "break-word" }}>
<div className="whitespace-nowrap">{key}:</div>
<div className={!showWhitespace ? "whitespace-pre-line" : ""}>{JSON.stringify(value)}</div>
<div className={showWhitespace ? "whitespace-normal" : "whitespace-pre-line"}>{JSON.stringify(value)}</div>
</div>
))}
</details>
@ -263,6 +263,94 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
);
};
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {};
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
const { t } = useTranslation();
const { notification } = App.useApp();
const tableColumns: TableProps<CertificateModel>["columns"] = [
{
key: "$index",
align: "center",
fixed: "left",
width: 50,
render: (_, __, index) => index + 1,
},
{
key: "type",
title: t("workflow_run_artifact.props.type"),
render: () => t("workflow_run_artifact.props.type.certificate"),
},
{
key: "name",
title: t("workflow_run_artifact.props.name"),
render: (_, record) => {
return (
<div className="max-w-full truncate">
<Typography.Text delete={!!record.deleted} ellipsis>
{record.subjectAltNames}
</Typography.Text>
</div>
);
},
},
{
key: "$action",
align: "end",
width: 32,
render: (_, record) => (
<div className="flex items-center justify-end">
<CertificateDetailDrawer
data={record}
trigger={
<Tooltip title={t("common.button.view")}>
<Button color="primary" disabled={!!record.deleted} icon={<IconBrowserShare size="1.25em" />} variant="text" />
</Tooltip>
}
/>
</div>
),
},
];
const [tableData, setTableData] = useState<CertificateModel[]>([]);
const { loading: tableLoading } = useRequest(
() => {
return listCertificatesByWorkflowRunId(runId);
},
{
refreshDeps: [runId],
onSuccess: (res) => {
setTableData(res.items);
},
onError: (err) => {
if (err instanceof ClientResponseError && err.isAbort) {
return;
}
console.error(err);
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
throw err;
},
}
);
return (
<>
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
<Table<CertificateModel>
columns={tableColumns}
dataSource={tableData}
loading={tableLoading}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}}
pagination={false}
rowKey={(record) => record.id}
size="small"
/>
</>
);
};
export default WorkflowRunDetail;

View File

@ -11,8 +11,11 @@
"access.action.create.button": "Create credential",
"access.action.create.modal.title": "Create credential",
"access.action.edit.button": "Edit credential",
"access.action.edit.modal.title": "Edit credential",
"access.action.duplicate.button": "Duplicate credential",
"access.action.duplicate.modal.title": "Duplicate credential",
"access.action.delete.button": "Delete credential",
"access.action.delete.modal.title": "Delete \"{{name}}\"",
"access.action.delete.modal.content": "Are you sure want to delete this credential? <br>This action cannot be undone.",
"access.action.batch_delete.modal.title": "Delete credentials",

View File

@ -8,6 +8,8 @@
"certificate.search.placeholder": "Search by certificate name or serial number ...",
"certificate.action.view.button": "View details",
"certificate.action.delete.button": "Delete certificate",
"certificate.action.delete.modal.title": "Delete \"{{name}}\"",
"certificate.action.delete.modal.content": "Are you sure want to delete this certificate? <br>This action cannot be undone.",
"certificate.action.batch_delete.modal.title": "Delete certificates",

View File

@ -9,8 +9,11 @@
"workflow.search.placeholder": "Search by workflow name ...",
"workflow.action.create.button": "Create workflow",
"workflow.action.edit.button": "Edit workflow",
"workflow.action.duplicate.button": "Duplicate workflow",
"workflow.action.duplicate.modal.title": "Duplicate \"{{name}}\"",
"workflow.action.duplicate.modal.content": "Are you sure to duplicate this workflow?",
"workflow.action.delete.button": "Delete workflow",
"workflow.action.delete.modal.title": "Delete \"{{name}}\"",
"workflow.action.delete.modal.content": "Are you sure want to delete this workflow? <br>This action cannot be undone.",
"workflow.action.batch_delete.modal.title": "Delete workflows",

View File

@ -10,8 +10,11 @@
"access.action.create.button": "新建授权",
"access.action.create.modal.title": "新建授权",
"access.action.edit.button": "编辑授权",
"access.action.edit.modal.title": "编辑授权",
"access.action.duplicate.button": "复制授权",
"access.action.duplicate.modal.title": "复制授权",
"access.action.delete.button": "删除授权",
"access.action.delete.modal.title": "删除「{{name}}」",
"access.action.delete.modal.content": "确定要删除该授权吗?<br>注意此操作不可撤销,请谨慎操作。",
"access.action.batch_delete.modal.title": "删除授权",

View File

@ -8,6 +8,8 @@
"certificate.search.placeholder": "按证书名称或序列号搜索……",
"certificate.action.view.button": "查看详情",
"certificate.action.delete.button": "删除证书",
"certificate.action.delete.modal.title": "删除「{{name}}」",
"certificate.action.delete.modal.content": "确定要删除该证书吗?<br>注意此操作不可撤销,请谨慎操作。",
"certificate.action.batch_delete.modal.title": "删除证书",

View File

@ -9,8 +9,11 @@
"workflow.search.placeholder": "按工作流名称搜索……",
"workflow.action.create.button": "新建工作流",
"workflow.action.edit.button": "编辑工作流",
"workflow.action.duplicate.button": "复制工作流",
"workflow.action.duplicate.modal.title": "复制「{{name}}」",
"workflow.action.duplicate.modal.content": "确定要复制该工作流吗?",
"workflow.action.delete.button": "删除工作流",
"workflow.action.delete.modal.title": "删除「{{name}}」",
"workflow.action.delete.modal.content": "确定要删除该工作流吗?<br>注意此操作不可撤销,请谨慎操作。",
"workflow.action.batch_delete.modal.title": "删除工作流",

View File

@ -12,7 +12,7 @@ import {
IconMenu2,
IconSettings,
} from "@tabler/icons-react";
import { Alert, Button, Divider, Drawer, Layout, Menu, type MenuProps, theme } from "antd";
import { Alert, Button, Drawer, Layout, Menu, type MenuProps, theme } from "antd";
import AppLocale from "@/components/AppLocale";
import AppTheme from "@/components/AppTheme";
@ -103,7 +103,25 @@ const ConsoleLayout = () => {
</Layout.Sider>
<Layout className="flex flex-col overflow-hidden">
<Layout.Header className="border-b shadow-sm md:hidden" style={{ padding: 0, borderBottomColor: themeToken.colorBorderSecondary }}>
<Layout.Header
className="relative border-b shadow-sm md:hidden"
style={{
padding: 0,
borderBottomColor: themeToken.colorBorderSecondary,
}}
>
<div className="absolute inset-0 z-0">
<div
className="h-full w-full"
style={{
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.063) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.063) 1px, transparent 1px)",
backgroundSize: "20px 20px",
}}
>
<div className="h-full w-full backdrop-blur-[1px]"></div>
</div>
</div>
<div className="flex size-full items-center justify-between overflow-hidden px-4">
<div className="flex items-center gap-4">
<SiderMenuDrawer trigger={<Button icon={<IconMenu2 size="1.25em" stroke="1.25" />} />} />

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { IconCirclePlus, IconCopy, IconDotsVertical, IconFingerprint, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
import { IconCirclePlus, IconCopy, IconDotsVertical, IconEdit, IconFingerprint, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Avatar, Button, Dropdown, Input, Skeleton, Table, type TableProps, Tabs, Typography, theme } from "antd";
import dayjs from "dayjs";
@ -87,9 +87,21 @@ const AccessList = () => {
<Dropdown
menu={{
items: [
{
key: "edit",
label: t("access.action.edit.button"),
icon: (
<span className="anticon scale-125">
<IconEdit size="1em" />
</span>
),
onClick: () => {
handleRecordDetailClick(record);
},
},
{
key: "duplicate",
label: t("common.button.duplicate"),
label: t("access.action.duplicate.button"),
icon: (
<span className="anticon scale-125">
<IconCopy size="1em" />
@ -104,7 +116,7 @@ const AccessList = () => {
},
{
key: "delete",
label: t("common.button.delete"),
label: t("access.action.delete.button"),
danger: true,
icon: (
<span className="anticon scale-125">

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { IconCertificate, IconDotsVertical, IconExternalLink, IconReload, IconTrash } from "@tabler/icons-react";
import { IconBrowserShare, IconCertificate, IconDotsVertical, IconExternalLink, IconReload, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Dropdown, Input, Segmented, Skeleton, Table, type TableProps, Typography, theme } from "antd";
import dayjs from "dayjs";
@ -146,9 +146,24 @@ const CertificateList = () => {
<Dropdown
menu={{
items: [
{
key: "view",
label: t("certificate.action.view.button"),
icon: (
<span className="anticon scale-125">
<IconBrowserShare size="1em" />
</span>
),
onClick: () => {
handleRecordDetailClick(record);
},
},
{
type: "divider",
},
{
key: "delete",
label: t("common.button.delete"),
label: t("certificate.action.delete.button"),
danger: true,
icon: (
<span className="anticon scale-125">

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { IconArrowBackUp, IconChevronDown, IconDots, IconHistory, IconPlayerPlay, IconRobot, IconTrash } from "@tabler/icons-react";
import { Alert, App, Button, Card, Dropdown, Flex, Form, Input, Space, Tabs } from "antd";
import { Alert, App, Button, Card, Dropdown, Flex, Form, Input, Segmented, Space, Tabs } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { isEqual } from "radash";
import { z } from "zod";
@ -194,55 +194,53 @@ const WorkflowDetail = () => {
return (
<div className="flex size-full flex-col">
<Card styles={{ body: { padding: 0 } }}>
<div className="px-6 py-4">
<div className="mx-auto max-w-320">
<div className="flex justify-between gap-2">
<div>
<h1>{workflow.name || "\u00A0"}</h1>
<p className="mb-0 text-base text-gray-500">{workflow.description || "\u00A0"}</p>
</div>
<Flex gap="small">
{initialized
? [
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
<Button key="enable" onClick={handleEnableChange}>
{workflow.enabled ? t("workflow.action.disable.button") : t("workflow.action.enable.button")}
</Button>,
<Dropdown
key="more"
menu={{
items: [
{
key: "delete",
label: t("common.button.delete"),
danger: true,
icon: <IconTrash size="1.25em" />,
onClick: () => {
handleDeleteClick();
},
},
],
}}
trigger={["click"]}
>
<Button icon={<IconChevronDown size="1.25em" />} iconPosition="end">
{t("common.button.more")}
</Button>
</Dropdown>,
]
: []}
</Flex>
<div className="px-6 py-4">
<div className="relative mx-auto max-w-320">
<div className="flex justify-between gap-2">
<div>
<h1>{workflow.name || "\u00A0"}</h1>
<p className="mb-0 text-base text-gray-500">{workflow.description || "\u00A0"}</p>
</div>
<Flex className="my-2" gap="small">
{initialized
? [
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
<Button key="enable" onClick={handleEnableChange}>
{workflow.enabled ? t("workflow.action.disable.button") : t("workflow.action.enable.button")}
</Button>,
<Dropdown
key="more"
menu={{
items: [
{
key: "delete",
label: t("common.button.delete"),
danger: true,
icon: <IconTrash size="1.25em" />,
onClick: () => {
handleDeleteClick();
},
},
],
}}
trigger={["click"]}
>
<Button icon={<IconChevronDown size="1.25em" />} iconPosition="end">
{t("common.button.more")}
</Button>
</Dropdown>,
]
: []}
</Flex>
</div>
<Tabs
className="-mb-4"
activeKey={tabValue}
defaultActiveKey="orchestration"
items={[
<div className="absolute -bottom-12 left-1/2 z-1 -translate-x-1/2">
<Segmented
className="shadow"
options={[
{
key: "orchestration",
label: t("workflow.detail.orchestration.tab"),
value: "orchestration",
label: <span className="px-2 text-sm">{t("workflow.detail.orchestration.tab")}</span>,
icon: (
<span className="anticon scale-125" role="img">
<IconRobot size="1em" />
@ -250,8 +248,8 @@ const WorkflowDetail = () => {
),
},
{
key: "runs",
label: t("workflow.detail.runs.tab"),
value: "runs",
label: <span className="px-2 text-sm">{t("workflow.detail.runs.tab")}</span>,
icon: (
<span className="anticon scale-125" role="img">
<IconHistory size="1em" />
@ -259,13 +257,16 @@ const WorkflowDetail = () => {
),
},
]}
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
tabBarStyle={{ border: "none" }}
onChange={(key) => setTabValue(key as typeof tabValue)}
size="large"
value={tabValue}
defaultValue="orchestration"
onChange={(value) => {
setTabValue(value as typeof tabValue);
}}
/>
</div>
</div>
</Card>
</div>
<Show when={tabValue === "orchestration"}>
<div className="min-h-[360px] flex-1 overflow-hidden p-4">
@ -280,7 +281,7 @@ const WorkflowDetail = () => {
}}
loading={!initialized}
>
<div className="absolute inset-x-6 top-4 z-2 mx-auto flex max-w-320 items-center justify-between gap-4">
<div className="absolute inset-x-6 top-4 z-2 mx-auto flex max-w-320 items-center justify-between gap-4 pt-6">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} showIcon type="warning" />
@ -316,14 +317,14 @@ const WorkflowDetail = () => {
</div>
</div>
<WorkflowElementsContainer className="pt-16" />
<WorkflowElementsContainer className="pt-24" />
</Card>
</div>
</Show>
<Show when={tabValue === "runs"}>
<div className="p-4">
<div className="mx-auto max-w-320">
<div className="mx-auto max-w-320 pt-6">
<WorkflowRuns workflowId={workflowId!} />
</div>
</div>

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { IconCirclePlus, IconCopy, IconDotsVertical, IconHierarchy3, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
import { IconCirclePlus, IconCopy, IconDotsVertical, IconEdit, IconHierarchy3, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Dropdown, Flex, Input, Segmented, Skeleton, Switch, Table, type TableProps, Typography, theme } from "antd";
import dayjs from "dayjs";
@ -133,9 +133,21 @@ const WorkflowList = () => {
<Dropdown
menu={{
items: [
{
key: "edit",
label: t("workflow.action.edit.button"),
icon: (
<span className="anticon scale-125">
<IconEdit size="1em" />
</span>
),
onClick: () => {
handleRecordDetailClick(record);
},
},
{
key: "duplicate",
label: t("common.button.duplicate"),
label: t("workflow.action.duplicate.button"),
icon: (
<span className="anticon scale-125">
<IconCopy size="1em" />
@ -150,7 +162,7 @@ const WorkflowList = () => {
},
{
key: "delete",
label: t("common.button.delete"),
label: t("workflow.action.delete.button"),
danger: true,
icon: (
<span className="anticon scale-125">