feat(ui): improve workflow elements scroll area

This commit is contained in:
Fu Diwei 2025-01-23 02:44:02 +08:00
parent b67049f9aa
commit 5cabceb08e
12 changed files with 197 additions and 129 deletions

View File

@ -105,7 +105,7 @@ func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartR
func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error {
// TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行
return nil
return errors.New("TODO: 尚未实现")
}
func (s *WorkflowService) Stop(ctx context.Context) {

19
ui/package-lock.json generated
View File

@ -13,6 +13,7 @@
"ahooks": "^3.8.4",
"antd": "^5.23.1",
"antd-zod": "^6.0.1",
"clsx": "^2.1.1",
"cron-parser": "^4.9.0",
"file-saver": "^2.0.5",
"i18next": "^24.2.1",
@ -27,6 +28,7 @@
"react-dom": "^18.3.1",
"react-i18next": "^15.4.0",
"react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
@ -4124,6 +4126,14 @@
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
@ -8563,6 +8573,15 @@
"integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==",
"dev": true
},
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz",

View File

@ -15,6 +15,7 @@
"ahooks": "^3.8.4",
"antd": "^5.23.1",
"antd-zod": "^6.0.1",
"clsx": "^2.1.1",
"cron-parser": "^4.9.0",
"file-saver": "^2.0.5",
"i18next": "^24.2.1",
@ -29,6 +30,7 @@
"react-dom": "^18.3.1",
"react-i18next": "^15.4.0",
"react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},

View File

@ -49,9 +49,7 @@ const DrawerForm = <T extends NonNullable<unknown> = any>({
const triggerEl = useTriggerElement(trigger, {
onClick: () => {
console.log("click");
setOpen(true);
console.log(open);
},
});

View File

@ -27,7 +27,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
const blob = new Blob([u8arr], { type: "application/zip" });
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
} catch (err) {
console.log(err);
console.error(err);
messageApi.warning(t("common.text.operation_failed"));
}
};

View File

@ -0,0 +1,41 @@
import { useState } from "react";
import { ExpandOutlined as ExpandOutlinedIcon, MinusOutlined as MinusOutlinedIcon, PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import { Button, Card, Typography } from "antd";
import WorkflowElements from "@/components/workflow/WorkflowElements";
import { mergeCls } from "@/utils/css";
export type WorkflowElementsProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
};
const WorkflowElementsContainer = ({ className, style, disabled }: WorkflowElementsProps) => {
const [scale, setScale] = useState(1);
return (
<div className={mergeCls("relative size-full overflow-hidden", className)} style={style}>
<div className="size-full overflow-auto">
<div className="relative z-[1]">
<div className="origin-center transition-transform duration-300" style={{ zoom: `${scale}` }}>
<div className="p-4">
<WorkflowElements disabled={disabled} />
</div>
</div>
</div>
</div>
<Card className="absolute bottom-4 right-6 z-[2] rounded-lg p-2 shadow-lg" styles={{ body: { padding: 0 } }}>
<div className="flex items-center gap-2">
<Button icon={<MinusOutlinedIcon />} disabled={scale <= 0.5} onClick={() => setScale((s) => Math.max(0.5, s - 0.1))} />
<Typography.Text className="min-w-[3em] text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<PlusOutlinedIcon />} disabled={scale >= 2} onClick={() => setScale((s) => Math.min(2, s + 0.1))} />
<Button icon={<ExpandOutlinedIcon />} onClick={() => setScale(1)} />
</div>
</Card>
</div>
);
};
export default WorkflowElementsContainer;

View File

@ -41,7 +41,7 @@ const ConsoleLayout = () => {
}
return (
<Layout className="min-h-screen" hasSider>
<Layout className="h-screen" hasSider>
<Layout.Sider className="fixed left-0 top-0 z-20 h-full max-md:static max-md:hidden" width="256px" theme="light">
<div className="flex size-full flex-col items-center justify-between overflow-hidden">
<div className="w-full">
@ -53,8 +53,8 @@ const ConsoleLayout = () => {
</div>
</Layout.Sider>
<Layout className="pl-[256px] max-md:pl-0">
<Layout.Header className="sticky inset-x-0 top-0 z-[19] p-0 shadow-sm" style={{ background: themeToken.colorBgContainer }}>
<Layout className="flex flex-col overflow-hidden pl-[256px] max-md:pl-0">
<Layout.Header className="p-0 shadow-sm" style={{ background: themeToken.colorBgContainer }}>
<div className="flex size-full items-center justify-between overflow-hidden px-4">
<div className="flex items-center gap-4">
<SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} />
@ -76,7 +76,7 @@ const ConsoleLayout = () => {
</div>
</Layout.Header>
<Layout.Content style={{ overflow: "initial" }}>
<Layout.Content className="flex-1 overflow-y-auto overflow-x-hidden">
<Outlet />
</Layout.Content>
</Layout>

View File

@ -7,10 +7,7 @@ import {
DeleteOutlined as DeleteOutlinedIcon,
DownOutlined as DownOutlinedIcon,
EllipsisOutlined as EllipsisOutlinedIcon,
ExpandOutlined as ExpandOutlinedIcon,
HistoryOutlined as HistoryOutlinedIcon,
MinusOutlined as MinusOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
UndoOutlined as UndoOutlinedIcon,
} from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components";
@ -22,7 +19,7 @@ import { z } from "zod";
import { startRun as startWorkflowRun } from "@/api/workflows";
import ModalForm from "@/components/ModalForm";
import Show from "@/components/Show";
import WorkflowElements from "@/components/workflow/WorkflowElements";
import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import WorkflowRuns from "@/components/workflow/WorkflowRuns";
import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
@ -40,8 +37,6 @@ const WorkflowDetail = () => {
const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const [scale, setScale] = useState(1);
const { id: workflowId } = useParams();
const { workflow, initialized, ...workflowState } = useWorkflowStore(
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
@ -58,15 +53,12 @@ const WorkflowDetail = () => {
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
const [isRunning, setIsRunning] = useState(false);
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
const [allowDiscard, setAllowDiscard] = useState(false);
const [allowRelease, setAllowRelease] = useState(false);
const [allowRun, setAllowRun] = useState(false);
const lastRunStatus = useMemo(() => {
return workflow.lastRunStatus;
}, [workflow]);
useEffect(() => {
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
}, [lastRunStatus]);
@ -206,123 +198,129 @@ const WorkflowDetail = () => {
};
return (
<div>
<div className="flex size-full flex-col">
{MessageContextHolder}
{ModalContextHolder}
{NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
<PageHeader
style={{ paddingBottom: 0 }}
title={workflow.name}
extra={
initialized
? [
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
<div>
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
<PageHeader
style={{ paddingBottom: 0 }}
title={workflow.name}
extra={
initialized
? [
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
<Button key="enable" onClick={handleEnableChange}>
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
</Button>,
<Button key="enable" onClick={handleEnableChange}>
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
</Button>,
<Dropdown
key="more"
menu={{
items: [
{
key: "delete",
label: t("workflow.action.delete"),
danger: true,
icon: <DeleteOutlinedIcon />,
onClick: () => {
handleDeleteClick();
},
},
],
}}
trigger={["click"]}
>
<Button icon={<DownOutlinedIcon />} iconPosition="end">
{t("common.button.more")}
</Button>
</Dropdown>,
]
: []
}
>
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
<Tabs
activeKey={tabValue}
defaultActiveKey="orchestration"
items={[
{ key: "orchestration", label: t("workflow.detail.orchestration.tab"), icon: <ApartmentOutlinedIcon /> },
{ key: "runs", label: t("workflow.detail.runs.tab"), icon: <HistoryOutlinedIcon /> },
]}
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
tabBarStyle={{ border: "none" }}
onChange={(key) => setTabValue(key as typeof tabValue)}
/>
</PageHeader>
</Card>
<div className="p-4">
<Card loading={!initialized}>
<Show when={tabValue === "orchestration"}>
<div className="relative">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
</Show>
</div>
<div className="flex justify-end ">
<Space>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
{t("workflow.detail.orchestration.action.run")}
</Button>
<Button.Group>
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
{t("workflow.detail.orchestration.action.release")}
</Button>
<Dropdown
menu={{
items: [
{
key: "discard",
disabled: !allowDiscard,
label: t("workflow.detail.orchestration.action.discard"),
icon: <UndoOutlinedIcon />,
onClick: handleDiscardClick,
<Dropdown
key="more"
menu={{
items: [
{
key: "delete",
label: t("workflow.action.delete"),
danger: true,
icon: <DeleteOutlinedIcon />,
onClick: () => {
handleDeleteClick();
},
],
}}
trigger={["click"]}
>
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
</Dropdown>
</Button.Group>
</Space>
</div>
</div>
<div className="fixed bottom-8 right-8 z-10 flex items-center gap-2 rounded-lg bg-white p-2 shadow-lg">
<Button icon={<MinusOutlinedIcon />} disabled={scale <= 0.5} onClick={() => setScale((s) => Math.max(0.5, s - 0.1))} />
<Typography.Text className="min-w-[3em] text-center">{Math.round(scale * 100)}%</Typography.Text>
<Button icon={<PlusOutlinedIcon />} disabled={scale >= 2} onClick={() => setScale((s) => Math.min(2, s + 0.1))} />
<Button icon={<ExpandOutlinedIcon />} onClick={() => setScale(1)} />
</div>
<div className="size-full origin-top px-12 py-8 transition-transform duration-300 max-md:px-4" style={{ transform: `scale(${scale})` }}>
<WorkflowElements />
</div>
</div>
</Show>
<Show when={tabValue === "runs"}>
<WorkflowRuns workflowId={workflowId!} />
</Show>
},
],
}}
trigger={["click"]}
>
<Button icon={<DownOutlinedIcon />} iconPosition="end">
{t("common.button.more")}
</Button>
</Dropdown>,
]
: []
}
>
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
<Tabs
activeKey={tabValue}
defaultActiveKey="orchestration"
items={[
{ key: "orchestration", label: t("workflow.detail.orchestration.tab"), icon: <ApartmentOutlinedIcon /> },
{ key: "runs", label: t("workflow.detail.runs.tab"), icon: <HistoryOutlinedIcon /> },
]}
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
tabBarStyle={{ border: "none" }}
onChange={(key) => setTabValue(key as typeof tabValue)}
/>
</PageHeader>
</Card>
</div>
<Show when={tabValue === "orchestration"}>
<div className="min-h-[360px] flex-1 overflow-hidden p-4">
<Card
className="size-full overflow-hidden"
styles={{
body: {
position: "relative",
height: "100%",
padding: 0,
},
}}
loading={!initialized}
>
<div className="absolute inset-x-6 top-4 z-[2] flex items-center justify-between gap-4">
<div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}>
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
</Show>
</div>
<div className="flex justify-end">
<Space>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
{t("workflow.detail.orchestration.action.run")}
</Button>
<Button.Group>
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
{t("workflow.detail.orchestration.action.release")}
</Button>
<Dropdown
menu={{
items: [
{
key: "discard",
disabled: !allowDiscard,
label: t("workflow.detail.orchestration.action.discard"),
icon: <UndoOutlinedIcon />,
onClick: handleDiscardClick,
},
],
}}
trigger={["click"]}
>
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
</Dropdown>
</Button.Group>
</Space>
</div>
</div>
<WorkflowElementsContainer className="pt-16" />
</Card>
</div>
</Show>
<Show when={tabValue === "runs"}>
<div className="p-4">
<Card loading={!initialized}>
<WorkflowRuns workflowId={workflowId!} />
</Card>
</div>
</Show>
</div>
);
};

View File

@ -109,7 +109,7 @@ const WorkflowNew = () => {
<div>
{NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
<PageHeader title={t("workflow.new.title")}>
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
</PageHeader>

View File

@ -14,7 +14,7 @@ export type ListWorkflowRunsRequest = {
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) {

6
ui/src/utils/css.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const mergeCls = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};

View File

@ -1,5 +1,9 @@
import { ClientResponseError } from "pocketbase";
export const getErrMsg = (error: unknown): string => {
if (error instanceof Error) {
if (error instanceof ClientResponseError) {
return error.response != null ? getErrMsg(error.response) : error.message;
} else if (error instanceof Error) {
return error.message;
} else if (typeof error === "object" && error != null) {
if ("message" in error) {