| @@ -0,0 +1,92 @@ | |||||
| export const createWebSocketMock = () => { | |||||
| class WebSocketMock { | |||||
| constructor(url) { | |||||
| this.url = url; | |||||
| this.readyState = WebSocket.OPEN; | |||||
| this.listeners = {}; | |||||
| this.count = 0; | |||||
| console.log("Mock WebSocket connected to:", url); | |||||
| // 模拟服务器推送消息 | |||||
| this.intervalId = setInterval(() => { | |||||
| this.count += 1; | |||||
| if (this.count > 5) { | |||||
| this.count = 0; | |||||
| clearInterval(this.intervalId); | |||||
| return; | |||||
| } | |||||
| this.sendMessage(JSON.stringify(logStreamData)); | |||||
| }, 3000); | |||||
| } | |||||
| sendMessage(data) { | |||||
| if (this.listeners["message"]) { | |||||
| this.listeners["message"].forEach((callback) => callback({ data })); | |||||
| } | |||||
| } | |||||
| addEventListener(event, callback) { | |||||
| if (!this.listeners[event]) { | |||||
| this.listeners[event] = []; | |||||
| } | |||||
| this.listeners[event].push(callback); | |||||
| } | |||||
| removeEventListener(event, callback) { | |||||
| if (this.listeners[event]) { | |||||
| this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); | |||||
| } | |||||
| } | |||||
| close() { | |||||
| this.readyState = WebSocket.CLOSED; | |||||
| console.log("Mock WebSocket closed"); | |||||
| } | |||||
| } | |||||
| return WebSocketMock; | |||||
| }; | |||||
| export const logStreamData = { | |||||
| streams: [ | |||||
| { | |||||
| stream: { | |||||
| workflows_argoproj_io_completed: 'false', | |||||
| workflows_argoproj_io_workflow: 'workflow-p2ddj', | |||||
| container: 'init', | |||||
| filename: | |||||
| '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', | |||||
| job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', | |||||
| namespace: 'argo', | |||||
| pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', | |||||
| stream: 'stderr', | |||||
| }, | |||||
| values: [ | |||||
| [ | |||||
| '1742179591969785990', | |||||
| 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', | |||||
| ], | |||||
| ], | |||||
| }, | |||||
| { | |||||
| stream: { | |||||
| filename: | |||||
| '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', | |||||
| job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', | |||||
| namespace: 'argo', | |||||
| pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', | |||||
| stream: 'stderr', | |||||
| workflows_argoproj_io_completed: 'false', | |||||
| workflows_argoproj_io_workflow: 'workflow-p2ddj', | |||||
| container: 'init', | |||||
| }, | |||||
| values: [ | |||||
| [ | |||||
| '1742179591973414064', | |||||
| 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', | |||||
| ], | |||||
| ], | |||||
| }, | |||||
| ], | |||||
| }; | |||||
| @@ -5,6 +5,7 @@ import type { Preview } from '@storybook/react'; | |||||
| import { App, ConfigProvider } from 'antd'; | import { App, ConfigProvider } from 'antd'; | ||||
| import zhCN from 'antd/locale/zh_CN'; | import zhCN from 'antd/locale/zh_CN'; | ||||
| import { initialize, mswLoader } from 'msw-storybook-addon'; | import { initialize, mswLoader } from 'msw-storybook-addon'; | ||||
| import { createWebSocketMock } from './mock/websocket.mock'; | |||||
| import './storybook.css'; | import './storybook.css'; | ||||
| /* | /* | ||||
| @@ -14,6 +15,10 @@ import './storybook.css'; | |||||
| */ | */ | ||||
| initialize(); | initialize(); | ||||
| // 替换全局 WebSocket 为 Mock 版本 | |||||
| // @ts-ignore | |||||
| global.WebSocket = createWebSocketMock(); | |||||
| const preview: Preview = { | const preview: Preview = { | ||||
| parameters: { | parameters: { | ||||
| controls: { | controls: { | ||||
| @@ -37,19 +37,20 @@ function LogGroup({ | |||||
| const [completed, setCompleted] = useState(false); | const [completed, setCompleted] = useState(false); | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||||
| const socketRef = useRef<WebSocket | undefined>(undefined); | const socketRef = useRef<WebSocket | undefined>(undefined); | ||||
| const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | ||||
| const elementRef = useRef<HTMLDivElement | null>(null); | |||||
| const logElementRef = useRef<HTMLDivElement | null>(null); | |||||
| // 如果是【运行中】状态,设置 hasRun 为 true,【运行中】或者从【运行中】切换到别的状态时,不显示【更多】按钮 | |||||
| const [hasRun, setHasRun] = useState(false); | |||||
| if (status === ExperimentStatus.Running && !hasRun) { | |||||
| setHasRun(true); | |||||
| } | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | |||||
| if (status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Running) { | ||||
| setupSockect(); | setupSockect(); | ||||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | |||||
| setCompleted(true); | |||||
| } | } | ||||
| preStatusRef.current = status; | |||||
| scrollToBottom(false); | |||||
| }, [status]); | }, [status]); | ||||
| // 鼠标拖到中不滚动到底部 | // 鼠标拖到中不滚动到底部 | ||||
| @@ -208,15 +209,17 @@ function LogGroup({ | |||||
| // }; | // }; | ||||
| // element.scrollTo(optons); | // element.scrollTo(optons); | ||||
| // } | // } | ||||
| elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' }); | |||||
| logElementRef?.current?.scrollIntoView({ | |||||
| block: 'end', | |||||
| behavior: smooth ? 'smooth' : 'instant', | |||||
| }); | |||||
| }; | }; | ||||
| const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | ||||
| const logText = log_content + logList.map((v) => v.log_content).join(''); | const logText = log_content + logList.map((v) => v.log_content).join(''); | ||||
| const showMoreBtn = | |||||
| status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; | |||||
| const showMoreBtn = !hasRun && !completed && showLog && logText !== ''; | |||||
| return ( | return ( | ||||
| <div className={styles['log-group']} ref={elementRef}> | |||||
| <div className={styles['log-group']} ref={logElementRef}> | |||||
| {log_type === 'resource' && ( | {log_type === 'resource' && ( | ||||
| <div className={styles['log-group__pod']} onClick={handleCollapse}> | <div className={styles['log-group__pod']} onClick={handleCollapse}> | ||||
| <div className={styles['log-group__pod__name']}>{pod_name}</div> | <div className={styles['log-group__pod__name']}>{pod_name}</div> | ||||
| @@ -15,13 +15,21 @@ export type ExperimentLog = { | |||||
| }; | }; | ||||
| type LogListProps = { | type LogListProps = { | ||||
| instanceName: string; // 实验实例 name | |||||
| instanceNamespace: string; // 实验实例 namespace | |||||
| pipelineNodeId: string; // 流水线节点 id | |||||
| workflowId?: string; // 实验实例工作流 id | |||||
| instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | |||||
| /** 实验实例 name */ | |||||
| instanceName: string; | |||||
| /** 实验实例 namespace */ | |||||
| instanceNamespace: string; | |||||
| /** 流水线节点 id */ | |||||
| pipelineNodeId: string; | |||||
| /** 实验实例工作流 id */ | |||||
| workflowId?: string; | |||||
| /** 实验实例节点开始运行时间 */ | |||||
| instanceNodeStartTime?: string; | |||||
| /** 实验实例节点运行状态 */ | |||||
| instanceNodeStatus?: ExperimentStatus; | instanceNodeStatus?: ExperimentStatus; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| @@ -35,23 +43,21 @@ function LogList({ | |||||
| className, | className, | ||||
| style, | style, | ||||
| }: LogListProps) { | }: LogListProps) { | ||||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||||
| const [logGroups, setLogGroups] = useState<ExperimentLog[]>([]); | |||||
| const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | ||||
| // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 | |||||
| // 当实例节点运行状态不是 Pending,获取实验日志组 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if ( | if ( | ||||
| instanceNodeStatus && | instanceNodeStatus && | ||||
| instanceNodeStatus !== ExperimentStatus.Pending && | instanceNodeStatus !== ExperimentStatus.Pending && | ||||
| (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) | |||||
| logGroups.length === 0 | |||||
| ) { | ) { | ||||
| getExperimentLog(); | getExperimentLog(); | ||||
| } | } | ||||
| preStatusRef.current = instanceNodeStatus; | |||||
| }, [instanceNodeStatus]); | }, [instanceNodeStatus]); | ||||
| // 获取实验日志 | |||||
| // 获取实验 Pods 组 | |||||
| const getExperimentLog = async () => { | const getExperimentLog = async () => { | ||||
| const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | ||||
| const params = { | const params = { | ||||
| @@ -71,7 +77,7 @@ function LogList({ | |||||
| log_type, | log_type, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| setLogList(list); | |||||
| setLogGroups(list); | |||||
| } else if (log_type === 'resource') { | } else if (log_type === 'resource') { | ||||
| const list = pods.map((v: string) => ({ | const list = pods.map((v: string) => ({ | ||||
| log_type, | log_type, | ||||
| @@ -79,7 +85,7 @@ function LogList({ | |||||
| log_content: '', | log_content: '', | ||||
| start_time, | start_time, | ||||
| })); | })); | ||||
| setLogList(list); | |||||
| setLogGroups(list); | |||||
| } | } | ||||
| } else { | } else { | ||||
| if (retryRef.current > 0) { | if (retryRef.current > 0) { | ||||
| @@ -93,8 +99,8 @@ function LogList({ | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> | <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> | ||||
| {logList.length > 0 ? ( | |||||
| logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||||
| {logGroups.length > 0 ? ( | |||||
| logGroups.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||||
| ) : ( | ) : ( | ||||
| <div className={styles['log-list__empty']}>暂无日志</div> | <div className={styles['log-list__empty']}>暂无日志</div> | ||||
| )} | )} | ||||
| @@ -192,7 +192,7 @@ function HyperParameterInstance() { | |||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: '寻优列表', | label: '寻优列表', | ||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | |||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -35,7 +35,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| }, []); | }, []); | ||||
| // 计算 column | // 计算 column | ||||
| const first: HyperParameterTrial | undefined = trialList[0]; | |||||
| const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined; | |||||
| const config: Record<string, any> = first?.config ?? {}; | const config: Record<string, any> = first?.config ?? {}; | ||||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | ||||
| const paramsNames = Object.keys(config); | const paramsNames = Object.keys(config); | ||||
| @@ -1,3 +1,4 @@ | |||||
| // 数据集列表 | |||||
| export const datasetListData = { | export const datasetListData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -76,6 +77,7 @@ export const datasetListData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 数据集版本列表 | |||||
| export const datasetVersionData = { | export const datasetVersionData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -107,6 +109,7 @@ export const datasetVersionData = { | |||||
| ], | ], | ||||
| }; | }; | ||||
| // 数据集详情 | |||||
| export const datasetDetailData = { | export const datasetDetailData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -137,6 +140,7 @@ export const datasetDetailData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 模型列表 | |||||
| export const modelListData = { | export const modelListData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -211,6 +215,7 @@ export const modelListData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 模型版本列表 | |||||
| export const modelVersionData = { | export const modelVersionData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -234,6 +239,7 @@ export const modelVersionData = { | |||||
| ], | ], | ||||
| }; | }; | ||||
| // 模型详情 | |||||
| export const modelDetailData = { | export const modelDetailData = { | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| code: 200, | code: 200, | ||||
| @@ -266,6 +272,7 @@ export const modelDetailData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 镜像列表 | |||||
| export const mirrorListData = { | export const mirrorListData = { | ||||
| code: 200, | code: 200, | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| @@ -384,6 +391,7 @@ export const mirrorListData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 镜像版本列表 | |||||
| export const mirrorVerionData = { | export const mirrorVerionData = { | ||||
| code: 200, | code: 200, | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| @@ -433,6 +441,7 @@ export const mirrorVerionData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 代码配置列表 | |||||
| export const codeListData = { | export const codeListData = { | ||||
| code: 200, | code: 200, | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| @@ -547,6 +556,7 @@ export const codeListData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 服务列表 | |||||
| export const serviceListData = { | export const serviceListData = { | ||||
| code: 200, | code: 200, | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| @@ -620,6 +630,7 @@ export const serviceListData = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| // 计算资源列表 | |||||
| export const computeResourceData = { | export const computeResourceData = { | ||||
| code: 200, | code: 200, | ||||
| msg: '操作成功', | msg: '操作成功', | ||||
| @@ -793,3 +804,45 @@ export const computeResourceData = { | |||||
| empty: false, | empty: false, | ||||
| }, | }, | ||||
| }; | }; | ||||
| // 日志组 | |||||
| export const logGroupData = { | |||||
| code: 200, | |||||
| msg: '操作成功', | |||||
| data: { | |||||
| log_type: 'normal', | |||||
| log_detail: { | |||||
| pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', | |||||
| log_content: | |||||
| '[2025-03-06 14:02:23] time="2025-03-06T14:02:23.068Z" level=info msg="capturing logs" argo=true\n[2025-03-06 14:02:23] Cloning into \'/tmp/traincode\'...\n[2025-03-06 14:02:23] Cloning public repository without authentication.\n[2025-03-06 14:02:23] Repository cloned successfully.\n[2025-03-06 14:02:24] time="2025-03-06T14:02:24.069Z" level=info msg="sub-process exited" argo=true error="<nil>"\n', | |||||
| start_time: '1741240944069759628', | |||||
| }, | |||||
| pods: ['workflow-txpb5-git-clone-05955a53-2484323670'], | |||||
| }, | |||||
| }; | |||||
| // 日志 | |||||
| export const logData = { | |||||
| code: 200, | |||||
| msg: '操作成功', | |||||
| data: { | |||||
| log_detail: { | |||||
| pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', | |||||
| log_content: | |||||
| '[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Main container completed" error="<nil>"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No Script output reference in workflow. Capturing script output ignored"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output parameters"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output artifacts"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="S3 Save path: /tmp/argo/outputs/logs/main.log, key: workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Creating minio client using static credentials" endpoint="minio.argo.svc.cluster.local:9000"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Saving file to s3" bucket=my-bucket endpoint="minio.argo.svc.cluster.local:9000" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log path=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Save artifact" artifactName=main-logs duration=1.092064185s error="<nil>" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="not deleting local artifact" localArtPath=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Successfully saved file: /tmp/argo/outputs/logs/main.log"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.408Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot create resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.418Z" level=info msg="Alloc=8553 TotalAlloc=14914 Sys=24421 NumGC=4 Goroutines=10"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.419Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io \\"workflow-txpb5-2484323670\\" is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot patch resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.427Z" level=info msg="Deadline monitor stopped"\n', | |||||
| start_time: '1741240945427542429', | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| export const logEmptyData = { | |||||
| code: 200, | |||||
| msg: '操作成功', | |||||
| data: { | |||||
| log_detail: { | |||||
| pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', | |||||
| log_content: '', | |||||
| start_time: '1741240945427542429', | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,90 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | |||||
| import type { Meta, StoryObj } from '@storybook/react'; | |||||
| import { http, HttpResponse } from 'msw'; | |||||
| import { logData, logEmptyData, logGroupData } from '../mockData'; | |||||
| let count = 0; | |||||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||||
| const meta = { | |||||
| title: 'Pages/LogList 日志组', | |||||
| component: LogList, | |||||
| parameters: { | |||||
| // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | |||||
| // layout: 'centered', | |||||
| msw: { | |||||
| handlers: [ | |||||
| http.post('/api/mmp/experimentIns/realTimeLog', () => { | |||||
| return HttpResponse.json(logGroupData); | |||||
| }), | |||||
| http.get('/api/mmp/experimentIns/pods/log', () => { | |||||
| if (count > 0) { | |||||
| count = 0; | |||||
| return HttpResponse.json(logEmptyData); | |||||
| } | |||||
| count += 1; | |||||
| return HttpResponse.json(logData); | |||||
| }), | |||||
| ], | |||||
| }, | |||||
| }, | |||||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||||
| tags: ['autodocs'], | |||||
| // More on argTypes: https://storybook.js.org/docs/api/argtypes | |||||
| argTypes: { | |||||
| instanceNodeStatus: { control: 'select', options: Object.values(ExperimentStatus) }, | |||||
| }, | |||||
| // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | |||||
| // args: { onClick: fn() }, | |||||
| } satisfies Meta<typeof LogList>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| /** 运行完成时 */ | |||||
| export const Succeeded: Story = { | |||||
| args: { | |||||
| instanceName: 'workflow-txpb5', | |||||
| instanceNamespace: 'argo', | |||||
| pipelineNodeId: 'git-clone-05955a53', | |||||
| workflowId: 'workflow-txpb5-2484323670', | |||||
| instanceNodeStartTime: '2025-03-06 14:02:14', | |||||
| instanceNodeStatus: ExperimentStatus.Succeeded, | |||||
| }, | |||||
| }; | |||||
| /** 无状态,空数据 */ | |||||
| export const NoStatus: Story = { | |||||
| args: { | |||||
| instanceName: 'workflow-txpb5', | |||||
| instanceNamespace: 'argo', | |||||
| pipelineNodeId: 'git-clone-05955a53', | |||||
| workflowId: 'workflow-txpb5-2484323670', | |||||
| instanceNodeStartTime: '2025-03-06 14:02:14', | |||||
| }, | |||||
| }; | |||||
| /** Pending */ | |||||
| export const Pending: Story = { | |||||
| args: { | |||||
| instanceName: 'workflow-txpb5', | |||||
| instanceNamespace: 'argo', | |||||
| pipelineNodeId: 'git-clone-05955a53', | |||||
| workflowId: 'workflow-txpb5-2484323670', | |||||
| instanceNodeStartTime: '2025-03-06 14:02:14', | |||||
| instanceNodeStatus: ExperimentStatus.Pending, | |||||
| }, | |||||
| }; | |||||
| export const Running: Story = { | |||||
| args: { | |||||
| instanceName: 'workflow-txpb5', | |||||
| instanceNamespace: 'argo', | |||||
| pipelineNodeId: 'git-clone-05955a53', | |||||
| workflowId: 'workflow-txpb5-2484323670', | |||||
| instanceNodeStartTime: '2025-03-06 14:02:14', | |||||
| instanceNodeStatus: ExperimentStatus.Running, | |||||
| }, | |||||
| }; | |||||
| @@ -88,7 +88,10 @@ export function camelCaseToUnderscore(obj: Record<string, any>) { | |||||
| } | } | ||||
| // null to undefined | // null to undefined | ||||
| export function nullToUndefined(obj: Record<string, any>) { | |||||
| export function nullToUndefined(obj: Record<string, any> | null) { | |||||
| if (obj === null) { | |||||
| return undefined; | |||||
| } | |||||
| if (!isPlainObject(obj)) { | if (!isPlainObject(obj)) { | ||||
| return obj; | return obj; | ||||
| } | } | ||||
| @@ -111,7 +114,10 @@ export function nullToUndefined(obj: Record<string, any>) { | |||||
| } | } | ||||
| // undefined to null | // undefined to null | ||||
| export function undefinedToNull(obj: Record<string, any>) { | |||||
| export function undefinedToNull(obj?: Record<string, any>) { | |||||
| if (obj === undefined) { | |||||
| return null; | |||||
| } | |||||
| if (!isPlainObject(obj)) { | if (!isPlainObject(obj)) { | ||||
| return obj; | return obj; | ||||
| } | } | ||||