| @@ -1,18 +1,28 @@ | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Input } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type ParameterInputData = { | |||
| value?: any; | |||
| showValue?: any; | |||
| fromSelect?: boolean; | |||
| } & Record<string, any>; | |||
| // 对象 | |||
| export type ParameterInputObject = { | |||
| value?: any; // 值 | |||
| showValue?: any; // 显示值 | |||
| fromSelect?: boolean; // 是否来自选择 | |||
| activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab | |||
| expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | |||
| checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | |||
| [key: string]: any; | |||
| }; | |||
| interface ParameterInputProps { | |||
| value?: ParameterInputData; | |||
| onChange?: (value: ParameterInputData) => void; | |||
| // 值类型 | |||
| export type ParameterInputValue = ParameterInputObject | string; | |||
| export interface ParameterInputProps { | |||
| value?: ParameterInputValue; | |||
| onChange?: (value?: ParameterInputValue) => void; | |||
| onClick?: () => void; | |||
| onRemove?: () => void; | |||
| canInput?: boolean; | |||
| textArea?: boolean; | |||
| placeholder?: string; | |||
| @@ -27,6 +37,7 @@ function ParameterInput({ | |||
| value, | |||
| onChange, | |||
| onClick, | |||
| onRemove, | |||
| canInput = true, | |||
| textArea = false, | |||
| allowClear, | |||
| @@ -42,8 +53,23 @@ function ParameterInput({ | |||
| valueObj.showValue = valueObj.value; | |||
| } | |||
| const isSelect = valueObj?.fromSelect; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| // 删除 | |||
| const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { | |||
| e.stopPropagation(); | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| onRemove?.(); | |||
| }; | |||
| return ( | |||
| <> | |||
| @@ -62,18 +88,7 @@ function ParameterInput({ | |||
| <span className="parameter-input__content__value">{valueObj?.showValue}</span> | |||
| <CloseOutlined | |||
| className="parameter-input__content__close-icon" | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: undefined, | |||
| checkedKeys: undefined, | |||
| }); | |||
| }} | |||
| onClick={handleRemove} | |||
| /> | |||
| </div> | |||
| ) : ( | |||
| @@ -93,9 +108,9 @@ function ParameterInput({ | |||
| onChange={(e) => | |||
| onChange?.({ | |||
| ...valueObj, | |||
| fromSelect: false, | |||
| value: e.target.value, | |||
| showValue: e.target.value, | |||
| fromSelect: false, | |||
| }) | |||
| } | |||
| /> | |||
| @@ -105,3 +120,12 @@ function ParameterInput({ | |||
| } | |||
| export default ParameterInput; | |||
| // 必填校验 | |||
| export const requiredValidator = (rule: RuleObject, value: any) => { | |||
| const trueValue = typeof value === 'object' ? value?.value : value; | |||
| if (!trueValue) { | |||
| return Promise.reject(rule.message || '必填项'); | |||
| } | |||
| return Promise.resolve(); | |||
| }; | |||
| @@ -19,7 +19,7 @@ function ExperimentText() { | |||
| const [message, setMessage, messageRef] = useStateRef({}); | |||
| const propsRef = useRef(); | |||
| const navgite = useNavigate(); | |||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||
| const locationParams = useParams(); // 新版本获取路由参数接口 | |||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | |||
| const graphRef = useRef(); | |||
| @@ -5,7 +5,7 @@ | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import ParameterInput from '@/components/ParameterInput'; | |||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| @@ -87,7 +87,7 @@ function ModelDeploymentCreate() { | |||
| }; | |||
| // 选择模型、镜像 | |||
| const selectResource = (name: string, type: ResourceSelectorType) => { | |||
| const selectResource = (formItemName: string, type: ResourceSelectorType) => { | |||
| let resource: ResourceSelectorResponse | undefined; | |||
| switch (type) { | |||
| case ResourceSelectorType.Model: | |||
| @@ -105,13 +105,25 @@ function ModelDeploymentCreate() { | |||
| onOk: (res) => { | |||
| if (res) { | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| form.setFieldValue(name, res.path); | |||
| form.setFieldValue(formItemName, res.path); | |||
| setSelectedMirror(res); | |||
| } else { | |||
| const showValue = `${res.name}:${res.version}`; | |||
| form.setFieldValue(name, { | |||
| ...pick(res, ['id', 'version', 'path']), | |||
| const { activeTab, id, name, version, path } = res; | |||
| const jsonObj = { | |||
| id, | |||
| version, | |||
| path, | |||
| }; | |||
| const value = JSON.stringify(jsonObj); | |||
| const showValue = `${name}:${version}`; | |||
| form.setFieldValue(formItemName, { | |||
| value, | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| expandedKeys: [id], | |||
| checkedKeys: [`${id}-${version}`], | |||
| ...jsonObj, | |||
| }); | |||
| setSelectedModel(res); | |||
| } | |||
| @@ -121,8 +133,9 @@ function ModelDeploymentCreate() { | |||
| } else { | |||
| setSelectedMirror(undefined); | |||
| } | |||
| form.setFieldValue(name, ''); | |||
| form.setFieldValue(formItemName, ''); | |||
| } | |||
| form.validateFields([formItemName]); | |||
| close(); | |||
| }, | |||
| }); | |||
| @@ -258,10 +271,11 @@ function ModelDeploymentCreate() { | |||
| name="model" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| validator: requiredValidator, | |||
| message: '请选择模型', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| placeholder="请选择模型" | |||
| @@ -269,6 +283,7 @@ function ModelDeploymentCreate() { | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||
| onChange={() => setSelectedModel(undefined)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -291,16 +306,18 @@ function ModelDeploymentCreate() { | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像', | |||
| validator: requiredValidator, | |||
| message: '请选择镜像', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| placeholder="请选择镜像" | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||
| onChange={() => setSelectedMirror(undefined)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -93,15 +93,17 @@ const EditPipeline = () => { | |||
| return; | |||
| } | |||
| const [propsRes, propsError] = await to(propsRef.current.getFieldsValue()); | |||
| if (propsError) { | |||
| message.error('节点必填项必须配置'); | |||
| return; | |||
| } | |||
| propsRef.current.propClose(); | |||
| setTimeout(() => { | |||
| const data = graph.save(); | |||
| console.log(data); | |||
| const errorNode = data.nodes.find((item) => { | |||
| return item.formError === true; | |||
| }); | |||
| if (errorNode) { | |||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | |||
| return; | |||
| } | |||
| const params = { | |||
| ...locationParams, | |||
| dag: JSON.stringify(data), | |||
| @@ -298,7 +300,7 @@ const EditPipeline = () => { | |||
| ); | |||
| }, | |||
| afterDraw(cfg, group) { | |||
| const image = group.addShape('image', { | |||
| group.addShape('image', { | |||
| attrs: { | |||
| x: -45, | |||
| y: -10, | |||
| @@ -325,7 +327,26 @@ const EditPipeline = () => { | |||
| draggable: true, | |||
| }); | |||
| } | |||
| if (cfg.formError) { | |||
| group.addShape('image', { | |||
| attrs: { | |||
| x: 43, | |||
| y: -24, | |||
| width: 18, | |||
| height: 18, | |||
| img: require('@/assets/img/pipeline-warning.png'), | |||
| cursor: 'pointer', | |||
| }, | |||
| draggable: false, | |||
| capture: false, | |||
| }); | |||
| } | |||
| const bbox = group.getBBox(); | |||
| if (cfg.formError) { | |||
| bbox.y += 6; | |||
| bbox.width -= 6; | |||
| bbox.height -= 6; | |||
| } | |||
| const anchorPoints = this.getAnchorPoints(cfg); | |||
| anchorPoints.forEach((anchorPos, i) => { | |||
| group.addShape('circle', { | |||
| @@ -345,18 +366,10 @@ const EditPipeline = () => { | |||
| draggable: true, | |||
| }); | |||
| }); | |||
| return image; | |||
| }, | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| // const anchorPoints = item | |||
| // .getContainer() | |||
| // .findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| // anchorPoints.forEach((point) => { | |||
| // if (value || point.get('links') > 0) point.show(); | |||
| // else point.hide(); | |||
| // }); | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point'); | |||
| @@ -617,30 +630,26 @@ const EditPipeline = () => { | |||
| const cloneDisplay = type === 'node' ? 'flex' : 'none'; | |||
| return ` | |||
| <div> | |||
| <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" code="clone"> | |||
| <svg class="pipeline-context-menu__item__icon"> | |||
| <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone"> | |||
| <svg class="pipeline-context-menu__item__icon" id="clone-svg"> | |||
| <use xlink:href="#icon-fuzhi1" /> | |||
| </svg> | |||
| <span>复制</span> | |||
| 复制 | |||
| </div> | |||
| <div class="pipeline-context-menu__item" code="delete"> | |||
| <svg class="pipeline-context-menu__item__icon"> | |||
| <div class="pipeline-context-menu__item" id="delete"> | |||
| <svg class="pipeline-context-menu__item__icon" id="delete-svg"> | |||
| <use xlink:href="#icon-shanchu1" /> | |||
| </svg> | |||
| <span>删除</span> | |||
| 删除 | |||
| </div> | |||
| </div>`; | |||
| }, | |||
| handleMenuClick: (target, item) => { | |||
| switch (target.getAttribute('code')) { | |||
| case 'delete': | |||
| graph.removeItem(item); | |||
| break; | |||
| case 'clone': | |||
| cloneElement(item); | |||
| break; | |||
| default: | |||
| break; | |||
| const id = target.id; | |||
| if (id.startsWith('clone')) { | |||
| cloneElement(item); | |||
| } else if (id.startsWith('delete')) { | |||
| graph.removeItem(item); | |||
| } | |||
| }, | |||
| // offsetX and offsetY include the padding of the parent container | |||
| @@ -697,7 +706,7 @@ const EditPipeline = () => { | |||
| </div> | |||
| <div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div> | |||
| </div> | |||
| <Props ref={propsRef} onParentChange={handleFormChange}></Props> | |||
| <Props ref={propsRef} onFormChange={handleFormChange}></Props> | |||
| <GlobalParamsDrawer | |||
| ref={paramsDrawerRef} | |||
| open={paramsDrawerOpen} | |||
| @@ -48,7 +48,8 @@ | |||
| cursor: pointer; | |||
| &:hover { | |||
| color: @primary-color; | |||
| color: #0d5ef8; | |||
| font-weight: 500; | |||
| background-color: .addAlpha(#8895a8, 0.11) []; | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ParameterInput from '@/components/ParameterInput'; | |||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| @@ -25,10 +25,10 @@ import { canInput, createMenuItems } from './utils'; | |||
| const { TextArea } = Input; | |||
| type PipelineNodeParameterProps = { | |||
| onParentChange: (data: PipelineNodeModelSerialize) => void; | |||
| onFormChange: (data: PipelineNodeModelSerialize) => void; | |||
| }; | |||
| const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParameterProps, ref) => { | |||
| const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => { | |||
| const [form] = Form.useForm(); | |||
| const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | |||
| {} as PipelineNodeModelSerialize, | |||
| @@ -37,19 +37,28 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | |||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| const afterOpenChange = () => { | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| console.log('getFieldsValue', form.getFieldsValue()); | |||
| const control_strategy = form.getFieldValue('control_strategy'); | |||
| const in_parameters = form.getFieldValue('in_parameters'); | |||
| const out_parameters = form.getFieldValue('out_parameters'); | |||
| onParentChange({ | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| const fields = form.getFieldsValue(); | |||
| const control_strategy = JSON.stringify(fields.control_strategy); | |||
| const in_parameters = JSON.stringify(fields.in_parameters); | |||
| const out_parameters = JSON.stringify(fields.out_parameters); | |||
| console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...form.getFieldsValue(), | |||
| control_strategy: JSON.stringify(control_strategy), | |||
| in_parameters: JSON.stringify(in_parameters), | |||
| out_parameters: JSON.stringify(out_parameters), | |||
| }); | |||
| ...fields, | |||
| control_strategy: control_strategy, | |||
| in_parameters: in_parameters, | |||
| out_parameters: out_parameters, | |||
| formError: !!error, | |||
| }; | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| const onClose = () => { | |||
| @@ -57,15 +66,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| }; | |||
| useImperativeHandle(ref, () => ({ | |||
| getFieldsValue: async () => { | |||
| const [propsRes, propsError] = await to(form.validateFields()); | |||
| if (propsRes && !propsError) { | |||
| const values = form.getFieldsValue(); | |||
| return values; | |||
| } else { | |||
| return Promise.reject(propsError); | |||
| } | |||
| }, | |||
| showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) { | |||
| if (e.item && e.item.getModel()) { | |||
| form.resetFields(); | |||
| @@ -115,7 +115,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| type = ResourceSelectorType.Mirror; | |||
| break; | |||
| } | |||
| const fieldValue = form.getFieldValue(formItemName); | |||
| const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | |||
| const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | |||
| @@ -162,8 +161,21 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| }); | |||
| } | |||
| } else { | |||
| form.setFieldValue(formItemName, ''); | |||
| 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.validateFields([formItemName]); | |||
| close(); | |||
| }, | |||
| }); | |||
| @@ -212,6 +224,18 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| ); | |||
| }; | |||
| // 必填项校验规则 | |||
| const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => { | |||
| return item.value.require | |||
| ? [ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '必填项', | |||
| }, | |||
| ] | |||
| : []; | |||
| }; | |||
| // 控制策略 | |||
| const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| @@ -232,7 +256,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| <Drawer | |||
| title="编辑任务" | |||
| placement="right" | |||
| rootStyle={{ marginTop: '45px' }} | |||
| rootStyle={{ marginTop: '52px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| @@ -351,7 +375,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| value: 'standard', | |||
| }} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| @@ -382,11 +405,14 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| > | |||
| <TextArea placeholder="请输入环境变量" allowClear /> | |||
| </Form.Item> | |||
| {/* 控制参数 */} | |||
| {controlStrategyList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['control_strategy', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'control_strategy')} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| @@ -401,11 +427,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| required={item.value.require ? true : false} | |||
| > | |||
| <div className={styles['pipeline-drawer__ref-row']}> | |||
| <Form.Item | |||
| name={['in_parameters', item.key]} | |||
| noStyle | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||
| {item.value.type === 'select' ? ( | |||
| <ParameterSelect /> | |||
| ) : ( | |||
| @@ -435,8 +457,9 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'out_parameters')} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| @@ -109,6 +109,29 @@ export function nullToUndefined(obj: Record<string, any>) { | |||
| return newObj; | |||
| } | |||
| // undefined to null | |||
| export function undefinedToNull(obj: Record<string, any>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| const value = obj[key]; | |||
| if (value === undefined) { | |||
| newObj[key] = null; | |||
| } else if (Array.isArray(value)) { | |||
| newObj[key] = value.map((item) => undefinedToNull(item)); | |||
| } else if (isPlainObject(value)) { | |||
| newObj[key] = undefinedToNull(value); | |||
| } else { | |||
| newObj[key] = value; | |||
| } | |||
| } | |||
| } | |||
| return newObj; | |||
| } | |||
| /** | |||
| * Changes the property names of an object based on a mapping provided. | |||
| * | |||
| @@ -7,13 +7,7 @@ export async function to<T>(promise: Promise<T>): Promise<[T, null] | [null, Err | |||
| const data = await promise; | |||
| return [data, null]; | |||
| } catch (error) { | |||
| if (error instanceof Error) { | |||
| return [null, error]; | |||
| } else if (typeof error === 'string') { | |||
| return [null, new Error(error)]; | |||
| } else { | |||
| return [null, new Error('Error')]; | |||
| } | |||
| return [null, error as Error]; | |||
| } | |||
| } | |||