mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(ui): enhance WorkflowRunDetail
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconBrowserShare, IconCheck, IconChevronRight, IconDownload, IconSettings2 } from "@tabler/icons-react";
|
||||
import { EditorState, FlowLayoutDefault } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconBrowserShare, IconCheck, IconDots, IconDownload, IconSettings2, IconTransferOut } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Button, Collapse, Divider, Dropdown, Empty, Skeleton, Spin, Table, type TableProps, Tooltip, Typography } from "antd";
|
||||
import { Alert, App, Button, Card, Divider, Dropdown, Empty, Skeleton, Spin, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@@ -17,6 +18,9 @@ import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/wor
|
||||
import { mergeCls } from "@/utils/css";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
import WorkflowDesigner from "./designer/Designer";
|
||||
import WorkflowToolbar from "./designer/Toolbar";
|
||||
import WorkflowGraphExportModal from "./WorkflowGraphExportModal";
|
||||
import WorkflowStatus from "./WorkflowStatus";
|
||||
|
||||
export interface WorkflowRunDetailProps {
|
||||
@@ -25,21 +29,127 @@ export interface WorkflowRunDetailProps {
|
||||
data: WorkflowRunModel;
|
||||
}
|
||||
|
||||
const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
|
||||
return (
|
||||
<div {...props}>
|
||||
<Show when={!!data}>
|
||||
<WorkflowRunLogs runId={data.id} runStatus={data.status} />
|
||||
</Show>
|
||||
const WorkflowRunDetail = ({ className, style, data }: WorkflowRunDetailProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
<Show when={!!data && data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
|
||||
<Divider />
|
||||
<WorkflowRunArtifacts runId={data.id} />
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<Alert
|
||||
message={
|
||||
<div className="text-xs">
|
||||
{data.endedAt
|
||||
? t("workflow_run.base.description_with_time_cost", {
|
||||
trigger: t(`workflow_run.base.trigger.${data.trigger}`),
|
||||
startedAt: dayjs(data.startedAt).format("YYYY-MM-DD HH:mm:ss"),
|
||||
timeCost: dayjs(data.endedAt).diff(dayjs(data.startedAt), "second") + "s",
|
||||
})
|
||||
: t("workflow_run.base.description", {
|
||||
trigger: t(`workflow_run.base.trigger.${data.trigger}`),
|
||||
startedAt: dayjs(data.startedAt).format("YYYY-MM-DD HH:mm:ss"),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
showIcon
|
||||
type={
|
||||
{
|
||||
[WORKFLOW_RUN_STATUSES.SUCCEEDED]: "success" as const,
|
||||
[WORKFLOW_RUN_STATUSES.FAILED]: "error" as const,
|
||||
[WORKFLOW_RUN_STATUSES.CANCELED]: "warning" as const,
|
||||
}[data.status] ?? ("info" as const)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-8">
|
||||
<Typography.Title level={5}>{t("workflow_run.process")}</Typography.Title>
|
||||
<WorkflowRunProcess runData={data} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
|
||||
<WorkflowRunLogs runId={data.id} runStatus={data.status} />
|
||||
</div>
|
||||
|
||||
<Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
|
||||
<div className="mt-8">
|
||||
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
|
||||
<WorkflowRunArtifacts runId={data.id} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowRunProcess = ({ runData }: { runData: WorkflowRunModel }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal();
|
||||
|
||||
const handleExportClick = () => {
|
||||
graphExportModal.open({ data: runData.graph! });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className="size-full overflow-hidden"
|
||||
styles={{
|
||||
body: {
|
||||
position: "relative",
|
||||
height: "240px",
|
||||
padding: 0,
|
||||
cursor: "grab",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<WorkflowDesigner
|
||||
defaultEditorState={EditorState.STATE_MOUSE_FRIENDLY_SELECT.id}
|
||||
defaultLayout={FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT}
|
||||
initialData={runData.graph}
|
||||
readonly
|
||||
>
|
||||
<div className="absolute bottom-4 z-10 w-full px-4">
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<WorkflowToolbar
|
||||
style={{
|
||||
backgroundColor: themeToken.colorBgContainer,
|
||||
borderRadius: themeToken.borderRadius,
|
||||
}}
|
||||
size="small"
|
||||
showMouseState={false}
|
||||
showLayout={false}
|
||||
showMinimap={false}
|
||||
showZoomLevel={false}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "export",
|
||||
label: t("workflow_run.process.menu.export"),
|
||||
icon: <IconTransferOut size="1.25em" />,
|
||||
onClick: handleExportClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<IconDots size="1.25em" />} size="small" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WorkflowDesigner>
|
||||
</Card>
|
||||
|
||||
<WorkflowGraphExportModal {...graphExportModalProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -131,7 +241,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
.map((group) => {
|
||||
const escape = (str: string) => str.replaceAll("\r", "\\r").replaceAll("\n", "\\n");
|
||||
return (
|
||||
group.name +
|
||||
`#${group.id} ${group.name}` +
|
||||
NEWLINE +
|
||||
group.records
|
||||
.map((record) => {
|
||||
@@ -156,49 +266,48 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
|
||||
<div className="rounded-md bg-black text-stone-200">
|
||||
<div className="flex items-center gap-2 p-4">
|
||||
<div className="grow overflow-hidden">
|
||||
<WorkflowStatus value={runStatus} />
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "show-timestamp",
|
||||
label: t("workflow_run.logs.menu.show_timestamps"),
|
||||
icon: <IconCheck className={showTimestamp ? "visible" : "invisible"} size="1.25em" />,
|
||||
onClick: () => setShowTimestamp(!showTimestamp),
|
||||
},
|
||||
{
|
||||
key: "show-whitespace",
|
||||
label: t("workflow_run.logs.menu.show_whitespaces"),
|
||||
icon: <IconCheck className={showWhitespace ? "visible" : "invisible"} size="1.25em" />,
|
||||
onClick: () => setShowWhitespace(!showWhitespace),
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "download-logs",
|
||||
label: t("workflow_run.logs.menu.download_logs"),
|
||||
icon: <IconDownload className="invisible" size="1.25em" />,
|
||||
onClick: handleDownloadClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" icon={<IconSettings2 size="1.25em" />} ghost={browserTheme === "light"} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="rounded-md bg-black text-stone-200">
|
||||
<div className="flex items-center gap-2 p-4">
|
||||
<div className="grow overflow-hidden">
|
||||
<WorkflowStatus value={runStatus} />
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "show-timestamp",
|
||||
label: t("workflow_run.logs.menu.show_timestamps"),
|
||||
icon: <IconCheck className={showTimestamp ? "visible" : "invisible"} size="1.25em" />,
|
||||
onClick: () => setShowTimestamp(!showTimestamp),
|
||||
},
|
||||
{
|
||||
key: "show-whitespace",
|
||||
label: t("workflow_run.logs.menu.show_whitespaces"),
|
||||
icon: <IconCheck className={showWhitespace ? "visible" : "invisible"} size="1.25em" />,
|
||||
onClick: () => setShowWhitespace(!showWhitespace),
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "download-logs",
|
||||
label: t("workflow_run.logs.menu.download_logs"),
|
||||
icon: <IconDownload className="invisible" size="1.25em" />,
|
||||
onClick: handleDownloadClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" icon={<IconSettings2 size="1.25em" />} ghost={browserTheme === "light"} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-0 bg-stone-800" />
|
||||
<Divider className="my-0 bg-stone-800" />
|
||||
|
||||
<div className="min-h-8 px-4 py-2">
|
||||
<Show
|
||||
when={!loading || listData.length > 0}
|
||||
fallback={
|
||||
@@ -207,32 +316,20 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
</Spin>
|
||||
}
|
||||
>
|
||||
<div className="py-2">
|
||||
<Collapse
|
||||
style={{ color: "inherit" }}
|
||||
bordered={false}
|
||||
defaultActiveKey={listData.map((group) => group.id)}
|
||||
expandIcon={({ isActive }) => <IconChevronRight className={mergeCls(isActive ? "rotate-90" : "", "transition-transform")} size="1.25em" />}
|
||||
items={listData.map((group) => {
|
||||
return {
|
||||
key: group.id,
|
||||
classNames: {
|
||||
header: "text-sm text-stone-200",
|
||||
body: "text-stone-200",
|
||||
},
|
||||
style: { color: "inherit", border: "none" },
|
||||
styles: {
|
||||
header: { color: "inherit" },
|
||||
},
|
||||
label: group.name,
|
||||
children: <div className="flex flex-col space-y-1">{group.records.map((record) => renderRecord(record))}</div>,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{listData.map((group) => {
|
||||
return (
|
||||
<div className="mb-3 text-stone-200">
|
||||
<div className="truncate text-xs leading-loose">
|
||||
<span className="font-mono text-stone-400">{`#${group.id}\u00A0`}</span>
|
||||
<span>{group.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">{group.records.map((record) => renderRecord(record))}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -286,7 +383,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<CertificateModel[]>([]);
|
||||
const { loading: tableLoading } = useRequest(
|
||||
const { loading } = useRequest(
|
||||
() => {
|
||||
return listCertificatesByWorkflowRunId(runId);
|
||||
},
|
||||
@@ -309,20 +406,17 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
|
||||
<Table<CertificateModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={tableLoading}
|
||||
locale={{
|
||||
emptyText: <Empty description={t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey={(record) => record.id}
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
<Table<CertificateModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty description={t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey={(record) => record.id}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { startTransition, useCallback, useState } from "react";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { useControllableValue, useGetState } from "ahooks";
|
||||
import { Button, Drawer, Flex } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
@@ -64,7 +64,8 @@ const WorkflowRunDetailDrawer = ({ afterClose, data, loading, trigger, ...props
|
||||
|
||||
const useDrawer = () => {
|
||||
type DataType = WorkflowRunDetailDrawerProps["data"];
|
||||
const [data, setData] = useState<DataType>();
|
||||
const [data, setData, getData] = useGetState<DataType>();
|
||||
const [loading, setLoading] = useState<boolean>();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
@@ -77,17 +78,34 @@ const useDrawer = () => {
|
||||
startTransition(() => {
|
||||
if (!open) {
|
||||
setData(void 0);
|
||||
setLoading(void 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
data,
|
||||
loading,
|
||||
open,
|
||||
onOpenChange,
|
||||
},
|
||||
|
||||
open: (data: NonNullable<DataType>) => {
|
||||
open: ({ data, loading }: { data: NonNullable<DataType>; loading?: boolean }) => {
|
||||
setData(data);
|
||||
setLoading(loading);
|
||||
setOpen(true);
|
||||
|
||||
return {
|
||||
safeUpdate: ({ data, loading }: { data?: NonNullable<DataType>; loading?: boolean }) => {
|
||||
if (data != null) {
|
||||
if (data.id !== getData()?.id) return; // 确保数据不脏读
|
||||
|
||||
setData(data);
|
||||
}
|
||||
|
||||
if (loading != null) {
|
||||
setLoading(loading);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface DesignerProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
defaultEditorState?: string;
|
||||
defaultLayout?: string;
|
||||
initialData?: FlowDocumentJSON;
|
||||
readonly?: boolean;
|
||||
onDocumentChange?: (ctx: FixedLayoutPluginContext) => void;
|
||||
@@ -39,7 +41,7 @@ export interface DesignerInstance extends FixedLayoutPluginContext {
|
||||
}
|
||||
|
||||
const Designer = forwardRef<DesignerInstance, DesignerProps>(
|
||||
({ className, style, children, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => {
|
||||
({ className, style, children, defaultEditorState, defaultLayout, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => {
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const rendered = useRef(false);
|
||||
@@ -47,6 +49,8 @@ const Designer = forwardRef<DesignerInstance, DesignerProps>(
|
||||
const flowgramEditorRef = useRef<FixedLayoutPluginContext>(null);
|
||||
const flowgramEditorProps = useMemo<FixedLayoutProps>(
|
||||
() => ({
|
||||
defaultLayout: defaultLayout,
|
||||
|
||||
initialData: initialData,
|
||||
|
||||
constants: {
|
||||
@@ -121,9 +125,13 @@ const Designer = forwardRef<DesignerInstance, DesignerProps>(
|
||||
],
|
||||
|
||||
onInit: (ctx) => {
|
||||
const maybeMobile = ["android", "ios", "iphone", "ipad", "micromessenger"].some((s) => navigator.userAgent.includes(s));
|
||||
if (maybeMobile) {
|
||||
ctx.playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);
|
||||
if (defaultEditorState != null) {
|
||||
ctx.playground.editorState.changeState(defaultEditorState);
|
||||
} else {
|
||||
const maybeMobile = ["android", "ios", "iphone", "ipad", "micromessenger"].some((s) => navigator.userAgent.includes(s));
|
||||
if (maybeMobile) {
|
||||
ctx.playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -136,7 +144,7 @@ const Designer = forwardRef<DesignerInstance, DesignerProps>(
|
||||
}, 1);
|
||||
},
|
||||
}),
|
||||
[themeToken, initialData, readonly, onDocumentChange]
|
||||
[defaultEditorState, defaultLayout, initialData, readonly, onDocumentChange, themeToken]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EditorState, FlowLayoutDefault, useClientContext, usePlaygroundTools, useRefresh } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconHandStop, IconLayoutCards, IconMatrix, IconMaximize, IconMinus, IconPlus } from "@tabler/icons-react";
|
||||
import { Button, Dropdown, Tooltip } from "antd";
|
||||
import { Button, type ButtonProps, Dropdown, Tooltip } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
import Minimap from "./Minimap";
|
||||
@@ -11,9 +12,26 @@ import Minimap from "./Minimap";
|
||||
export interface ToolbarProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: ButtonProps["size"];
|
||||
showLayout?: boolean;
|
||||
showMinimap?: boolean;
|
||||
showMouseState?: boolean;
|
||||
showZoom?: boolean;
|
||||
showZoomFit?: boolean;
|
||||
showZoomLevel?: boolean;
|
||||
}
|
||||
|
||||
const Toolbar = ({ className, style }: ToolbarProps) => {
|
||||
const Toolbar = ({
|
||||
className,
|
||||
style,
|
||||
size,
|
||||
showLayout = true,
|
||||
showMinimap = true,
|
||||
showMouseState = true,
|
||||
showZoom = true,
|
||||
showZoomFit = true,
|
||||
showZoomLevel = true,
|
||||
}: ToolbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ctx = useClientContext();
|
||||
@@ -29,6 +47,12 @@ const Toolbar = ({ className, style }: ToolbarProps) => {
|
||||
return () => d.dispose();
|
||||
}, [playground]);
|
||||
|
||||
const buttonIconSize = useMemo(() => {
|
||||
if (size === "large") return "1.5em";
|
||||
if (size === "small") return "1em";
|
||||
return "1.25em";
|
||||
}, [size]);
|
||||
|
||||
const [isMinimapVisible, setIsMinimapVisible] = useState(() => window.screen.availWidth >= 1024);
|
||||
|
||||
const [isMouseFriendly, setIsMouseFriendly] = useState(() => playground.editorState.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id));
|
||||
@@ -58,55 +82,80 @@ const Toolbar = ({ className, style }: ToolbarProps) => {
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Tooltip title={isMouseFriendly ? t("workflow.detail.design.toolbar.hand_mode") : t("workflow.detail.design.toolbar.pointer_mode")}>
|
||||
<Button
|
||||
ghost={isMouseFriendly}
|
||||
icon={<IconHandStop size="1.25em" />}
|
||||
type={isMouseFriendly ? "primary" : "default"}
|
||||
onClick={handleToggleMouseFriendly}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Show when={showMouseState}>
|
||||
<Tooltip title={isMouseFriendly ? t("workflow.detail.design.toolbar.hand_mode") : t("workflow.detail.design.toolbar.pointer_mode")}>
|
||||
<Button
|
||||
ghost={isMouseFriendly}
|
||||
icon={<IconHandStop size={buttonIconSize} />}
|
||||
size={size}
|
||||
type={isMouseFriendly ? "primary" : "default"}
|
||||
onClick={handleToggleMouseFriendly}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.zoomout")}>
|
||||
<Button icon={<IconMinus size="1.25em" />} onClick={() => tools.zoomout()} />
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
...[200, 100, 75, 50, 25].map((zoom) => ({
|
||||
key: `${zoom}%`,
|
||||
label: `${zoom}%`,
|
||||
onClick: () => tools.updateZoom(zoom / 100),
|
||||
})),
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "auto",
|
||||
label: t("workflow.detail.design.toolbar.auto_fit"),
|
||||
onClick: () => tools.fitView(),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button className="w-16 text-center">{Math.round(tools.zoom * 100)}%</Button>
|
||||
</Dropdown>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.zoomin")}>
|
||||
<Button icon={<IconPlus size="1.25em" />} onClick={() => tools.zoomin()} />
|
||||
</Tooltip>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.auto_fit")}>
|
||||
<Button icon={<IconMaximize size="1.25em" />} onClick={() => tools.fitView()} />
|
||||
</Tooltip>
|
||||
<Show when={showZoom}>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.zoomout")}>
|
||||
<Button icon={<IconMinus size={buttonIconSize} />} size={size} onClick={() => tools.zoomout()} />
|
||||
</Tooltip>
|
||||
<Show when={showZoomLevel}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
...[200, 100, 75, 50, 25].map((zoom) => ({
|
||||
key: `${zoom}%`,
|
||||
label: `${zoom}%`,
|
||||
onClick: () => tools.updateZoom(zoom / 100),
|
||||
})),
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "auto",
|
||||
label: t("workflow.detail.design.toolbar.auto_fit"),
|
||||
onClick: () => tools.fitView(),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button className="w-16 text-center" size={size}>
|
||||
{Math.round(tools.zoom * 100)}%
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Show>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.zoomin")}>
|
||||
<Button icon={<IconPlus size={buttonIconSize} />} size={size} onClick={() => tools.zoomin()} />
|
||||
</Tooltip>
|
||||
<Show when={showZoomFit}>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.auto_fit")}>
|
||||
<Button icon={<IconMaximize size={buttonIconSize} />} size={size} onClick={() => tools.fitView()} />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Tooltip title={tools.isVertical ? t("workflow.detail.design.toolbar.vertical_layout") : t("workflow.detail.design.toolbar.horizontal_layout")}>
|
||||
<Button icon={<IconLayoutCards className={mergeCls({ ["rotate-90"]: tools.isVertical })} size="1.25em" />} onClick={handleToggleLayout} />
|
||||
</Tooltip>
|
||||
<Show when={showLayout}>
|
||||
<Tooltip title={tools.isVertical ? t("workflow.detail.design.toolbar.vertical_layout") : t("workflow.detail.design.toolbar.horizontal_layout")}>
|
||||
<Button
|
||||
icon={<IconLayoutCards className={mergeCls({ ["rotate-90"]: tools.isVertical })} size={buttonIconSize} />}
|
||||
size={size}
|
||||
onClick={handleToggleLayout}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.minimap")}>
|
||||
<Button icon={<IconMatrix size="1.25em" />} ghost={isMinimapVisible} type={isMinimapVisible ? "primary" : "default"} onClick={handleToggleMinimap} />
|
||||
</Tooltip>
|
||||
{isMinimapVisible && <Minimap className="absolute right-0 bottom-[42px]" />}
|
||||
<Show when={showMinimap}>
|
||||
<Tooltip title={t("workflow.detail.design.toolbar.minimap")}>
|
||||
<Button
|
||||
icon={<IconMatrix size={buttonIconSize} />}
|
||||
ghost={isMinimapVisible}
|
||||
type={isMinimapVisible ? "primary" : "default"}
|
||||
size={size}
|
||||
onClick={handleToggleMinimap}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isMinimapVisible && <Minimap className="absolute right-0 bottom-[42px]" />}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type WorkflowModel } from "./workflow";
|
||||
import { type WorkflowGraph, type WorkflowModel } from "./workflow";
|
||||
|
||||
export interface WorkflowRunModel extends BaseModel {
|
||||
workflowRef: string;
|
||||
@@ -6,6 +6,7 @@ export interface WorkflowRunModel extends BaseModel {
|
||||
trigger: string;
|
||||
startedAt: ISO8601String;
|
||||
endedAt: ISO8601String;
|
||||
graph?: WorkflowGraph;
|
||||
error?: string;
|
||||
expand?: {
|
||||
workflowRef?: Pick<WorkflowModel, "id" | "name" | "description">;
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
"workflow_run.props.started_at": "Started at",
|
||||
"workflow_run.props.ended_at": "Ended at",
|
||||
|
||||
"workflow_run.base.description": "Triggered {{trigger}} at {{startedAt}}",
|
||||
"workflow_run.base.description_with_time_cost": "Triggered {{trigger}} at {{startedAt}}. Time cost: {{timeCost}}.",
|
||||
"workflow_run.base.trigger.scheduled": "scheduledly",
|
||||
"workflow_run.base.trigger.manual": "manually",
|
||||
|
||||
"workflow_run.process": "Process",
|
||||
"workflow_run.process.menu.export": "Export",
|
||||
|
||||
"workflow_run.logs": "Logs",
|
||||
"workflow_run.logs.menu.show_timestamps": "Show timestamps",
|
||||
"workflow_run.logs.menu.show_whitespaces": "Show whitespaces",
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
"workflow_run.props.started_at": "开始时间",
|
||||
"workflow_run.props.ended_at": "完成时间",
|
||||
|
||||
"workflow_run.base.description": "{{trigger}}触发于 {{startedAt}}",
|
||||
"workflow_run.base.description_with_time_cost": "{{trigger}}触发于 {{startedAt}},总计用时 {{timeCost}}。",
|
||||
"workflow_run.base.trigger.scheduled": "定时",
|
||||
"workflow_run.base.trigger.manual": "手动",
|
||||
|
||||
"workflow_run.process": "流程",
|
||||
"workflow_run.process.menu.export": "导出",
|
||||
|
||||
"workflow_run.logs": "日志",
|
||||
"workflow_run.logs.menu.show_timestamps": "显示日期时间",
|
||||
"workflow_run.logs.menu.show_whitespaces": "显示转义换行符",
|
||||
|
||||
@@ -40,10 +40,10 @@ const ConsoleLayout = () => {
|
||||
};
|
||||
|
||||
const handleDocumentClick = () => {
|
||||
if (i18n.language.startsWith("en")) {
|
||||
window.open(APP_DOCUMENT_URL + "/en/", "_blank");
|
||||
} else {
|
||||
if (i18n.language.startsWith("zh")) {
|
||||
window.open(APP_DOCUMENT_URL, "_blank");
|
||||
} else {
|
||||
window.open(APP_DOCUMENT_URL + "/en/", "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,13 +26,11 @@ import WorkflowStatus from "@/components/workflow/WorkflowStatus";
|
||||
import { type Statistics } from "@/domain/statistics";
|
||||
import { type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { useBrowserTheme } from "@/hooks";
|
||||
import { list as listWorkflowRuns } from "@/repository/workflowRun";
|
||||
import { get as getWorkflowRun, list as listWorkflowRuns } from "@/repository/workflowRun";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -48,17 +46,7 @@ const Dashboard = () => {
|
||||
|
||||
<div className="mt-8">
|
||||
<h3>{t("dashboard.shortcut")}</h3>
|
||||
<div className="flex items-center gap-4 not-md:flex-wrap">
|
||||
<Button icon={<IconCirclePlus size="1em" />} shape="round" size="large" type="primary" onClick={() => navigate("/workflows/new")}>
|
||||
<span className="text-sm">{t("dashboard.shortcut.create_workflow")}</span>
|
||||
</Button>
|
||||
<Button icon={<IconLock size="1em" />} shape="round" size="large" onClick={() => navigate("/settings/account")}>
|
||||
<span className="text-sm">{t("dashboard.shortcut.change_account")}</span>
|
||||
</Button>
|
||||
<Button icon={<IconPlugConnected size="1em" />} shape="round" size="large" onClick={() => navigate("/settings/ssl-provider")}>
|
||||
<span className="text-sm">{t("dashboard.shortcut.configure_ca")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Shortcuts />
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
@@ -224,6 +212,46 @@ const StatisticCards = ({ className, style }: { className?: string; style?: Reac
|
||||
);
|
||||
};
|
||||
|
||||
const Shortcuts = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className="flex items-center gap-4 not-md:flex-wrap">
|
||||
<Button
|
||||
className="shadow"
|
||||
icon={<IconCirclePlus color="var(--color-primary)" size="1.25em" />}
|
||||
shape="round"
|
||||
size="large"
|
||||
onClick={() => navigate("/workflows/new")}
|
||||
>
|
||||
<span className="text-sm">{t("dashboard.shortcut.create_workflow")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="shadow"
|
||||
icon={<IconLock color="var(--color-warning)" size="1.25em" />}
|
||||
shape="round"
|
||||
size="large"
|
||||
onClick={() => navigate("/settings/account")}
|
||||
>
|
||||
<span className="text-sm">{t("dashboard.shortcut.change_account")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="shadow"
|
||||
icon={<IconPlugConnected color="var(--color-info)" size="1.25em" />}
|
||||
shape="round"
|
||||
size="large"
|
||||
onClick={() => navigate("/settings/ssl-provider")}
|
||||
>
|
||||
<span className="text-sm">{t("dashboard.shortcut.configure_ca")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowRunHistoryTable = ({ className, style }: { className?: string; style?: React.CSSProperties }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -332,7 +360,10 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
|
||||
const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer();
|
||||
|
||||
const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => {
|
||||
detailDrawer.open(workflowRun);
|
||||
const drawer = detailDrawer.open({ data: workflowRun, loading: true });
|
||||
getWorkflowRun(workflowRun.id).then((data) => {
|
||||
drawer.safeUpdate({ data, loading: false });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,7 @@ import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { useAppSettings, useZustandShallowSelector } from "@/hooks";
|
||||
import {
|
||||
get as getWorkflowRun,
|
||||
list as listWorkflowRuns,
|
||||
remove as removeWorkflowRun,
|
||||
subscribe as subscribeWorkflowRun,
|
||||
@@ -260,7 +261,10 @@ const WorkflowDetailRuns = () => {
|
||||
const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer();
|
||||
|
||||
const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => {
|
||||
detailDrawer.open(workflowRun);
|
||||
const drawer = detailDrawer.open({ data: workflowRun, loading: true });
|
||||
getWorkflowRun(workflowRun.id).then((data) => {
|
||||
drawer.safeUpdate({ data, loading: false });
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecordCancelClick = (workflowRun: WorkflowRunModel) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const list = async (request: ListRequest) => {
|
||||
const perPage = request.perPage || 10;
|
||||
|
||||
return await pb.collection(COLLECTION_NAME_WORKFLOW).getList<WorkflowModel>(page, perPage, {
|
||||
expand: request.expand ? "lastRunRef" : void 0,
|
||||
expand: request.expand ? ["lastRunRef"].join(",") : void 0,
|
||||
fields: [
|
||||
"id",
|
||||
"name",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const list = async (request: ListRequest) => {
|
||||
const page = request.page || 1;
|
||||
const perPage = request.perPage || 10;
|
||||
return await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList<WorkflowRunModel>(page, perPage, {
|
||||
expand: request.expand ? "workflowRef" : void 0,
|
||||
expand: request.expand ? ["workflowRef"].join(",") : void 0,
|
||||
fields: [
|
||||
"id",
|
||||
"status",
|
||||
@@ -43,6 +43,15 @@ export const list = async (request: ListRequest) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const get = async (id: string) => {
|
||||
return await getPocketBase()
|
||||
.collection(COLLECTION_NAME_WORKFLOW_RUN)
|
||||
.getOne<WorkflowRunModel>(id, {
|
||||
expand: ["workflowRef"].join(","),
|
||||
requestKey: null,
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel> | MaybeModelRecordWithId<WorkflowRunModel>[]) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user