Browse Source

chore: merge dev-zw

dev-zw-home
zhaowei 5 months ago
parent
commit
b18f69e17a
57 changed files with 3009 additions and 1098 deletions
  1. +1
    -0
      .gitignore
  2. +4
    -0
      react-ui/config/proxy.ts
  3. +1294
    -0
      react-ui/mock/components.ts
  4. +1
    -1
      react-ui/package.json
  5. +9
    -51
      react-ui/src/components/CodeSelect/index.tsx
  6. +20
    -0
      react-ui/src/components/FormInfo/index.tsx
  7. +4
    -1
      react-ui/src/components/ParameterInput/index.tsx
  8. +83
    -39
      react-ui/src/components/ParameterSelect/config.tsx
  9. +102
    -41
      react-ui/src/components/ParameterSelect/index.tsx
  10. +29
    -44
      react-ui/src/components/ResourceSelect/index.tsx
  11. +4
    -5
      react-ui/src/components/ResourceSelectorModal/config.tsx
  12. +24
    -16
      react-ui/src/components/ResourceSelectorModal/index.tsx
  13. +8
    -0
      react-ui/src/enums/index.ts
  14. +71
    -47
      react-ui/src/hooks/useComputingResource.ts
  15. +2
    -2
      react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx
  16. +2
    -2
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  17. +24
    -17
      react-ui/src/pages/AutoML/components/CreateForm/utils.ts
  18. +9
    -9
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  19. +7
    -121
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  20. +7
    -113
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  21. +20
    -2
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  22. +121
    -0
      react-ui/src/pages/Dataset/components/EditVersionModal/index.tsx
  23. +7
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.less
  24. +98
    -54
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  25. +10
    -0
      react-ui/src/pages/Dataset/config.tsx
  26. +6
    -10
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  27. +8
    -7
      react-ui/src/pages/Experiment/Info/index.jsx
  28. +6
    -0
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.less
  29. +7
    -7
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  30. +5
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  31. +20
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  32. +121
    -94
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  33. +13
    -32
      react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx
  34. +6
    -0
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
  35. +6
    -6
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
  36. +8
    -8
      react-ui/src/pages/Experiment/index.jsx
  37. +1
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
  38. +2
    -2
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
  39. +1
    -0
      react-ui/src/pages/Mirror/Info/index.tsx
  40. +4
    -8
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  41. +24
    -6
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  42. +2
    -2
      react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx
  43. +2
    -2
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx
  44. +1
    -1
      react-ui/src/pages/ModelDeployment/types.ts
  45. +55
    -21
      react-ui/src/pages/Pipeline/Info/index.jsx
  46. +5
    -13
      react-ui/src/pages/Pipeline/Info/utils.tsx
  47. +18
    -11
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  48. +25
    -4
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.less
  49. +349
    -261
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  50. +5
    -5
      react-ui/src/pages/Pipeline/index.jsx
  51. +36
    -1
      react-ui/src/services/dataset/index.js
  52. +89
    -0
      react-ui/src/services/external/index.ts
  53. +140
    -0
      react-ui/src/state/jcdResource.ts
  54. +53
    -0
      react-ui/src/state/systemResource.ts
  55. +16
    -15
      react-ui/src/types.ts
  56. +9
    -15
      react-ui/src/utils/format.ts
  57. +5
    -0
      react-ui/src/utils/sessionStorage.ts

+ 1
- 0
.gitignore View File

@@ -65,3 +65,4 @@ mvnw
/react-ui/types/tsconfig.tsbuildinfo /react-ui/types/tsconfig.tsbuildinfo
/react-ui/storybook-static /react-ui/storybook-static
/react-ui/.storybook/scripts /react-ui/.storybook/scripts
/react-ui/dist.zip

+ 4
- 0
react-ui/config/proxy.ts View File

@@ -23,7 +23,11 @@ export default {
target: 'http://172.20.32.197:31213', // 开发环境 target: 'http://172.20.32.197:31213', // 开发环境
// target: 'http://172.20.32.235:31213', // 测试环境 // target: 'http://172.20.32.235:31213', // 测试环境
// target: 'http://172.20.32.44:8082', // target: 'http://172.20.32.44:8082',
<<<<<<< HEAD
// target: 'http://172.20.32.164:8082', // target: 'http://172.20.32.164:8082',
=======
target: 'http://172.20.32.164:8082',
>>>>>>> dev-zw
// 配置了这个可以从 http 代理到 https // 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie // 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true, changeOrigin: true,


+ 1294
- 0
react-ui/mock/components.ts
File diff suppressed because it is too large
View File


+ 1
- 1
react-ui/package.json View File

@@ -8,7 +8,7 @@
"build": "max build", "build": "max build",
"deploy": "npm run build && npm run gh-pages", "deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev", "dev": "npm run start:dev",
"dev-no-sso": "cross-env NO_SSO=true npm run start:dev",
"dev-no-sso": "cross-env NO_SSO=true npm run start:mock",
"docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
"docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",


+ 9
- 51
react-ui/src/components/CodeSelect/index.tsx View File

@@ -4,7 +4,7 @@
* @Description: 流水线选择代码配置表单 * @Description: 流水线选择代码配置表单
*/ */


import CodeSelectorModal from '@/components/CodeSelectorModal';
import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd'; import { Button } from 'antd';
@@ -18,19 +18,9 @@ export {
type ParameterInputValue, type ParameterInputValue,
} from '../ParameterInput'; } from '../ParameterInput';


export type CodeSelectProps = ParameterInputProps;

// 服务的需要的代码配置数据格式
export type ServerCodeData = {
id: number;
name: string;
code_path: string;
branch: string;
username: string;
password: string;
ssh_private_key: string;
is_public: boolean;
};
export interface CodeSelectProps extends ParameterInputProps {
value?: CodeConfigData;
}


/** 代码配置选择表单组件 */ /** 代码配置选择表单组件 */
function CodeSelect({ function CodeSelect({
@@ -44,50 +34,18 @@ function CodeSelect({
}: CodeSelectProps) { }: CodeSelectProps) {
// 选择代码配置 // 选择代码配置
const selectResource = () => { const selectResource = () => {
const codeData = value as ServerCodeData;
const defaultSelected =
value && typeof value === 'object'
? {
id: codeData.id,
code_repo_name: codeData.name,
git_url: codeData.code_path,
git_branch: codeData.branch,
git_user_name: codeData.username,
git_password: codeData.password,
ssh_key: codeData.ssh_private_key,
is_public: codeData.is_public,
}
: undefined;
const defaultSelected: CodeConfigData | undefined =
value && typeof value === 'object' ? value : undefined;
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected: defaultSelected, defaultSelected: defaultSelected,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const {
id,
code_repo_name,
git_url,
git_branch,
git_user_name,
git_password,
ssh_key,
is_public,
} = res;
const jsonObj = {
id,
name: code_repo_name,
code_path: git_url,
branch: git_branch,
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
is_public,
};
const jsonObjStr = JSON.stringify(jsonObj);
const { code_repo_name } = res;
onChange?.({ onChange?.({
value: jsonObjStr,
...res,
value: code_repo_name,
showValue: code_repo_name, showValue: code_repo_name,
fromSelect: true, fromSelect: true,
...jsonObj,
}); });
} else { } else {
onChange?.(undefined); onChange?.(undefined);


+ 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 { formatEnum } from '@/utils/format';
import { Typography, type SelectProps } from 'antd'; import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -16,6 +17,8 @@ type FormInfoProps = {
options?: SelectProps['options']; options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */ /** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames']; fieldNames?: SelectProps['fieldNames'];
/** 全局参数 */
globalParams?: PipelineGlobalParam[] | null;
/** 自定义类名 */ /** 自定义类名 */
className?: string; className?: string;
/** 自定义样式 */ /** 自定义样式 */
@@ -32,12 +35,29 @@ function FormInfo({
select = false, select = false,
options, options,
fieldNames, fieldNames,
globalParams,
className, className,
style, style,
}: FormInfoProps) { }: FormInfoProps) {
let showValue = value; let showValue = value;
if (value && typeof value === 'object' && valuePropName) { if (value && typeof value === 'object' && valuePropName) {
showValue = value[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) { } else if (select === true && options) {
let _options: SelectProps['options'] = options; let _options: SelectProps['options'] = options;
if (fieldNames) { if (fieldNames) {


+ 4
- 1
react-ui/src/components/ParameterInput/index.tsx View File

@@ -9,6 +9,7 @@ import { CloseOutlined } from '@ant-design/icons';
import { ConfigProvider, Form, Input, Typography } from 'antd'; import { ConfigProvider, Form, Input, Typography } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReactNode } from 'react';
import './index.less'; import './index.less';


// 如果值是对象时的类型 // 如果值是对象时的类型
@@ -55,6 +56,8 @@ export interface ParameterInputProps {
disabled?: boolean; disabled?: boolean;
/** 元素 id */ /** 元素 id */
id?: string; id?: string;
/** 带标签的 input,设置后置标签 */
addonAfter?: ReactNode;
} }


function ParameterInput({ function ParameterInput({
@@ -75,7 +78,7 @@ function ParameterInput({
const valueObj = const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) { if (valueObj && !valueObj.showValue) {
valueObj.showValue = valueObj.value;
valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : '';
} }
const isSelect = valueObj?.fromSelect; const isSelect = valueObj?.fromSelect;
const placeholder = valueObj?.placeholder || rest?.placeholder; const placeholder = valueObj?.placeholder || rest?.placeholder;


+ 83
- 39
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -1,29 +1,34 @@
import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config';
import { ServiceData } from '@/pages/ModelDeployment/types'; import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment'; import { getServiceListReq } from '@/services/modelDeployment';
import type { JCCResourceImage, JCCResourceStandard, JCCResourceType } from '@/state/jcdResource';
import { filterResourceStandard, resourceFieldNames } from '@/state/systemResource';
import { type SelectProps } from 'antd'; import { type SelectProps } from 'antd';
import { pick } from 'lodash';

// id 从 number 转换为 string
const convertId = (item: any) => ({
...item,
id: JSON.stringify({
id: `${item.id}`,
name: item.name,
identifier: item.identifier,
owner: item.owner,
}),
});


export type SelectPropsConfig = { export type SelectPropsConfig = {
getOptions: () => Promise<any>; // 获取下拉数据
getOptions?: () => Promise<any>; // 获取下拉数据
fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
filterOption?: SelectProps['filterOption']; // 过滤函数 filterOption?: SelectProps['filterOption']; // 过滤函数
isObjectValue: boolean; // value 是对象
getValue?: (value: any) => string | number; // 对象类型时,获取其值
getLabel?: (value: any) => string; // 对象类型时,获取其 label
}; };


export const paramSelectConfig: Record<string, SelectPropsConfig> = {
export const ParameterSelectTypeList = [
'dataset',
'model',
'service',
'resource',
'remote-image',
'remote-resource-type',
'remote-resource',
] as const;

export type ParameterSelectDataType = (typeof ParameterSelectTypeList)[number];

export const paramSelectConfig: Record<ParameterSelectDataType, SelectPropsConfig> = {
dataset: { dataset: {
getOptions: async () => { getOptions: async () => {
const res = await getDatasetList({ const res = await getDatasetList({
@@ -31,13 +36,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
}, },
fieldNames: {
label: 'name',
value: 'id',
optionFilterProp: 'label',
getValue: (value: DatasetData) => {
return value.id;
},
getLabel: (value: DatasetData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
model: { model: {
getOptions: async () => { getOptions: async () => {
@@ -46,13 +54,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
},
optionFilterProp: 'label',
getValue: (value: ModelData) => {
return value.id;
}, },
fieldNames: {
label: 'name',
value: 'id',
getLabel: (value: ModelData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
service: { service: {
getOptions: async () => { getOptions: async () => {
@@ -60,25 +71,58 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
page: 0, page: 0,
size: 1000, size: 1000,
}); });
return (
res?.data?.content?.map((item: ServiceData) => ({
label: item.service_name,
value: JSON.stringify(pick(item, ['id', 'service_name'])),
})) ?? []
);
},
fieldNames: {
label: 'label',
value: 'value',
return res?.data?.content ?? [];
}, },
optionFilterProp: 'label', optionFilterProp: 'label',
getValue: (value: ServiceData) => {
return value.id;
},
getLabel: (value: ServiceData) => {
return value.service_name;
},
isObjectValue: true,
}, },
resource: { resource: {
getOptions: async () => {
// 不需要这个函数
return [];
},
fieldNames: resourceFieldNames, fieldNames: resourceFieldNames,
filterOption: filterResourceStandard as SelectProps['filterOption'], filterOption: filterResourceStandard as SelectProps['filterOption'],
isObjectValue: false,
},
'remote-resource-type': {
optionFilterProp: 'label',
isObjectValue: false,
getValue: (value: JCCResourceType) => {
return value.value;
},
getLabel: (value: JCCResourceType) => {
return value.label;
},
},
'remote-image': {
optionFilterProp: 'label',
getValue: (value: JCCResourceImage) => {
return value.imageID;
},
getLabel: (value: JCCResourceImage) => {
return value.name;
},
isObjectValue: true,
},
'remote-resource': {
optionFilterProp: 'label',
getValue: (value: JCCResourceStandard) => {
return value.id;
},
getLabel: (value: JCCResourceStandard) => {
const cpu = value.baseResourceSpecs.find((v) => v.type === 'CPU');
const ram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'RAM');
const vram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'VRAM');
const cpuText = cpu ? `CPU:${cpu.availableValue}, ` : '';
const ramText = ram ? `内存: ${ram.availableValue}${ram.availableUnit?.toUpperCase()}` : '';
const vramText = vram
? `(显存${vram.availableValue}${vram.availableUnit?.toUpperCase()})`
: '';
return `${value.type}: ${value.availableCount}*${value.name}${vramText}, ${cpuText}${ramText}`;
},
isObjectValue: true,
}, },
}; };

+ 102
- 41
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -4,19 +4,25 @@
* @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/ */


import { useComputingResource } from '@/hooks/useComputingResource';
import jccResourceState from '@/state/jcdResource';
import systemResourceState, { getSystemResources } from '@/state/systemResource';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { useSnapshot } from '@umijs/max';
import { Select, type SelectProps } from 'antd'; import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import FormInfo from '../FormInfo'; import FormInfo from '../FormInfo';
import { paramSelectConfig } from './config';
import { paramSelectConfig, type ParameterSelectDataType } from './config';

export { ParameterSelectTypeList, type ParameterSelectDataType } from './config';


export type ParameterSelectObject = { export type ParameterSelectObject = {
value: any; value: any;
[key: string]: any; [key: string]: any;
}; };


export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource';
type SelectOptions = SelectProps['options'];

const identityFunc = (value: any) => value;


export interface ParameterSelectProps extends SelectProps { export interface ParameterSelectProps extends SelectProps {
/** 类型 */ /** 类型 */
@@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps {
display?: boolean; display?: boolean;
/** 值,支持对象,对象必须包含 value */ /** 值,支持对象,对象必须包含 value */
value?: string | ParameterSelectObject; value?: string | ParameterSelectObject;
/** 用于流水线, 流水线资源规格要求 id 为字符串 */
isPipeline?: boolean;
/** 修改后回调 */ /** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void; onChange?: (value: string | ParameterSelectObject) => void;
} }
@@ -36,69 +40,126 @@ function ParameterSelect({
dataType, dataType,
display = false, display = false,
value, value,
isPipeline = false,
onChange, onChange,
...rest ...rest
}: ParameterSelectProps) { }: ParameterSelectProps) {
const [options, setOptions] = useState<SelectProps['options']>([]);
const [options, setOptions] = useState<SelectOptions>([]);
const propsConfig = paramSelectConfig[dataType]; const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;
const [resourceStandardList] = useComputingResource();
const computingResource = isPipeline
? resourceStandardList.map((v) => ({
...v,
id: String(v.id),
}))
: resourceStandardList;
const {
getLabel = identityFunc,
getValue = identityFunc,
getOptions,
filterOption,
fieldNames,
optionFilterProp,
isObjectValue,
} = propsConfig;
const selectValue = typeof value === 'object' && value !== null ? value.value : value;
// 数据集、模型、服务,对象转换成 json 字符串
const valueText =
typeof selectValue === 'object' && selectValue !== null ? getValue(selectValue) : selectValue;
const jccResourceSnap = useSnapshot(jccResourceState);
const { getResourceTypes } = jccResourceSnap;
const systemResourceSnap = useSnapshot(systemResourceState);

const objectOptions = useMemo(() => {
return dataType === 'remote-resource-type'
? jccResourceSnap.types
: dataType === 'remote-image'
? jccResourceSnap.images
: dataType === 'remote-resource'
? jccResourceSnap.resources
: options;
}, [dataType, options, jccResourceSnap.types, jccResourceSnap.images, jccResourceSnap.resources]);

// 将对象类型转换为 Select Options
const converObjectToOptions = useCallback(
(v: any) => {
return {
label: getLabel(v),
value: getValue(v),
};
},
[getLabel, getValue],
);

// 数据集、模型、服务获取数据后,进行转换
const objectSelectOptions = useMemo(() => {
return objectOptions?.map(converObjectToOptions);
}, [converObjectToOptions, objectOptions]);

// 快速得到选中的对象
const valueMap = useMemo(() => {
const map = new Map<string | number, any>();
objectOptions?.forEach((v) => {
map.set(getValue(v), v);
});

return map;
}, [objectOptions, getValue]);


useEffect(() => { useEffect(() => {
// 获取下拉数据 // 获取下拉数据
const getSelectOptions = async () => { const getSelectOptions = async () => {
if (!propsConfig) {
return;
}
const getOptions = propsConfig.getOptions;
const [res] = await to(getOptions());
if (res) {
setOptions(res);
if (getOptions) {
const [res] = await to(getOptions());
if (res) {
setOptions(res);
}
} else if (dataType === 'remote-resource-type') {
getResourceTypes();
} else if (dataType === 'resource') {
getSystemResources();
} }
}; };

getSelectOptions(); getSelectOptions();
}, [propsConfig]);
}, [getOptions, dataType, getResourceTypes]);


const selectOptions = dataType === 'resource' ? computingResource : options;
const selectOptions = (
dataType === 'resource' ? systemResourceSnap.resources : objectSelectOptions
) as SelectOptions;


const handleChange = (text: string) => { const handleChange = (text: string) => {
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: text,
});
// 数据集、模型、服务,转换成对象
if (isObjectValue) {
// 设置为 null 是因为 antv g6 的 bug
// 如果值为 undefined 时, graph.changeData(data) 会保留前面的值
const selectValue = text ? valueMap.get(text) : null;
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
} else { } else {
onChange?.(text);
const selectValue = text ? text : '';
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
} }
}; };


// 只用于展示,FormInfo 组件带有 Tooltip // 只用于展示,FormInfo 组件带有 Tooltip
if (display) { if (display) {
return ( return (
<FormInfo
select
value={valueText}
options={selectOptions}
fieldNames={propsConfig?.fieldNames}
></FormInfo>
<FormInfo select value={valueText} options={selectOptions} fieldNames={fieldNames}></FormInfo>
); );
} }


return ( return (
<Select <Select
{...rest} {...rest}
filterOption={propsConfig?.filterOption}
options={selectOptions} options={selectOptions}
fieldNames={propsConfig?.fieldNames}
optionFilterProp={propsConfig?.optionFilterProp}
fieldNames={fieldNames}
optionFilterProp={optionFilterProp}
filterOption={filterOption}
value={valueText} value={valueText}
onChange={handleChange} onChange={handleChange}
showSearch showSearch


+ 29
- 44
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -10,10 +10,10 @@ import ResourceSelectorModal, {
ResourceSelectorType, ResourceSelectorType,
selectorTypeConfig, selectorTypeConfig,
} from '@/components/ResourceSelectorModal'; } from '@/components/ResourceSelectorModal';
import { CommonTabKeys } from '@/enums';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button, ConfigProvider } from 'antd'; import { Button, ConfigProvider } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { pick } from 'lodash';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less'; import './index.less';


@@ -27,6 +27,8 @@ export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse
interface ResourceSelectProps extends ParameterInputProps { interface ResourceSelectProps extends ParameterInputProps {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
type: ResourceSelectorType; type: ResourceSelectorType;
/** 值 */
value?: ResourceSelectorResponse;
} }


// 获取选择数据集、模型、镜像后面按钮 icon // 获取选择数据集、模型、镜像后面按钮 icon
@@ -47,68 +49,51 @@ function ResourceSelect({
}: ResourceSelectProps) { }: ResourceSelectProps) {
const { componentSize } = ConfigProvider.useConfig(); const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize; const mySize = size || componentSize;
let selectedResource: ResourceSelectorResponse | undefined = undefined;
if (
value &&
typeof value === 'object' &&
value.activeTab &&
value.id &&
value.name &&
value.version &&
value.path &&
(type === ResourceSelectorType.Mirror || (value.identifier && value.owner))
) {
selectedResource = pick(value, [
'activeTab',
'id',
'identifier',
'name',
'owner',
'version',
'path',
]) as ResourceSelectorResponse;
let defaultActiveTab: CommonTabKeys | undefined,
defaultExpandedKeys: string[] = [],
defaultCheckedKeys: string[] = [];
if (value && typeof value === 'object') {
defaultActiveTab = value.activeTab;
if (type === ResourceSelectorType.Mirror) {
if (value.image_id && value.id) {
defaultExpandedKeys = [`${value.image_id}`];
defaultCheckedKeys = [`${value.image_id}-${value.id}`];
}
} else if (value.id && value.version) {
defaultExpandedKeys = [value.id];
defaultCheckedKeys = [`${value.id}-${value.version}`];
}
} }


// 选择数据集、模型、镜像 // 选择数据集、模型、镜像
const selectResource = () => { const selectResource = () => {
const { close } = openAntdModal(ResourceSelectorModal, { const { close } = openAntdModal(ResourceSelectorModal, {
type, type,
defaultExpandedKeys: selectedResource ? [selectedResource.id] : [],
defaultCheckedKeys: selectedResource
? [`${selectedResource.id}-${selectedResource.version}`]
: [],
defaultActiveTab: selectedResource?.activeTab,
defaultExpandedKeys: defaultExpandedKeys,
defaultCheckedKeys: defaultCheckedKeys,
defaultActiveTab: defaultActiveTab,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
const { activeTab, ...rest } = res;
const { url } = rest;
onChange?.({ onChange?.({
value: path,
showValue: path,
...rest,
value: url,
showValue: url,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
id,
name,
version,
path,
}); });
} else { } else {
const jsonObj = {
id,
name,
version,
path,
identifier,
owner,
};
const jsonObjStr = JSON.stringify(jsonObj);
const { activeTab, ...rest } = res;
const { name, version } = rest;
const showValue = `${name}:${version}`; const showValue = `${name}:${version}`;
onChange?.({ onChange?.({
value: jsonObjStr,
...rest,
value: showValue,
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
...jsonObj,
}); });
} }
} else { } else {


+ 4
- 5
react-ui/src/components/ResourceSelectorModal/config.tsx View File

@@ -52,9 +52,9 @@ const convertResourceVersionToTreeData = (
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: ResourceVersionData) => ({ return list.map((item: ResourceVersionData) => ({
...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']), ...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']),
version: item.name,
title: item.name,
key: `${parentId}-${item.name}`, key: `${parentId}-${item.name}`,
title: item.name,
version: item.name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
})); }));
@@ -66,12 +66,11 @@ const convertMirrorVersionToTreeData = (
list: MirrorVersionData[], list: MirrorVersionData[],
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: MirrorVersionData) => ({ return list.map((item: MirrorVersionData) => ({
url: item.url,
title: item.tag_name,
...item,
key: `${parentId}-${item.id}`, key: `${parentId}-${item.id}`,
title: item.tag_name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
description: item.description,
})); }));
}; };




+ 24
- 16
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -8,6 +8,7 @@ import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CommonTabKeys } from '@/enums'; import { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config'; import { ResourceFileData } from '@/pages/Dataset/config';
import { type MirrorVersionData } from '@/pages/Mirror/Info';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd'; import { Input, Tabs, Tree } from 'antd';
@@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig };
// 选择数据集、模型、镜像的返回类型 // 选择数据集、模型、镜像的返回类型
export type ResourceSelectorResponse = { export type ResourceSelectorResponse = {
activeTab: CommonTabKeys; // 是我的还是公开的 activeTab: CommonTabKeys; // 是我的还是公开的
id: string; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
version: string; // 数据集\模型\镜像版本
path: string; // 数据集\模型\镜像版本路径
identifier: string; // 数据集\模型 identifier,镜像这个字段为空
owner: string; // 数据集\模型 owner,镜像这个字段为空
};
id: string; // 数据集\模型 id
name: string; // 数据集\模型 name
identifier: string; // 数据集\模型 identifier
owner: string; // 数据集\模型 owner
version: string; // 数据集\模型 version
path: string; // 数据集\模型 版本路径
} & MirrorVersionData;


export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
@@ -241,15 +242,22 @@ function ResourceSelectorModal({
const name = (treeNode?.title ?? '') as string; const name = (treeNode?.title ?? '') as string;
const identifier = (treeNode?.identifier ?? '') as string; const identifier = (treeNode?.identifier ?? '') as string;
const owner = (treeNode?.owner ?? '') as string; const owner = (treeNode?.owner ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
identifier,
owner,
activeTab: activeTab,
};
const childNode = treeNode.children.filter((v: TreeDataNode) => v.key === last)[0];
const res =
type === ResourceSelectorType.Mirror
? {
activeTab: activeTab,
...childNode,
}
: {
activeTab: activeTab,
id: Number(id),
name,
path: versionPath,
version,
identifier,
owner,
};
onOk?.(res); onOk?.(res);
} else { } else {
onOk?.(undefined); onOk?.(undefined);


+ 8
- 0
react-ui/src/enums/index.ts View File

@@ -163,3 +163,11 @@ export enum AutoMLTrailStatus {
CANCELLED = 'CANCELLED', // 取消 CANCELLED = 'CANCELLED', // 取消
MEMOUT = 'MEMOUT', // 内存溢出 MEMOUT = 'MEMOUT', // 内存溢出
} }

// 流水线组件类型
export enum ComponentType {
Ref = 'ref',
Select = 'select',
Map = 'map',
Str = 'str',
}

+ 71
- 47
react-ui/src/hooks/useComputingResource.ts View File

@@ -4,66 +4,90 @@
* @Description: 资源规格 hook * @Description: 资源规格 hook
*/ */


import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { to } from '@/utils/promise';
import { type SelectProps } from 'antd';
import { useCallback, useEffect, useState } from 'react';
// import { getComputingResourceReq } from '@/services/pipeline';
// import { ComputingResource } from '@/types';
// import { to } from '@/utils/promise';
// import { type SelectProps } from 'antd';
// import { useCallback, useEffect, useState } from 'react';


const computingResource: ComputingResource[] = [];
// const computingResource: ComputingResource[] = [];


/** 过滤资源规格 */
export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
input: string,
option?: ComputingResource,
) => {
return (
option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false
);
};
// /** 过滤资源规格 */
// export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
// input: string,
// option?: ComputingResource,
// ) => {
// return (
// option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false
// );
// };


/** 资源规格字段 */
export const resourceFieldNames = {
label: 'description',
value: 'id',
};
// /** 资源规格字段 */
// export const resourceFieldNames = {
// label: 'description',
// value: 'id',
// };


/** 获取资源规格 */
export function useComputingResource() {
const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);
// /** 获取资源规格 */
// export function useComputingResource() {
// const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);


useEffect(() => {
// 获取资源规格列表数据
const getComputingResource = async () => {
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && Array.isArray(res.data.content)) {
setResourceStandardList(res.data.content);
computingResource.splice(0, computingResource.length, ...res.data.content);
}
};
// useEffect(() => {
// // 获取资源规格列表数据
// const getComputingResource = async () => {
// const params = {
// page: 0,
// size: 1000,
// resource_type: '',
// };
// const [res] = await to(getComputingResourceReq(params));
// if (res && res.data && Array.isArray(res.data.content)) {
// setResourceStandardList(res.data.content);
// computingResource.splice(0, computingResource.length, ...res.data.content);
// }
// };

// if (computingResource.length > 0) {
// setResourceStandardList(computingResource);
// } else {
// getComputingResource();
// }
// }, []);


if (computingResource.length > 0) {
setResourceStandardList(computingResource);
} else {
getComputingResource();
}
// // 根据 standard 获取 description
// const getDescription = useCallback(
// (id?: string | number) => {
// if (!id) {
// return undefined;
// }
// return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description;
// },
// [resourceStandardList],
// );

// return [resourceStandardList, getDescription] as const;
// }

import state, { getSystemResources } from '@/state/systemResource';
import { useSnapshot } from '@umijs/max';
import { useCallback, useEffect } from 'react';

export const useSystemResource = () => {
useEffect(() => {
getSystemResources();
}, []); }, []);


// 根据 standard 获取 description
const snap = useSnapshot(state);
/* 根据 standard 获取 description */
const getDescription = useCallback( const getDescription = useCallback(
(id?: string | number) => { (id?: string | number) => {
if (!id) { if (!id) {
return undefined; return undefined;
} }
return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description;
return snap.resources.find((item) => Number(item.id) === Number(id))?.description;
}, },
[resourceStandardList],
[snap.resources],
); );


return [resourceStandardList, getDescription] as const;
}
return getDescription;
};

+ 2
- 2
react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx View File

@@ -1,6 +1,6 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { import {
classifierAlgorithms, classifierAlgorithms,
FrameworkType, FrameworkType,
@@ -39,7 +39,7 @@ function BasicInfo({
instanceStatus, instanceStatus,
isInstance = false, isInstance = false,
}: BasicInfoProps) { }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();
const basicDatas: BasicInfoData[] = useMemo(() => { const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) { if (!info) {
return []; return [];


+ 2
- 2
react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx View File

@@ -6,7 +6,7 @@ import {
autoMLEnsembleClassOptions, autoMLEnsembleClassOptions,
autoMLTaskTypeOptions, autoMLTaskTypeOptions,
} from '@/enums'; } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { import {
classificationAlgorithms, classificationAlgorithms,
featureAlgorithms, featureAlgorithms,
@@ -76,7 +76,7 @@ function AutoMLBasic({
instanceStatus, instanceStatus,
isInstance = false, isInstance = false,
}: AutoMLBasicProps) { }: AutoMLBasicProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();
const basicDatas: BasicInfoData[] = useMemo(() => { const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) { if (!info) {
return []; return [];


+ 24
- 17
react-ui/src/pages/AutoML/components/CreateForm/utils.ts View File

@@ -42,37 +42,44 @@ export const regressorAlgorithms = [


// 特征预处理算法 // 特征预处理算法
export const featureAlgorithms = [ export const featureAlgorithms = [
{ label: 'densifier (数据增稠)', value: 'densifier' },
{ label: 'densifier (特征变换-数据增稠)', value: 'densifier' },
{ {
label: 'extra_trees_preproc_for_classification (分类任务极端随机树)',
label: 'extra_trees_preproc_for_classification (特征选择-分类任务极端随机树)',
value: 'extra_trees_preproc_for_classification', value: 'extra_trees_preproc_for_classification',
}, },
{ {
label: 'extra_trees_preproc_for_regression (回归任务极端随机树)',
label: 'extra_trees_preproc_for_regression (特征选择-回归任务极端随机树)',
value: 'extra_trees_preproc_for_regression', value: 'extra_trees_preproc_for_regression',
}, },
{ label: 'fast_ica (快速独立成分分析)', value: 'fast_ica' },
{ label: 'feature_agglomeration (特征聚合)', value: 'feature_agglomeration' },
{ label: 'kernel_pca (核主成分分析)', value: 'kernel_pca' },
{ label: 'kitchen_sinks (随机特征映射)', value: 'kitchen_sinks' },
{ label: 'liblinear_svc_preprocessor (线性svc预处理器)', value: 'liblinear_svc_preprocessor' },
{ label: 'fast_ica (特征选择-快速独立成分分析)', value: 'fast_ica' },
{ label: 'feature_agglomeration (特征变换-特征聚合)', value: 'feature_agglomeration' },
{ label: 'kernel_pca (特征选择-核主成分分析)', value: 'kernel_pca' },
{ label: 'kitchen_sinks (特征变换-随机特征映射)', value: 'kitchen_sinks' },
{
label: 'liblinear_svc_preprocessor (特征选择-线性svc预处理器)',
value: 'liblinear_svc_preprocessor',
},
{ label: 'miss_value_impute (缺失值填充)', value: 'miss_value_impute' },
{ label: 'no_preprocessing (无预处理)', value: 'no_preprocessing' }, { label: 'no_preprocessing (无预处理)', value: 'no_preprocessing' },
{ label: 'nystroem_sampler (尼斯特罗姆采样器)', value: 'nystroem_sampler' },
{ label: 'pca (主成分分析)', value: 'pca' },
{ label: 'polynomial (多项式特征扩展)', value: 'polynomial' },
{ label: 'random_trees_embedding (随机森林特征嵌入)', value: 'random_trees_embedding' },
{ label: 'nystroem_sampler (特征变换-尼斯特罗姆采样器)', value: 'nystroem_sampler' },
{ label: 'pca (特征选择-主成分分析)', value: 'pca' },
{ label: 'polynomial (特征变换-多项式特征扩展)', value: 'polynomial' },
{ label: 'random_trees_embedding (特征变换-随机森林特征嵌入)', value: 'random_trees_embedding' },
{ {
label: 'select_percentile_classification (基于百分位的分类特征选择)',
label: 'select_percentile_classification 特征选择-基于百分位的分类特征选择)',
value: 'select_percentile_classification', value: 'select_percentile_classification',
}, },
{ {
label: 'select_percentile_regression (基于百分位的回归特征选择)',
label: 'select_percentile_regression (特征选择-基于百分位的回归特征选择)',
value: 'select_percentile_regression', value: 'select_percentile_regression',
}, },
{ {
label: 'select_rates_classification (基于比率的分类特征选择)',
label: 'select_rates_classification (特征选择-基于比率的分类特征选择)',
value: 'select_rates_classification', value: 'select_rates_classification',
}, },
{ label: 'select_rates_regression (基于比率的回归特征选择)', value: 'select_rates_regression' },
{ label: 'truncatedSVD (截断奇异值分解)', value: 'truncatedSVD' },
{
label: 'select_rates_regression (特征选择-基于比率的回归特征选择)',
value: 'select_rates_regression',
},
{ label: 'truncatedSVD (特征变换-截断奇异值分解)', value: 'truncatedSVD' },
]; ];

+ 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 handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data; const { type, payload } = e.data;
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload;
const { experimentId, experimentInsId, status /*finishTime*/ } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId); 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 ( if (
!currentIns || !currentIns ||


+ 7
- 121
react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx View File

@@ -1,30 +1,8 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addDataset } from '@/services/dataset/index.js'; import { addDataset } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import {
getFileListFromEvent,
limitUploadFileType,
removeUploadedFile,
validateUploadFiles,
} from '@/utils/ui';
import {
Button,
Form,
Input,
Radio,
Select,
Upload,
UploadFile,
message,
type ModalProps,
type UploadProps,
} from 'antd';
import { omit } from 'lodash';
import { useState } from 'react';
import styles from './index.less';
import { Form, Input, Radio, Select, message, type ModalProps } from 'antd';


interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
typeList: CategoryData[]; typeList: CategoryData[];
@@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
} }


function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) {
const [uuid] = useState(Date.now());

// 上传组件参数
const uploadProps: UploadProps = {
action: resourceConfig[ResourceType.Dataset].uploadAction,
headers: {
Authorization: getAccessToken() || '',
},
defaultFileList: [],
accept: '.zip,.tgz',
beforeUpload: limitUploadFileType('zip,tgz'),
onRemove: removeUploadedFile,
};

// 上传请求 // 上传请求
const createDataset = async (params: any) => { const createDataset = async (params: any) => {
const [res] = await to(addDataset(params)); const [res] = await to(addDataset(params));
@@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr


// 提交 // 提交
const onFinish = (formData: any) => { const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const params = {
...omit(formData, ['fileList']),
dataset_source: DataSource.Create,
dataset_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {};
return {
file_name: data.fileName,
file_size: data.fileSize,
url: data.url,
};
}),
};
createDataset(params);
}
const params = {
...formData,
dataset_source: DataSource.Create,
};
createDataset(params);
}; };


return ( return (
@@ -108,32 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
> >
<Input placeholder="请输入数据名称" showCount allowClear maxLength={40} /> <Input placeholder="请输入数据名称" showCount allowClear maxLength={40} />
</Form.Item> </Form.Item>
<Form.Item
label="数据集版本"
name="version"
rules={[
{
required: true,
message: '请输入数据集版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '数据集版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`数据集版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`数据集版本不能为 origin`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item label="数据集分类" name="data_type"> <Form.Item label="数据集分类" name="data_type">
<Select <Select
allowClear allowClear
@@ -172,24 +99,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
allowClear allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item <Form.Item
label="可见性" label="可见性"
name="is_public" name="is_public"
@@ -200,29 +109,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
<Radio value={true}>公开</Radio> <Radio value={true}>公开</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item
label="数据集文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传数据集文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }}>
<Button
className={styles['upload-button']}
type="default"
icon={<KFIcon type="icon-shangchuan" />}
>
上传文件
</Button>
<div className={styles['upload-tip']}>只允许上传 .zip 和 .tgz 格式文件</div>
</Upload>
</Form.Item>
</Form> </Form>
</KFModal> </KFModal>
); );


+ 7
- 113
react-ui/src/pages/Dataset/components/AddModelModal/index.tsx View File

@@ -1,25 +1,8 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js'; import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui';
import {
Button,
Form,
Input,
Radio,
Select,
Upload,
UploadFile,
message,
type ModalProps,
type UploadProps,
} from 'antd';
import { omit } from 'lodash';
import { useState } from 'react';
import styles from '../AddDatasetModal/index.less';
import { Form, Input, Radio, Select, message, type ModalProps } from 'antd';


interface AddModelModalProps extends Omit<ModalProps, 'onOk'> { interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {
typeList: CategoryData[]; typeList: CategoryData[];
@@ -28,18 +11,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {
} }


function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) { function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) {
const [uuid] = useState(Date.now());

// 上传组件参数
const uploadProps: UploadProps = {
action: resourceConfig[ResourceType.Model].uploadAction,
headers: {
Authorization: getAccessToken() || '',
},
defaultFileList: [],
onRemove: removeUploadedFile,
};

// 上传请求 // 上传请求
const createModel = async (params: any) => { const createModel = async (params: any) => {
const [res] = await to(addModel(params)); const [res] = await to(addModel(params));
@@ -51,22 +22,11 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)


// 提交 // 提交
const onFinish = (formData: any) => { const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const params = {
...omit(formData, ['fileList']),
model_source: DataSource.Create,
model_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {};
return {
file_name: data.fileName,
file_size: data.fileSize,
url: data.url,
};
}),
};
createModel(params);
}
const params = {
...formData,
model_source: DataSource.Create,
};
createModel(params);
}; };


return ( return (
@@ -99,32 +59,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
> >
<Input placeholder="请输入模型名称" showCount allowClear maxLength={40} /> <Input placeholder="请输入模型名称" showCount allowClear maxLength={40} />
</Form.Item> </Form.Item>
<Form.Item
label="模型版本"
name="version"
rules={[
{
required: true,
message: '请输入模型版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '模型版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`模型版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`模型版本不能为 origin`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入模型版本" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item label="模型框架" name="model_type"> <Form.Item label="模型框架" name="model_type">
<Select <Select
allowClear allowClear
@@ -163,24 +97,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
allowClear allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item <Form.Item
label="可见性" label="可见性"
name="is_public" name="is_public"
@@ -191,28 +107,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Radio value={true}>公开</Radio> <Radio value={true}>公开</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item
label="模型文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传模型文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }}>
<Button
className={styles['upload-button']}
type="default"
icon={<KFIcon type="icon-shangchuan" />}
>
上传文件
</Button>
</Upload>
</Form.Item>
</Form> </Form>
</KFModal> </KFModal>
); );


+ 20
- 2
react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx View File

@@ -15,7 +15,7 @@ import {
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import styles from '../AddDatasetModal/index.less'; import styles from '../AddDatasetModal/index.less';


interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
@@ -40,6 +40,23 @@ function AddVersionModal({
}: AddVersionModalProps) { }: AddVersionModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];
const [form] = Form.useForm();

useEffect(() => {
const getNextVersion = async () => {
const request = config.getNextVersion;
const params = {
identifier,
owner,
};
const [res] = await to(request(params));
if (res && res.data) {
const nextVersion = res.data;
form.setFieldValue('version', nextVersion);
}
};
getNextVersion();
}, [identifier, owner, config, form]);


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
@@ -109,6 +126,7 @@ function AddVersionModal({
}} }}
onFinish={onFinish} onFinish={onFinish}
autoComplete="off" autoComplete="off"
form={form}
> >
<Form.Item <Form.Item
label={`${name}名称`} label={`${name}名称`}
@@ -146,7 +164,7 @@ function AddVersionModal({
}, },
]} ]}
> >
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear />
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"


+ 121
- 0
react-ui/src/pages/Dataset/components/EditVersionModal/index.tsx View File

@@ -0,0 +1,121 @@
import KFModal from '@/components/KFModal';
import { DataSource, ResourceData, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { to } from '@/utils/promise';
import { Form, Input, message, type ModalProps } from 'antd';

interface EditVersionModalProps extends Omit<ModalProps, 'onOk'> {
resourceType: ResourceType;
resourceVersion: ResourceData;
onOk: () => void;
}

function EditVersionModal({ resourceType, resourceVersion, onOk, ...rest }: EditVersionModalProps) {
const config = resourceConfig[resourceType];
const { name: resoureName, version, version_desc } = resourceVersion;

// 修改请求
const editDatasetVersion = async (params: any) => {
const request = config.editVersion;
const [res] = await to(request(params));
if (res) {
message.success('编辑成功');
onOk?.();
}
};

// 提交
const onFinish = (formData: any) => {
const params = {
...resourceVersion,
...formData,
[config.sourceParamKey]: DataSource.Create,
};
editDatasetVersion(params);
};

const name = config.name;

return (
<KFModal
{...rest}
title="编辑版本"
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
>
<Form
name="form"
layout="vertical"
initialValues={{
name: resoureName,
version: version,
version_desc: version_desc,
}}
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label={`${name}名称`}
name="name"
rules={[
{
required: true,
message: `请输入${name}名称`,
},
]}
>
<Input disabled placeholder={`请输入${name}名称`} />
</Form.Item>
<Form.Item
label={`${name}版本`}
name="version"
rules={[
{
required: true,
message: `请输入${name}版本`,
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`${name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${name}版本不能为 origin`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled />
</Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
</Form>
</KFModal>
);
}

export default EditVersionModal;

+ 7
- 0
react-ui/src/pages/Dataset/components/ResourceInfo/index.less View File

@@ -28,8 +28,15 @@
border-radius: 4px; border-radius: 4px;
} }


&__desc {
margin-bottom: 0 !important;
color: @text-color;
font-size: @font-size;
}

&__praise { &__praise {
display: flex; display: flex;
flex: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 70px; width: 70px;


+ 98
- 54
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -4,6 +4,7 @@
* @Description: 数据集、模型详情 * @Description: 数据集、模型详情
*/ */


import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { import {
ResourceData, ResourceData,
@@ -19,10 +20,11 @@ import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max'; import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Flex, Select, Tabs } from 'antd';
import { App, Button, Flex, Select, Tabs, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import AddVersionModal from '../AddVersionModal'; import AddVersionModal from '../AddVersionModal';
import EditVersionModal from '../EditVersionModal';
import ResourceIntro from '../ResourceIntro'; import ResourceIntro from '../ResourceIntro';
import ResourceVersion from '../ResourceVersion'; import ResourceVersion from '../ResourceVersion';
import VersionCompareModal from '../VersionCompareModal'; import VersionCompareModal from '../VersionCompareModal';
@@ -61,21 +63,24 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
const { message } = App.useApp(); const { message } = App.useApp();


// 获取详情 // 获取详情
const getResourceDetail = useCallback(async () => {
const params = {
id: resourceId,
owner,
name,
identifier,
version,
is_public,
};
const request = config.getInfo;
const [res] = await to(request(params));
if (res && res.data) {
setInfo(res.data);
}
}, [config, resourceId, owner, name, identifier, version, is_public]);
const getResourceDetail = useCallback(
async (version: string | undefined) => {
const params = {
id: resourceId,
owner,
name,
identifier,
version,
is_public,
};
const request = config.getInfo;
const [res] = await to(request(params));
if (res && res.data) {
setInfo(res.data);
}
},
[config, resourceId, owner, name, identifier, is_public],
);


// 获取版本列表 // 获取版本列表
const getVersionList = useCallback( const getVersionList = useCallback(
@@ -100,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
} }
} else { } else {
setVersion(undefined); setVersion(undefined);
getResourceDetail(undefined);
} }
}, },
[config, owner, identifier, versionParam],
[config, owner, identifier, versionParam, getResourceDetail],
); );


useEffect(() => { useEffect(() => {
if (version) { if (version) {
getResourceDetail();
getResourceDetail(version);
} }
}, [version, getResourceDetail]); }, [version, getResourceDetail]);


@@ -116,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}, [getVersionList]); }, [getVersionList]);


// 新建版本 // 新建版本
const showModal = () => {
const showAddVersionModal = () => {
const { close } = openAntdModal(AddVersionModal, { const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType, resourceType: resourceType,
resourceId: resourceId, resourceId: resourceId,
@@ -132,6 +138,18 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}); });
}; };


// 版本编辑
const showEditVersionModal = () => {
const { close } = openAntdModal(EditVersionModal, {
resourceType: resourceType,
resourceVersion: info,
onOk: () => {
getResourceDetail();
close();
},
});
};

// 选择版本 // 选择版本
const showVersionSelector = () => { const showVersionSelector = () => {
const { close } = openAntdModal(VersionSelectorModal, { const { close } = openAntdModal(VersionSelectorModal, {
@@ -278,44 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
<span>{info.praises_count}</span> <span>{info.praises_count}</span>
</div> </div>
</Flex> </Flex>
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
icon={<KFIcon type="icon-banbenduibi" />}
onClick={showVersionSelector}
>
版本对比
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
onClick={handleDelete}
icon={<KFIcon type="icon-shanchu" />}
disabled={!version}
danger
{version ? (
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button
type="default"
onClick={showAddVersionModal}
icon={<KFIcon type="icon-xinjian2" />}
>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
icon={<KFIcon type="icon-banbenduibi" />}
onClick={showVersionSelector}
>
版本对比
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
onClick={handleDelete}
icon={<KFIcon type="icon-shanchu" />}
danger
>
删除版本
</Button>
</Flex>
) : (
<Typography.Paragraph
className={styles['resource-info__top__desc']}
ellipsis={{ tooltip: info.description }}
> >
删除版本
</Button>
</Flex>
{info.description ?? '暂无描述'}
</Typography.Paragraph>
)}
</div> </div>
<div className={styles['resource-info__bottom']}> <div className={styles['resource-info__bottom']}>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['resource-info__bottom__legend']}>
{activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />}
</div>
{version ? (
<>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['resource-info__bottom__legend']}>
{activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />}
</div>
</>
) : (
<KFEmpty
style={{ height: '100%' }}
type={EmptyType.NoData}
title="暂无版本"
content={`请创建${config.name}版本`}
hasFooter={true}
buttonTitle="创建版本"
onButtonClick={showAddVersionModal}
/>
)}
</div> </div>
</div> </div>
); );


+ 10
- 0
react-ui/src/pages/Dataset/config.tsx View File

@@ -9,11 +9,15 @@ import {
deleteDatasetVersion, deleteDatasetVersion,
deleteModel, deleteModel,
deleteModelVersion, deleteModelVersion,
editDatasetVersion,
editModelVersion,
getDatasetInfo, getDatasetInfo,
getDatasetList, getDatasetList,
getDatasetNextVersionReq,
getDatasetVersionList, getDatasetVersionList,
getModelInfo, getModelInfo,
getModelList, getModelList,
getModelNextVersionReq,
getModelVersionList, getModelVersionList,
} from '@/services/dataset/index.js'; } from '@/services/dataset/index.js';
import { limitUploadFileType } from '@/utils/ui'; import { limitUploadFileType } from '@/utils/ui';
@@ -36,9 +40,11 @@ type ResourceTypeInfo = {
getVersions: (params: any) => Promise<any>; // 获取版本列表 getVersions: (params: any) => Promise<any>; // 获取版本列表
deleteRecord: (params: any) => Promise<any>; // 删除 deleteRecord: (params: any) => Promise<any>; // 删除
addVersion: (params: any) => Promise<any>; // 新增版本 addVersion: (params: any) => Promise<any>; // 新增版本
editVersion: (params: any) => Promise<any>; // 编辑版本
deleteVersion: (params: any) => Promise<any>; // 删除版本 deleteVersion: (params: any) => Promise<any>; // 删除版本
getInfo: (params: any) => Promise<any>; // 获取详情 getInfo: (params: any) => Promise<any>; // 获取详情
compareVersion: (params: any) => Promise<any>; // 版本对比 compareVersion: (params: any) => Promise<any>; // 版本对比
getNextVersion: (params: any) => Promise<any>; // 获取下一个版本
name: string; // 名称 name: string; // 名称
typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用
tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用
@@ -65,9 +71,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getDatasetVersionList, getVersions: getDatasetVersionList,
deleteRecord: deleteDataset, deleteRecord: deleteDataset,
addVersion: addDatasetVersion, addVersion: addDatasetVersion,
editVersion: editDatasetVersion,
deleteVersion: deleteDatasetVersion, deleteVersion: deleteDatasetVersion,
getInfo: getDatasetInfo, getInfo: getDatasetInfo,
compareVersion: compareDatasetVersion, compareVersion: compareDatasetVersion,
getNextVersion: getDatasetNextVersionReq,
name: '数据集', name: '数据集',
typeParamKey: 'data_type', typeParamKey: 'data_type',
tagParamKey: 'data_tag', tagParamKey: 'data_tag',
@@ -103,9 +111,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getModelVersionList, getVersions: getModelVersionList,
deleteRecord: deleteModel, deleteRecord: deleteModel,
addVersion: addModelVersion, addVersion: addModelVersion,
editVersion: editModelVersion,
deleteVersion: deleteModelVersion, deleteVersion: deleteModelVersion,
getInfo: getModelInfo, getInfo: getModelInfo,
compareVersion: compareModelVersion, compareVersion: compareModelVersion,
getNextVersion: getModelNextVersionReq,
name: '模型', name: '模型',
typeParamKey: 'model_type', typeParamKey: 'model_type',
tagParamKey: 'model_tag', tagParamKey: 'model_tag',


+ 6
- 10
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -4,10 +4,11 @@
* @Description: 开发环境列表 * @Description: 开发环境列表
*/ */


import { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { DevEditorStatus } from '@/enums'; import { DevEditorStatus } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config'; import { DatasetData, ModelData } from '@/pages/Dataset/config';
import { import {
deleteEditorReq, deleteEditorReq,
@@ -17,12 +18,7 @@ import {
} from '@/services/developmentEnvironment'; } from '@/services/developmentEnvironment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { parseJsonText } from '@/utils'; import { parseJsonText } from '@/utils';
import {
formatCodeConfig,
formatDataset,
formatModel,
type SelectedCodeConfig,
} from '@/utils/format';
import { formatCodeConfig, formatDataset, formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -55,7 +51,7 @@ export type EditorData = {
dataset?: string | DatasetData; dataset?: string | DatasetData;
model?: string | ModelData; model?: string | ModelData;
image?: string; image?: string;
code_config?: string | SelectedCodeConfig;
code_config?: string | CodeConfigData;
}; };


function EditorList() { function EditorList() {
@@ -70,7 +66,7 @@ function EditorList() {
pageSize: 10, pageSize: 10,
}, },
); );
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


// 获取编辑器列表 // 获取编辑器列表
const getEditorList = useCallback(async () => { const getEditorList = useCallback(async () => {
@@ -211,7 +207,7 @@ function EditorList() {
const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => { const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();


const codeConfig = record.code_config as SelectedCodeConfig;
const codeConfig = record.code_config as CodeConfigData;
const url = formatCodeConfig(codeConfig)?.url; const url = formatCodeConfig(codeConfig)?.url;
if (url) { if (url) {
window.open(url, '_blank'); window.open(url, '_blank');


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

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


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


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


+ 6
- 0
react-ui/src/pages/Experiment/components/AddExperimentModal/index.less View File

@@ -6,4 +6,10 @@
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 6px; border-radius: 6px;
} }

:global {
.ant-form-item-row {
align-items: center;
}
}
} }

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


// 根据参数设置 label // 根据参数设置 label
export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { 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]})`; const label = param.param_name + `(${paramTypes[param.param_type]})`;
return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; 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 RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums'; import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs, Typography } from 'antd'; import { Drawer, Tabs, Typography } from 'antd';
@@ -25,6 +25,7 @@ type ExperimentDrawerProps = {
instanceNodeStatus?: ExperimentStatus; // 实例节点状态 instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间 instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
globalParams?: PipelineGlobalParam[] | null; // 全局参数
}; };


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




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

@@ -15,4 +15,24 @@
font-size: @font-size; font-size: @font-size;
background: #f8fbff; 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 FormInfo from '@/components/FormInfo';
import ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; 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'; import styles from './index.less';


type ExperimentParameterProps = { type ExperimentParameterProps = {
nodeData: PipelineNodeModelSerialize; 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]) => ({ const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
@@ -80,96 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
> >
<FormInfo /> <FormInfo />
</Form.Item> </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']}> <div className={styles['experiment-parameter__title']}>
<SubAreaTitle <SubAreaTitle
image={require('@/assets/img/duty-message.png')} image={require('@/assets/img/duty-message.png')}
title="任务信息"
title="输入参数"
></SubAreaTitle> ></SubAreaTitle>
</div> </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> </Form>
); );
} }


+ 13
- 32
react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx View File

@@ -3,12 +3,10 @@ import KFModal from '@/components/KFModal';
import { import {
DataSource, DataSource,
ResourceType, ResourceType,
ResourceVersionData,
resourceConfig, resourceConfig,
type ResourceData, type ResourceData,
} from '@/pages/Dataset/config'; } from '@/pages/Dataset/config';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Form, Input, ModalProps, Select } from 'antd'; import { Form, Input, ModalProps, Select } from 'antd';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -44,7 +42,6 @@ function ExportModelModal({
}: ExportModelModalProps) { }: ExportModelModalProps) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [resources, setResources] = useState<ResourceData[]>([]); const [resources, setResources] = useState<ResourceData[]>([]);
const [versions, setVersions] = useState<ResourceVersionData[]>([]);
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];


const layout = { const layout = {
@@ -77,35 +74,24 @@ function ExportModelModal({
return undefined; return undefined;
}; };


// 版本 tooltip
const getTooltip = () => {
const id = form.getFieldValue('id');
const resource = getSelectedResource(id);
const name = resource?.name ?? '';
const versionNames = versions.map((item: ResourceVersionData) => item.name).join('、');
const tooltip =
versions.length > 0 ? `${name}有以下版本:\n${versionNames}\n注意不能重复` : undefined;
return tooltip;
};

// 处理数据集、模型选择变化 // 处理数据集、模型选择变化
const handleResourceChange = (id: number | undefined) => { const handleResourceChange = (id: number | undefined) => {
if (id) { if (id) {
getRecourceVersions(id);
getRecourceNextVersion(id);
} else { } else {
setVersions([]);
form.setFieldValue('version', '');
} }
}; };


// 获取数据集、模型版本列表
const getRecourceVersions = async (id: number) => {
// 获取数据集、模型下一个版本
const getRecourceNextVersion = async (id: number) => {
const resource = getSelectedResource(id); const resource = getSelectedResource(id);
if (!resource) { if (!resource) {
return; return;
} }
const [res] = await to(config.getVersions(pick(resource, ['identifier', 'owner'])));
const [res] = await to(config.getNextVersion(pick(resource, ['identifier', 'owner'])));
if (res && res.data) { if (res && res.data) {
setVersions(res.data);
form.setFieldValue('version', res.data);
} }
}; };


@@ -184,15 +170,6 @@ function ExportModelModal({
<Form.Item <Form.Item
label={`${config.name}版本`} label={`${config.name}版本`}
name="version" name="version"
tooltip={
getTooltip()
? {
overlayClassName: styles['export-model-modal__tooltip'],
title: getTooltip(),
icon: <InfoCircleOutlined />,
}
: undefined
}
rules={[ rules={[
{ required: true, message: `请输入${config.name}版本` }, { required: true, message: `请输入${config.name}版本` },
{ {
@@ -205,8 +182,6 @@ function ExportModelModal({
return Promise.reject(`${config.name}版本不能为 master`); return Promise.reject(`${config.name}版本不能为 master`);
} else if (value === 'origin') { } else if (value === 'origin') {
return Promise.reject(`${config.name}版本不能为 origin`); return Promise.reject(`${config.name}版本不能为 origin`);
} else if (value && versions.map((item) => item.name).includes(value)) {
return Promise.reject(`${config.name}版本已存在`);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@@ -214,7 +189,13 @@ function ExportModelModal({
}, },
]} ]}
> >
<Input placeholder={`请输入${config.name}版本`} maxLength={64} showCount allowClear />
<Input
placeholder={`请输入${config.name}版本`}
maxLength={64}
showCount
allowClear
disabled
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"


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

@@ -4,6 +4,12 @@
overflow-y: auto; overflow-y: auto;
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 8px; border-radius: 8px;

:global {
.ant-form-item-row {
align-items: center;
}
}
} }
.params-empty { .params-empty {
:global { :global {


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

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


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


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

@@ -226,14 +226,14 @@ function Experiment() {
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload; const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId); 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 ( if (
!currentIns || !currentIns ||


+ 1
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx View File

@@ -323,6 +323,7 @@ function ExecuteConfig() {
className={styles['hyper-parameter__body__name']} className={styles['hyper-parameter__body__name']}
{...restField} {...restField}
name={[name, 'name']} name={[name, 'name']}
dependencies={fields.map((_, i) => ['parameters', i, 'name'])}
required required
rules={[ rules={[
{ {


+ 2
- 2
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

@@ -1,6 +1,6 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic';
import { import {
schedulerAlgorithms, schedulerAlgorithms,
@@ -41,7 +41,7 @@ function HyperParameterBasic({
instanceStatus, instanceStatus,
isInstance = false, isInstance = false,
}: HyperParameterBasicProps) { }: HyperParameterBasicProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const basicDatas: BasicInfoData[] = useMemo(() => { const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) { if (!info) {


+ 1
- 0
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -46,6 +46,7 @@ export type MirrorInfoData = {
}; };


export type MirrorVersionData = { export type MirrorVersionData = {
image_id: number;
id: number; id: number;
version: string; version: string;
url: string; url: string;


+ 4
- 8
react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -23,7 +23,7 @@ import { removeFormListItem } from '@/utils/ui';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd'; import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd';
import { omit, pick } from 'lodash';
import { omit } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';
@@ -79,7 +79,7 @@ function CreateServiceVersion() {
if (res.model && typeof res.model === 'object') { if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' }); model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value // 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
// model.value = model.showValue;
} }
// 环境变量 // 环境变量
if (res.env_variables && typeof res.env_variables === 'object') { if (res.env_variables && typeof res.env_variables === 'object') {
@@ -117,7 +117,6 @@ function CreateServiceVersion() {
// 创建版本 // 创建版本
const createServiceVersion = async (formData: FormData) => { const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables']; const envList = formData['env_variables'];
const model = formData['model'];
const envVariables = envList?.reduce((acc, cur) => { const envVariables = envList?.reduce((acc, cur) => {
acc[cur.key] = cur.value; acc[cur.key] = cur.value;
return acc; return acc;
@@ -125,13 +124,9 @@ function CreateServiceVersion() {


// 根据后台要求,修改表单数据 // 根据后台要求,修改表单数据
const object = { const object = {
...omit(formData, ['replicas', 'env_variables', 'model']),
...omit(formData, ['replicas', 'env_variables']),
replicas: Number(formData.replicas), replicas: Number(formData.replicas),
env_variables: envVariables, env_variables: envVariables,
model: changePropertyName(
pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
{ showValue: 'show_value' },
),
service_id: serviceId, service_id: serviceId,
}; };


@@ -427,6 +422,7 @@ function CreateServiceVersion() {
{...restField} {...restField}
name={[name, 'key']} name={[name, 'key']}
style={{ flex: 1 }} style={{ flex: 1 }}
dependencies={fields.map((_, i) => ['env_variables', i, 'key'])}
rules={[ rules={[
{ {
validator: (_, value) => { validator: (_, value) => {


+ 24
- 6
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -9,7 +9,8 @@ import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; import { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { ModelData } from '@/pages/Dataset/config';
import { import {
deleteServiceVersionReq, deleteServiceVersionReq,
getServiceInfoReq, getServiceInfoReq,
@@ -18,6 +19,7 @@ import {
} from '@/services/modelDeployment'; } from '@/services/modelDeployment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -87,7 +89,7 @@ function ServiceInfo() {
format: formatDate, format: formatDate,
}, },
]; ];
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


// 获取服务详情 // 获取服务详情
const getServiceInfo = useCallback(async () => { const getServiceInfo = useCallback(async () => {
@@ -110,8 +112,8 @@ function ServiceInfo() {
if (res && res.data) { if (res && res.data) {
const { content = [], totalElements = 0 } = res.data; const { content = [], totalElements = 0 } = res.data;
content.forEach((item: ServiceVersionData) => { content.forEach((item: ServiceVersionData) => {
if (item.model && !item.model.show_value) {
item.model.show_value = `${item.model.name}:${item.model.version}`;
if (item.model && !item.model.showValue) {
item.model.showValue = `${item.model.name}:${item.model.version}`;
} }
}); });
setTableData(content); setTableData(content);
@@ -258,6 +260,20 @@ function ServiceInfo() {
}, },
}; };


// 去模型
const gotoModel = (record: ServiceVersionData, e: React.MouseEvent) => {
e.stopPropagation();

const model = record.model as any as ModelData;
const link = formatModel(model)?.link;
if (link) {
setCacheState({
pagination,
});
navigate(link);
}
};

const columns: TableProps<ServiceVersionData>['columns'] = [ const columns: TableProps<ServiceVersionData>['columns'] = [
{ {
title: '序号', title: '序号',
@@ -278,10 +294,12 @@ function ServiceInfo() {
}, },
{ {
title: '模型版本', title: '模型版本',
dataIndex: ['model', 'show_value'],
dataIndex: ['model', 'showValue'],
key: 'model', key: 'model',
width: '20%', width: '20%',
render: tableCellRender(true),
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoModel,
}),
}, },
{ {
title: '镜像版本', title: '镜像版本',


+ 2
- 2
react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx View File

@@ -1,6 +1,6 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import { ServiceRunStatus } from '@/enums'; import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { ServiceVersionData } from '@/pages/ModelDeployment/types'; import { ServiceVersionData } from '@/pages/ModelDeployment/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { formatMirror, formatModel } from '@/utils/format'; import { formatMirror, formatModel } from '@/utils/format';
@@ -36,7 +36,7 @@ const formatEnvText = (env?: Record<string, string>) => {
}; };


function VersionBasicInfo({ info }: BasicInfoProps) { function VersionBasicInfo({ info }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const datas: BasicInfoData[] = [ const datas: BasicInfoData[] = [
{ {


+ 2
- 2
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx View File

@@ -1,6 +1,6 @@
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { ServiceRunStatus } from '@/enums'; import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; import { type ServiceVersionData } from '@/pages/ModelDeployment/types';
import { getServiceVersionCompareReq } from '@/services/modelDeployment'; import { getServiceVersionCompareReq } from '@/services/modelDeployment';
import { isEmpty } from '@/utils'; import { isEmpty } from '@/utils';
@@ -42,7 +42,7 @@ const formatEnvText = (env: Record<string, string>) => {


function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) { function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) {
const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); const [compareData, setCompareData] = useState<CompareData | undefined>(undefined);
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const fields: FiledType[] = useMemo( const fields: FiledType[] = useMemo(
() => [ () => [


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

@@ -34,7 +34,7 @@ export type ServiceVersionData = {
path: string; path: string;
identifier: string; identifier: string;
owner: string; owner: string;
show_value: string;
showValue: string;
}; };
code_config: { code_config: {
// 代码配置 // 代码配置


+ 55
- 21
react-ui/src/pages/Pipeline/Info/index.jsx View File

@@ -3,7 +3,7 @@ import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible'; import { useVisible } from '@/hooks/useVisible';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { fittingString, s8 } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
@@ -54,11 +54,20 @@ const EditPipeline = () => {
const onDragEnd = (val) => { const onDragEnd = (val) => {
const { x, y } = val; const { x, y } = val;
const point = graph.getPointByClient(x, y); const point = graph.getPointByClient(x, y);

let label = val.label;
const data = graph.save();
const nodeLabels = data.nodes.map((v) => v.label);
if (nodeLabels.includes(label)) {
label += '-' + s8();
}

// 元模型 // 元模型
const model = { const model = {
...val, ...val,
x: point.x, x: point.x,
y: point.y, y: point.y,
label,
id: val.component_name + '-' + s8(), id: val.component_name + '-' + s8(),
isCluster: false, isCluster: false,
formError: true, formError: true,
@@ -90,24 +99,29 @@ const EditPipeline = () => {


// 保存 // 保存
const savePipeline = async (isBack) => { const savePipeline = async (isBack) => {
const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
if (globalParamError) {
message.error('全局参数配置有误');
openParamsDrawer();
return;
}
closeParamsDrawer();
// 验证全局参数
// 现在改为关闭的时候就验证了
// const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
// if (globalParamError) {
// message.error('全局参数配置有误');
// openParamsDrawer();
// return;
// }
// closeParamsDrawer();


const [propsRes, propsError] = await to(propsRef.current.validateFields());
if (propsError) {
message.error('节点必填项必须配置');
return;
}
propsRef.current.close();
// 以前没有遮挡【保存】按钮时有用
// 验证节点必填参数
// const [propsRes, propsError] = await to(propsRef.current.validateFields());
// if (propsError) {
// message.error('节点必填项必须配置');
// return;
// }
// propsRef.current.close();


setTimeout(() => { setTimeout(() => {
const data = graph.save(); const data = graph.save();
// console.log(data); // console.log(data);
// 验证节点必填参数
const errorNode = data.nodes.find((item) => item.formError === true); const errorNode = data.nodes.find((item) => item.formError === true);
if (errorNode) { if (errorNode) {
message.error(`【${errorNode.label}】节点配置验证失败`); message.error(`【${errorNode.label}】节点配置验证失败`);
@@ -117,11 +131,25 @@ const EditPipeline = () => {
} }
return; return;
} }

// 验证节点名称是否有重命名
const nodeLabels = data.nodes.map((v) => v.label);
for (let i = 0; i < nodeLabels.length; i++) {
const current = nodeLabels[i];
for (let j = i + 1; j < nodeLabels.length; j++) {
const next = nodeLabels[j];
if (current === next) {
message.error(`存在重名的【${current}】节点`);
return;
}
}
}

const params = { const params = {
...locationParams, ...locationParams,
name: workflowInfo?.name, name: workflowInfo?.name,
dag: JSON.stringify(data),
global_param: JSON.stringify(globalParamRes.global_param),
dag: data,
global_param: globalParam,
}; };
saveWorkflow(params).then((ret) => { saveWorkflow(params).then((ret) => {
message.success('保存成功'); message.success('保存成功');
@@ -290,7 +318,7 @@ const EditPipeline = () => {
const { global_param, dag } = res.data; const { global_param, dag } = res.data;
setGlobalParam(global_param || []); setGlobalParam(global_param || []);
if (dag) { if (dag) {
getGraphData(parseJsonText(dag));
getGraphData(dag);
} }
} }
}; };
@@ -299,13 +327,19 @@ const EditPipeline = () => {
const openNodeDrawer = (node, validate = false) => { const openNodeDrawer = (node, validate = false) => {
// 获取所有的上游节点 // 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, node); const parentNodes = findAllParentNodes(graph, node);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// q全局参数
const globalParams = globalParamRef.current;
// 打开节点编辑抽屉 // 打开节点编辑抽屉
propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
}; };


// 关闭全局参数节点,获取全局参数
const closeGlobalParamsDrawer = () => {
const { global_param } = paramsDrawerRef.current.getFieldsValue();
setGlobalParam(global_param);
closeParamsDrawer();
};

// 初始化图 // 初始化图
const initGraph = () => { const initGraph = () => {
const contextMenu = initMenu(); const contextMenu = initMenu();
@@ -730,7 +764,7 @@ const EditPipeline = () => {
ref={paramsDrawerRef} ref={paramsDrawerRef}
open={paramsDrawerOpen} open={paramsDrawerOpen}
globalParam={globalParam} globalParam={globalParam}
onClose={closeParamsDrawer}
onClose={closeGlobalParamsDrawer}
></GlobalParamsDrawer> ></GlobalParamsDrawer>
</div> </div>
); );


+ 5
- 13
react-ui/src/pages/Pipeline/Info/utils.tsx View File

@@ -1,5 +1,4 @@
import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types'; import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types';
import { parseJsonText } from '@/utils';
import { Graph, INode } from '@antv/g6'; import { Graph, INode } from '@antv/g6';
import { type MenuProps } from 'antd'; import { type MenuProps } from 'antd';


@@ -42,8 +41,7 @@ export function createMenuItems(
): MenuProps['items'] { ): MenuProps['items'] {
const nodes: MenuProps['items'] = parentNodes.map((item) => { const nodes: MenuProps['items'] = parentNodes.map((item) => {
const model = item.getModel(); const model = item.getModel();
const out_parameters = model.out_parameters as string | undefined | null;
const out_parametersObj = parseJsonText(out_parameters);
const out_parametersObj = model.out_parameters as Record<string, PipelineNodeModelParameter>;
const outParametersList = Object.keys(out_parametersObj ?? {}); const outParametersList = Object.keys(out_parametersObj ?? {});
return { return {
key: model.id as string, key: model.id as string,
@@ -72,15 +70,6 @@ export function createMenuItems(
} }
} }


export function getInParameterComponent(
parameter: PipelineNodeModelParameter,
): React.ReactNode | null {
if (parameter.value) {
}

return null;
}

// 判断是否允许输入 // 判断是否允许输入
export function canInput(parameter: PipelineNodeModelParameter) { export function canInput(parameter: PipelineNodeModelParameter) {
const { type, item_type } = parameter; const { type, item_type } = parameter;
@@ -89,6 +78,9 @@ export function canInput(parameter: PipelineNodeModelParameter) {
(item_type === 'dataset' || (item_type === 'dataset' ||
item_type === 'model' || item_type === 'model' ||
item_type === 'image' || item_type === 'image' ||
item_type === 'code')
item_type === 'code' ||
item_type === 'remote-dataset' ||
item_type === 'remote-model' ||
item_type === 'remote-code')
); );
} }

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

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal'; 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 { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
@@ -42,6 +42,7 @@ const GlobalParamsDrawer = forwardRef(
form.setFieldValue(name, null); form.setFieldValue(name, null);
}; };


// 处理删除
const removeParameter = (name: number, remove: (param: number) => void) => { const removeParameter = (name: number, remove: (param: number) => void) => {
modalConfirm({ modalConfirm({
title: '删除后,该全局参数将不可恢复', title: '删除后,该全局参数将不可恢复',
@@ -52,6 +53,16 @@ const GlobalParamsDrawer = forwardRef(
}); });
}; };


// 处理关闭
const handleClose = async () => {
try {
await form.validateFields();
onClose();
} catch {
return false;
}
};

return ( return (
<Drawer <Drawer
rootStyle={{ marginTop: '55px' }} rootStyle={{ marginTop: '55px' }}
@@ -59,7 +70,7 @@ const GlobalParamsDrawer = forwardRef(
placement="right" placement="right"
closeIcon={false} closeIcon={false}
getContainer={false} getContainer={false}
onClose={onClose}
onClose={handleClose}
open={open} open={open}
width={520} width={520}
> >
@@ -81,7 +92,7 @@ const GlobalParamsDrawer = forwardRef(
{...restField} {...restField}
name={[name, 'param_name']} name={[name, 'param_name']}
label="参数名称" label="参数名称"
validateTrigger={[]}
dependencies={fields.map((_, i) => ['global_param', i, 'param_name'])}
rules={[ rules={[
{ required: true, message: '请输入参数名称' }, { required: true, message: '请输入参数名称' },
{ {
@@ -97,11 +108,7 @@ const GlobalParamsDrawer = forwardRef(
}, },
]} ]}
> >
<Input
placeholder="请输入参数名称"
allowClear
onBlur={() => form.validateFields()}
/>
<Input placeholder="请输入参数名称" allowClear />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...restField} {...restField}
@@ -124,9 +131,9 @@ const GlobalParamsDrawer = forwardRef(
<Radio.Group <Radio.Group
onChange={() => handleTypeChange(['global_param', name, 'param_value'])} 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> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item


+ 25
- 4
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.less View File

@@ -21,10 +21,7 @@
background: #f8fbff; background: #f8fbff;
} }


&__ref-row {
display: flex;
align-items: center;

&__component {
&__select-button { &__select-button {
display: flex; display: flex;
flex: none; flex: none;
@@ -34,5 +31,29 @@
padding-right: 0; padding-right: 0;
padding-left: 0; padding-left: 0;
} }

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

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

&__add-button {
border-color: .addAlpha(@primary-color, 0.5) [];
box-shadow: none !important;
&:hover {
border-style: solid;
}
}
} }
} }

+ 349
- 261
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -1,32 +1,44 @@
import { type ServerCodeData } from '@/components/CodeSelect';
import CodeSelectorModal from '@/components/CodeSelectorModal';
import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import ParameterSelect, {
type ParameterSelectDataType,
type ParameterSelectObject,
ParameterSelectTypeList,
} from '@/components/ParameterSelect';
import ResourceSelectorModal, { import ResourceSelectorModal, {
ResourceSelectorType, ResourceSelectorType,
selectorTypeConfig, selectorTypeConfig,
} from '@/components/ResourceSelectorModal'; } from '@/components/ResourceSelectorModal';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { CommonTabKeys, ComponentType } from '@/enums';
import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils';
import state from '@/state/jcdResource';
import { import {
PipelineGlobalParam, PipelineGlobalParam,
PipelineNodeModel, PipelineNodeModel,
PipelineNodeModelParameter, PipelineNodeModelParameter,
PipelineNodeModelSerialize, PipelineNodeModelSerialize,
} from '@/types'; } from '@/types';
import { parseJsonText } from '@/utils';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { removeFormListItem } from '@/utils/ui';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { INode } from '@antv/g6'; import { INode } from '@antv/g6';
import { Button, Drawer, Form, Input, MenuProps } from 'antd';
import { useSnapshot } from '@umijs/max';
import { Button, Drawer, Flex, Form, Input, MenuProps } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';
import { NamePath } from 'antd/es/form/interface'; import { NamePath } from 'antd/es/form/interface';
import { omit } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import PropsLabel from '../PropsLabel'; import PropsLabel from '../PropsLabel';
import styles from './index.less'; import styles from './index.less';
const { TextArea } = Input;

// 表单列表数据
export type FormListVariable = {
name: string; // 参数名
value: string; // 参数值
};


type PipelineNodeParameterProps = { type PipelineNodeParameterProps = {
onFormChange: (data: PipelineNodeModelSerialize) => void; onFormChange: (data: PipelineNodeModelSerialize) => void;
@@ -37,14 +49,10 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize, {} as PipelineNodeModelSerialize,
); );
const nodeId = Form.useWatch('id', form) as string;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);
const snap = useSnapshot(state);
const { setCurrentType } = snap;


const afterOpenChange = async () => { const afterOpenChange = async () => {
if (!open) { if (!open) {
@@ -53,11 +61,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
// 不管是否验证成功,都需要获取表单数据 // 不管是否验证成功,都需要获取表单数据
const fields = form.getFieldsValue(); const fields = form.getFieldsValue();


// 保存字段顺序
// const control_strategy = {
// ...stagingItem.control_strategy,
// ...fields.control_strategy,
// };
// 保持原有字段和顺序
const task_info = {
...stagingItem.task_info,
...fields.task_info,
};

const control_strategy = {
...stagingItem.control_strategy,
...fields.control_strategy,
};


const in_parameters = { const in_parameters = {
...stagingItem.in_parameters, ...stagingItem.in_parameters,
@@ -68,17 +81,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
...fields.out_parameters, ...fields.out_parameters,
}; };


// console.log('getFieldsValue', fields);
console.log('getFieldsValue', fields);


const res = { const res = {
...stagingItem,
...fields,
// control_strategy: JSON.stringify(control_strategy),
in_parameters: JSON.stringify(in_parameters),
out_parameters: JSON.stringify(out_parameters),
...omit(stagingItem, ['control_strategy', 'task_info', 'in_parameters', 'out_parameters']),
...omit(fields, ['control_strategy', 'task_info', 'in_parameters', 'out_parameters']),
task_info: task_info,
control_strategy: control_strategy,
in_parameters: in_parameters,
out_parameters: out_parameters,
formError: !!error, formError: !!error,
}; };
// console.log('res', res);
console.log('res', res);
onFormChange(res); onFormChange(res);
} }
}; };
@@ -96,19 +110,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
validate: boolean = false, validate: boolean = false,
) { ) {
try { try {
const nodeData: PipelineNodeModelSerialize = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
// control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', nodeData);
setStagingItem({ setStagingItem({
...nodeData,
...model,
}); });
form.resetFields(); form.resetFields();
form.setFieldsValue({ form.setFieldsValue({
...nodeData,
...model,
}); });
if (validate) { if (validate) {
form.validateFields(); form.validateFields();
@@ -120,6 +127,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete


// 参数下拉菜单 // 参数下拉菜单
setMenuItems(createMenuItems(params, parentNodes)); setMenuItems(createMenuItems(params, parentNodes));

// 云际组件,设置 store 当前资源类型
if (model.id.startsWith('remote-task')) {
const resourceType = model.in_parameters['--resource_type'].value;
setCurrentType(resourceType);
}
}, },
close: () => { close: () => {
onClose(); onClose();
@@ -137,7 +150,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
} }
}, },
}), }),
[form, open],
[form, open, setCurrentType],
); );


// ref 类型选择 // ref 类型选择
@@ -145,7 +158,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
formItemName: NamePath, formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => { ) => {
if (item.item_type === 'code') {
if (item.item_type === 'code' || item.item_type === 'remote-code') {
selectCodeConfig(formItemName, item); selectCodeConfig(formItemName, item);
} else { } else {
selectResource(formItemName, item); selectResource(formItemName, item);
@@ -157,47 +170,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
formItemName: NamePath, formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => { ) => {
const jsonValue = form.getFieldValue(formItemName)?.value;
const fieldValue = parseJsonText(jsonValue) as ServerCodeData;
const defaultSelected = fieldValue
? {
id: fieldValue.id,
code_repo_name: fieldValue.name,
git_url: fieldValue.code_path,
git_branch: fieldValue.branch,
git_user_name: fieldValue.username,
git_password: fieldValue.password,
ssh_key: fieldValue.ssh_private_key,
is_public: fieldValue.is_public,
}
: undefined;
const defaultSelected = form.getFieldValue(formItemName)?.value as CodeConfigData;
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected, defaultSelected,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const {
id,
code_repo_name,
git_url,
git_branch,
git_user_name,
git_password,
ssh_key,
is_public,
} = res;
const value = JSON.stringify({
id,
name: code_repo_name,
code_path: git_url,
branch: git_branch,
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
is_public,
});
const { code_repo_name } = res;
form.setFieldValue(formItemName, { form.setFieldValue(formItemName, {
...item, ...item,
value,
value: res,
showValue: code_repo_name, showValue: code_repo_name,
fromSelect: true, fromSelect: true,
}); });
@@ -216,9 +197,11 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
let type: ResourceSelectorType; let type: ResourceSelectorType;
switch (item.item_type) { switch (item.item_type) {
case 'dataset': case 'dataset':
case 'remote-dataset':
type = ResourceSelectorType.Dataset; type = ResourceSelectorType.Dataset;
break; break;
case 'model': case 'model':
case 'remote-model':
type = ResourceSelectorType.Model; type = ResourceSelectorType.Model;
break; break;
default: default:
@@ -237,36 +220,24 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
const { activeTab, id, version, path } = res;
if (formItemName === 'image') {
// 单独的选择镜像
form.setFieldValue(formItemName, path);
} else {
// 输入参数选择镜像
form.setFieldValue(formItemName, {
...item,
value: path,
showValue: path,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
}
} else {
const { activeTab, id, name, version, path, identifier, owner } = res;
const value = JSON.stringify({
id,
name,
version,
path,
identifier,
owner,
const { activeTab, ...rest } = res;
const { url, id, image_id } = rest;
form.setFieldValue(formItemName, {
...item,
value: rest,
showValue: url,
fromSelect: true,
activeTab,
expandedKeys: [`${image_id}`],
checkedKeys: [`${image_id}-${id}`],
}); });
} else {
const { activeTab, ...rest } = res;
const { id, name, version } = rest;
const showValue = `${name}:${version}`; const showValue = `${name}:${version}`;
form.setFieldValue(formItemName, { form.setFieldValue(formItemName, {
...item, ...item,
value,
value: rest,
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
@@ -275,19 +246,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
}); });
} }
} else { } else {
if (type === ResourceSelectorType.Mirror && formItemName === 'image') {
form.setFieldValue(formItemName, undefined);
} else {
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
}
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
} }
form.validateFields([formItemName]); form.validateFields([formItemName]);
close(); close();
@@ -298,14 +265,14 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
// 获取选择数据集、模型后面按钮 icon // 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item: { item_type: string }) => { const getSelectBtnIcon = (item: { item_type: string }) => {
const type = item.item_type; const type = item.item_type;
if (type === 'code') {
if (type === 'code' || type === 'remote-code') {
return <KFIcon type="icon-xuanzedaimapeizhi" />; return <KFIcon type="icon-xuanzedaimapeizhi" />;
} }


let selectorType: ResourceSelectorType; let selectorType: ResourceSelectorType;
if (type === 'dataset') {
if (type === 'dataset' || type === 'remote-dataset') {
selectorType = ResourceSelectorType.Dataset; selectorType = ResourceSelectorType.Dataset;
} else if (type === 'model') {
} else if (type === 'model' || type === 'remote-model') {
selectorType = ResourceSelectorType.Model; selectorType = ResourceSelectorType.Model;
} else { } else {
selectorType = ResourceSelectorType.Mirror; selectorType = ResourceSelectorType.Mirror;
@@ -323,16 +290,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
// form item label // form item label
const getLabel = ( const getLabel = (
item: { key: string; value: PipelineNodeModelParameter }, item: { key: string; value: PipelineNodeModelParameter },
namePrefix: string,
parentName: string,
) => { ) => {
return item.value.type === 'select' ? (
return item.value.type === ComponentType.Select || item.value.type === ComponentType.Map ? (
item.value.label + '(' + item.key + ')' item.value.label + '(' + item.key + ')'
) : ( ) : (
<PropsLabel <PropsLabel
menuItems={menuItems} menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'} title={item.value.label + '(' + item.key + ')'}
onClick={(value) => { onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
handleParameterClick([parentName, item.key], {
...item.value, ...item.value,
value, value,
fromSelect: true, fromSelect: true,
@@ -380,21 +347,249 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
return rules; return rules;
}; };


// 云际组件,选择类型后,重置镜像和资源,获取镜像、资源列表
const handleParameterSelect = (
value: ParameterSelectObject,
itemType: string,
parentName: string,
) => {
if (itemType === 'remote-resource-type') {
snap.setCurrentType(value.value);
const remoteImage = form.getFieldValue([parentName, '--image']);
form.setFieldValue([parentName, '--image'], { ...remoteImage, value: undefined });
const remoteResource = form.getFieldValue([parentName, '--resource']);
form.setFieldValue([parentName, '--resource'], { ...remoteResource, value: undefined });
}
};

// 表单组件
const getFormComponent = (
item: { key: string; value: PipelineNodeModelParameter },
parentName: string,
) => {
return (
<>
{item.value.type === ComponentType.Ref && (
<Flex align="center">
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
</Form.Item>
<Form.Item noStyle>
<Button
size="small"
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectRefData([parentName, item.key], item.value)}
className={styles['pipeline-drawer__component__select-button']}
>
{item.value.label}
</Button>
</Form.Item>
</Flex>
)}
{item.value.type === ComponentType.Select &&
(ParameterSelectTypeList.includes(item.value.item_type as ParameterSelectDataType) ? (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterSelect
dataType={item.value.item_type as ParameterSelectDataType}
placeholder={item.value.placeholder}
onChange={(value) =>
handleParameterSelect(
value as ParameterSelectObject,
item.value.item_type,
parentName,
)
}
/>
</Form.Item>
) : null)}
{item.value.type === ComponentType.Map && (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<Form.List name={[parentName, item.key, 'value']}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Flex
key={key}
gap="0 8px"
style={{ width: '100%' }}
className={styles['pipeline-drawer__component__list-row']}
>
<Form.Item
{...restField}
name={[name, 'name']}
style={{ flex: 1, minWidth: 0 }}
dependencies={fields.map((_, i) => [
parentName,
item.key,
'value',
i,
'name',
])}
rules={[
{
validator: (_, value) => {
if (!value) {
return Promise.reject(new Error('请输入变量名'));
}
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value)) {
return Promise.reject(
new Error(
'变量名只支持字母、数字、下划线、中横线并且必须以字母或下划线开头',
),
);
}
// 判断不能重名
const list = form
.getFieldValue([parentName, item.key, 'value'])
.filter(
(item: FormListVariable | undefined) =>
item !== undefined && item !== null,
);

const names = list.map((item: FormListVariable) => item.name);
if (new Set(names).size !== names.length) {
return Promise.reject(new Error('变量名不能重复'));
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" allowClear />
</Form.Item>
<span style={{ lineHeight: '32px' }}>=</span>
<Form.Item
{...restField}
name={[name, 'value']}
style={{ flex: 1, minWidth: 0 }}
rules={[
{
validator: requiredValidator,
message: '请输入变量值',
},
]}
>
{/* <Input placeholder="请输入变量值" allowClear /> */}
<ParameterInput
placeholder="请输入变量值"
allowClear
addonAfter={
<PropsLabel
menuItems={menuItems}
title=""
onClick={(value) => {
handleParameterClick(
[parentName, item.key, 'value', name, 'value'],
{
...item.value,
value,
fromSelect: true,
showValue: value,
},
);
}}
/>
}
></ParameterInput>
</Form.Item>
<Flex
style={{
width: '76px',
height: '32px',
}}
align="center"
>
<Button
style={{
marginRight: '3px',
}}
shape="circle"
size="middle"
type="text"
icon={<MinusCircleOutlined />}
onClick={() => {
removeFormListItem(
form,
'env_variables',
name,
remove,
['key', 'value'],
'删除后,该变量将不可恢复',
);
}}
></Button>
{index === fields.length - 1 && (
<Button
shape="circle"
size="middle"
type="text"
icon={<PlusCircleOutlined />}
onClick={() => add()}
></Button>
)}
</Flex>
</Flex>
))}
{fields.length === 0 && (
<Button
className={styles['pipeline-drawer__component__add-button']}
color="primary"
variant="dashed"
icon={<PlusOutlined />}
block
onClick={() => add()}
>
新增
</Button>
)}
</>
)}
</Form.List>
</Form.Item>
)}
{item.value.type === ComponentType.Str && (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
</Form.Item>
)}
</>
);
};

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

// 控制策略 // 控制策略
// const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
// ([key, value]) => ({ key, value }),
// );
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {})
.map(([key, value]) => ({ key, value }))
.filter((v) => v.value.visible === true);


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


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


return ( return (
<Drawer <Drawer
@@ -406,7 +601,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
onClose={onClose} onClose={onClose}
afterOpenChange={afterOpenChange} afterOpenChange={afterOpenChange}
open={open} open={open}
width={520}
width={620}
className={styles['pipeline-drawer']} className={styles['pipeline-drawer']}
> >
<Form <Form
@@ -455,114 +650,37 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
> >
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
{hasTaskInfo && (
<>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
size="small"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['pipeline-drawer__ref-row__select-button']}
>
选择镜像
</Button>
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" />
</Form.Item>
{/* <Form.Item
name="mount_path"
label={
<PropsLabel
menuItems={menuItems}
title="挂载路径"
onClick={(value) => {
handleParameterClick('mount_path', value);
}}
/>
}
>
<Input placeholder="请输入挂载路径" allowClear />
</Form.Item> */}
<Form.Item
name="env_variables"
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
onClick={(value) => {
handleParameterClick('env_variables', value);
}}
/>
}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{/* 控制参数 */}
{/* {controlStrategyList.map((item) => (

{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) => (
<Form.Item <Form.Item
key={item.key} key={item.key}
name={['control_strategy', item.key]}
label={getLabel(item, 'task_info')}
required={item.value.require ? true : false} required={item.value.require ? true : false}
>
{getFormComponent(item, 'task_info')}
</Form.Item>
))}

{/* 控制参数 */}
{controlStrategyList.map((item) => (
<Form.Item
key={item.key}
label={getLabel(item, 'control_strategy')} label={getLabel(item, 'control_strategy')}
rules={getFormRules(item)}
required={item.value.require ? true : false}
> >
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
{getFormComponent(item, 'control_strategy')}
</Form.Item> </Form.Item>
))} */}
</>
)}
))}


{/* 输入参数 */} {/* 输入参数 */}
{inParametersList.length > 0 && ( {inParametersList.length > 0 && (
@@ -579,42 +697,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
label={getLabel(item, 'in_parameters')} label={getLabel(item, 'in_parameters')}
required={item.value.require ? true : false} required={item.value.require ? true : false}
> >
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle>
{item.value.type === 'select' ? (
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect
isPipeline
dataType={item.value.item_type as ParameterSelectDataType}
placeholder={item.value.placeholder}
/>
) : null
) : (
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
)}
</Form.Item>
{item.value.type === 'ref' && (
<Form.Item noStyle>
<Button
size="small"
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectRefData(['in_parameters', item.key], item.value)}
className={styles['pipeline-drawer__ref-row__select-button']}
>
{item.value.label}
</Button>
</Form.Item>
)}
</div>
{getFormComponent(item, 'in_parameters')}
</Form.Item> </Form.Item>
))} ))}
</> </>
)} )}

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


+ 5
- 5
react-ui/src/pages/Pipeline/index.jsx View File

@@ -24,7 +24,7 @@ const { TextArea } = Input;
const Pipeline = () => { const Pipeline = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const navigate = useNavigate(); const navigate = useNavigate();
const [formId, setFormId] = useState(null);
const [editRecord, setEditRecord] = useState(null);
const [dialogTitle, setDialogTitle] = useState('新建流水线'); const [dialogTitle, setDialogTitle] = useState('新建流水线');
const [pipeList, setPipeList] = useState([]); const [pipeList, setPipeList] = useState([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -75,7 +75,7 @@ const Pipeline = () => {
if (ret.code === 200) { if (ret.code === 200) {
form.resetFields(); form.resetFields();
form.setFieldsValue({ ...ret.data }); form.setFieldsValue({ ...ret.data });
setFormId(ret.data.id);
setEditRecord(ret.data);
setDialogTitle('编辑流水线'); setDialogTitle('编辑流水线');
setIsModalOpen(true); setIsModalOpen(true);
} }
@@ -99,7 +99,7 @@ const Pipeline = () => {
// 显示 modal // 显示 modal
const showModal = () => { const showModal = () => {
form.resetFields(); form.resetFields();
setFormId(null);
setEditRecord(null);
setDialogTitle('新建流水线'); setDialogTitle('新建流水线');
setIsModalOpen(true); setIsModalOpen(true);
}; };
@@ -111,8 +111,8 @@ const Pipeline = () => {


// 表单提交 // 表单提交
const onFinish = (values) => { const onFinish = (values) => {
if (formId) {
editWorkflow({ ...values, id: formId }).then((ret) => {
if (editRecord) {
editWorkflow({ ...editRecord, ...values }).then((ret) => {
setIsModalOpen(false); setIsModalOpen(false);
message.success('编辑成功'); message.success('编辑成功');
getList(); getList();


+ 36
- 1
react-ui/src/services/dataset/index.js View File

@@ -58,6 +58,14 @@ export function addDatasetVersion(data) {
}); });
} }


// 编辑数据集版本
export function editDatasetVersion(data) {
return request(`/api/mmp/newdataset/updateVersionDesc`, {
method: 'PUT',
data,
});
}

// 下载数据集所有文件 // 下载数据集所有文件
export function downloadAllFiles(params) { export function downloadAllFiles(params) {
return request(`/api/mmp/newdataset/downloadAllFiles`, { return request(`/api/mmp/newdataset/downloadAllFiles`, {
@@ -82,6 +90,15 @@ export function compareDatasetVersion(data) {
}); });
} }


// 获取数据集下个版本号
export function getDatasetNextVersionReq(data) {
return request(`/api/mmp/newdataset/queryNextVersion`, {
method: 'POST',
data,
});
}


// ----------------------------模型--------------------------------- // ----------------------------模型---------------------------------


// 分页查询模型列表 // 分页查询模型列表
@@ -132,6 +149,15 @@ export function addModelVersion(data) {
}); });
} }


// 编辑模型版本
export function editModelVersion(data) {
return request(`/api/mmp/newmodel/updateVersionDesc`, {
method: 'PUT',
data,
});
}


// 删除模型版本 // 删除模型版本
export function deleteModelVersion(params) { export function deleteModelVersion(params) {
return request(`/api/mmp/newmodel/deleteVersion`, { return request(`/api/mmp/newmodel/deleteVersion`, {
@@ -140,6 +166,14 @@ export function deleteModelVersion(params) {
}); });
} }


// 获取模型下个版本号
export function getModelNextVersionReq(data) {
return request(`/api/mmp/newmodel/queryNextVersion`, {
method: 'POST',
data,
});
}

// 获取模型依赖 // 获取模型依赖
export function getModelAtlasReq(params) { export function getModelAtlasReq(params) {
return request(`/api/mmp/newmodel/getModelDependencyTree`, { return request(`/api/mmp/newmodel/getModelDependencyTree`, {
@@ -201,4 +235,5 @@ export function unpraiseResourceReq(id) {
return request(`/api/mmp/newmodel/unpraise/${id}`, { return request(`/api/mmp/newmodel/unpraise/${id}`, {
method: 'DELETE', method: 'DELETE',
}); });
}
}


+ 89
- 0
react-ui/src/services/external/index.ts View File

@@ -0,0 +1,89 @@
// 外部系统

import { request } from '@umijs/max';

const jccBaseUrl = 'https://jcc.jointcloud.net';

// 云际系统登录
export function jccLoginReq() {
return request(`${jccBaseUrl}/jcc-admin/admin/login`, {
method: 'POST',
data: {
username: 'iflytek',
password: 'iflytek@123',
},
headers: {
isToken: false,
},
skipLoading: true,
skipValidating: true,
});
}

// 云际系统获取资源类型
export function jccGetResourceTypesReq(token: string, userId: number) {
return request(`${jccBaseUrl}/jsm/jobSet/resourceRange`, {
method: 'POST',
data: {
userID: userId,
},
headers: {
authorization: `${token}`,
isToken: false,
},
skipLoading: true,
skipValidating: true,
});
}

// 云际系统获取资源镜像
export function jccGetImagesReq(token: string, cardTypes: string[]) {
return request(`${jccBaseUrl}/jsm/jobSet/queryImages`, {
method: 'POST',
data: {
cardTypes: cardTypes,
},
headers: {
authorization: `${token}`,
isToken: false,
},
skipLoading: true,
skipValidating: true,
});
}

// 云际系统获取资源列表
export function jccGetResourcesReq(token: string, cardType: string) {
return request(`${jccBaseUrl}/jsm/jobSet/queryResource`, {
method: 'POST',
data: {
queryResource: {
cpu: {
min: 0,
max: 0,
},
memory: {
min: 0,
max: 0,
},
gpu: {
min: 0,
max: 0,
},
storage: {
min: 0,
max: 0,
},
type: cardType,
},
resourceType: 'Train',
clusterIDs: ['1865927992266461184', ''],
},
headers: {
authorization: `${token}`,
isToken: false,
},
skipLoading: true,
skipValidating: true,
});
}

+ 140
- 0
react-ui/src/state/jcdResource.ts View File

@@ -0,0 +1,140 @@
import {
jccGetImagesReq,
jccGetResourcesReq,
jccGetResourceTypesReq,
jccLoginReq,
} from '@/services/external';
import { to } from '@/utils/promise';
import { proxy } from '@umijs/max';

export type JCCResourceRange = {
type: string;
};

export type JCCResourceType = {
label: string;
value: string;
};

export interface JCCResourceImage {
imageID: number;
name: string;
createTime: Date;
clusterImages: JCCClusterImage[];
}

export interface JCCClusterImage {
imageID: number;
clusterID: string;
originImageType: string;
originImageID: string;
originImageName: string;
cards: JCCCard[];
}

export interface JCCCard {
originImageID: string;
card: string;
}

export interface JCCResourceStandard {
id: number;
sourceKey: string;
type: string;
name: string;
totalCount: number;
availableCount: number;
changeType: number;
status: number;
region: string;
clusterId: string;
costPerUnit: number;
costType: string;
tag: string;
userId: number;
createTime: Date;
updateTime: Date;
baseResourceSpecs: JCCBaseResourceSpec[];
}

export interface JCCBaseResourceSpec {
id: number;
resourceSpecId: number;
type: string;
name: string;
totalValue: number;
totalUnit: string;
availableValue: number;
availableUnit: string;
userId: number;
createTime: Date;
updateTime: Date;
}

type JCCResourceTypeStore = {
token: string;
types: JCCResourceType[];
images: JCCResourceImage[];
resources: JCCResourceStandard[];
currentType: string | undefined | null;
jccLogin: () => void;
getResourceTypes: () => void;
getImages: (cardTypes: string[]) => void;
getResources: (cardType: string) => void;
setCurrentType: (cardType: string) => void;
};

const state = proxy<JCCResourceTypeStore>({
token: '',
types: [],
images: [],
resources: [],
currentType: undefined,
jccLogin: async () => {
const [res] = await to(jccLoginReq());
if (res && res.code === 200 && res.data) {
const { tokenHead, token } = res.data;
state.token = tokenHead + token;
}
},
getResourceTypes: async () => {
const [loginRes] = await to(jccLoginReq());
if (loginRes && loginRes.code === 200 && loginRes.data) {
const { tokenHead, token, jsmUserInfo } = loginRes.data;
state.token = tokenHead + token;
const userID = jsmUserInfo?.data?.userID;
const [res] = await to(jccGetResourceTypesReq(tokenHead + token, userID));
if (res && res.code === 'OK' && res.data) {
state.types = res.data.resourceRanges?.map((v: JCCResourceRange) => ({
label: v.type,
value: v.type,
}));
}
}
},
getImages: async (cardTypes: string[]) => {
const [res] = await to(jccGetImagesReq(state.token, cardTypes));
if (res && res.code === 'OK' && res.data) {
state.images = res.data.images;
}
},
getResources: async (cardType: string) => {
const [res] = await to(jccGetResourcesReq(state.token, cardType));
if (res && res.code === 'OK' && res.data) {
state.resources = res.data.resource;
}
},
setCurrentType: (cardType: string | undefined | null) => {
if (state.currentType !== cardType) {
state.currentType = cardType;
state.images = [];
state.resources = [];
if (cardType) {
state.getImages([cardType]);
state.getResources(cardType);
}
}
},
});

export default state;

+ 53
- 0
react-ui/src/state/systemResource.ts View File

@@ -0,0 +1,53 @@
/*
* @Author: 赵伟
* @Date: 2024-10-10 08:51:41
* @Description: 资源规格
*/

import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { to } from '@/utils/promise';
import { proxy } from '@umijs/max';
import { type SelectProps } from 'antd';

/** 过滤资源规格 */
export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
input: string,
option?: ComputingResource,
) => {
return (
option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false
);
};

/** 资源规格字段 */
export const resourceFieldNames = {
label: 'description',
value: 'id',
};

export interface SystemResourceStore {
resources: ComputingResource[];
}

const state = proxy<SystemResourceStore>({
resources: [],
});

/** 获取资源列表 */
export const getSystemResources = async () => {
if (state.resources.length > 0) {
return;
}
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && Array.isArray(res.data.content)) {
state.resources = res.data.content;
}
};

export default state;

+ 16
- 15
react-ui/src/types.ts View File

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


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

// 流水线全局参数 // 流水线全局参数
export type PipelineGlobalParam = { export type PipelineGlobalParam = {
param_name: string; param_name: string;
description: string; description: string;
param_type: number;
param_type: PipelineGlobalParamType;
param_value: number | string | boolean; param_value: number | string | boolean;
is_sensitive: number; is_sensitive: number;
}; };
@@ -66,9 +72,10 @@ export type PipelineNodeModel = {
label: string; label: string;
experimentStartTime: string; experimentStartTime: string;
experimentEndTime?: string | null; experimentEndTime?: string | null;
control_strategy: string;
in_parameters: string;
out_parameters: string;
control_strategy: Record<string, PipelineNodeModelParameter>;
in_parameters: Record<string, PipelineNodeModelParameter>;
task_info: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
component_label: string; component_label: string;
icon_path: string; icon_path: string;
workflowId?: string; workflowId?: string;
@@ -82,11 +89,12 @@ export type PipelineNodeModelParameter = {
label: string; label: string;
value: any; value: any;
require?: number; require?: number;
visible: boolean;
placeholder?: string; placeholder?: string;
describe?: string; describe?: string;
fromSelect?: boolean;
showValue?: any;
editable?: number;
editable?: boolean;
showValue?: any; // 前端显示用
fromSelect?: boolean; // 前端显示用
activeTab?: string; // ResourceSelectorModal tab activeTab?: string; // ResourceSelectorModal tab
expandedKeys?: string[]; // ResourceSelectorModal expandedKeys expandedKeys?: string[]; // ResourceSelectorModal expandedKeys
checkedKeys?: string[]; // ResourceSelectorModal checkedKeys checkedKeys?: string[]; // ResourceSelectorModal checkedKeys
@@ -110,14 +118,7 @@ export type KeysToCamelCase<T> = {
}; };


// 序列化后的流水线节点 // 序列化后的流水线节点
export type PipelineNodeModelSerialize = Omit<
PipelineNodeModel,
'in_parameters' | 'out_parameters'
> & {
// control_strategy: Record<string, PipelineNodeModelParameter>;
in_parameters: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
};
export type PipelineNodeModelSerialize = PipelineNodeModel;


// 资源规格 // 资源规格
export type ComputingResource = { export type ComputingResource = {


+ 9
- 15
react-ui/src/utils/format.ts View File

@@ -1,4 +1,5 @@
import { BasicInfoLink } from '@/components/BasicInfo/types'; import { BasicInfoLink } from '@/components/BasicInfo/types';
import { CodeConfigData } from '@/components/CodeSelectorModal';
import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import { import {
@@ -12,13 +13,6 @@ import { getGitUrl } from '@/utils';
// 格式化日期 // 格式化日期
export { formatDate } from '@/utils/date'; export { formatDate } from '@/utils/date';


export type SelectedCodeConfig = {
code_path: string;
branch: string;
showValue?: string; // 前端使用的
show_value?: string; // 后端使用的
};

/** /**
* 格式化数据集数组 * 格式化数据集数组
* *
@@ -77,7 +71,7 @@ export const formatMirror = (mirror: ResourceSelectorResponse): string | undefin
if (!mirror) { if (!mirror) {
return undefined; return undefined;
} }
return mirror.path;
return mirror.url;
}; };


/** /**
@@ -87,20 +81,20 @@ export const formatMirror = (mirror: ResourceSelectorResponse): string | undefin
* @return 基本信息链接对象 * @return 基本信息链接对象
*/ */
export const formatCodeConfig = ( export const formatCodeConfig = (
project?: ProjectDependency | SelectedCodeConfig,
project?: ProjectDependency | CodeConfigData,
): BasicInfoLink | undefined => { ): BasicInfoLink | undefined => {
if (!project) { if (!project) {
return undefined; return undefined;
} }
// 创建表单,CodeSelect 组件返回,目前有流水线、模型部署、超参数自动寻优创建时选择了代码配置
if ('code_path' in project) {
const { showValue, show_value, code_path, branch } = project;
// 创建表单,CodeSelect 组件返回,目前有流水线、超参数自动寻优、主动学习、开发环境创建时选择了代码配置
if ('code_repo_name' in project) {
const { code_repo_name, git_url, git_branch } = project;
return { return {
value: showValue || show_value,
url: getGitUrl(code_path, branch),
value: code_repo_name,
url: getGitUrl(git_url, git_branch),
}; };
} else { } else {
// 数据集和模型的代码配置
// 数据集和模型详情的代码配置
const { url, branch, name } = project; const { url, branch, name } = project;
return { return {
value: name, value: name,


+ 5
- 0
react-ui/src/utils/sessionStorage.ts View File

@@ -13,8 +13,13 @@ export default class SessionStorage {
static readonly aimUrlKey = 'aim-url'; static readonly aimUrlKey = 'aim-url';
/** tensorBoard url */ /** tensorBoard url */
static readonly tensorBoardUrlKey = 'tensor-board-url'; static readonly tensorBoardUrlKey = 'tensor-board-url';
<<<<<<< HEAD
/** 登录之前的地址 */ /** 登录之前的地址 */
static readonly redirectUrl = 'redirect-url'; static readonly redirectUrl = 'redirect-url';
=======
// /** 云际系统 Token */
// static readonly jccTokenKey = 'jcc-token';
>>>>>>> dev-zw


/** /**
* 获取 SessionStorage 值 * 获取 SessionStorage 值


Loading…
Cancel
Save