import { useState } from "react"; import { useTranslation } from "react-i18next"; import { CheckCircleOutlined as CheckCircleOutlinedIcon, CheckOutlined as CheckOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, DownloadOutlined as DownloadOutlinedIcon, RightOutlined as RightOutlinedIcon, SelectOutlined as SelectOutlinedIcon, SettingOutlined as SettingOutlinedIcon, StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { useRequest } from "ahooks"; import { Button, Collapse, Divider, Dropdown, Empty, Flex, Skeleton, Space, Spin, Table, type TableProps, Tooltip, Typography, notification, theme, } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; import type { WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/workflowLog"; import { mergeCls } from "@/utils/css"; import { getErrMsg } from "@/utils/error"; export type WorkflowRunDetailProps = { className?: string; style?: React.CSSProperties; data: WorkflowRunModel; }; const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => { return (
); }; const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: string }) => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { theme: browserTheme } = useBrowserTheme(); type Log = Pick; type LogGroup = { id: string; name: string; records: Log[] }; const [listData, setListData] = useState([]); const { loading } = useRequest( () => { return listLogsByWorkflowRunId(runId); }, { refreshDeps: [runId, runStatus], pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.RUNNING ? 3000 : 0, pollingWhenHidden: false, throttleWait: 500, onSuccess: (res) => { if (res.items.length === listData.flatMap((e) => e.records).length) return; setListData( res.items.reduce((acc, e) => { let group = acc.at(-1); if (!group || group.id !== e.nodeId) { group = { id: e.nodeId, name: e.nodeName, records: [] }; acc.push(group); } group.records.push({ timestamp: e.timestamp, level: e.level, message: e.message, data: e.data }); return acc; }, [] as LogGroup[]) ); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); throw err; }, } ); const [showTimestamp, setShowTimestamp] = useState(true); const [showWhitespace, setShowWhitespace] = useState(true); const renderBadge = () => { switch (runStatus) { case WORKFLOW_RUN_STATUSES.PENDING: return ( {t("workflow_run.props.status.pending")} ); case WORKFLOW_RUN_STATUSES.RUNNING: return ( {t("workflow_run.props.status.running")} ); case WORKFLOW_RUN_STATUSES.SUCCEEDED: return ( {t("workflow_run.props.status.succeeded")} ); case WORKFLOW_RUN_STATUSES.FAILED: return ( {t("workflow_run.props.status.failed")} ); case WORKFLOW_RUN_STATUSES.CANCELED: return ( {t("workflow_run.props.status.canceled")} ); } return <>; }; const renderRecord = (record: Log) => { let message = <>{record.message}; if (record.data != null && Object.keys(record.data).length > 0) { message = (
{record.message} {Object.entries(record.data).map(([key, value]) => (
{key}:
{JSON.stringify(value)}
))}
); } return (
{showTimestamp ?
[{dayjs(record.timestamp).format("YYYY-MM-DD HH:mm:ss")}]
: <>}
{message}
); }; const handleDownloadClick = () => { const NEWLINE = "\n"; const logstr = listData .map((group) => { const escape = (str: string) => str.replaceAll("\r", "\\r").replaceAll("\n", "\\n"); return ( group.name + NEWLINE + group.records .map((record) => { const datetime = dayjs(record.timestamp).format("YYYY-MM-DDTHH:mm:ss.SSSZ"); const level = record.level; const message = record.message; const data = record.data && Object.keys(record.data).length > 0 ? JSON.stringify(record.data) : ""; return `[${datetime}] [${level}] ${escape(message)} ${escape(data)}`.trim(); }) .join(NEWLINE) ); }) .join(NEWLINE + NEWLINE); const blob = new Blob([logstr], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `certimate_workflow_run_#${runId}_logs.txt`; a.click(); URL.revokeObjectURL(url); a.remove(); }; return ( <> {t("workflow_run.logs")}
{renderBadge()}
, onClick: () => setShowTimestamp(!showTimestamp), }, { key: "show-whitespace", label: t("workflow_run.logs.menu.show_whitespaces"), icon: , onClick: () => setShowWhitespace(!showWhitespace), }, { type: "divider", }, { key: "download-logs", label: t("workflow_run.logs.menu.download_logs"), icon: , onClick: handleDownloadClick, }, ], }} trigger={["click"]} >
0} fallback={ } >
group.id)} expandIcon={({ isActive }) => } 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:
{group.records.map((record) => renderRecord(record))}
, }; })} />
); }; const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { const { t } = useTranslation(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); const tableColumns: TableProps["columns"] = [ { key: "$index", align: "center", fixed: "left", width: 50, render: (_, __, index) => index + 1, }, { key: "type", title: t("workflow_run_artifact.props.type"), render: () => t("workflow_run_artifact.props.type.certificate"), }, { key: "name", title: t("workflow_run_artifact.props.name"), ellipsis: true, render: (_, record) => { return ( {record.subjectAltNames} ); }, }, { key: "$action", align: "end", width: 120, render: (_, record) => (