Browse Source

feat: 实验适配新组件模板

pull/273/head
zhaowei 6 months ago
parent
commit
7b0f26a767
13 changed files with 227 additions and 146 deletions
  1. +20
    -0
      react-ui/src/components/FormInfo/index.tsx
  2. +3
    -2
      react-ui/src/components/ParameterSelect/index.tsx
  3. +9
    -9
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  4. +8
    -7
      react-ui/src/pages/Experiment/Info/index.jsx
  5. +7
    -7
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  6. +5
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  7. +20
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  8. +121
    -94
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  9. +6
    -6
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
  10. +8
    -8
      react-ui/src/pages/Experiment/index.jsx
  11. +4
    -4
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  12. +9
    -6
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  13. +7
    -1
      react-ui/src/types.ts

+ 20
- 0
react-ui/src/components/FormInfo/index.tsx View File

@@ -1,3 +1,4 @@
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { formatEnum } from '@/utils/format';
import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames';
@@ -16,6 +17,8 @@ type FormInfoProps = {
options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames'];
/** 全局参数 */
globalParams?: PipelineGlobalParam[] | null;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
@@ -32,12 +35,29 @@ function FormInfo({
select = false,
options,
fieldNames,
globalParams,
className,
style,
}: FormInfoProps) {
let showValue = value;
if (value && typeof value === 'object' && valuePropName) {
showValue = value[valuePropName];
const reg = /^\$\{(.*)\}$/;
if (value.fromSelect && Array.isArray(globalParams) && globalParams.length > 0) {
const match = reg.exec(showValue);
if (match) {
const paramName = match[1];
const foundParam = globalParams.find((v) => v.param_name === paramName);
if (foundParam) {
showValue =
foundParam.param_type === PipelineGlobalParamType.Boolean // 布尔类型转换
? foundParam.param_value
? 'true'
: 'false'
: foundParam.param_value;
}
}
}
} else if (select === true && options) {
let _options: SelectProps['options'] = options;
if (fieldNames) {


+ 3
- 2
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -110,13 +110,14 @@ function ParameterSelect({
onChange?.(selectValue);
}
} else {
const selectValue = text ? text : '';
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: text,
value: selectValue,
});
} else {
onChange?.(text);
onChange?.(selectValue);
}
}
};


+ 9
- 9
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -169,16 +169,16 @@ function ExperimentList({ type }: ExperimentListProps) {
const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data;
if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload;
const { experimentId, experimentInsId, status /*finishTime*/ } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );

if (
!currentIns ||


+ 8
- 7
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -95,16 +95,13 @@ function ExperimentText() {
return;
}

const workflow = parseJsonText(dag);
const workflow = dag;
const experimentStatusObjs = parseJsonText(nodes_status);
if (!workflow || !workflow.nodes) {
return;
}

workflow.nodes.forEach((item) => {
item.in_parameters = parseJsonText(item.in_parameters);
item.out_parameters = parseJsonText(item.out_parameters);
item.control_strategy = parseJsonText(item.control_strategy);
item.imgName = item.img.slice(0, item.img.length - 4);
});
workflowRef.current = workflow;
@@ -140,8 +137,11 @@ function ExperimentText() {
} else if (status === ExperimentStatus.Running) {
// 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点
const node =
workflow.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running || item.experimentStatus === ExperimentStatus.Pending) ??
workflow.nodes[0];
workflow.nodes.find(
(item) =>
item.experimentStatus === ExperimentStatus.Running ||
item.experimentStatus === ExperimentStatus.Pending,
) ?? workflow.nodes[0];
if (node) {
setExperimentNodeData(node);
openPropsDrawer();
@@ -567,12 +567,13 @@ function ExperimentText() {
instanceNodeStatus={experimentNodeData.experimentStatus}
instanceNodeStartTime={experimentNodeData.experimentStartTime}
instanceNodeEndTime={experimentNodeData.experimentEndTime}
globalParams={experimentIns?.global_param}
></ExperimentDrawer>
) : null}
<ParamsModal
open={paramsModalOpen}
onCancel={closeParamsModal}
globalParam={experimentIns?.global_param}
globalParams={experimentIns?.global_param}
></ParamsModal>
</div>
);


+ 7
- 7
react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx View File

@@ -1,7 +1,7 @@
import createExperimentIcon from '@/assets/img/create-experiment.png';
import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd';
import { useState } from 'react';
@@ -32,7 +32,7 @@ interface Workflow {
// 根据参数设置输入组件
export const getParamComponent = (paramType: number): JSX.Element => {
// 防止后台返回不是 number 类型
if (Number(paramType) === 3) {
if (Number(paramType) === PipelineGlobalParamType.Boolean) {
return (
<Radio.Group>
<Radio value={1}>是</Radio>
@@ -50,7 +50,7 @@ export const getParamComponent = (paramType: number): JSX.Element => {
export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => {
const rules = [];
// 防止后台返回不是 number 类型
if (Number(paramType) === 2) {
if (Number(paramType) === PipelineGlobalParamType.Number) {
rules.push({
pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/,
message: '整型必须是数字',
@@ -64,10 +64,10 @@ export const getParamRules = (paramType: number, required: boolean = false): For

// 根据参数设置 label
export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => {
const paramTypes: Readonly<Record<number, string>> = {
1: '字符串',
2: '整型',
3: '布尔类型',
const paramTypes: Readonly<Record<PipelineGlobalParamType, string>> = {
[PipelineGlobalParamType.String]: '字符串',
[PipelineGlobalParamType.Number]: '整型',
[PipelineGlobalParamType.Boolean]: '布尔类型',
};
const label = param.param_name + `(${paramTypes[param.param_type]})`;
return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>;


+ 5
- 2
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -1,7 +1,7 @@
import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types';
import { formatDate } from '@/utils/date';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs, Typography } from 'antd';
@@ -25,6 +25,7 @@ type ExperimentDrawerProps = {
instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
globalParams?: PipelineGlobalParam[] | null; // 全局参数
};

const ExperimentDrawer = ({
@@ -41,6 +42,7 @@ const ExperimentDrawer = ({
instanceNodeStatus,
instanceNodeStartTime,
instanceNodeEndTime,
globalParams,
}: ExperimentDrawerProps) => {
// 如果性能有问题,可以进一步拆解
const items = useMemo(
@@ -66,7 +68,7 @@ const ExperimentDrawer = ({
key: '2',
label: '配置参数',
icon: <DatabaseOutlined />,
children: <ExperimentParameter nodeData={instanceNodeData} />,
children: <ExperimentParameter nodeData={instanceNodeData} globalParams={globalParams} />,
},
{
key: '3',
@@ -94,6 +96,7 @@ const ExperimentDrawer = ({
experimentName,
experimentId,
pipelineId,
globalParams,
],
);



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

@@ -15,4 +15,24 @@
font-size: @font-size;
background: #f8fbff;
}

&__form-list {
:global {
.ant-row {
padding: 0 !important;
}
}

&:last-child {
:global {
.ant-form-item {
margin-bottom: 0 !important;
}
}
}
}

&__list-empty {
color: @text-color-tertiary;
}
}

+ 121
- 94
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -1,25 +1,92 @@
import FormInfo from '@/components/FormInfo';
import ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { PipelineNodeModelSerialize } from '@/types';
import { Form } from 'antd';
import { ComponentType } from '@/enums';
import type {
PipelineGlobalParam,
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
import { Flex, Form } from 'antd';
import styles from './index.less';

type ExperimentParameterProps = {
nodeData: PipelineNodeModelSerialize;
globalParams?: PipelineGlobalParam[] | null; // 全局参数
};

function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
function ExperimentParameter({ nodeData, globalParams }: ExperimentParameterProps) {
// 表单组件
const getFormComponent = (
item: { key: string; value: PipelineNodeModelParameter },
parentName: string,
) => {
return (
<Form.Item
key={item.key}
name={[parentName, item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
>
{item.value.type === ComponentType.Map && (
<Form.List name={[parentName, item.key, 'value']}>
{(fields) => (
<>
{fields.length > 0 ? (
fields.map(({ key, name, ...restField }) => (
<Flex
key={key}
gap="0 8px"
style={{ width: '100%' }}
className={styles['experiment-parameter__form-list']}
>
<Form.Item
{...restField}
name={[name, 'name']}
style={{ flex: 1, minWidth: 0 }}
>
<FormInfo />
</Form.Item>
<span style={{ lineHeight: '32px' }}>=</span>
<Form.Item
{...restField}
name={[name, 'value']}
style={{ flex: 1, minWidth: 0 }}
>
<FormInfo valuePropName="showValue" globalParams={globalParams} />
</Form.Item>
</Flex>
))
) : (
<div className={styles['experiment-parameter__list-empty']}>无参数</div>
)}
</>
)}
</Form.List>
)}
{item.value.type === ComponentType.Select &&
(['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect dataType={item.value.item_type as ParameterSelectDataType} display />
) : null)}
{item.value.type !== ComponentType.Map && item.value.type !== ComponentType.Select && (
<FormInfo valuePropName="showValue" globalParams={globalParams} />
)}
</Form.Item>
);
};

// 基本参数
const basicParametersList = Object.entries(nodeData.task_info ?? {})
.map(([key, value]) => ({
key,
value,
}))
.filter((v) => v.value.visible === true);

// 控制策略
// const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map(
// ([key, value]) => ({ key, value }),
// );
const nodeId = nodeData.id;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');
const controlStrategyList = Object.entries(nodeData.control_strategy ?? {})
.map(([key, value]) => ({ key, value }))
.filter((v) => v.value.visible === true);

// 输入参数
const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
@@ -80,96 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
>
<FormInfo />
</Form.Item>
{hasTaskInfo && (

{basicParametersList.length + controlStrategyList.length > 0 && (
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
)}

{/* 基本参数 */}
{basicParametersList.map((item) => getFormComponent(item, 'task_info'))}

{/* 控制参数 */}
{controlStrategyList.map((item) => getFormComponent(item, 'control_strategy'))}

{/* 输入参数 */}
{inParametersList.length > 0 && (
<>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
title="输入参数"
></SubAreaTitle>
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<FormInfo />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<FormInfo />
</Form.Item>
{inParametersList.map((item) => getFormComponent(item, 'in_parameters'))}
</>
)}

<Form.Item label="启动命令" name="command">
<FormInfo textArea />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请输入资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" display />
</Form.Item>
{/* <Form.Item label="挂载路径" name="mount_path">
<FormInfo />
</Form.Item> */}
<Form.Item label="环境变量" name="env_variables">
<FormInfo textArea />
</Form.Item>
{/* {controlStrategyList.map((item) => (
<Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}>
<FormInfo valuePropName="showValue" />
</Form.Item>
))} */}
{/* 输出参数 */}
{outParametersList.length > 0 && (
<>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/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 }]}
>
<FormInfo valuePropName="showValue" />
</Form.Item>
))}
</>
)}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/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 }]}
>
{item.value.type === 'select' ? (
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect dataType={item.value.item_type as any} display />
) : null
) : (
<FormInfo valuePropName="showValue" />
)}
</Form.Item>
))}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/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 }]}
>
<FormInfo valuePropName="showValue" />
</Form.Item>
))}
</Form>
);
}


+ 6
- 6
react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx View File

@@ -14,10 +14,10 @@ import styles from './index.less';
type ParamsModalProps = {
open: boolean;
onCancel: () => void;
globalParam?: PipelineGlobalParam[] | null;
globalParams?: PipelineGlobalParam[] | null;
};

function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) {
return (
<KFModal
title="执行参数"
@@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }}
width={825}
>
{Array.isArray(globalParam) && globalParam.length > 0 ? (
{Array.isArray(globalParams) && globalParams.length > 0 ? (
<div className={styles['params-container']}>
<Form
name="view_params_form"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
initialValues={{ global_param: globalParam }}
initialValues={{ global_param: globalParams }}
labelAlign="left"
disabled
>
@@ -45,9 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
{...restField}
key={key}
name={[name, 'param_value']}
label={getParamLabel(globalParam[name])}
label={getParamLabel(globalParams[name])}
>
{getParamComponent(globalParam[name]['param_type'])}
{getParamComponent(globalParams[name]['param_type'])}
</Form.Item>
))
}


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

@@ -226,14 +226,14 @@ function Experiment() {
if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );

if (
!currentIns ||


+ 4
- 4
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx View File

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal';
import { type PipelineGlobalParam } from '@/types';
import { type PipelineGlobalParam, PipelineGlobalParamType } from '@/types';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { PlusOutlined } from '@ant-design/icons';
@@ -131,9 +131,9 @@ const GlobalParamsDrawer = forwardRef(
<Radio.Group
onChange={() => handleTypeChange(['global_param', name, 'param_value'])}
>
<Radio value={1}>字符串</Radio>
<Radio value={2}>整型</Radio>
<Radio value={3}>布尔类型</Radio>
<Radio value={PipelineGlobalParamType.String}>字符串</Radio>
<Radio value={PipelineGlobalParamType.Number}>整型</Radio>
<Radio value={PipelineGlobalParamType.Boolean}>布尔类型</Radio>
</Radio.Group>
</Form.Item>
<Form.Item


+ 9
- 6
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -614,12 +614,14 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
<Input disabled />
</Form.Item>

<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
{basicParametersList.length + controlStrategyList.length > 0 && (
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
)}

{/* 基本参数 */}
{basicParametersList.map((item) => (
@@ -663,6 +665,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
))}
</>
)}

{/* 输出参数 */}
{outParametersList.length > 0 && (
<>


+ 7
- 1
react-ui/src/types.ts View File

@@ -29,11 +29,17 @@ export type GlobalInitialState = {
clientInfo?: ClientInfo;
};

export enum PipelineGlobalParamType {
String = 1,
Number = 2,
Boolean = 3,
}

// 流水线全局参数
export type PipelineGlobalParam = {
param_name: string;
description: string;
param_type: number;
param_type: PipelineGlobalParamType;
param_value: number | string | boolean;
is_sensitive: number;
};


Loading…
Cancel
Save