feat(ui): enhance WorkflowRunDetail

This commit is contained in:
Fu Diwei
2025-08-26 13:00:20 +08:00
parent 7b71593ad3
commit 0c7ba0503f
12 changed files with 400 additions and 170 deletions
+185 -91
View File
@@ -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(() => {
+98 -49
View File
@@ -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>
);
+2 -1
View File
@@ -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": "显示转义换行符",
+3 -3
View File
@@ -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");
}
};
+46 -15
View File
@@ -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) => {
+1 -1
View File
@@ -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",
+10 -1
View File
@@ -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();