From 2692d54b2de04426920765f2c595934408899433 Mon Sep 17 00:00:00 2001 From: zhaowei Date: Tue, 12 Aug 2025 15:31:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=81=E8=A3=85=E4=BA=91=E9=99=85?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParameterSelect/config.tsx | 60 ++++++-- .../src/components/ParameterSelect/index.tsx | 69 ++++++---- .../components/PipelineNodeDrawer/index.tsx | 51 ++++++- react-ui/src/services/external/index.ts | 87 ++++++++++++ react-ui/src/state/jcdResource.ts | 128 ++++++++++++++++++ react-ui/src/utils/sessionStorage.ts | 2 + 6 files changed, 354 insertions(+), 43 deletions(-) create mode 100644 react-ui/src/services/external/index.ts create mode 100644 react-ui/src/state/jcdResource.ts diff --git a/react-ui/src/components/ParameterSelect/config.tsx b/react-ui/src/components/ParameterSelect/config.tsx index 722c174c..651d2f1b 100644 --- a/react-ui/src/components/ParameterSelect/config.tsx +++ b/react-ui/src/components/ParameterSelect/config.tsx @@ -3,6 +3,7 @@ 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 { type SelectProps } from 'antd'; export type SelectPropsConfig = { @@ -10,12 +11,21 @@ export type SelectPropsConfig = { fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 filterOption?: SelectProps['filterOption']; // 过滤函数 - getValue: (value: any) => string | number; - getLabel: (value: any) => string; isObjectValue: boolean; // value 是对象 + getValue?: (value: any) => string | number; // 对象类型时,获取其值 + getLabel?: (value: any) => string; // 对象类型时,获取其 label }; -export const paramSelectConfig: Record = { +export type ParameterSelectDataType = + | 'dataset' + | 'model' + | 'service' + | 'resource' + | 'remote-image' + | 'remote-resource-type' + | 'remote-resource'; + +export const paramSelectConfig: Record = { dataset: { getOptions: async () => { const res = await getDatasetList({ @@ -72,14 +82,44 @@ export const paramSelectConfig: Record = { resource: { fieldNames: resourceFieldNames, filterOption: filterResourceStandard as SelectProps['filterOption'], - // 不会用到 - getValue: () => { - return ''; + isObjectValue: false, + }, + 'remote-resource-type': { + optionFilterProp: 'label', + isObjectValue: false, + getValue: (value: JCCResourceType) => { + return value.value; }, - // 不会用的 - getLabel: () => { - return ''; + getLabel: (value: JCCResourceType) => { + return value.label; }, - isObjectValue: false, + }, + '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, }, }; diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 3c1ab102..7bef834d 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -5,18 +5,22 @@ */ import { useComputingResource } from '@/hooks/useComputingResource'; +import state from '@/state/jcdResource'; import { to } from '@/utils/promise'; +import { useSnapshot } from '@umijs/max'; import { Select, type SelectProps } from 'antd'; -import { useEffect, useMemo, 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 { type ParameterSelectDataType }; export type ParameterSelectObject = { value: any; [key: string]: any; }; -export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource'; +const identityFunc = (value: any) => value; export interface ParameterSelectProps extends SelectProps { /** 类型 */ @@ -25,8 +29,6 @@ export interface ParameterSelectProps extends SelectProps { display?: boolean; /** 值,支持对象,对象必须包含 value */ value?: string | ParameterSelectObject; - /** 用于流水线, 流水线资源规格要求 id 为字符串 */ - isPipeline?: boolean; /** 修改后回调 */ onChange?: (value: string | ParameterSelectObject) => void; } @@ -36,15 +38,14 @@ function ParameterSelect({ dataType, display = false, value, - // isPipeline = false, onChange, ...rest }: ParameterSelectProps) { const [options, setOptions] = useState([]); const propsConfig = paramSelectConfig[dataType]; const { - getLabel, - getValue, + getLabel = identityFunc, + getValue = identityFunc, getOptions, filterOption, fieldNames, @@ -56,28 +57,43 @@ function ParameterSelect({ const valueText = typeof selectValue === 'object' && selectValue !== null ? getValue(selectValue) : selectValue; const [resourceStandardList] = useComputingResource(); - // const computingResource = isPipeline - // ? resourceStandardList.map((v) => ({ - // ...v, - // id: String(v.id), - // })) - // : resourceStandardList; - - const objectOptions = useMemo(() => { - return options?.map((v) => ({ - label: getLabel(v), - value: getValue(v), - })); - }, [getLabel, getValue, options]); + const snap = useSnapshot(state); + const { getResourceTypes } = snap; + + const objectOptions = + dataType === 'remote-resource-type' + ? snap.types + : dataType === 'remote-image' + ? snap.images + : dataType === 'remote-resource' + ? snap.resources + : options; + + // 将对象类型转换为 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(); - options?.forEach((v) => { + objectOptions?.forEach((v) => { map.set(getValue(v), v); }); return map; - }, [options, getValue]); + }, [objectOptions, getValue]); useEffect(() => { // 获取下拉数据 @@ -87,13 +103,14 @@ function ParameterSelect({ if (res) { setOptions(res); } + } else if (dataType === 'remote-resource-type') { + getResourceTypes(); } }; - getSelectOptions(); - }, [getOptions]); + }, [getOptions, dataType, getResourceTypes]); - const selectOptions = dataType === 'resource' ? resourceStandardList : objectOptions; + const selectOptions = dataType === 'resource' ? resourceStandardList : objectSelectOptions; const handleChange = (text: string) => { // 数据集、模型、服务,转换成对象 diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index 32bbbc72..68691d15 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,7 +1,10 @@ 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, +} from '@/components/ParameterSelect'; import ResourceSelectorModal, { ResourceSelectorType, selectorTypeConfig, @@ -9,6 +12,7 @@ import ResourceSelectorModal, { import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys, ComponentType } from '@/enums'; import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; +import state from '@/state/jcdResource'; import { PipelineGlobalParam, PipelineNodeModel, @@ -20,6 +24,7 @@ import { to } from '@/utils/promise'; import { removeFormListItem } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { INode } from '@antv/g6'; +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'; @@ -45,6 +50,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete ); const [open, setOpen] = useState(false); const [menuItems, setMenuItems] = useState([]); + const snap = useSnapshot(state); const afterOpenChange = async () => { if (!open) { @@ -144,7 +150,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete formItemName: NamePath, item: PipelineNodeModelParameter | Pick, ) => { - if (item.item_type === 'code') { + if (item.item_type === 'code' || item.item_type === 'remote-code') { selectCodeConfig(formItemName, item); } else { selectResource(formItemName, item); @@ -183,9 +189,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: @@ -249,14 +257,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 ; } 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; @@ -331,6 +339,21 @@ 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 }, @@ -361,12 +384,26 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete )} {item.value.type === ComponentType.Select && - (['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + ([ + 'dataset', + 'model', + 'service', + 'resource', + 'remote-resource-type', + 'remote-image', + 'remote-resource', + ].includes(item.value.item_type) ? ( + handleParameterSelect( + value as ParameterSelectObject, + item.value.item_type, + parentName, + ) + } /> ) : null)} diff --git a/react-ui/src/services/external/index.ts b/react-ui/src/services/external/index.ts new file mode 100644 index 00000000..22c7b439 --- /dev/null +++ b/react-ui/src/services/external/index.ts @@ -0,0 +1,87 @@ +// 外部系统 + +import { request } from '@umijs/max'; + +// 云际系统登录 +export function jccLoginReq() { + return request(`http://119.45.255.234:30180/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(`http://119.45.255.234:30180/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(`http://119.45.255.234:30180/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(`http://119.45.255.234:30180/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, + }); +} diff --git a/react-ui/src/state/jcdResource.ts b/react-ui/src/state/jcdResource.ts new file mode 100644 index 00000000..53cbcb38 --- /dev/null +++ b/react-ui/src/state/jcdResource.ts @@ -0,0 +1,128 @@ +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; + getResourceTypes: () => void; + getImages: (cardTypes: string[]) => void; + getResources: (cardType: string) => void; + setCurrentType: (cardType: string) => void; +}; + +const state = proxy({ + token: '', + types: [], + images: [], + resources: [], + currentType: undefined, + 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) => { + state.currentType = cardType; + if (cardType) { + state.getImages([cardType]); + state.getResources(cardType); + } + }, +}); + +export default state; diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 88e48f79..0bbdd1f2 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -13,6 +13,8 @@ export default class SessionStorage { static readonly aimUrlKey = 'aim-url'; /** tensorBoard url */ static readonly tensorBoardUrlKey = 'tensor-board-url'; + // /** 云际系统 Token */ + // static readonly jccTokenKey = 'jcc-token'; /** * 获取 SessionStorage 值