| @@ -65,3 +65,4 @@ mvnw | |||
| /react-ui/types/tsconfig.tsbuildinfo | |||
| /react-ui/storybook-static | |||
| /react-ui/.storybook/scripts | |||
| /react-ui/dist.zip | |||
| @@ -23,7 +23,11 @@ export default { | |||
| target: 'http://172.20.32.197:31213', // 开发环境 | |||
| // target: 'http://172.20.32.235:31213', // 测试环境 | |||
| // target: 'http://172.20.32.44:8082', | |||
| <<<<<<< HEAD | |||
| // target: 'http://172.20.32.164:8082', | |||
| ======= | |||
| target: 'http://172.20.32.164:8082', | |||
| >>>>>>> dev-zw | |||
| // 配置了这个可以从 http 代理到 https | |||
| // 依赖 origin 的功能可能需要这个,比如 cookie | |||
| changeOrigin: true, | |||
| @@ -8,7 +8,7 @@ | |||
| "build": "max build", | |||
| "deploy": "npm run build && npm run gh-pages", | |||
| "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-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | |||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | |||
| @@ -4,7 +4,7 @@ | |||
| * @Description: 流水线选择代码配置表单 | |||
| */ | |||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||
| import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button } from 'antd'; | |||
| @@ -18,19 +18,9 @@ export { | |||
| type ParameterInputValue, | |||
| } 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({ | |||
| @@ -44,50 +34,18 @@ function CodeSelect({ | |||
| }: CodeSelectProps) { | |||
| // 选择代码配置 | |||
| 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, { | |||
| defaultSelected: defaultSelected, | |||
| onOk: (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?.({ | |||
| value: jsonObjStr, | |||
| ...res, | |||
| value: code_repo_name, | |||
| showValue: code_repo_name, | |||
| fromSelect: true, | |||
| ...jsonObj, | |||
| }); | |||
| } else { | |||
| onChange?.(undefined); | |||
| @@ -1,3 +1,4 @@ | |||
| import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types'; | |||
| import { formatEnum } from '@/utils/format'; | |||
| import { Typography, type SelectProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| @@ -16,6 +17,8 @@ type FormInfoProps = { | |||
| options?: SelectProps['options']; | |||
| /** 自定义节点 label、value 的字段 */ | |||
| fieldNames?: SelectProps['fieldNames']; | |||
| /** 全局参数 */ | |||
| globalParams?: PipelineGlobalParam[] | null; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| @@ -32,12 +35,29 @@ function FormInfo({ | |||
| select = false, | |||
| options, | |||
| fieldNames, | |||
| globalParams, | |||
| className, | |||
| style, | |||
| }: FormInfoProps) { | |||
| let showValue = value; | |||
| if (value && typeof value === 'object' && valuePropName) { | |||
| showValue = value[valuePropName]; | |||
| const reg = /^\$\{(.*)\}$/; | |||
| if (value.fromSelect && Array.isArray(globalParams) && globalParams.length > 0) { | |||
| const match = reg.exec(showValue); | |||
| if (match) { | |||
| const paramName = match[1]; | |||
| const foundParam = globalParams.find((v) => v.param_name === paramName); | |||
| if (foundParam) { | |||
| showValue = | |||
| foundParam.param_type === PipelineGlobalParamType.Boolean // 布尔类型转换 | |||
| ? foundParam.param_value | |||
| ? 'true' | |||
| : 'false' | |||
| : foundParam.param_value; | |||
| } | |||
| } | |||
| } | |||
| } else if (select === true && options) { | |||
| let _options: SelectProps['options'] = options; | |||
| if (fieldNames) { | |||
| @@ -9,6 +9,7 @@ import { CloseOutlined } from '@ant-design/icons'; | |||
| import { ConfigProvider, Form, Input, Typography } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import { ReactNode } from 'react'; | |||
| import './index.less'; | |||
| // 如果值是对象时的类型 | |||
| @@ -55,6 +56,8 @@ export interface ParameterInputProps { | |||
| disabled?: boolean; | |||
| /** 元素 id */ | |||
| id?: string; | |||
| /** 带标签的 input,设置后置标签 */ | |||
| addonAfter?: ReactNode; | |||
| } | |||
| function ParameterInput({ | |||
| @@ -75,7 +78,7 @@ function ParameterInput({ | |||
| const valueObj = | |||
| typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; | |||
| if (valueObj && !valueObj.showValue) { | |||
| valueObj.showValue = valueObj.value; | |||
| valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : ''; | |||
| } | |||
| const isSelect = valueObj?.fromSelect; | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| @@ -1,29 +1,34 @@ | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource'; | |||
| import { DatasetData, ModelData } from '@/pages/Dataset/config'; | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| 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 { 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 = { | |||
| getOptions: () => Promise<any>; // 获取下拉数据 | |||
| getOptions?: () => Promise<any>; // 获取下拉数据 | |||
| fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 | |||
| optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 | |||
| 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: { | |||
| getOptions: async () => { | |||
| const res = await getDatasetList({ | |||
| @@ -31,13 +36,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| size: 1000, | |||
| 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: { | |||
| getOptions: async () => { | |||
| @@ -46,13 +54,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| size: 1000, | |||
| 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: { | |||
| getOptions: async () => { | |||
| @@ -60,25 +71,58 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| page: 0, | |||
| 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', | |||
| getValue: (value: ServiceData) => { | |||
| return value.id; | |||
| }, | |||
| getLabel: (value: ServiceData) => { | |||
| return value.service_name; | |||
| }, | |||
| isObjectValue: true, | |||
| }, | |||
| resource: { | |||
| getOptions: async () => { | |||
| // 不需要这个函数 | |||
| return []; | |||
| }, | |||
| fieldNames: resourceFieldNames, | |||
| 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, | |||
| }, | |||
| }; | |||
| @@ -4,19 +4,25 @@ | |||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||
| */ | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import jccResourceState from '@/state/jcdResource'; | |||
| import systemResourceState, { getSystemResources } from '@/state/systemResource'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useSnapshot } from '@umijs/max'; | |||
| import { Select, type SelectProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import FormInfo from '../FormInfo'; | |||
| import { paramSelectConfig } from './config'; | |||
| import { paramSelectConfig, type ParameterSelectDataType } from './config'; | |||
| export { ParameterSelectTypeList, type ParameterSelectDataType } from './config'; | |||
| export type ParameterSelectObject = { | |||
| value: 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 { | |||
| /** 类型 */ | |||
| @@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps { | |||
| display?: boolean; | |||
| /** 值,支持对象,对象必须包含 value */ | |||
| value?: string | ParameterSelectObject; | |||
| /** 用于流水线, 流水线资源规格要求 id 为字符串 */ | |||
| isPipeline?: boolean; | |||
| /** 修改后回调 */ | |||
| onChange?: (value: string | ParameterSelectObject) => void; | |||
| } | |||
| @@ -36,69 +40,126 @@ function ParameterSelect({ | |||
| dataType, | |||
| display = false, | |||
| value, | |||
| isPipeline = false, | |||
| onChange, | |||
| ...rest | |||
| }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState<SelectProps['options']>([]); | |||
| const [options, setOptions] = useState<SelectOptions>([]); | |||
| 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(() => { | |||
| // 获取下拉数据 | |||
| 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(); | |||
| }, [propsConfig]); | |||
| }, [getOptions, dataType, getResourceTypes]); | |||
| const selectOptions = dataType === 'resource' ? computingResource : options; | |||
| const selectOptions = ( | |||
| dataType === 'resource' ? systemResourceSnap.resources : objectSelectOptions | |||
| ) as SelectOptions; | |||
| 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 { | |||
| onChange?.(text); | |||
| const selectValue = text ? text : ''; | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: selectValue, | |||
| }); | |||
| } else { | |||
| onChange?.(selectValue); | |||
| } | |||
| } | |||
| }; | |||
| // 只用于展示,FormInfo 组件带有 Tooltip | |||
| if (display) { | |||
| return ( | |||
| <FormInfo | |||
| select | |||
| value={valueText} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| ></FormInfo> | |||
| <FormInfo select value={valueText} options={selectOptions} fieldNames={fieldNames}></FormInfo> | |||
| ); | |||
| } | |||
| return ( | |||
| <Select | |||
| {...rest} | |||
| filterOption={propsConfig?.filterOption} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| optionFilterProp={propsConfig?.optionFilterProp} | |||
| fieldNames={fieldNames} | |||
| optionFilterProp={optionFilterProp} | |||
| filterOption={filterOption} | |||
| value={valueText} | |||
| onChange={handleChange} | |||
| showSearch | |||
| @@ -10,10 +10,10 @@ import ResourceSelectorModal, { | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/components/ResourceSelectorModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button, ConfigProvider } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { pick } from 'lodash'; | |||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | |||
| import './index.less'; | |||
| @@ -27,6 +27,8 @@ export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse | |||
| interface ResourceSelectProps extends ParameterInputProps { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| type: ResourceSelectorType; | |||
| /** 值 */ | |||
| value?: ResourceSelectorResponse; | |||
| } | |||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||
| @@ -47,68 +49,51 @@ function ResourceSelect({ | |||
| }: ResourceSelectProps) { | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| 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 { close } = openAntdModal(ResourceSelectorModal, { | |||
| type, | |||
| defaultExpandedKeys: selectedResource ? [selectedResource.id] : [], | |||
| defaultCheckedKeys: selectedResource | |||
| ? [`${selectedResource.id}-${selectedResource.version}`] | |||
| : [], | |||
| defaultActiveTab: selectedResource?.activeTab, | |||
| defaultExpandedKeys: defaultExpandedKeys, | |||
| defaultCheckedKeys: defaultCheckedKeys, | |||
| defaultActiveTab: defaultActiveTab, | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| const { activeTab, ...rest } = res; | |||
| const { url } = rest; | |||
| onChange?.({ | |||
| value: path, | |||
| showValue: path, | |||
| ...rest, | |||
| value: url, | |||
| showValue: url, | |||
| fromSelect: true, | |||
| activeTab, | |||
| id, | |||
| name, | |||
| version, | |||
| path, | |||
| }); | |||
| } 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}`; | |||
| onChange?.({ | |||
| value: jsonObjStr, | |||
| ...rest, | |||
| value: showValue, | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| ...jsonObj, | |||
| }); | |||
| } | |||
| } else { | |||
| @@ -52,9 +52,9 @@ const convertResourceVersionToTreeData = ( | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: ResourceVersionData) => ({ | |||
| ...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']), | |||
| version: item.name, | |||
| title: item.name, | |||
| key: `${parentId}-${item.name}`, | |||
| title: item.name, | |||
| version: item.name, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| })); | |||
| @@ -66,12 +66,11 @@ const convertMirrorVersionToTreeData = ( | |||
| list: MirrorVersionData[], | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: MirrorVersionData) => ({ | |||
| url: item.url, | |||
| title: item.tag_name, | |||
| ...item, | |||
| key: `${parentId}-${item.id}`, | |||
| title: item.tag_name, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| description: item.description, | |||
| })); | |||
| }; | |||
| @@ -8,6 +8,7 @@ import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { ResourceFileData } from '@/pages/Dataset/config'; | |||
| import { type MirrorVersionData } from '@/pages/Mirror/Info'; | |||
| import { to } from '@/utils/promise'; | |||
| import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | |||
| import { Input, Tabs, Tree } from 'antd'; | |||
| @@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig }; | |||
| // 选择数据集、模型、镜像的返回类型 | |||
| export type ResourceSelectorResponse = { | |||
| 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'> { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| @@ -241,15 +242,22 @@ function ResourceSelectorModal({ | |||
| const name = (treeNode?.title ?? '') as string; | |||
| const identifier = (treeNode?.identifier ?? '') 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); | |||
| } else { | |||
| onOk?.(undefined); | |||
| @@ -163,3 +163,11 @@ export enum AutoMLTrailStatus { | |||
| CANCELLED = 'CANCELLED', // 取消 | |||
| MEMOUT = 'MEMOUT', // 内存溢出 | |||
| } | |||
| // 流水线组件类型 | |||
| export enum ComponentType { | |||
| Ref = 'ref', | |||
| Select = 'select', | |||
| Map = 'map', | |||
| Str = 'str', | |||
| } | |||
| @@ -4,66 +4,90 @@ | |||
| * @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( | |||
| (id?: string | number) => { | |||
| if (!id) { | |||
| 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; | |||
| }; | |||
| @@ -1,6 +1,6 @@ | |||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||
| import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { | |||
| classifierAlgorithms, | |||
| FrameworkType, | |||
| @@ -39,7 +39,7 @@ function BasicInfo({ | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: BasicInfoProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| return []; | |||
| @@ -6,7 +6,7 @@ import { | |||
| autoMLEnsembleClassOptions, | |||
| autoMLTaskTypeOptions, | |||
| } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { | |||
| classificationAlgorithms, | |||
| featureAlgorithms, | |||
| @@ -76,7 +76,7 @@ function AutoMLBasic({ | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: AutoMLBasicProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| return []; | |||
| @@ -42,37 +42,44 @@ export const regressorAlgorithms = [ | |||
| // 特征预处理算法 | |||
| 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', | |||
| }, | |||
| { | |||
| label: 'extra_trees_preproc_for_regression (回归任务极端随机树)', | |||
| label: '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: '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', | |||
| }, | |||
| { | |||
| label: 'select_percentile_regression (基于百分位的回归特征选择)', | |||
| label: 'select_percentile_regression (特征选择-基于百分位的回归特征选择)', | |||
| value: 'select_percentile_regression', | |||
| }, | |||
| { | |||
| label: 'select_rates_classification (基于比率的分类特征选择)', | |||
| label: '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' }, | |||
| ]; | |||
| @@ -169,16 +169,16 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| const handleMessage = (e: MessageEvent) => { | |||
| const { type, payload } = e.data; | |||
| if (type === ExperimentCompleted) { | |||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||
| const { experimentId, experimentInsId, status /*finishTime*/ } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| console.log( | |||
| '实验实例状态变化', | |||
| currentIns?.status, | |||
| status, | |||
| experimentId, | |||
| experimentInsId, | |||
| finishTime, | |||
| ); | |||
| // console.log( | |||
| // '实验实例状态变化', | |||
| // currentIns?.status, | |||
| // status, | |||
| // experimentId, | |||
| // experimentInsId, | |||
| // finishTime, | |||
| // ); | |||
| if ( | |||
| !currentIns || | |||
| @@ -1,30 +1,8 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| 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 { 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'> { | |||
| typeList: CategoryData[]; | |||
| @@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| } | |||
| 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 [res] = await to(addDataset(params)); | |||
| @@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| // 提交 | |||
| 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 ( | |||
| @@ -108,32 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| > | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={40} /> | |||
| </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"> | |||
| <Select | |||
| allowClear | |||
| @@ -172,24 +99,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| allowClear | |||
| /> | |||
| </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 | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -200,29 +109,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </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> | |||
| </KFModal> | |||
| ); | |||
| @@ -1,25 +1,8 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| 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 { 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'> { | |||
| typeList: CategoryData[]; | |||
| @@ -28,18 +11,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> { | |||
| } | |||
| 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 [res] = await to(addModel(params)); | |||
| @@ -51,22 +22,11 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| // 提交 | |||
| 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 ( | |||
| @@ -99,32 +59,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| > | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={40} /> | |||
| </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"> | |||
| <Select | |||
| allowClear | |||
| @@ -163,24 +97,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| allowClear | |||
| /> | |||
| </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 | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -191,28 +107,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </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> | |||
| </KFModal> | |||
| ); | |||
| @@ -15,7 +15,7 @@ import { | |||
| type UploadProps, | |||
| } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| import { useState } from 'react'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from '../AddDatasetModal/index.less'; | |||
| interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||
| @@ -40,6 +40,23 @@ function AddVersionModal({ | |||
| }: AddVersionModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| 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 = { | |||
| @@ -109,6 +126,7 @@ function AddVersionModal({ | |||
| }} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| <Form.Item | |||
| 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 | |||
| label="版本描述" | |||
| @@ -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; | |||
| @@ -28,8 +28,15 @@ | |||
| border-radius: 4px; | |||
| } | |||
| &__desc { | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| } | |||
| &__praise { | |||
| display: flex; | |||
| flex: none; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 70px; | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 数据集、模型详情 | |||
| */ | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { | |||
| ResourceData, | |||
| @@ -19,10 +20,11 @@ import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| 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 { useCallback, useEffect, useState } from 'react'; | |||
| import AddVersionModal from '../AddVersionModal'; | |||
| import EditVersionModal from '../EditVersionModal'; | |||
| import ResourceIntro from '../ResourceIntro'; | |||
| import ResourceVersion from '../ResourceVersion'; | |||
| import VersionCompareModal from '../VersionCompareModal'; | |||
| @@ -61,21 +63,24 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| 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( | |||
| @@ -100,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| } | |||
| } else { | |||
| setVersion(undefined); | |||
| getResourceDetail(undefined); | |||
| } | |||
| }, | |||
| [config, owner, identifier, versionParam], | |||
| [config, owner, identifier, versionParam, getResourceDetail], | |||
| ); | |||
| useEffect(() => { | |||
| if (version) { | |||
| getResourceDetail(); | |||
| getResourceDetail(version); | |||
| } | |||
| }, [version, getResourceDetail]); | |||
| @@ -116,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| }, [getVersionList]); | |||
| // 新建版本 | |||
| const showModal = () => { | |||
| const showAddVersionModal = () => { | |||
| const { close } = openAntdModal(AddVersionModal, { | |||
| resourceType: resourceType, | |||
| 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 { close } = openAntdModal(VersionSelectorModal, { | |||
| @@ -278,44 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| <span>{info.praises_count}</span> | |||
| </div> | |||
| </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 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> | |||
| ); | |||
| @@ -9,11 +9,15 @@ import { | |||
| deleteDatasetVersion, | |||
| deleteModel, | |||
| deleteModelVersion, | |||
| editDatasetVersion, | |||
| editModelVersion, | |||
| getDatasetInfo, | |||
| getDatasetList, | |||
| getDatasetNextVersionReq, | |||
| getDatasetVersionList, | |||
| getModelInfo, | |||
| getModelList, | |||
| getModelNextVersionReq, | |||
| getModelVersionList, | |||
| } from '@/services/dataset/index.js'; | |||
| import { limitUploadFileType } from '@/utils/ui'; | |||
| @@ -36,9 +40,11 @@ type ResourceTypeInfo = { | |||
| getVersions: (params: any) => Promise<any>; // 获取版本列表 | |||
| deleteRecord: (params: any) => Promise<any>; // 删除 | |||
| addVersion: (params: any) => Promise<any>; // 新增版本 | |||
| editVersion: (params: any) => Promise<any>; // 编辑版本 | |||
| deleteVersion: (params: any) => Promise<any>; // 删除版本 | |||
| getInfo: (params: any) => Promise<any>; // 获取详情 | |||
| compareVersion: (params: any) => Promise<any>; // 版本对比 | |||
| getNextVersion: (params: any) => Promise<any>; // 获取下一个版本 | |||
| name: string; // 名称 | |||
| typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 | |||
| tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 | |||
| @@ -65,9 +71,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| getVersions: getDatasetVersionList, | |||
| deleteRecord: deleteDataset, | |||
| addVersion: addDatasetVersion, | |||
| editVersion: editDatasetVersion, | |||
| deleteVersion: deleteDatasetVersion, | |||
| getInfo: getDatasetInfo, | |||
| compareVersion: compareDatasetVersion, | |||
| getNextVersion: getDatasetNextVersionReq, | |||
| name: '数据集', | |||
| typeParamKey: 'data_type', | |||
| tagParamKey: 'data_tag', | |||
| @@ -103,9 +111,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| getVersions: getModelVersionList, | |||
| deleteRecord: deleteModel, | |||
| addVersion: addModelVersion, | |||
| editVersion: editModelVersion, | |||
| deleteVersion: deleteModelVersion, | |||
| getInfo: getModelInfo, | |||
| compareVersion: compareModelVersion, | |||
| getNextVersion: getModelNextVersionReq, | |||
| name: '模型', | |||
| typeParamKey: 'model_type', | |||
| tagParamKey: 'model_tag', | |||
| @@ -4,10 +4,11 @@ | |||
| * @Description: 开发环境列表 | |||
| */ | |||
| import { CodeConfigData } from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { DevEditorStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { DatasetData, ModelData } from '@/pages/Dataset/config'; | |||
| import { | |||
| deleteEditorReq, | |||
| @@ -17,12 +18,7 @@ import { | |||
| } from '@/services/developmentEnvironment'; | |||
| import themes from '@/styles/theme.less'; | |||
| 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 { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| @@ -55,7 +51,7 @@ export type EditorData = { | |||
| dataset?: string | DatasetData; | |||
| model?: string | ModelData; | |||
| image?: string; | |||
| code_config?: string | SelectedCodeConfig; | |||
| code_config?: string | CodeConfigData; | |||
| }; | |||
| function EditorList() { | |||
| @@ -70,7 +66,7 @@ function EditorList() { | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| // 获取编辑器列表 | |||
| const getEditorList = useCallback(async () => { | |||
| @@ -211,7 +207,7 @@ function EditorList() { | |||
| const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| const codeConfig = record.code_config as SelectedCodeConfig; | |||
| const codeConfig = record.code_config as CodeConfigData; | |||
| const url = formatCodeConfig(codeConfig)?.url; | |||
| if (url) { | |||
| window.open(url, '_blank'); | |||
| @@ -95,16 +95,13 @@ function ExperimentText() { | |||
| return; | |||
| } | |||
| const workflow = parseJsonText(dag); | |||
| const workflow = dag; | |||
| const experimentStatusObjs = parseJsonText(nodes_status); | |||
| if (!workflow || !workflow.nodes) { | |||
| return; | |||
| } | |||
| workflow.nodes.forEach((item) => { | |||
| item.in_parameters = parseJsonText(item.in_parameters); | |||
| item.out_parameters = parseJsonText(item.out_parameters); | |||
| item.control_strategy = parseJsonText(item.control_strategy); | |||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||
| }); | |||
| workflowRef.current = workflow; | |||
| @@ -140,8 +137,11 @@ function ExperimentText() { | |||
| } else if (status === ExperimentStatus.Running) { | |||
| // 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点 | |||
| const node = | |||
| workflow.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running || item.experimentStatus === ExperimentStatus.Pending) ?? | |||
| workflow.nodes[0]; | |||
| workflow.nodes.find( | |||
| (item) => | |||
| item.experimentStatus === ExperimentStatus.Running || | |||
| item.experimentStatus === ExperimentStatus.Pending, | |||
| ) ?? workflow.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| @@ -567,12 +567,13 @@ function ExperimentText() { | |||
| instanceNodeStatus={experimentNodeData.experimentStatus} | |||
| instanceNodeStartTime={experimentNodeData.experimentStartTime} | |||
| instanceNodeEndTime={experimentNodeData.experimentEndTime} | |||
| globalParams={experimentIns?.global_param} | |||
| ></ExperimentDrawer> | |||
| ) : null} | |||
| <ParamsModal | |||
| open={paramsModalOpen} | |||
| onCancel={closeParamsModal} | |||
| globalParam={experimentIns?.global_param} | |||
| globalParams={experimentIns?.global_param} | |||
| ></ParamsModal> | |||
| </div> | |||
| ); | |||
| @@ -6,4 +6,10 @@ | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 6px; | |||
| } | |||
| :global { | |||
| .ant-form-item-row { | |||
| align-items: center; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import createExperimentIcon from '@/assets/img/create-experiment.png'; | |||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; | |||
| import { useState } from 'react'; | |||
| @@ -32,7 +32,7 @@ interface Workflow { | |||
| // 根据参数设置输入组件 | |||
| export const getParamComponent = (paramType: number): JSX.Element => { | |||
| // 防止后台返回不是 number 类型 | |||
| if (Number(paramType) === 3) { | |||
| if (Number(paramType) === PipelineGlobalParamType.Boolean) { | |||
| return ( | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| @@ -50,7 +50,7 @@ export const getParamComponent = (paramType: number): JSX.Element => { | |||
| export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => { | |||
| const rules = []; | |||
| // 防止后台返回不是 number 类型 | |||
| if (Number(paramType) === 2) { | |||
| if (Number(paramType) === PipelineGlobalParamType.Number) { | |||
| rules.push({ | |||
| pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/, | |||
| message: '整型必须是数字', | |||
| @@ -64,10 +64,10 @@ export const getParamRules = (paramType: number, required: boolean = false): For | |||
| // 根据参数设置 label | |||
| export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { | |||
| const paramTypes: Readonly<Record<number, string>> = { | |||
| 1: '字符串', | |||
| 2: '整型', | |||
| 3: '布尔类型', | |||
| const paramTypes: Readonly<Record<PipelineGlobalParamType, string>> = { | |||
| [PipelineGlobalParamType.String]: '字符串', | |||
| [PipelineGlobalParamType.Number]: '整型', | |||
| [PipelineGlobalParamType.Boolean]: '布尔类型', | |||
| }; | |||
| const label = param.param_name + `(${paramTypes[param.param_type]})`; | |||
| return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; | |||
| @@ -1,7 +1,7 @@ | |||
| import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs, Typography } from 'antd'; | |||
| @@ -25,6 +25,7 @@ type ExperimentDrawerProps = { | |||
| instanceNodeStatus?: ExperimentStatus; // 实例节点状态 | |||
| instanceNodeStartTime?: string; // 开始时间 | |||
| instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 | |||
| globalParams?: PipelineGlobalParam[] | null; // 全局参数 | |||
| }; | |||
| const ExperimentDrawer = ({ | |||
| @@ -41,6 +42,7 @@ const ExperimentDrawer = ({ | |||
| instanceNodeStatus, | |||
| instanceNodeStartTime, | |||
| instanceNodeEndTime, | |||
| globalParams, | |||
| }: ExperimentDrawerProps) => { | |||
| // 如果性能有问题,可以进一步拆解 | |||
| const items = useMemo( | |||
| @@ -66,7 +68,7 @@ const ExperimentDrawer = ({ | |||
| key: '2', | |||
| label: '配置参数', | |||
| icon: <DatabaseOutlined />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} globalParams={globalParams} />, | |||
| }, | |||
| { | |||
| key: '3', | |||
| @@ -94,6 +96,7 @@ const ExperimentDrawer = ({ | |||
| experimentName, | |||
| experimentId, | |||
| pipelineId, | |||
| globalParams, | |||
| ], | |||
| ); | |||
| @@ -15,4 +15,24 @@ | |||
| font-size: @font-size; | |||
| background: #f8fbff; | |||
| } | |||
| &__form-list { | |||
| :global { | |||
| .ant-row { | |||
| padding: 0 !important; | |||
| } | |||
| } | |||
| &:last-child { | |||
| :global { | |||
| .ant-form-item { | |||
| margin-bottom: 0 !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__list-empty { | |||
| color: @text-color-tertiary; | |||
| } | |||
| } | |||
| @@ -1,25 +1,92 @@ | |||
| import FormInfo from '@/components/FormInfo'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { Form } from 'antd'; | |||
| import { ComponentType } from '@/enums'; | |||
| import type { | |||
| PipelineGlobalParam, | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { Flex, Form } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ExperimentParameterProps = { | |||
| nodeData: PipelineNodeModelSerialize; | |||
| globalParams?: PipelineGlobalParam[] | null; // 全局参数 | |||
| }; | |||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| function ExperimentParameter({ nodeData, globalParams }: ExperimentParameterProps) { | |||
| // 表单组件 | |||
| const getFormComponent = ( | |||
| item: { key: string; value: PipelineNodeModelParameter }, | |||
| parentName: string, | |||
| ) => { | |||
| return ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={[parentName, item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| {item.value.type === ComponentType.Map && ( | |||
| <Form.List name={[parentName, item.key, 'value']}> | |||
| {(fields) => ( | |||
| <> | |||
| {fields.length > 0 ? ( | |||
| fields.map(({ key, name, ...restField }) => ( | |||
| <Flex | |||
| key={key} | |||
| gap="0 8px" | |||
| style={{ width: '100%' }} | |||
| className={styles['experiment-parameter__form-list']} | |||
| > | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| style={{ flex: 1, minWidth: 0 }} | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <span style={{ lineHeight: '32px' }}>=</span> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'value']} | |||
| style={{ flex: 1, minWidth: 0 }} | |||
| > | |||
| <FormInfo valuePropName="showValue" globalParams={globalParams} /> | |||
| </Form.Item> | |||
| </Flex> | |||
| )) | |||
| ) : ( | |||
| <div className={styles['experiment-parameter__list-empty']}>无</div> | |||
| )} | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| )} | |||
| {item.value.type === ComponentType.Select && | |||
| (['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect dataType={item.value.item_type as ParameterSelectDataType} display /> | |||
| ) : null)} | |||
| {item.value.type !== ComponentType.Map && item.value.type !== ComponentType.Select && ( | |||
| <FormInfo valuePropName="showValue" globalParams={globalParams} /> | |||
| )} | |||
| </Form.Item> | |||
| ); | |||
| }; | |||
| // 基本参数 | |||
| const basicParametersList = Object.entries(nodeData.task_info ?? {}) | |||
| .map(([key, value]) => ({ | |||
| key, | |||
| value, | |||
| })) | |||
| .filter((v) => v.value.visible === true); | |||
| // 控制策略 | |||
| // const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( | |||
| // ([key, value]) => ({ key, value }), | |||
| // ); | |||
| const nodeId = nodeData.id; | |||
| const hasTaskInfo = | |||
| nodeId && | |||
| !nodeId.startsWith('git-clone') && | |||
| !nodeId.startsWith('dataset-export') && | |||
| !nodeId.startsWith('model-export'); | |||
| const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}) | |||
| .map(([key, value]) => ({ key, value })) | |||
| .filter((v) => v.value.visible === true); | |||
| // 输入参数 | |||
| const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({ | |||
| @@ -80,96 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| {hasTaskInfo && ( | |||
| {basicParametersList.length + controlStrategyList.length > 0 && ( | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="任务信息" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| )} | |||
| {/* 基本参数 */} | |||
| {basicParametersList.map((item) => getFormComponent(item, 'task_info'))} | |||
| {/* 控制参数 */} | |||
| {controlStrategyList.map((item) => getFormComponent(item, 'control_strategy'))} | |||
| {/* 输入参数 */} | |||
| {inParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="任务信息" | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| <Form.Item | |||
| label="镜像" | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像', | |||
| }, | |||
| ]} | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <Form.Item label="工作目录" name="working_directory"> | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| {inParametersList.map((item) => getFormComponent(item, 'in_parameters'))} | |||
| </> | |||
| )} | |||
| <Form.Item label="启动命令" name="command"> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="资源规格" | |||
| name="resources_standard" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入资源规格', | |||
| }, | |||
| ]} | |||
| > | |||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> | |||
| </Form.Item> | |||
| {/* <Form.Item label="挂载路径" name="mount_path"> | |||
| <FormInfo /> | |||
| </Form.Item> */} | |||
| <Form.Item label="环境变量" name="env_variables"> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| {/* {controlStrategyList.map((item) => ( | |||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} */} | |||
| {/* 输出参数 */} | |||
| {outParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输出参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {outParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} | |||
| </> | |||
| )} | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {inParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['in_parameters', item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| {item.value.type === 'select' ? ( | |||
| ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect dataType={item.value.item_type as any} display /> | |||
| ) : null | |||
| ) : ( | |||
| <FormInfo valuePropName="showValue" /> | |||
| )} | |||
| </Form.Item> | |||
| ))} | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输出参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {outParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} | |||
| </Form> | |||
| ); | |||
| } | |||
| @@ -3,12 +3,10 @@ import KFModal from '@/components/KFModal'; | |||
| import { | |||
| DataSource, | |||
| ResourceType, | |||
| ResourceVersionData, | |||
| resourceConfig, | |||
| type ResourceData, | |||
| } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| import { InfoCircleOutlined } from '@ant-design/icons'; | |||
| import { Form, Input, ModalProps, Select } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| @@ -44,7 +42,6 @@ function ExportModelModal({ | |||
| }: ExportModelModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [resources, setResources] = useState<ResourceData[]>([]); | |||
| const [versions, setVersions] = useState<ResourceVersionData[]>([]); | |||
| const config = resourceConfig[resourceType]; | |||
| const layout = { | |||
| @@ -77,35 +74,24 @@ function ExportModelModal({ | |||
| 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) => { | |||
| if (id) { | |||
| getRecourceVersions(id); | |||
| getRecourceNextVersion(id); | |||
| } else { | |||
| setVersions([]); | |||
| form.setFieldValue('version', ''); | |||
| } | |||
| }; | |||
| // 获取数据集、模型版本列表 | |||
| const getRecourceVersions = async (id: number) => { | |||
| // 获取数据集、模型下一个版本 | |||
| const getRecourceNextVersion = async (id: number) => { | |||
| const resource = getSelectedResource(id); | |||
| if (!resource) { | |||
| 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) { | |||
| setVersions(res.data); | |||
| form.setFieldValue('version', res.data); | |||
| } | |||
| }; | |||
| @@ -184,15 +170,6 @@ function ExportModelModal({ | |||
| <Form.Item | |||
| label={`${config.name}版本`} | |||
| name="version" | |||
| tooltip={ | |||
| getTooltip() | |||
| ? { | |||
| overlayClassName: styles['export-model-modal__tooltip'], | |||
| title: getTooltip(), | |||
| icon: <InfoCircleOutlined />, | |||
| } | |||
| : undefined | |||
| } | |||
| rules={[ | |||
| { required: true, message: `请输入${config.name}版本` }, | |||
| { | |||
| @@ -205,8 +182,6 @@ function ExportModelModal({ | |||
| return Promise.reject(`${config.name}版本不能为 master`); | |||
| } else if (value === 'origin') { | |||
| return Promise.reject(`${config.name}版本不能为 origin`); | |||
| } else if (value && versions.map((item) => item.name).includes(value)) { | |||
| return Promise.reject(`${config.name}版本已存在`); | |||
| } else { | |||
| 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 | |||
| label="版本描述" | |||
| @@ -4,6 +4,12 @@ | |||
| overflow-y: auto; | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 8px; | |||
| :global { | |||
| .ant-form-item-row { | |||
| align-items: center; | |||
| } | |||
| } | |||
| } | |||
| .params-empty { | |||
| :global { | |||
| @@ -14,10 +14,10 @@ import styles from './index.less'; | |||
| type ParamsModalProps = { | |||
| open: boolean; | |||
| onCancel: () => void; | |||
| globalParam?: PipelineGlobalParam[] | null; | |||
| globalParams?: PipelineGlobalParam[] | null; | |||
| }; | |||
| function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) { | |||
| return ( | |||
| <KFModal | |||
| title="执行参数" | |||
| @@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| cancelButtonProps={{ style: { display: 'none' } }} | |||
| width={825} | |||
| > | |||
| {Array.isArray(globalParam) && globalParam.length > 0 ? ( | |||
| {Array.isArray(globalParams) && globalParams.length > 0 ? ( | |||
| <div className={styles['params-container']}> | |||
| <Form | |||
| name="view_params_form" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| initialValues={{ global_param: globalParam }} | |||
| initialValues={{ global_param: globalParams }} | |||
| labelAlign="left" | |||
| disabled | |||
| > | |||
| @@ -45,9 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| {...restField} | |||
| key={key} | |||
| name={[name, 'param_value']} | |||
| label={getParamLabel(globalParam[name])} | |||
| label={getParamLabel(globalParams[name])} | |||
| > | |||
| {getParamComponent(globalParam[name]['param_type'])} | |||
| {getParamComponent(globalParams[name]['param_type'])} | |||
| </Form.Item> | |||
| )) | |||
| } | |||
| @@ -226,14 +226,14 @@ function Experiment() { | |||
| if (type === ExperimentCompleted) { | |||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| console.log( | |||
| '实验实例状态变化', | |||
| currentIns?.status, | |||
| status, | |||
| experimentId, | |||
| experimentInsId, | |||
| finishTime, | |||
| ); | |||
| // console.log( | |||
| // '实验实例状态变化', | |||
| // currentIns?.status, | |||
| // status, | |||
| // experimentId, | |||
| // experimentInsId, | |||
| // finishTime, | |||
| // ); | |||
| if ( | |||
| !currentIns || | |||
| @@ -323,6 +323,7 @@ function ExecuteConfig() { | |||
| className={styles['hyper-parameter__body__name']} | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| dependencies={fields.map((_, i) => ['parameters', i, 'name'])} | |||
| required | |||
| rules={[ | |||
| { | |||
| @@ -1,6 +1,6 @@ | |||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||
| import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | |||
| import { | |||
| schedulerAlgorithms, | |||
| @@ -41,7 +41,7 @@ function HyperParameterBasic({ | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: HyperParameterBasicProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| @@ -46,6 +46,7 @@ export type MirrorInfoData = { | |||
| }; | |||
| export type MirrorVersionData = { | |||
| image_id: number; | |||
| id: number; | |||
| version: string; | |||
| url: string; | |||
| @@ -23,7 +23,7 @@ import { removeFormListItem } from '@/utils/ui'; | |||
| import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| 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 { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; | |||
| import styles from './index.less'; | |||
| @@ -79,7 +79,7 @@ function CreateServiceVersion() { | |||
| if (res.model && typeof res.model === 'object') { | |||
| model = changePropertyName(res.model, { show_value: 'showValue' }); | |||
| // 接口返回是数据没有 value 值,但是 form 需要 value | |||
| model.value = model.showValue; | |||
| // model.value = model.showValue; | |||
| } | |||
| // 环境变量 | |||
| if (res.env_variables && typeof res.env_variables === 'object') { | |||
| @@ -117,7 +117,6 @@ function CreateServiceVersion() { | |||
| // 创建版本 | |||
| const createServiceVersion = async (formData: FormData) => { | |||
| const envList = formData['env_variables']; | |||
| const model = formData['model']; | |||
| const envVariables = envList?.reduce((acc, cur) => { | |||
| acc[cur.key] = cur.value; | |||
| return acc; | |||
| @@ -125,13 +124,9 @@ function CreateServiceVersion() { | |||
| // 根据后台要求,修改表单数据 | |||
| const object = { | |||
| ...omit(formData, ['replicas', 'env_variables', 'model']), | |||
| ...omit(formData, ['replicas', 'env_variables']), | |||
| replicas: Number(formData.replicas), | |||
| env_variables: envVariables, | |||
| model: changePropertyName( | |||
| pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), | |||
| { showValue: 'show_value' }, | |||
| ), | |||
| service_id: serviceId, | |||
| }; | |||
| @@ -427,6 +422,7 @@ function CreateServiceVersion() { | |||
| {...restField} | |||
| name={[name, 'key']} | |||
| style={{ flex: 1 }} | |||
| dependencies={fields.map((_, i) => ['env_variables', i, 'key'])} | |||
| rules={[ | |||
| { | |||
| validator: (_, value) => { | |||
| @@ -9,7 +9,8 @@ import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { ModelData } from '@/pages/Dataset/config'; | |||
| import { | |||
| deleteServiceVersionReq, | |||
| getServiceInfoReq, | |||
| @@ -18,6 +19,7 @@ import { | |||
| } from '@/services/modelDeployment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatModel } from '@/utils/format'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| @@ -87,7 +89,7 @@ function ServiceInfo() { | |||
| format: formatDate, | |||
| }, | |||
| ]; | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| // 获取服务详情 | |||
| const getServiceInfo = useCallback(async () => { | |||
| @@ -110,8 +112,8 @@ function ServiceInfo() { | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| 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); | |||
| @@ -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'] = [ | |||
| { | |||
| title: '序号', | |||
| @@ -278,10 +294,12 @@ function ServiceInfo() { | |||
| }, | |||
| { | |||
| title: '模型版本', | |||
| dataIndex: ['model', 'show_value'], | |||
| dataIndex: ['model', 'showValue'], | |||
| key: 'model', | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| render: tableCellRender(true, TableCellValueType.Link, { | |||
| onClick: gotoModel, | |||
| }), | |||
| }, | |||
| { | |||
| title: '镜像版本', | |||
| @@ -1,6 +1,6 @@ | |||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatMirror, formatModel } from '@/utils/format'; | |||
| @@ -36,7 +36,7 @@ const formatEnvText = (env?: Record<string, string>) => { | |||
| }; | |||
| function VersionBasicInfo({ info }: BasicInfoProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const datas: BasicInfoData[] = [ | |||
| { | |||
| @@ -1,6 +1,6 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { getServiceVersionCompareReq } from '@/services/modelDeployment'; | |||
| import { isEmpty } from '@/utils'; | |||
| @@ -42,7 +42,7 @@ const formatEnvText = (env: Record<string, string>) => { | |||
| function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) { | |||
| const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const fields: FiledType[] = useMemo( | |||
| () => [ | |||
| @@ -34,7 +34,7 @@ export type ServiceVersionData = { | |||
| path: string; | |||
| identifier: string; | |||
| owner: string; | |||
| show_value: string; | |||
| showValue: string; | |||
| }; | |||
| code_config: { | |||
| // 代码配置 | |||
| @@ -3,7 +3,7 @@ import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { useVisible } from '@/hooks/useVisible'; | |||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||
| import { fittingString, s8 } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6 from '@antv/g6'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| @@ -54,11 +54,20 @@ const EditPipeline = () => { | |||
| const onDragEnd = (val) => { | |||
| const { x, y } = val; | |||
| 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 = { | |||
| ...val, | |||
| x: point.x, | |||
| y: point.y, | |||
| label, | |||
| id: val.component_name + '-' + s8(), | |||
| isCluster: false, | |||
| formError: true, | |||
| @@ -90,24 +99,29 @@ const EditPipeline = () => { | |||
| // 保存 | |||
| 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(() => { | |||
| const data = graph.save(); | |||
| // console.log(data); | |||
| // 验证节点必填参数 | |||
| const errorNode = data.nodes.find((item) => item.formError === true); | |||
| if (errorNode) { | |||
| message.error(`【${errorNode.label}】节点配置验证失败`); | |||
| @@ -117,11 +131,25 @@ const EditPipeline = () => { | |||
| } | |||
| 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 = { | |||
| ...locationParams, | |||
| name: workflowInfo?.name, | |||
| dag: JSON.stringify(data), | |||
| global_param: JSON.stringify(globalParamRes.global_param), | |||
| dag: data, | |||
| global_param: globalParam, | |||
| }; | |||
| saveWorkflow(params).then((ret) => { | |||
| message.success('保存成功'); | |||
| @@ -290,7 +318,7 @@ const EditPipeline = () => { | |||
| const { global_param, dag } = res.data; | |||
| setGlobalParam(global_param || []); | |||
| if (dag) { | |||
| getGraphData(parseJsonText(dag)); | |||
| getGraphData(dag); | |||
| } | |||
| } | |||
| }; | |||
| @@ -299,13 +327,19 @@ const EditPipeline = () => { | |||
| const openNodeDrawer = (node, validate = false) => { | |||
| // 获取所有的上游节点 | |||
| 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); | |||
| }; | |||
| // 关闭全局参数节点,获取全局参数 | |||
| const closeGlobalParamsDrawer = () => { | |||
| const { global_param } = paramsDrawerRef.current.getFieldsValue(); | |||
| setGlobalParam(global_param); | |||
| closeParamsDrawer(); | |||
| }; | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| const contextMenu = initMenu(); | |||
| @@ -730,7 +764,7 @@ const EditPipeline = () => { | |||
| ref={paramsDrawerRef} | |||
| open={paramsDrawerOpen} | |||
| globalParam={globalParam} | |||
| onClose={closeParamsDrawer} | |||
| onClose={closeGlobalParamsDrawer} | |||
| ></GlobalParamsDrawer> | |||
| </div> | |||
| ); | |||
| @@ -1,5 +1,4 @@ | |||
| import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { Graph, INode } from '@antv/g6'; | |||
| import { type MenuProps } from 'antd'; | |||
| @@ -42,8 +41,7 @@ export function createMenuItems( | |||
| ): MenuProps['items'] { | |||
| const nodes: MenuProps['items'] = parentNodes.map((item) => { | |||
| 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 ?? {}); | |||
| return { | |||
| 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) { | |||
| const { type, item_type } = parameter; | |||
| @@ -89,6 +78,9 @@ export function canInput(parameter: PipelineNodeModelParameter) { | |||
| (item_type === 'dataset' || | |||
| item_type === 'model' || | |||
| item_type === 'image' || | |||
| item_type === 'code') | |||
| item_type === 'code' || | |||
| item_type === 'remote-dataset' || | |||
| item_type === 'remote-model' || | |||
| item_type === 'remote-code') | |||
| ); | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { type PipelineGlobalParam, PipelineGlobalParamType } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { PlusOutlined } from '@ant-design/icons'; | |||
| @@ -42,6 +42,7 @@ const GlobalParamsDrawer = forwardRef( | |||
| form.setFieldValue(name, null); | |||
| }; | |||
| // 处理删除 | |||
| const removeParameter = (name: number, remove: (param: number) => void) => { | |||
| modalConfirm({ | |||
| title: '删除后,该全局参数将不可恢复', | |||
| @@ -52,6 +53,16 @@ const GlobalParamsDrawer = forwardRef( | |||
| }); | |||
| }; | |||
| // 处理关闭 | |||
| const handleClose = async () => { | |||
| try { | |||
| await form.validateFields(); | |||
| onClose(); | |||
| } catch { | |||
| return false; | |||
| } | |||
| }; | |||
| return ( | |||
| <Drawer | |||
| rootStyle={{ marginTop: '55px' }} | |||
| @@ -59,7 +70,7 @@ const GlobalParamsDrawer = forwardRef( | |||
| placement="right" | |||
| closeIcon={false} | |||
| getContainer={false} | |||
| onClose={onClose} | |||
| onClose={handleClose} | |||
| open={open} | |||
| width={520} | |||
| > | |||
| @@ -81,7 +92,7 @@ const GlobalParamsDrawer = forwardRef( | |||
| {...restField} | |||
| name={[name, 'param_name']} | |||
| label="参数名称" | |||
| validateTrigger={[]} | |||
| dependencies={fields.map((_, i) => ['global_param', i, 'param_name'])} | |||
| rules={[ | |||
| { required: true, message: '请输入参数名称' }, | |||
| { | |||
| @@ -97,11 +108,7 @@ const GlobalParamsDrawer = forwardRef( | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入参数名称" | |||
| allowClear | |||
| onBlur={() => form.validateFields()} | |||
| /> | |||
| <Input placeholder="请输入参数名称" allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| @@ -124,9 +131,9 @@ const GlobalParamsDrawer = forwardRef( | |||
| <Radio.Group | |||
| onChange={() => handleTypeChange(['global_param', name, 'param_value'])} | |||
| > | |||
| <Radio value={1}>字符串</Radio> | |||
| <Radio value={2}>整型</Radio> | |||
| <Radio value={3}>布尔类型</Radio> | |||
| <Radio value={PipelineGlobalParamType.String}>字符串</Radio> | |||
| <Radio value={PipelineGlobalParamType.Number}>整型</Radio> | |||
| <Radio value={PipelineGlobalParamType.Boolean}>布尔类型</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| @@ -21,10 +21,7 @@ | |||
| background: #f8fbff; | |||
| } | |||
| &__ref-row { | |||
| display: flex; | |||
| align-items: center; | |||
| &__component { | |||
| &__select-button { | |||
| display: flex; | |||
| flex: none; | |||
| @@ -34,5 +31,29 @@ | |||
| padding-right: 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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; | |||
| import ParameterSelect, { | |||
| type ParameterSelectDataType, | |||
| type ParameterSelectObject, | |||
| ParameterSelectTypeList, | |||
| } from '@/components/ParameterSelect'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/components/ResourceSelectorModal'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { CommonTabKeys, ComponentType } from '@/enums'; | |||
| import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; | |||
| import state from '@/state/jcdResource'; | |||
| import { | |||
| PipelineGlobalParam, | |||
| PipelineNodeModel, | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { removeFormListItem } from '@/utils/ui'; | |||
| import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| 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 { NamePath } from 'antd/es/form/interface'; | |||
| import { omit } from 'lodash'; | |||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | |||
| import PropsLabel from '../PropsLabel'; | |||
| import styles from './index.less'; | |||
| const { TextArea } = Input; | |||
| // 表单列表数据 | |||
| export type FormListVariable = { | |||
| name: string; // 参数名 | |||
| value: string; // 参数值 | |||
| }; | |||
| type PipelineNodeParameterProps = { | |||
| onFormChange: (data: PipelineNodeModelSerialize) => void; | |||
| @@ -37,14 +49,10 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| const [stagingItem, setStagingItem] = useState<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 [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| const snap = useSnapshot(state); | |||
| const { setCurrentType } = snap; | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| @@ -53,11 +61,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| // 不管是否验证成功,都需要获取表单数据 | |||
| 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 = { | |||
| ...stagingItem.in_parameters, | |||
| @@ -68,17 +81,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| ...fields.out_parameters, | |||
| }; | |||
| // console.log('getFieldsValue', fields); | |||
| console.log('getFieldsValue', fields); | |||
| 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, | |||
| }; | |||
| // console.log('res', res); | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| @@ -96,19 +110,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| validate: boolean = false, | |||
| ) { | |||
| 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({ | |||
| ...nodeData, | |||
| ...model, | |||
| }); | |||
| form.resetFields(); | |||
| form.setFieldsValue({ | |||
| ...nodeData, | |||
| ...model, | |||
| }); | |||
| if (validate) { | |||
| form.validateFields(); | |||
| @@ -120,6 +127,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| // 参数下拉菜单 | |||
| setMenuItems(createMenuItems(params, parentNodes)); | |||
| // 云际组件,设置 store 当前资源类型 | |||
| if (model.id.startsWith('remote-task')) { | |||
| const resourceType = model.in_parameters['--resource_type'].value; | |||
| setCurrentType(resourceType); | |||
| } | |||
| }, | |||
| close: () => { | |||
| onClose(); | |||
| @@ -137,7 +150,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| } | |||
| }, | |||
| }), | |||
| [form, open], | |||
| [form, open, setCurrentType], | |||
| ); | |||
| // ref 类型选择 | |||
| @@ -145,7 +158,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| formItemName: NamePath, | |||
| item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | |||
| ) => { | |||
| if (item.item_type === 'code') { | |||
| if (item.item_type === 'code' || item.item_type === 'remote-code') { | |||
| selectCodeConfig(formItemName, item); | |||
| } else { | |||
| selectResource(formItemName, item); | |||
| @@ -157,47 +170,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| formItemName: NamePath, | |||
| 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, { | |||
| defaultSelected, | |||
| onOk: (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, { | |||
| ...item, | |||
| value, | |||
| value: res, | |||
| showValue: code_repo_name, | |||
| fromSelect: true, | |||
| }); | |||
| @@ -216,9 +197,11 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| let type: ResourceSelectorType; | |||
| switch (item.item_type) { | |||
| case 'dataset': | |||
| case 'remote-dataset': | |||
| type = ResourceSelectorType.Dataset; | |||
| break; | |||
| case 'model': | |||
| case 'remote-model': | |||
| type = ResourceSelectorType.Model; | |||
| break; | |||
| default: | |||
| @@ -237,36 +220,24 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| onOk: (res) => { | |||
| if (res) { | |||
| 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}`; | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value, | |||
| value: rest, | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| @@ -275,19 +246,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| }); | |||
| } | |||
| } 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]); | |||
| close(); | |||
| @@ -298,14 +265,14 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (item: { item_type: string }) => { | |||
| const type = item.item_type; | |||
| if (type === 'code') { | |||
| if (type === 'code' || type === 'remote-code') { | |||
| return <KFIcon type="icon-xuanzedaimapeizhi" />; | |||
| } | |||
| let selectorType: ResourceSelectorType; | |||
| if (type === 'dataset') { | |||
| if (type === 'dataset' || type === 'remote-dataset') { | |||
| selectorType = ResourceSelectorType.Dataset; | |||
| } else if (type === 'model') { | |||
| } else if (type === 'model' || type === 'remote-model') { | |||
| selectorType = ResourceSelectorType.Model; | |||
| } else { | |||
| selectorType = ResourceSelectorType.Mirror; | |||
| @@ -323,16 +290,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| // form item label | |||
| const getLabel = ( | |||
| 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 + ')' | |||
| ) : ( | |||
| <PropsLabel | |||
| menuItems={menuItems} | |||
| title={item.value.label + '(' + item.key + ')'} | |||
| onClick={(value) => { | |||
| handleParameterClick([namePrefix, item.key], { | |||
| handleParameterClick([parentName, item.key], { | |||
| ...item.value, | |||
| value, | |||
| fromSelect: true, | |||
| @@ -380,21 +347,249 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| 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 ( | |||
| <Drawer | |||
| @@ -406,7 +601,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| onClose={onClose} | |||
| afterOpenChange={afterOpenChange} | |||
| open={open} | |||
| width={520} | |||
| width={620} | |||
| className={styles['pipeline-drawer']} | |||
| > | |||
| <Form | |||
| @@ -455,114 +650,37 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| > | |||
| <Input disabled /> | |||
| </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 | |||
| key={item.key} | |||
| name={['control_strategy', item.key]} | |||
| label={getLabel(item, 'task_info')} | |||
| 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')} | |||
| rules={getFormRules(item)} | |||
| required={item.value.require ? true : false} | |||
| > | |||
| <ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput> | |||
| {getFormComponent(item, 'control_strategy')} | |||
| </Form.Item> | |||
| ))} */} | |||
| </> | |||
| )} | |||
| ))} | |||
| {/* 输入参数 */} | |||
| {inParametersList.length > 0 && ( | |||
| @@ -579,42 +697,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| label={getLabel(item, 'in_parameters')} | |||
| 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> | |||
| ))} | |||
| </> | |||
| )} | |||
| {/* 输出参数 */} | |||
| {outParametersList.length > 0 && ( | |||
| <> | |||
| @@ -24,7 +24,7 @@ const { TextArea } = Input; | |||
| const Pipeline = () => { | |||
| const [form] = Form.useForm(); | |||
| const navigate = useNavigate(); | |||
| const [formId, setFormId] = useState(null); | |||
| const [editRecord, setEditRecord] = useState(null); | |||
| const [dialogTitle, setDialogTitle] = useState('新建流水线'); | |||
| const [pipeList, setPipeList] = useState([]); | |||
| const [total, setTotal] = useState(0); | |||
| @@ -75,7 +75,7 @@ const Pipeline = () => { | |||
| if (ret.code === 200) { | |||
| form.resetFields(); | |||
| form.setFieldsValue({ ...ret.data }); | |||
| setFormId(ret.data.id); | |||
| setEditRecord(ret.data); | |||
| setDialogTitle('编辑流水线'); | |||
| setIsModalOpen(true); | |||
| } | |||
| @@ -99,7 +99,7 @@ const Pipeline = () => { | |||
| // 显示 modal | |||
| const showModal = () => { | |||
| form.resetFields(); | |||
| setFormId(null); | |||
| setEditRecord(null); | |||
| setDialogTitle('新建流水线'); | |||
| setIsModalOpen(true); | |||
| }; | |||
| @@ -111,8 +111,8 @@ const Pipeline = () => { | |||
| // 表单提交 | |||
| const onFinish = (values) => { | |||
| if (formId) { | |||
| editWorkflow({ ...values, id: formId }).then((ret) => { | |||
| if (editRecord) { | |||
| editWorkflow({ ...editRecord, ...values }).then((ret) => { | |||
| setIsModalOpen(false); | |||
| message.success('编辑成功'); | |||
| getList(); | |||
| @@ -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) { | |||
| 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) { | |||
| 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) { | |||
| return request(`/api/mmp/newmodel/getModelDependencyTree`, { | |||
| @@ -201,4 +235,5 @@ export function unpraiseResourceReq(id) { | |||
| return request(`/api/mmp/newmodel/unpraise/${id}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| } | |||
| @@ -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, | |||
| }); | |||
| } | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -29,11 +29,17 @@ export type GlobalInitialState = { | |||
| clientInfo?: ClientInfo; | |||
| }; | |||
| export enum PipelineGlobalParamType { | |||
| String = 1, | |||
| Number = 2, | |||
| Boolean = 3, | |||
| } | |||
| // 流水线全局参数 | |||
| export type PipelineGlobalParam = { | |||
| param_name: string; | |||
| description: string; | |||
| param_type: number; | |||
| param_type: PipelineGlobalParamType; | |||
| param_value: number | string | boolean; | |||
| is_sensitive: number; | |||
| }; | |||
| @@ -66,9 +72,10 @@ export type PipelineNodeModel = { | |||
| label: string; | |||
| experimentStartTime: string; | |||
| 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; | |||
| icon_path: string; | |||
| workflowId?: string; | |||
| @@ -82,11 +89,12 @@ export type PipelineNodeModelParameter = { | |||
| label: string; | |||
| value: any; | |||
| require?: number; | |||
| visible: boolean; | |||
| placeholder?: string; | |||
| describe?: string; | |||
| fromSelect?: boolean; | |||
| showValue?: any; | |||
| editable?: number; | |||
| editable?: boolean; | |||
| showValue?: any; // 前端显示用 | |||
| fromSelect?: boolean; // 前端显示用 | |||
| activeTab?: string; // ResourceSelectorModal tab | |||
| expandedKeys?: string[]; // ResourceSelectorModal expandedKeys | |||
| 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 = { | |||
| @@ -1,4 +1,5 @@ | |||
| import { BasicInfoLink } from '@/components/BasicInfo/types'; | |||
| import { CodeConfigData } from '@/components/CodeSelectorModal'; | |||
| import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||
| import { | |||
| @@ -12,13 +13,6 @@ import { getGitUrl } from '@/utils'; | |||
| // 格式化日期 | |||
| 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) { | |||
| return undefined; | |||
| } | |||
| return mirror.path; | |||
| return mirror.url; | |||
| }; | |||
| /** | |||
| @@ -87,20 +81,20 @@ export const formatMirror = (mirror: ResourceSelectorResponse): string | undefin | |||
| * @return 基本信息链接对象 | |||
| */ | |||
| export const formatCodeConfig = ( | |||
| project?: ProjectDependency | SelectedCodeConfig, | |||
| project?: ProjectDependency | CodeConfigData, | |||
| ): BasicInfoLink | undefined => { | |||
| if (!project) { | |||
| 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 { | |||
| value: showValue || show_value, | |||
| url: getGitUrl(code_path, branch), | |||
| value: code_repo_name, | |||
| url: getGitUrl(git_url, git_branch), | |||
| }; | |||
| } else { | |||
| // 数据集和模型的代码配置 | |||
| // 数据集和模型详情的代码配置 | |||
| const { url, branch, name } = project; | |||
| return { | |||
| value: name, | |||
| @@ -13,8 +13,13 @@ export default class SessionStorage { | |||
| static readonly aimUrlKey = 'aim-url'; | |||
| /** tensorBoard url */ | |||
| static readonly tensorBoardUrlKey = 'tensor-board-url'; | |||
| <<<<<<< HEAD | |||
| /** 登录之前的地址 */ | |||
| static readonly redirectUrl = 'redirect-url'; | |||
| ======= | |||
| // /** 云际系统 Token */ | |||
| // static readonly jccTokenKey = 'jcc-token'; | |||
| >>>>>>> dev-zw | |||
| /** | |||
| * 获取 SessionStorage 值 | |||