Browse Source

refactor: 重构实验

pull/47/head
cp3hnu 1 year ago
parent
commit
869c12808c
78 changed files with 754 additions and 811 deletions
  1. +0
    -0
      react-ui/public/assets/images/component-icon-1-Failed.png
  2. +0
    -0
      react-ui/public/assets/images/component-icon-1-Omitted.png
  3. +0
    -0
      react-ui/public/assets/images/component-icon-1-Pending.png
  4. +0
    -0
      react-ui/public/assets/images/component-icon-1-Running.png
  5. +0
    -0
      react-ui/public/assets/images/component-icon-1-Skipped.png
  6. +0
    -0
      react-ui/public/assets/images/component-icon-1-Succeeded.png
  7. +0
    -0
      react-ui/public/assets/images/component-icon-1.png
  8. +0
    -0
      react-ui/public/assets/images/component-icon-2-Failed.png
  9. +0
    -0
      react-ui/public/assets/images/component-icon-2-Omitted.png
  10. +0
    -0
      react-ui/public/assets/images/component-icon-2-Pending.png
  11. +0
    -0
      react-ui/public/assets/images/component-icon-2-Running.png
  12. +0
    -0
      react-ui/public/assets/images/component-icon-2-Skipped.png
  13. +0
    -0
      react-ui/public/assets/images/component-icon-2-Succeeded.png
  14. +0
    -0
      react-ui/public/assets/images/component-icon-2.png
  15. +0
    -0
      react-ui/public/assets/images/component-icon-3-Failed.png
  16. +0
    -0
      react-ui/public/assets/images/component-icon-3-Omitted.png
  17. +0
    -0
      react-ui/public/assets/images/component-icon-3-Pending.png
  18. +0
    -0
      react-ui/public/assets/images/component-icon-3-Running.png
  19. +0
    -0
      react-ui/public/assets/images/component-icon-3-Skipped.png
  20. +0
    -0
      react-ui/public/assets/images/component-icon-3-Succeeded.png
  21. +0
    -0
      react-ui/public/assets/images/component-icon-3.png
  22. +0
    -0
      react-ui/public/assets/images/component-icon-4-Failed.png
  23. +0
    -0
      react-ui/public/assets/images/component-icon-4-Omitted.png
  24. +0
    -0
      react-ui/public/assets/images/component-icon-4-Pending.png
  25. +0
    -0
      react-ui/public/assets/images/component-icon-4-Running.png
  26. +0
    -0
      react-ui/public/assets/images/component-icon-4-Skipped.png
  27. +0
    -0
      react-ui/public/assets/images/component-icon-4-Succeeded.png
  28. +0
    -0
      react-ui/public/assets/images/component-icon-4.png
  29. +0
    -0
      react-ui/public/assets/images/component-icon-5-Failed.png
  30. +0
    -0
      react-ui/public/assets/images/component-icon-5-Omitted.png
  31. +0
    -0
      react-ui/public/assets/images/component-icon-5-Pending.png
  32. +0
    -0
      react-ui/public/assets/images/component-icon-5-Running.png
  33. +0
    -0
      react-ui/public/assets/images/component-icon-5-Skipped.png
  34. +0
    -0
      react-ui/public/assets/images/component-icon-5-Succeeded.png
  35. +0
    -0
      react-ui/public/assets/images/component-icon-5.png
  36. +0
    -0
      react-ui/public/assets/images/component-icon-6-Failed.png
  37. +0
    -0
      react-ui/public/assets/images/component-icon-6-Omitted.png
  38. +0
    -0
      react-ui/public/assets/images/component-icon-6-Pending.png
  39. +0
    -0
      react-ui/public/assets/images/component-icon-6-Running.png
  40. +0
    -0
      react-ui/public/assets/images/component-icon-6-Skipped.png
  41. +0
    -0
      react-ui/public/assets/images/component-icon-6-Succeeded.png
  42. +0
    -0
      react-ui/public/assets/images/component-icon-6.png
  43. +0
    -0
      react-ui/public/assets/images/component-icon-7-Failed.png
  44. +0
    -0
      react-ui/public/assets/images/component-icon-7-Omitted.png
  45. +0
    -0
      react-ui/public/assets/images/component-icon-7-Pending.png
  46. +0
    -0
      react-ui/public/assets/images/component-icon-7-Running.png
  47. +0
    -0
      react-ui/public/assets/images/component-icon-7-Skipped.png
  48. +0
    -0
      react-ui/public/assets/images/component-icon-7-Succeeded.png
  49. +0
    -0
      react-ui/public/assets/images/component-icon-7.png
  50. +0
    -0
      react-ui/public/assets/images/component-icon-8-Failed.png
  51. +0
    -0
      react-ui/public/assets/images/component-icon-8-Omitted.png
  52. +0
    -0
      react-ui/public/assets/images/component-icon-8-Pending.png
  53. +0
    -0
      react-ui/public/assets/images/component-icon-8-Running.png
  54. +0
    -0
      react-ui/public/assets/images/component-icon-8-Skipped.png
  55. +0
    -0
      react-ui/public/assets/images/component-icon-8-Succeeded.png
  56. +0
    -0
      react-ui/public/assets/images/component-icon-8.png
  57. +16
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  58. +162
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  59. +38
    -0
      react-ui/src/pages/Experiment/components/ExperimentResult/index.less
  60. +59
    -0
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  61. +34
    -0
      react-ui/src/pages/Experiment/components/LogGroup/index.less
  62. +16
    -28
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  63. +3
    -0
      react-ui/src/pages/Experiment/components/LogList/index.less
  64. +21
    -0
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  65. +0
    -19
      react-ui/src/pages/Experiment/experimentText/LogList.tsx
  66. +56
    -120
      react-ui/src/pages/Experiment/experimentText/index.jsx
  67. +26
    -85
      react-ui/src/pages/Experiment/experimentText/index.less
  68. +0
    -34
      react-ui/src/pages/Experiment/experimentText/logGroup.less
  69. +0
    -439
      react-ui/src/pages/Experiment/experimentText/props.jsx
  70. +37
    -0
      react-ui/src/pages/Experiment/experimentText/props.less
  71. +171
    -0
      react-ui/src/pages/Experiment/experimentText/props.tsx
  72. +1
    -3
      react-ui/src/pages/Experiment/index.jsx
  73. +2
    -0
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  74. +8
    -6
      react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx
  75. +58
    -68
      react-ui/src/pages/Pipeline/editPipeline/props.jsx
  76. +1
    -4
      react-ui/src/pages/Workspace/components/ExperimentTable/index.tsx
  77. +33
    -0
      react-ui/src/types.ts
  78. +12
    -5
      react-ui/src/utils/date.ts

react-ui/public/assets/images/pipeline/component-icon-1-Failed.png → react-ui/public/assets/images/component-icon-1-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-1-Omitted.png → react-ui/public/assets/images/component-icon-1-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-1-Pending.png → react-ui/public/assets/images/component-icon-1-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-1-Running.png → react-ui/public/assets/images/component-icon-1-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-1-Skipped.png → react-ui/public/assets/images/component-icon-1-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-1-Succeeded.png → react-ui/public/assets/images/component-icon-1-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-1.png → react-ui/public/assets/images/component-icon-1.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Failed.png → react-ui/public/assets/images/component-icon-2-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Omitted.png → react-ui/public/assets/images/component-icon-2-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Pending.png → react-ui/public/assets/images/component-icon-2-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Running.png → react-ui/public/assets/images/component-icon-2-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Skipped.png → react-ui/public/assets/images/component-icon-2-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-2-Succeeded.png → react-ui/public/assets/images/component-icon-2-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-2.png → react-ui/public/assets/images/component-icon-2.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Failed.png → react-ui/public/assets/images/component-icon-3-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Omitted.png → react-ui/public/assets/images/component-icon-3-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Pending.png → react-ui/public/assets/images/component-icon-3-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Running.png → react-ui/public/assets/images/component-icon-3-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Skipped.png → react-ui/public/assets/images/component-icon-3-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-3-Succeeded.png → react-ui/public/assets/images/component-icon-3-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-3.png → react-ui/public/assets/images/component-icon-3.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Failed.png → react-ui/public/assets/images/component-icon-4-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Omitted.png → react-ui/public/assets/images/component-icon-4-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Pending.png → react-ui/public/assets/images/component-icon-4-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Running.png → react-ui/public/assets/images/component-icon-4-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Skipped.png → react-ui/public/assets/images/component-icon-4-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-4-Succeeded.png → react-ui/public/assets/images/component-icon-4-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-4.png → react-ui/public/assets/images/component-icon-4.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Failed.png → react-ui/public/assets/images/component-icon-5-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Omitted.png → react-ui/public/assets/images/component-icon-5-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Pending.png → react-ui/public/assets/images/component-icon-5-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Running.png → react-ui/public/assets/images/component-icon-5-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Skipped.png → react-ui/public/assets/images/component-icon-5-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-5-Succeeded.png → react-ui/public/assets/images/component-icon-5-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-5.png → react-ui/public/assets/images/component-icon-5.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Failed.png → react-ui/public/assets/images/component-icon-6-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Omitted.png → react-ui/public/assets/images/component-icon-6-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Pending.png → react-ui/public/assets/images/component-icon-6-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Running.png → react-ui/public/assets/images/component-icon-6-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Skipped.png → react-ui/public/assets/images/component-icon-6-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-6-Succeeded.png → react-ui/public/assets/images/component-icon-6-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-6.png → react-ui/public/assets/images/component-icon-6.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Failed.png → react-ui/public/assets/images/component-icon-7-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Omitted.png → react-ui/public/assets/images/component-icon-7-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Pending.png → react-ui/public/assets/images/component-icon-7-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Running.png → react-ui/public/assets/images/component-icon-7-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Skipped.png → react-ui/public/assets/images/component-icon-7-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-7-Succeeded.png → react-ui/public/assets/images/component-icon-7-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-7.png → react-ui/public/assets/images/component-icon-7.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Failed.png → react-ui/public/assets/images/component-icon-8-Failed.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Omitted.png → react-ui/public/assets/images/component-icon-8-Omitted.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Pending.png → react-ui/public/assets/images/component-icon-8-Pending.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Running.png → react-ui/public/assets/images/component-icon-8-Running.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Skipped.png → react-ui/public/assets/images/component-icon-8-Skipped.png View File


react-ui/public/assets/images/pipeline/component-icon-8-Succeeded.png → react-ui/public/assets/images/component-icon-8-Succeeded.png View File


react-ui/public/assets/images/pipeline/component-icon-8.png → react-ui/public/assets/images/component-icon-8.png View File


+ 16
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.less View File

@@ -0,0 +1,16 @@
.experiment-parameter {
padding-top: 8px;

&__title {
display: flex;
align-items: center;
height: 43px;
margin-right: 8px;
margin-bottom: 20px;
margin-left: 8px;
padding: 0 24px;
color: @text-color;
font-size: @font-size;
background: #f8fbff;
}
}

+ 162
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -0,0 +1,162 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { PipelineNodeModelSerialize } from '@/types';
import { Form, Input, type FormProps } from 'antd';
import styles from './index.less';
const { TextArea } = Input;

type ExperimentParameterProps = {
form: FormProps['form'];
nodeData: PipelineNodeModelSerialize;
};

function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
// 控制策略
const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
);

// 输入参数
const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
key,
value,
}));

// 输出参数
const outParametersList = Object.entries(nodeData.out_parameters ?? {}).map(([key, value]) => ({
key,
value,
}));

return (
<Form
name="form"
layout="vertical"
labelCol={{
span: 24,
}}
wrapperCol={{
span: 24,
}}
form={form}
style={{
maxWidth: 600,
}}
autoComplete="off"
className={styles['experiment-parameter']}
>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle>
</div>
<Form.Item
label="任务名称"
name="label"
rules={[
{
required: true,
message: '请输入任务名称',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
label="任务ID"
name="id"
rules={[
{
required: true,
message: '请输入任务id',
},
]}
>
<Input disabled />
</Form.Item>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="任务信息"></SubAreaTitle>
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<Input disabled />
</Form.Item>

<Form.Item label="启动命令" name="command">
<TextArea disabled />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请输入资源规格',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item label="挂载路径" name="mount_path">
<Input disabled />
</Form.Item>
<Form.Item label="环境变量" name="env_variables">
<TextArea disabled />
</Form.Item>
{controlStrategyList.map((item) => (
<Form.Item
key={item.key}
name={['control_strategy', item.key]}
label={item.value.label}
getValueProps={(e) => {
return { value: e.value };
}}
>
<Input disabled />
</Form.Item>
))}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输入参数"></SubAreaTitle>
</div>
{inParametersList.map((item) => (
<Form.Item
key={item.key}
name={['in_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
}}
>
<Input disabled />
</Form.Item>
))}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输出参数"></SubAreaTitle>
</div>
{outParametersList.map((item) => (
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
}}
>
<Input disabled />
</Form.Item>
))}
</Form>
);
}

export default ExperimentParameter;

+ 38
- 0
react-ui/src/pages/Experiment/components/ExperimentResult/index.less View File

@@ -0,0 +1,38 @@
.experiment-result {
padding: 8px;
color: @text-color;
font-size: 14px;

&__content {
padding: 10px 20px 20px 20px;
background-color: rgba(234, 234, 234, 0.5);
}

&__item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}

&__name {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid rgba(234, 234, 234, 0.8);
}

&__file {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
padding: 0 20px 0 0;

&:last-child {
margin-bottom: 0;
}
}
}
}

+ 59
- 0
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -0,0 +1,59 @@
import { downLoadZip } from '@/utils/downloadfile';
import { Button } from 'antd';
import styles from './index.less';
type ExperimentResultProps = {
results?: ExperimentResultData[] | null;
};

type ExperimentResultData = {
name: string;
path: string;
type: string;
value: {
name: string;
size: string;
}[];
};

function ExperimentResult({ results }: ExperimentResultProps) {
const exportResult = (val: string) => {
downLoadZip(`/api/mmp/minioStorage/download`, { path: val });
};

return (
<div className={styles['experiment-result']}>
<div className={styles['experiment-result__content']}>
{results?.map((item) => (
<div key={item.name} className={styles['experiment-result__item']}>
<div className={styles['experiment-result__item__name']}>
<span>{item.name}</span>
<Button
size="small"
type="link"
onClick={() => {
exportResult(item.path);
}}
>
下载
</Button>
{/* <a style={{ marginRight: '10px' }}>导出到模型库</a>
<a style={{ marginRight: '10px' }}>导出到数据集</a> */}
</div>
<div style={{ margin: '15px 0' }} className={styles['experiment-result__item__file']}>
<span>文件名称</span>
<span>文件大小</span>
</div>
{item.value?.map((ele) => (
<div className={styles['experiment-result__item__file']} key={ele.name}>
<span>{ele.name}</span>
<span>{ele.size}</span>
</div>
))}
</div>
))}
</div>
</div>
);
}

export default ExperimentResult;

+ 34
- 0
react-ui/src/pages/Experiment/components/LogGroup/index.less View File

@@ -0,0 +1,34 @@
.log-group {
padding-bottom: 10px;

&__pod {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
background: rgba(234, 234, 234, 0.5);
cursor: pointer;

&__name {
margin-right: 10px;
color: @text-color;
font-size: 14px;
}
}

&__detail {
padding: 15px;
color: white;
font-size: 14px;
white-space: pre-line;
word-break: break-all;
background: #19253b;
}

&__more-button {
display: flex;
justify-content: center;
color: white;
background: #19253b;
}
}

react-ui/src/pages/Experiment/experimentText/logGroup.tsx → react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -1,16 +1,19 @@
/*
* @Author: 赵伟
* @Date: 2024-05-16 08:47:46
* @Description: 日志组件
*/

import { useStateRef } from '@/hooks'; import { useStateRef } from '@/hooks';
import { ExperimentLog } from '@/pages/Experiment/experimentText/props';
import { ExperimentStatus } from '@/pages/Experiment/status';
import { getExperimentPodsLog } from '@/services/experiment/index.js'; import { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ExperimentStatus } from '../status';
import styles from './logGroup.less';
import styles from './index.less';


export type LogGroupProps = {
log_type: 'normal' | 'resource'; // 日志类型
pod_name?: string; // 分布式名称
log_content?: string; // 日志内容
start_time?: string; // 日志开始时间
export type LogGroupProps = ExperimentLog & {
status: ExperimentStatus; // 实验状态 status: ExperimentStatus; // 实验状态
}; };


@@ -23,7 +26,7 @@ function LogGroup({
log_type = 'normal', log_type = 'normal',
pod_name = '', pod_name = '',
log_content = '', log_content = '',
start_time = '',
start_time,
status = ExperimentStatus.Pending, status = ExperimentStatus.Pending,
}: LogGroupProps) { }: LogGroupProps) {
const [collapse, setCollapse] = useState(true); const [collapse, setCollapse] = useState(true);
@@ -57,21 +60,6 @@ function LogGroup({
setCompleted(true); setCompleted(true);
} }
}; };
// 请求实时日志
// const requestExperimentPodsRealtimeLog = async () => {
// const params = {
// pod_name,
// namespace: namespace,
// container_name: log_type === 'resource' ? '' : 'main',
// };
// const res = await getExperimentPodsRealtimeLog(params);
// const { log_detail } = res.data;
// if (log_detail && log_detail.log_content) {
// setLogList((list) => list.concat(log_detail));
// } else {
// setCompleted(true);
// }
// };


// 处理折叠 // 处理折叠
const handleCollapse = async () => { const handleCollapse = async () => {
@@ -101,15 +89,15 @@ function LogGroup({
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 !== 'Running' && showLog && !completed && logText !== ''; const showMoreBtn = status !== 'Running' && showLog && !completed && logText !== '';
return ( return (
<div className={styles.log_group}>
<div className={styles['log-group']}>
{log_type === 'resource' && ( {log_type === 'resource' && (
<div className={styles.log_group_pod} onClick={handleCollapse}>
<div className={styles.log_group_pod_name}>{pod_name}</div>
<div className={styles['log-group__pod']} onClick={handleCollapse}>
<div className={styles['log-group__pod__name']}>{pod_name}</div>
{collapse ? <DownOutlined /> : <UpOutlined />} {collapse ? <DownOutlined /> : <UpOutlined />}
</div> </div>
)} )}
{showLog && <div className={styles.log_group_detail}>{logText}</div>}
<div className={styles.log_group_more_button}>
{showLog && <div className={styles['log-group__detail']}>{logText}</div>}
<div className={styles['log-group__more-button']}>
{showMoreBtn && ( {showMoreBtn && (
<Button <Button
type="text" type="text"

+ 3
- 0
react-ui/src/pages/Experiment/components/LogList/index.less View File

@@ -0,0 +1,3 @@
.log-list {
padding: 8px;
}

+ 21
- 0
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -0,0 +1,21 @@
import { ExperimentLog } from '@/pages/Experiment/experimentText/props';
import { ExperimentStatus } from '@/pages/Experiment/status';
import LogGroup from '../LogGroup';
import styles from './index.less';

type LogListProps = {
list: ExperimentLog[];
status: ExperimentStatus;
};

function LogList({ list = [], status }: LogListProps) {
return (
<div className={styles['log-list']}>
{list.map((v) => (
<LogGroup key={v.pod_name} {...v} status={status} />
))}
</div>
);
}

export default LogList;

+ 0
- 19
react-ui/src/pages/Experiment/experimentText/LogList.tsx View File

@@ -1,19 +0,0 @@
import { ExperimentStatus } from '../status';
import LogGroup, { type LogGroupProps } from './logGroup';

type LogListProps = {
list: Omit<LogGroupProps, 'status'>[];
status: ExperimentStatus;
};

function LogList({ list = [], status }: LogListProps) {
return (
<div>
{list.map((v) => (
<LogGroup key={v.pod_name} {...v} status={status} />
))}
</div>
);
}

export default LogList;

+ 56
- 120
react-ui/src/pages/Experiment/experimentText/index.jsx View File

@@ -1,11 +1,10 @@
import { useVisible } from '@/hooks';
import { useStateRef, useVisible } from '@/hooks';
import { getExperimentIns } from '@/services/experiment/index.js'; import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js';
import { elapsedTime, formatDate } from '@/utils/date'; import { elapsedTime, formatDate } from '@/utils/date';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { Button } from 'antd'; import { Button } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { s8 } from '../../../utils'; import { s8 } from '../../../utils';
import ParamsModal from '../components/ViewParamsModal'; import ParamsModal from '../components/ViewParamsModal';
@@ -13,38 +12,15 @@ import { experimentStatusInfo } from '../status';
import styles from './index.less'; import styles from './index.less';
import Props from './props'; import Props from './props';


let graph = null;

function ExperimentText() { function ExperimentText() {
const [message, setMessage] = useState({});
const messageRef = useRef(message);
const [message, setMessage, messageRef] = useStateRef({});
const propsRef = useRef(); const propsRef = useRef();
const navgite = useNavigate(); const navgite = useNavigate();
const locationParams = useParams(); //新版本获取路由参数接口 const locationParams = useParams(); //新版本获取路由参数接口
let graph = null;
const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);


const timers = (time) => {
let timer = new Date(time);
let hours = timer.getHours(); //转换成时
let minutes = timer.getMinutes(); //转换成分
let secend = timer.getSeconds(); //转换成秒

let str = `${minutes}分${secend}秒`;
return str;
};
const pipelineContainer = useEmotionCss(() => {
return {
display: 'flex',
backgroundColor: '#fff',
height: '98vh',
};
});
const graphStyle = useEmotionCss(() => {
return {
width: '100%',
backgroundColor: '#f9fafb',
flex: 1,
};
});
const graphRef = useRef(); const graphRef = useRef();
const onDragEnd = (val) => { const onDragEnd = (val) => {
console.log(val, 'eee'); console.log(val, 'eee');
@@ -60,12 +36,8 @@ function ExperimentText() {
id: val.component_name + '-' + s8(), id: val.component_name + '-' + s8(),
isCluster: false, isCluster: false,
}; };
console.log(graph, model);

graph.addItem('node', model, true); graph.addItem('node', model, true);
console.log(graph);
}; };
const formChange = (val) => {};
const handlerClick = (e) => { const handlerClick = (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) { if (e.target.get('name') !== 'anchor-point' && e.item) {
propsRef.current.showDrawer(e, locationParams.id, messageRef.current); propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
@@ -73,7 +45,6 @@ function ExperimentText() {
}; };
const getGraphData = (data) => { const getGraphData = (data) => {
if (graph) { if (graph) {
console.log(graph);
graph.data(data); graph.data(data);
graph.render(); graph.render();
} else { } else {
@@ -83,77 +54,39 @@ function ExperimentText() {
} }
}; };
const getFirstWorkflow = (val) => { const getFirstWorkflow = (val) => {
getWorkflowById(val).then((ret) => {
if (graph && ret.data && ret.data.dag) {
console.log(JSON.parse(ret.data.dag));
getExperimentIns(locationParams.id).then((res) => {
if (res.code == 200) {
console.log(ret.data, 'data');
setMessage(res.data);
const experimentStatusObjs = JSON.parse(res.data.nodes_status);
const newNodeList = JSON.parse(ret.data.dag).nodes.map((item) => {
console.log(experimentStatusObjs);
getWorkflowById(val).then((pipelineRes) => {
if (graph && pipelineRes.data && pipelineRes.data.dag) {
getExperimentIns(locationParams.id).then((experimentRes) => {
if (experimentRes.code === 200) {
setMessage(experimentRes.data);
const experimentStatusObjs = JSON.parse(experimentRes.data.nodes_status);
const newNodeList = JSON.parse(pipelineRes.data.dag).nodes.map((item) => {
return { return {
...item, ...item,
experimentEndTime:
experimentStatusObjs &&
experimentStatusObjs[item.id] &&
experimentStatusObjs[item.id].finishedAt,
experimentStartTime:
experimentStatusObjs &&
experimentStatusObjs[item.id] &&
experimentStatusObjs[item.id].startedAt,
experimentStatus:
experimentStatusObjs &&
experimentStatusObjs[item.id] &&
experimentStatusObjs[item.id].phase,
component_id:
experimentStatusObjs &&
experimentStatusObjs[item.id] &&
experimentStatusObjs[item.id].id,
img:
experimentStatusObjs &&
experimentStatusObjs[item.id] &&
experimentStatusObjs[item.id].phase
? item.img.slice(0, item.img.length - 4) +
'-' +
experimentStatusObjs[item.id].phase +
'.png'
: item.img,
experimentEndTime: experimentStatusObjs?.[item.id]?.finishedAt,
experimentStartTime: experimentStatusObjs?.[item.id]?.startedAt,
experimentStatus: experimentStatusObjs?.[item.id]?.phase,
component_id: experimentStatusObjs?.[item.id]?.id,
img: experimentStatusObjs?.[item.id]?.phase
? item.img.slice(0, item.img.length - 4) +
'-' +
experimentStatusObjs[item.id].phase +
'.png'
: item.img,
}; };
}); });
const newData = { ...JSON.parse(ret.data.dag), nodes: newNodeList };

const newData = { ...JSON.parse(pipelineRes.data.dag), nodes: newNodeList };
getGraphData(newData); getGraphData(newData);
// setExperimentStatusObj(JSON.parse(ret.data.nodes_status))
} }
}); });
} }
// graph&&graph.data(JSON.parse(ret.dag))
// graph.render()
}); });
}; };
// const getExperimentIn=(val)=>{
// getExperimentIns(val).then(ret=>{
// if(ret.code==200){
// console.log(JSON.parse(ret.data.nodes_status));
// setExperimentStatusObj(JSON.parse(ret.data.nodes_status))
// setTimeout(() => {
// console.log(experimentStatusObj);

// }, 1000);
// }


// })
// }
useEffect(() => { useEffect(() => {
initGraph(); initGraph();
getFirstWorkflow(locationParams.workflowId); getFirstWorkflow(locationParams.workflowId);
}, []); }, []);
useEffect(() => {
// Update the refs whenever the state changes
messageRef.current = message;
}, [message]);


const initGraph = () => { const initGraph = () => {
const fittingString = (str, maxWidth, fontSize) => { const fittingString = (str, maxWidth, fontSize) => {
@@ -374,6 +307,8 @@ function ExperimentText() {
}, },
// linkCenter: true, // linkCenter: true,
fitView: true, fitView: true,
minZoom: 0.5,
maxZoom: 3,
fitViewPadding: [320, 320, 220, 320], fitViewPadding: [320, 320, 220, 320],
}); });
graph.on('node:click', handlerClick); graph.on('node:click', handlerClick);
@@ -385,38 +320,39 @@ function ExperimentText() {
}; };
}; };
return ( return (
<div className={pipelineContainer}>
<div className={styles.centerContainer}>
<div className={styles.buttonList}>
<div className={styles.allMessageItem}>启动时间:{formatDate(message.create_time)}</div>
<div className={styles.allMessageItem}>
执行时长:
{message.finish_time
? elapsedTime(new Date(message.create_time), new Date(message.finish_time))
: elapsedTime(new Date(message.create_time), new Date())}
</div>
<div className={styles.allMessageItem}>
状态:
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
marginRight: '6px',
backgroundColor: experimentStatusInfo[message.status]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[message.status]?.color }}>
{experimentStatusInfo[message.status]?.label}
</span>
</div>
<Button className={styles.param_button} onClick={openParamsModal}>
执行参数
</Button>
<div className={styles['pipeline-container']}>
<div className={styles['pipeline-container__top']}>
<div className={styles['pipeline-container__top__info']}>
启动时间:{formatDate(message.create_time)}
</div>
<div className={styles['pipeline-container__top__info']}>
执行时长:
{elapsedTime(message.create_time, message.finish_time)}
</div>
<div className={styles['pipeline-container__top__info']}>
状态:
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
marginRight: '6px',
backgroundColor: experimentStatusInfo[message.status]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[message.status]?.color }}>
{experimentStatusInfo[message.status]?.label}
</span>
</div> </div>
<div className={graphStyle} ref={graphRef} id={styles.graphStyle}></div>
<Button
className={styles['pipeline-container__top__param-button']}
onClick={openParamsModal}
>
执行参数
</Button>
</div> </div>
<Props ref={propsRef} onParentChange={formChange}></Props>
<div className={styles['pipeline-container__graph']} ref={graphRef}></div>
<Props ref={propsRef}></Props>
<ParamsModal <ParamsModal
open={paramsModalOpen} open={paramsModalOpen}
onCancel={closeParamsModal} onCancel={closeParamsModal}


+ 26
- 85
react-ui/src/pages/Experiment/experimentText/index.less View File

@@ -1,90 +1,31 @@
#graph {
position: relative;
width: 100%;
.pipeline-container {
height: 100%; height: 100%;
}
.editPipelinePropsContent {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: #1d1d20;
font-size: 15px;
font-family: 'Alibaba';
background: #f8fbff;
}
.centerContainer {
display: flex;
flex: 1;
flex-direction: column;
}
.buttonList {
display: flex;
align-items: center;
width: 100%;
height: 56px;
padding: 0 30px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
}
.drawBox{
:global{
.ant-drawer .ant-drawer-body{
overflow: hidden;
}
}
}
.experimentDrawer{
:global{
.ant-tabs >.ant-tabs-nav .ant-tabs-nav-list{
margin-left: 24px;
}
.ant-drawer .ant-drawer-body{
overflow-y: hidden;
background-color: #fff;

&__top {
display: flex;
align-items: center;
width: 100%;
height: 56px;
padding: 0 30px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);

&__info {
display: flex;
align-items: center;
margin-right: 30px;
color: rgba(29, 29, 32, 0.8);
font-size: 15px;
} }
.ant-tabs {
height: calc(100% - 160px);
overflow-y: auto;
&__param-button {
margin-right: 0;
margin-left: auto;
} }
} }

}
.detailBox {
display: flex;
align-items: center;
margin-bottom: 15px;
color: #1d1d20;
font-size: 15px;
padding-left: 24px;

}
.allMessageItem {
display: flex;
align-items: center;
margin-right: 30px;
color: rgba(29, 29, 32, 0.8);
font-size: 15px;
}
.param_button {
margin-right: 0;
margin-left: auto;
}
.resultTop {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.resultContent {
display: flex;

align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
padding: 0 20px 0 0;
&__graph {
width: 100%;
height: calc(100% - 56px);
background-color: @background-color;
}
} }

+ 0
- 34
react-ui/src/pages/Experiment/experimentText/logGroup.less View File

@@ -1,34 +0,0 @@
.log_group {
padding-bottom: 10px;
}

.log_group_pod {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
background: rgba(234, 234, 234, 0.5);
cursor: pointer;

&_name {
margin-right: 10px;
color: #1d1d20;
font-size: 14px;
}
}

.log_group_detail {
padding: 15px;
color: white;
font-size: 14px;
white-space: pre-line;
word-break: break-all;
background: #19253b;
}

.log_group_more_button {
display: flex;
justify-content: center;
color: white;
background: #19253b;
}

+ 0
- 439
react-ui/src/pages/Experiment/experimentText/props.jsx View File

@@ -1,439 +0,0 @@
import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js';
import { elapsedTime, formatDate } from '@/utils/date';
import { downLoadZip } from '@/utils/downloadfile';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Form, Input, Tabs, message } from 'antd';
import moment from 'moment';
import { forwardRef, useImperativeHandle, useState } from 'react';
import LogList from './LogList';
import Styles from './index.less';
const { TextArea } = Input;
const Props = forwardRef(({ onParentChange }, ref) => {
const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState({});
const [resultObj, setResultObj] = useState([]);
const [logList, setLogList] = useState([]);
const statusObj = {
Running: '运行中',
Succeeded: '成功',
Pending: '等待中',
Failed: '失败',
Error: '错误',
Terminated: '终止',
Skipped: '未执行',
Omitted: '未执行',
};
const statusColorObj = {
Running: '#165bff',
Succeeded: '#63a728',
Pending: '#f981eb',
Failed: '#c73131',
Error: '#c73131',
Terminated: '#8a8a8a',
Skipped: '#8a8a8a',
Omitted: '#8a8a8ae',
};
const exportResult = (e, val) => {
const hide = message.loading('正在下载');
hide();
downLoadZip(`/api/mmp/minioStorage/download`, { path: val });
};
const timers = (time) => {
let timer = new Date(time);
let hours = timer.getHours(); //转换成时
let minutes = timer.getMinutes(); //转换成分
let secend = timer.getSeconds(); //转换成秒

let str = `${minutes}分${secend}秒`;
return str;
};
const items = [
{
key: '1',
label: '日志详情',
children: <LogList list={logList} status={stagingItem.experimentStatus}></LogList>,
icon: <ProfileOutlined />,
},
{
key: '2',
label: '配置参数',
icon: <DatabaseOutlined />,
children: (
<Form
name="form"
form={form}
layout="vertical"
labelCol={{
span: 16,
}}
wrapperCol={{
span: 24,
}}
style={{
maxWidth: 600,
}}
initialValues={{
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/static-message.png'}
alt=""
/>
基本信息
</div>
<Form.Item
label="任务名称"
name="label"
rules={[
{
required: true,
message: '请输入任务名称',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
label="任务ID"
name="id"
rules={[
{
required: true,
message: '请输入任务id',
},
]}
>
<Input disabled />
</Form.Item>
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
任务信息
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<Input disabled />
</Form.Item>

<Form.Item label="启动命令" name="command">
<TextArea disabled />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请输入资源规格',
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item label="挂载路径" name="mount_path">
<Input disabled />
</Form.Item>
<Form.Item label="环境变量" name="env_variables">
<TextArea disabled />
</Form.Item>
{stagingItem.control_strategy &&
Object.keys(stagingItem.control_strategy) &&
Object.keys(stagingItem.control_strategy).length > 0
? Object.keys(stagingItem.control_strategy).map((item) => (
<Form.Item
key={item}
label={stagingItem.control_strategy[item].label}
disabled
name={item}
>
<Input disabled />
</Form.Item>
))
: ''}
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '13px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
输入参数
</div>
{stagingItem.in_parameters &&
Object.keys(stagingItem.in_parameters) &&
Object.keys(stagingItem.in_parameters).length > 0
? Object.keys(stagingItem.in_parameters).map((item) => (
<Form.Item
key={item}
label={stagingItem.in_parameters[item].label + '(' + item + ')'}
name={item}
disabled
rules={[{ required: stagingItem.in_parameters[item].require ? true : false }]}
>
<Input disabled />
</Form.Item>
))
: ''}
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
输出参数
</div>
{stagingItem.out_parameters &&
Object.keys(stagingItem.out_parameters) &&
Object.keys(stagingItem.out_parameters).length > 0
? Object.keys(stagingItem.out_parameters).map((item) => (
<Form.Item
key={item}
label={stagingItem.out_parameters[item].label + '(' + item + ')'}
disabled
rules={[{ required: stagingItem.out_parameters[item].require ? true : false }]}
name={item}
>
<Input disabled />
</Form.Item>
))
: ''}
</Form>
),
},
{
key: '3',
label: '输出结果',
children: (
<div
style={{
minHeight: '740px',
background: '#f4f4f4',
color: '#000',
fontSize: '14px',
padding: '0 10px 20px 20px',
}}
>
{resultObj && resultObj.length > 0
? resultObj.map((item) => (
<div key={item.name}>
<div className={Styles.resultTop}>
<span>{item.name}</span>
<div style={{ display: 'flex' }}>
<a
onClick={(e) => {
exportResult(e, item.path);
}}
style={{ marginRight: '10px' }}
>
下载
</a>
{/* <a style={{ marginRight: '10px' }}>导出到模型库</a>
<a style={{ marginRight: '10px' }}>导出到数据集</a> */}
</div>
</div>
<div style={{ margin: '15px 0' }} className={Styles.resultContent}>
<span>文件名称</span>
<span>文件大小</span>
</div>
{item.value && item.value.length > 0
? item.value.map((ele) => (
<div className={Styles.resultContent} key={ele.name}>
<span>{ele.name}</span>
<span>{ele.size}</span>
</div>
))
: null}
</div>
))
: null}
</div>
),
icon: <ProfileOutlined />,
},
];
const [open, setOpen] = useState(false);
const afterOpenChange = () => {
if (!open) {
console.log(111, open);

console.log(stagingItem, form.getFieldsValue());
for (let i in form.getFieldsValue()) {
for (let j in stagingItem.in_parameters) {
if (i == j) {
console.log(j, i);
stagingItem.in_parameters[j].value = form.getFieldsValue()[i];
}
}
for (let p in stagingItem.out_parameters) {
if (i == p) {
stagingItem.out_parameters[p].value = form.getFieldsValue()[i];
}
}
for (let k in stagingItem.control_strategy) {
if (i == k) {
stagingItem.control_strategy[k].value = form.getFieldsValue()[i];
}
}
}
// setStagingItem({...stagingItem,})
console.log(stagingItem.control_strategy);
onParentChange({
...stagingItem,
control_strategy: JSON.stringify(stagingItem.control_strategy),
in_parameters: JSON.stringify(stagingItem.in_parameters),
out_parameters: JSON.stringify(stagingItem.out_parameters),
...form.getFieldsValue(),
});
// onParentChange({...stagingItem,...form.getFieldsValue()})
}
};
const onClose = () => {
setOpen(false);
};
const onFinish = (values) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
};
useImperativeHandle(ref, () => ({
showDrawer(e, id, message) {
setLogList([]);
if (e.item && e.item.getModel().component_id) {
const model = e.item.getModel() || {};
const start_time = moment(model.experimentStartTime).valueOf() * 1.0e6;
const params = {
task_id: model.id,
component_id: model.component_id,
name: message.argo_ins_name,
namespace: message.argo_ins_ns,
start_time: start_time,
};
getQueryByExperimentLog(params).then((ret) => {
const { log_type, pods, log_detail } = ret.data;
if (log_type === 'normal') {
const list = [
{
...log_detail,
log_type,
},
];
setLogList(list);
} else if (log_type === 'resource') {
const list = pods.map((v) => ({
log_type,
pod_name: v,
log_content: '',
start_time,
}));
setLogList(list);
}

getNodeResult({ id, node_id: e.item.getModel().id }).then((res) => {
setResultObj(res.data);
form.resetFields();
form.setFieldsValue({
...e.item.getModel(),
in_parameters: JSON.parse(e.item.getModel().in_parameters),
out_parameters: JSON.parse(e.item.getModel().out_parameters),
control_strategy: JSON.parse(e.item.getModel().control_strategy),
});
setStagingItem({
...e.item.getModel(),
in_parameters: JSON.parse(e.item.getModel().in_parameters),
out_parameters: JSON.parse(e.item.getModel().out_parameters),
control_strategy: JSON.parse(e.item.getModel().control_strategy),
});
setOpen(true);
});
});
} else {
form.resetFields();
form.setFieldsValue({
...e.item.getModel(),
in_parameters: JSON.parse(e.item.getModel().in_parameters),
out_parameters: JSON.parse(e.item.getModel().out_parameters),
control_strategy: JSON.parse(e.item.getModel().control_strategy),
});
setStagingItem({
...e.item.getModel(),
in_parameters: JSON.parse(e.item.getModel().in_parameters),
out_parameters: JSON.parse(e.item.getModel().out_parameters),
control_strategy: JSON.parse(e.item.getModel().control_strategy),
});
setOpen(true);
}
// console.log(e.item.getModel().in_parameters);
},
}));
return (
<div className={Styles.drawBox}>
<Drawer
title="任务执行详情"
placement="right"
rootStyle={{ marginTop: '68px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={420}
className={Styles.experimentDrawer}
destroyOnClose={true}
>
<div className={Styles.detailBox} style={{ marginTop: '15px' }}>
任务名称:{stagingItem.label}
</div>
<div className={Styles.detailBox}>
执行状态:
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
marginRight: '6px',
backgroundColor: statusColorObj[stagingItem.experimentStatus],
}}
></div>
<span style={{ color: statusColorObj[stagingItem.experimentStatus] }}>
{statusObj[stagingItem.experimentStatus]}
</span>
</div>
<div className={Styles.detailBox}>
启动时间:{formatDate(stagingItem.experimentStartTime)}
</div>
<div className={Styles.detailBox}>
耗时:
{stagingItem.experimentEndTime
? elapsedTime(
new Date(stagingItem.experimentStartTime),
new Date(stagingItem.experimentEndTime),
)
: elapsedTime(new Date(stagingItem.experimentStartTime), new Date())}
</div>
<Tabs defaultActiveKey="1" items={items} />
</Drawer>
</div>
);
});

export default Props;

+ 37
- 0
react-ui/src/pages/Experiment/experimentText/props.less View File

@@ -0,0 +1,37 @@
.experiment-drawer {
:global {
.ant-drawer-body {
overflow-y: hidden;
}
}

&__tabs {
height: calc(100% - 170px);
:global {
.ant-tabs-nav {
padding-left: 24px;
background-color: #f8fbff;
border: 1px solid #e0eaff;
}
.ant-tabs-content-holder {
overflow-y: auto;
}
}
}

&__info {
display: flex;
align-items: center;
margin-bottom: 15px;
padding-left: 24px;
color: @text-color;
font-size: 15px;
}

&__status-dot {
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
}
}

+ 171
- 0
react-ui/src/pages/Experiment/experimentText/props.tsx View File

@@ -0,0 +1,171 @@
import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Form, Tabs } from 'antd';
import dayjs from 'dayjs';
import { forwardRef, useImperativeHandle, useState } from 'react';
import ExperimentParameter from '../components/ExperimentParameter';
import ExperimentResult from '../components/ExperimentResult';
import LogList from '../components/LogList';
import { experimentStatusInfo } from '../status';
import styles from './props.less';

export type ExperimentLog = {
log_type: 'normal' | 'resource'; // 日志类型
pod_name?: string; // 分布式名称
log_content?: string; // 日志内容
start_time?: string; // 日志开始时间
};

const Props = forwardRef((_, ref) => {
const [form] = Form.useForm();
const [experimentNodeData, setExperimentNodeData] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize,
);
const [experimentResults, setExperimentResults] = useState([]);
const [experimentLogList, setExperimentLogList] = useState<ExperimentLog[]>([]);

const items = [
{
key: '1',
label: '日志详情',
children: (
<LogList list={experimentLogList} status={experimentNodeData.experimentStatus}></LogList>
),
icon: <ProfileOutlined />,
},
{
key: '2',
label: '配置参数',
icon: <DatabaseOutlined />,
children: <ExperimentParameter form={form} nodeData={experimentNodeData} />,
},
{
key: '3',
label: '输出结果',
children: <ExperimentResult results={experimentResults}></ExperimentResult>,
icon: <ProfileOutlined />,
},
];
const [open, setOpen] = useState(false);
const onClose = () => {
setOpen(false);
};

// 获取实验日志
const getExperimentLog = async (params: any, start_time: number) => {
const [res] = await to(getQueryByExperimentLog(params));
if (res && res.data) {
const { log_type, pods, log_detail } = res.data;
if (log_type === 'normal') {
const list = [
{
...log_detail,
log_type,
},
];
setExperimentLogList(list);
} else if (log_type === 'resource') {
const list = pods.map((v: string) => ({
log_type,
pod_name: v,
log_content: '',
start_time,
}));
setExperimentLogList(list);
}
}
};

// 获取实验结果
const getExperimentResult = async (params: any) => {
const [res] = await to(getNodeResult(params));
if (res && res.data) {
setExperimentResults(res.data);
}
};

useImperativeHandle(ref, () => ({
showDrawer(e: any, id: string, message: any) {
setOpen(true);

// 获取实验参数
const model = e.item.getModel();
try {
const nodeData = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
setExperimentNodeData(nodeData);
form.setFieldsValue(nodeData);
} catch (error) {
console.log(error);
}

// 获取实验日志和实验结果
setExperimentLogList([]);
setExperimentResults([]);
if (e.item && e.item.getModel()) {
const model = e.item.getModel();
const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6;
const params = {
task_id: model.id,
component_id: model.component_id,
name: message.argo_ins_name,
namespace: message.argo_ins_ns,
start_time: start_time,
};
getExperimentLog(params, start_time);
getExperimentResult({ id, node_id: model.id });
}
},
}));
return (
<Drawer
title="任务执行详情"
placement="right"
getContainer={false}
closeIcon={false}
onClose={onClose}
open={open}
width={520}
className={styles['experiment-drawer']}
destroyOnClose={true}
>
<div style={{ paddingTop: '15px' }}>
<div className={styles['experiment-drawer__info']}>
任务名称:{experimentNodeData.label}
</div>
<div className={styles['experiment-drawer__info']}>
执行状态:
<div
className={styles['experiment-drawer__status-dot']}
style={{
backgroundColor: experimentStatusInfo[experimentNodeData.experimentStatus]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[experimentNodeData.experimentStatus]?.color }}>
{experimentStatusInfo[experimentNodeData.experimentStatus]?.label}
</span>
</div>
<div className={styles['experiment-drawer__info']}>
启动时间:{formatDate(experimentNodeData.experimentStartTime)}
</div>
<div className={styles['experiment-drawer__info']}>
耗时:
{elapsedTime(
experimentNodeData.experimentStartTime,
experimentNodeData.experimentEndTime,
)}
</div>
</div>
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
</Drawer>
);
});

export default Props;

+ 1
- 3
react-ui/src/pages/Experiment/index.jsx View File

@@ -437,9 +437,7 @@ function Experiment() {
</div> </div>
<div className={Styles.description}> <div className={Styles.description}>
<div style={{ width: '50%' }}> <div style={{ width: '50%' }}>
{item.finish_time
? elapsedTime(new Date(item.create_time), new Date(item.finish_time))
: elapsedTime(new Date(item.create_time), new Date())}
{elapsedTime(item.create_time, item.finish_time)}
</div> </div>
<div style={{ width: '50%' }}>{formatDate(item.create_time)}</div> <div style={{ width: '50%' }}>{formatDate(item.create_time)}</div>
</div> </div>


+ 2
- 0
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -606,6 +606,8 @@ const EditPipeline = () => {
}, },
// linkCenter: true, // linkCenter: true,
fitView: true, fitView: true,
minZoom: 0.5,
maxZoom: 3,
fitViewPadding: [320, 320, 220, 320], fitViewPadding: [320, 320, 220, 320],
}); });
// graph.on('dblclick', (e) => { // graph.on('dblclick', (e) => {


+ 8
- 6
react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx View File

@@ -19,7 +19,7 @@ const ModelMenus = ({ onParDragEnd }) => {
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
label: data.component_label, label: data.component_label,
img: `/assets/images/pipeline/${data.icon_path}.png`,
img: `/assets/images/${data.icon_path}.png`,
}); });
}; };
const { Panel } = Collapse; const { Panel } = Collapse;
@@ -45,11 +45,13 @@ const ModelMenus = ({ onParDragEnd }) => {
}} }}
className={Styles.collapseItem} className={Styles.collapseItem}
> >
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/pipeline/${ele.icon_path}.png`}
alt=""
/>
{ele.icon_path && (
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
alt=""
/>
)}
{ele.component_label} {ele.component_label}
</div> </div>
)) ))


+ 58
- 68
react-ui/src/pages/Pipeline/editPipeline/props.jsx View File

@@ -1,4 +1,5 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import SubAreaTitle from '@/components/SubAreaTitle';
import { getComputingResourceReq } from '@/services/pipeline'; import { getComputingResourceReq } from '@/services/pipeline';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
@@ -7,7 +8,7 @@ import { pick } from 'lodash';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import PropsLabel from '../components/PropsLabel'; import PropsLabel from '../components/PropsLabel';
import ResourceSelectorModal, { ResourceSelectorType } from '../components/ResourceSelectorModal'; import ResourceSelectorModal, { ResourceSelectorType } from '../components/ResourceSelectorModal';
import Styles from './editPipeline.less';
import styles from './editPipeline.less';
import { createMenuItems } from './utils'; import { createMenuItems } from './utils';
const { TextArea } = Input; const { TextArea } = Input;


@@ -18,7 +19,8 @@ const Props = forwardRef(({ onParentChange }, ref) => {
const [selectedModel, setSelectedModel] = useState(undefined); // 选择的模型,为了再次打开时恢复原来的选择 const [selectedModel, setSelectedModel] = useState(undefined); // 选择的模型,为了再次打开时恢复原来的选择
const [selectedDataset, setSelectedDataset] = useState(undefined); // 选择的数据集,为了再次打开时恢复原来的选择 const [selectedDataset, setSelectedDataset] = useState(undefined); // 选择的数据集,为了再次打开时恢复原来的选择
const [resourceStandardList, setResourceStandardList] = useState([]); // 资源规模列表 const [resourceStandardList, setResourceStandardList] = useState([]); // 资源规模列表
const [items, setItems] = useState([]);
const [menuItems, setMenuItems] = useState([]);

useEffect(() => { useEffect(() => {
getComputingResource(); getComputingResource();
}, []); }, []);
@@ -38,7 +40,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {


const afterOpenChange = () => { const afterOpenChange = () => {
if (!open) { if (!open) {
// console.log('zzzz', stagingItem, form.getFieldsValue());
console.log('zzzz', form.getFieldsValue());
const control_strategy = form.getFieldValue('control_strategy'); const control_strategy = form.getFieldValue('control_strategy');
const in_parameters = form.getFieldValue('in_parameters'); const in_parameters = form.getFieldValue('in_parameters');
const out_parameters = form.getFieldValue('out_parameters'); const out_parameters = form.getFieldValue('out_parameters');
@@ -68,29 +70,32 @@ const Props = forwardRef(({ onParentChange }, ref) => {
if (e.item && e.item.getModel()) { if (e.item && e.item.getModel()) {
form.resetFields(); form.resetFields();
const model = e.item.getModel(); const model = e.item.getModel();
const nodeData = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
setStagingItem({
...nodeData,
});
form.setFieldsValue({
...nodeData,
});
try {
const nodeData = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
setStagingItem({
...nodeData,
});
form.setFieldsValue({
...nodeData,
});
} catch (error) {
console.log(error);
}
setSelectedModel(undefined); setSelectedModel(undefined);
setSelectedDataset(undefined); setSelectedDataset(undefined);
setOpen(true); setOpen(true);


setItems(createMenuItems(params, parentNodes));
// 参数下拉菜单
setMenuItems(createMenuItems(params, parentNodes));
} }
}, },
propClose: async () => {
setOpen(false);
const [openRes, propsError] = await to(setOpen(false));
console.log(setOpen(false));
propClose: () => {
close();
}, },
})); }));


@@ -161,6 +166,11 @@ const Props = forwardRef(({ onParentChange }, ref) => {
return computing_resource.toLocaleLowerCase().includes(input.toLocaleLowerCase()); return computing_resource.toLocaleLowerCase().includes(input.toLocaleLowerCase());
}; };


// 参数回填
const handleParameterClick = (name, value) => {
form.setFieldValue(name, value);
};

// 控制策略 // 控制策略
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
([key, value]) => ({ key, value }), ([key, value]) => ({ key, value }),
@@ -189,7 +199,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
afterOpenChange={afterOpenChange} afterOpenChange={afterOpenChange}
open={open} open={open}
width={520} width={520}
className={Styles.editPipelineProps}
className={styles.editPipelineProps}
> >
<Form <Form
name="form" name="form"
@@ -206,13 +216,8 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}} }}
autoComplete="off" autoComplete="off"
> >
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '13px', marginRight: '10px' }}
src={'/assets/images/static-message.png'}
alt=""
/>
基本信息
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle>
</div> </div>
<Form.Item <Form.Item
label="任务名称" label="任务名称"
@@ -238,16 +243,11 @@ const Props = forwardRef(({ onParentChange }, ref) => {
> >
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
任务信息
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="任务信息"></SubAreaTitle>
</div> </div>
<Form.Item label="镜像" required> <Form.Item label="镜像" required>
<div className={Styles['ref-row']}>
<div className={styles['ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}> <Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear /> <Input placeholder="请输入或选择镜像" allowClear />
</Form.Item> </Form.Item>
@@ -256,7 +256,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
type="link" type="link"
icon={getSelectBtnIcon({ item_type: 'image' })} icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })} onClick={() => selectResource('image', { item_type: 'image' })}
className={Styles['select-button']}
className={styles['select-button']}
> >
选择镜像 选择镜像
</Button> </Button>
@@ -267,10 +267,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name="working_directory" name="working_directory"
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title="工作目录" title="工作目录"
onClick={(value) => { onClick={(value) => {
form.setFieldValue('working_directory', value);
handleParameterClick('working_directory', value);
}} }}
/> />
} }
@@ -281,10 +281,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name="command" name="command"
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
title="工作目录"
menuItems={menuItems}
title="启动命令"
onClick={(value) => { onClick={(value) => {
form.setFieldValue('command', value);
handleParameterClick('command', value);
}} }}
/> />
} }
@@ -316,10 +316,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name="mount_path" name="mount_path"
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title="挂载路径" title="挂载路径"
onClick={(value) => { onClick={(value) => {
form.setFieldValue('mount_path', value);
handleParameterClick('mount_path', value);
}} }}
/> />
} }
@@ -330,10 +330,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name="env_variables" name="env_variables"
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title="环境变量" title="环境变量"
onClick={(value) => { onClick={(value) => {
form.setFieldValue('env_variables', value);
handleParameterClick('env_variables', value);
}} }}
/> />
} }
@@ -346,10 +346,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name={['control_strategy', item.key]} name={['control_strategy', item.key]}
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title={item.value.label} title={item.value.label}
onClick={(value) => { onClick={(value) => {
form.setFieldValue(['control_strategy', item.key], {
handleParameterClick(['control_strategy', item.key], {
...item.value, ...item.value,
value, value,
}); });
@@ -369,23 +369,18 @@ const Props = forwardRef(({ onParentChange }, ref) => {
<Input placeholder={item.value.label} allowClear /> <Input placeholder={item.value.label} allowClear />
</Form.Item> </Form.Item>
))} ))}
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
输入参数
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输入参数"></SubAreaTitle>
</div> </div>
{inParametersList.map((item) => ( {inParametersList.map((item) => (
<Form.Item <Form.Item
key={item.key} key={item.key}
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'} title={item.value.label + '(' + item.key + ')'}
onClick={(value) => { onClick={(value) => {
form.setFieldValue(['in_parameters', item.key], {
handleParameterClick(['in_parameters', item.key], {
...item.value, ...item.value,
value, value,
}); });
@@ -394,7 +389,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
} }
required={item.value.require ? true : false} required={item.value.require ? true : false}
> >
<div className={Styles['ref-row']}>
<div className={styles['ref-row']}>
<Form.Item <Form.Item
name={['in_parameters', item.key]} name={['in_parameters', item.key]}
noStyle noStyle
@@ -417,7 +412,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
type="link" type="link"
icon={getSelectBtnIcon(item.value)} icon={getSelectBtnIcon(item.value)}
onClick={() => selectResource(['in_parameters', item.key], item.value)} onClick={() => selectResource(['in_parameters', item.key], item.value)}
className={Styles['select-button']}
className={styles['select-button']}
> >
{item.value.label} {item.value.label}
</Button> </Button>
@@ -426,13 +421,8 @@ const Props = forwardRef(({ onParentChange }, ref) => {
</div> </div>
</Form.Item> </Form.Item>
))} ))}
<div className={Styles.editPipelinePropsContent}>
<img
style={{ width: '15px', marginRight: '10px' }}
src={'/assets/images/duty-message.png'}
alt=""
/>
输出参数
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输出参数"></SubAreaTitle>
</div> </div>
{outParametersList.map((item) => ( {outParametersList.map((item) => (
<Form.Item <Form.Item
@@ -440,10 +430,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
name={['out_parameters', item.key]} name={['out_parameters', item.key]}
label={ label={
<PropsLabel <PropsLabel
menuItems={items}
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'} title={item.value.label + '(' + item.key + ')'}
onClick={(value) => { onClick={(value) => {
form.setFieldValue(['out_parameters', item.key], {
handleParameterClick(['out_parameters', item.key], {
...item.value, ...item.value,
value, value,
}); });


+ 1
- 4
react-ui/src/pages/Workspace/components/ExperimentTable/index.tsx View File

@@ -34,10 +34,7 @@ function ExperimentTable({ tableData = [], style }: ExperimentTableProps) {
/> />
</div> </div>
<div className={styles['experiment-table__duration']}> <div className={styles['experiment-table__duration']}>
{elapsedTime(
new Date(item.create_time),
item.finish_time ? new Date(item.finish_time) : new Date(),
)}
{elapsedTime(item.create_time, item.finish_time)}
</div> </div>
<div className={styles['experiment-table__date']}>{formatDate(item.create_time)}</div> <div className={styles['experiment-table__date']}>{formatDate(item.create_time)}</div>
<div className={styles['experiment-table__operation']}> <div className={styles['experiment-table__operation']}>


+ 33
- 0
react-ui/src/types.ts View File

@@ -4,6 +4,8 @@
* @Description: 定义全局类型,比如无关联的页面都需要要的类型 * @Description: 定义全局类型,比如无关联的页面都需要要的类型
*/ */


import { ExperimentStatus } from '@/pages/Experiment/status';

// 流水线全局参数 // 流水线全局参数
export type PipelineGlobalParam = { export type PipelineGlobalParam = {
param_name: string; param_name: string;
@@ -28,3 +30,34 @@ export type ExperimentInstance = {
nodes_status: string; nodes_status: string;
global_param: PipelineGlobalParam[]; global_param: PipelineGlobalParam[];
}; };

// 流水线节点
export type PipelineNodeModel = {
id: string;
experimentStatus: ExperimentStatus;
label: string;
experimentStartTime: string;
experimentEndTime?: string | null;
control_strategy: string;
in_parameters: string;
out_parameters: string;
};

// 流水线
export type PipelineNodeModelParameter = {
label: string;
value: any;
require: number;
type: string;
placeholder?: string;
describe?: string;
};

export type PipelineNodeModelSerialize = Omit<
PipelineNodeModel,
'control_strategy' | 'in_parameters' | 'out_parameters'
> & {
control_strategy: Record<string, PipelineNodeModelParameter>;
in_parameters: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
};

+ 12
- 5
react-ui/src/utils/date.ts View File

@@ -3,15 +3,22 @@ import dayjs from 'dayjs';
/** /**
* Calculates the elapsed time between two dates and returns a formatted string representing the duration. * Calculates the elapsed time between two dates and returns a formatted string representing the duration.
* *
* @param {Date} beginDate - The starting date.
* @param {Date} endDate - The ending date.
* @param {string | null | undefined} begin - The starting date.
* @param {string | null | undefined} end - The ending date.
* @return {string} The formatted elapsed time string. * @return {string} The formatted elapsed time string.
*/ */
export const elapsedTime = (beginDate: Date, endDate: Date): string => {
if (!isValidDate(beginDate) || !isValidDate(endDate)) {
export const elapsedTime = (begin?: string | null, end?: string | null): string => {
if (begin === undefined || begin === null) {
return '--'; return '--';
} }
const timestamp = endDate.getTime() - beginDate.getTime();

const beginDate = dayjs(begin);
const endDate = end === undefined || end === null ? dayjs() : dayjs(end);
if (!beginDate.isValid() || !endDate.isValid()) {
return '--';
}

const timestamp = endDate.valueOf() - beginDate.valueOf();
if (timestamp < 0) { if (timestamp < 0) {
return '时间有误'; return '时间有误';
} }


Loading…
Cancel
Save