mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-09 14:09:52 +00:00
feat(ui): improve workflow elements scroll area
This commit is contained in:
parent
b67049f9aa
commit
5cabceb08e
@ -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
19
ui/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -49,9 +49,7 @@ const DrawerForm = <T extends NonNullable<unknown> = any>({
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, {
|
||||
onClick: () => {
|
||||
console.log("click");
|
||||
setOpen(true);
|
||||
console.log(open);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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"));
|
||||
}
|
||||
};
|
||||
|
41
ui/src/components/workflow/WorkflowElementsContainer.tsx
Normal file
41
ui/src/components/workflow/WorkflowElementsContainer.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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
6
ui/src/utils/css.ts
Normal 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));
|
||||
};
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user