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

19
ui/package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
const blob = new Blob([u8arr], { type: "application/zip" }); const blob = new Blob([u8arr], { type: "application/zip" });
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`); saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
} catch (err) { } catch (err) {
console.log(err); console.error(err);
messageApi.warning(t("common.text.operation_failed")); 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 ( 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"> <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="flex size-full flex-col items-center justify-between overflow-hidden">
<div className="w-full"> <div className="w-full">
@ -53,8 +53,8 @@ const ConsoleLayout = () => {
</div> </div>
</Layout.Sider> </Layout.Sider>
<Layout className="pl-[256px] max-md:pl-0"> <Layout className="flex flex-col overflow-hidden 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.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 size-full items-center justify-between overflow-hidden px-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} /> <SiderMenuDrawer trigger={<Button className="md:hidden" icon={<MenuOutlinedIcon />} size="large" />} />
@ -76,7 +76,7 @@ const ConsoleLayout = () => {
</div> </div>
</Layout.Header> </Layout.Header>
<Layout.Content style={{ overflow: "initial" }}> <Layout.Content className="flex-1 overflow-y-auto overflow-x-hidden">
<Outlet /> <Outlet />
</Layout.Content> </Layout.Content>
</Layout> </Layout>

View File

@ -7,10 +7,7 @@ import {
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
DownOutlined as DownOutlinedIcon, DownOutlined as DownOutlinedIcon,
EllipsisOutlined as EllipsisOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon,
ExpandOutlined as ExpandOutlinedIcon,
HistoryOutlined as HistoryOutlinedIcon, HistoryOutlined as HistoryOutlinedIcon,
MinusOutlined as MinusOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
UndoOutlined as UndoOutlinedIcon, UndoOutlined as UndoOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
@ -22,7 +19,7 @@ import { z } from "zod";
import { startRun as startWorkflowRun } from "@/api/workflows"; import { startRun as startWorkflowRun } from "@/api/workflows";
import ModalForm from "@/components/ModalForm"; import ModalForm from "@/components/ModalForm";
import Show from "@/components/Show"; import Show from "@/components/Show";
import WorkflowElements from "@/components/workflow/WorkflowElements"; import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
import WorkflowRuns from "@/components/workflow/WorkflowRuns"; import WorkflowRuns from "@/components/workflow/WorkflowRuns";
import { isAllNodesValidated } from "@/domain/workflow"; import { isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
@ -40,8 +37,6 @@ const WorkflowDetail = () => {
const [modalApi, ModalContextHolder] = Modal.useModal(); const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const [scale, setScale] = useState(1);
const { id: workflowId } = useParams(); const { id: workflowId } = useParams();
const { workflow, initialized, ...workflowState } = useWorkflowStore( const { workflow, initialized, ...workflowState } = useWorkflowStore(
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
@ -58,15 +53,12 @@ const WorkflowDetail = () => {
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
const [allowDiscard, setAllowDiscard] = useState(false); const [allowDiscard, setAllowDiscard] = useState(false);
const [allowRelease, setAllowRelease] = useState(false); const [allowRelease, setAllowRelease] = useState(false);
const [allowRun, setAllowRun] = useState(false); const [allowRun, setAllowRun] = useState(false);
const lastRunStatus = useMemo(() => {
return workflow.lastRunStatus;
}, [workflow]);
useEffect(() => { useEffect(() => {
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
}, [lastRunStatus]); }, [lastRunStatus]);
@ -206,12 +198,13 @@ const WorkflowDetail = () => {
}; };
return ( return (
<div> <div className="flex size-full flex-col">
{MessageContextHolder} {MessageContextHolder}
{ModalContextHolder} {ModalContextHolder}
{NotificationContextHolder} {NotificationContextHolder}
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}> <div>
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
<PageHeader <PageHeader
style={{ paddingBottom: 0 }} style={{ paddingBottom: 0 }}
title={workflow.name} title={workflow.name}
@ -263,18 +256,28 @@ const WorkflowDetail = () => {
/> />
</PageHeader> </PageHeader>
</Card> </Card>
</div>
<div className="p-4">
<Card loading={!initialized}>
<Show when={tabValue === "orchestration"}> <Show when={tabValue === "orchestration"}>
<div className="relative"> <div className="min-h-[360px] flex-1 overflow-hidden p-4">
<div className="flex items-center justify-between gap-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"> <div className="flex-1 overflow-hidden">
<Show when={workflow.hasDraft!}> <Show when={workflow.hasDraft!}>
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" /> <Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
</Show> </Show>
</div> </div>
<div className="flex justify-end "> <div className="flex justify-end">
<Space> <Space>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}> <Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
{t("workflow.detail.orchestration.action.run")} {t("workflow.detail.orchestration.action.run")}
@ -305,24 +308,19 @@ const WorkflowDetail = () => {
</Space> </Space>
</div> </div>
</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})` }}> <WorkflowElementsContainer className="pt-16" />
<WorkflowElements /> </Card>
</div>
</div> </div>
</Show> </Show>
<Show when={tabValue === "runs"}> <Show when={tabValue === "runs"}>
<div className="p-4">
<Card loading={!initialized}>
<WorkflowRuns workflowId={workflowId!} /> <WorkflowRuns workflowId={workflowId!} />
</Show>
</Card> </Card>
</div> </div>
</Show>
</div> </div>
); );
}; };

View File

@ -109,7 +109,7 @@ const WorkflowNew = () => {
<div> <div>
{NotificationContextHolder} {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")}> <PageHeader title={t("workflow.new.title")}>
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph> <Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
</PageHeader> </PageHeader>

View File

@ -14,7 +14,7 @@ export type ListWorkflowRunsRequest = {
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 = ""; let filter = "";
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (request.workflowId) { 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 => { 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; return error.message;
} else if (typeof error === "object" && error != null) { } else if (typeof error === "object" && error != null) {
if ("message" in error) { if ("message" in error) {