diff --git a/src/components/label-selector/autocomplete-item.tsx b/src/components/label-selector/autocomplete-item.tsx new file mode 100644 index 00000000..4d51194c --- /dev/null +++ b/src/components/label-selector/autocomplete-item.tsx @@ -0,0 +1,138 @@ +import AutoComplete from '@/components/seal-form/auto-complete'; +import { MinusOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Tooltip } from 'antd'; +import _ from 'lodash'; +import React, { useMemo, useState } from 'react'; +import { useLabelSelectorContext } from './context'; +import './styles/label-item.less'; +interface LabelItemProps { + label: { + key: string; + value: string; + }; + labels?: Record; + labelKey?: string; + labelValue?: string; + keyAddon?: React.ReactNode; + valueAddon?: React.ReactNode; + seperator?: string; + labelList: { key: string; value: string }[]; + disabled?: boolean; + onDelete?: () => void; + onChange?: (params: { key: string; value: string }) => void; + onPaste?: (e: any) => void; + onBlur?: (e: any, type: string) => void; +} +const LabelItem: React.FC = ({ + labels, + label, + labelList, + seperator, + keyAddon, + valueAddon, + disabled, + onChange, + onDelete, + onBlur +}) => { + const intl = useIntl(); + const [open, setOpen] = useState(false); + const { options } = useLabelSelectorContext(); + + const keyOptions = useMemo(() => { + return options?.filter( + (item) => !_.has(labels, item.value) || item.value === label.key + ); + }, [labels, options]); + + const valueOptions = useMemo(() => { + return options?.find((item) => item.value === label.key)?.children || []; + }, [label.key, options]); + + const handleOnValueChange = (value: string) => { + onChange?.({ + key: label.key, + value: value + }); + }; + + const handleOnKeyChange = (key: any) => { + onChange?.({ + key, + value: label.value + }); + }; + + const handleKeyOnBlur = (e: any, type: string) => { + const val = e.target.value; + // has duplicate key + const duplicates = _.filter( + labelList, + (item: Global.BaseListItem) => val && val === item.key + ); + if (duplicates.length > 1) { + setOpen(true); + onChange?.({ + key: '', + value: label.value + }); + setTimeout(() => { + setOpen(false); + }, 1000); + } else { + setOpen(false); + } + onBlur?.(e, type); + }; + + return ( +
+
+ {keyAddon ?? ( + + handleKeyOnBlur(e, 'key')} + > + + )} +
+ {seperator && {seperator}} +
+ {valueAddon ?? ( + onBlur?.(e, 'value')} + > + )} +
+ {!disabled && ( + + )} +
+ ); +}; + +export default LabelItem; diff --git a/src/components/label-selector/context.ts b/src/components/label-selector/context.ts new file mode 100644 index 00000000..93304790 --- /dev/null +++ b/src/components/label-selector/context.ts @@ -0,0 +1,25 @@ +import React from 'react'; + +interface LabelSelectorContextProps { + options?: Array<{ + label: string; + value: string | number; + children?: { label: string; value: string | number }[]; + }>; + currentData?: Record; +} + +export const LabelSelectorContext = + React.createContext( + {} as LabelSelectorContextProps + ); + +export const useLabelSelectorContext = () => { + const context = React.useContext(LabelSelectorContext); + if (!context) { + throw new Error( + 'useLabelSelectorContext must be used within a LabelSelectorProvider' + ); + } + return context; +}; diff --git a/src/components/label-selector/index.tsx b/src/components/label-selector/index.tsx index 0c173ce1..d2b162a1 100644 --- a/src/components/label-selector/index.tsx +++ b/src/components/label-selector/index.tsx @@ -9,6 +9,7 @@ interface LabelSelectorProps { btnText?: string; description?: React.ReactNode; disabled?: boolean; + isAutoComplete?: boolean; onChange?: (labels: Record) => void; onBlur?: (e: any, type: string, index: number) => void; onDelete?: (index: number) => void; @@ -22,7 +23,8 @@ const LabelSelector: React.FC = ({ disabled, label, btnText, - description + description, + isAutoComplete }) => { const intl = useIntl(); const [labelsData, setLabelsData] = useState({}); @@ -89,6 +91,7 @@ const LabelSelector: React.FC = ({ description={ description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' }) } + isAutoComplete={isAutoComplete} labels={labelsData} labelList={labelList} onChange={handleLabelsChange} diff --git a/src/components/label-selector/inner.tsx b/src/components/label-selector/inner.tsx index 5f440ae6..0e017976 100644 --- a/src/components/label-selector/inner.tsx +++ b/src/components/label-selector/inner.tsx @@ -1,12 +1,14 @@ import { useIntl } from '@umijs/max'; import _ from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; +import AutoCompleteItem from './autocomplete-item'; import LabelItem from './label-item'; import Wrapper from './wrapper'; interface LabelSelectorProps { labels: Record; label?: string; btnText?: string; + isAutoComplete?: boolean; labelList: Array<{ key: string; value: string }>; onLabelListChange: (list: { key: string; value: string }[]) => void; onChange?: (labels: Record) => void; @@ -28,10 +30,15 @@ const Inner: React.FC = ({ disabled, label, btnText, - description + description, + isAutoComplete }) => { const intl = useIntl(); + useEffect(() => { + console.log('labels changed in Inner', labels); + }, [labels]); + const updateLabels = (list: { key: string; value: string }[]) => { const newLabels = _.reduce( list, @@ -81,21 +88,38 @@ const Inner: React.FC = ({ btnText={btnText} > <> - {labelList?.map((item: any, index: number) => { - return ( - handleOnDelete(index)} - onChange={(obj) => handleOnChange(index, obj)} - onPaste={(e) => onPaste?.(e, index)} - onBlur={(e: any, type: string) => onBlur?.(e, type, index)} - /> - ); - })} + {isAutoComplete + ? labelList?.map((item: any, index: number) => { + return ( + handleOnDelete(index)} + onChange={(obj) => handleOnChange(index, obj)} + onPaste={(e) => onPaste?.(e, index)} + onBlur={(e: any, type: string) => onBlur?.(e, type, index)} + /> + ); + }) + : labelList?.map((item: any, index: number) => { + return ( + handleOnDelete(index)} + onChange={(obj) => handleOnChange(index, obj)} + onPaste={(e) => onPaste?.(e, index)} + onBlur={(e: any, type: string) => onBlur?.(e, type, index)} + /> + ); + })} ); diff --git a/src/components/seal-form/auto-complete.tsx b/src/components/seal-form/auto-complete.tsx index b08dd006..46d4a913 100644 --- a/src/components/seal-form/auto-complete.tsx +++ b/src/components/seal-form/auto-complete.tsx @@ -17,6 +17,7 @@ const SealAutoComplete: React.FC< trim = true, onSelect, onBlur, + checkStatus, extra, style, addAfter, @@ -83,7 +84,7 @@ const SealAutoComplete: React.FC< ; + +type CascaderOption = { + label: string; + value: string | number; + parent?: boolean; + disabled?: boolean; + index?: number; + children?: CascaderOption[]; +} & Partial; + interface FormContextProps { isGGUF?: boolean; formKey: DeployFormKey; source: string; pageAction: PageActionType; - gpuOptions: any[]; + gpuOptions: CascaderOption[]; + workerLabelOptions: CascaderOption[]; backendOptions: BackendOption[]; onValuesChange?: (changedValues: any, allValues: any) => void; onBackendChange: (backend: string, option: any) => void; diff --git a/src/pages/llmodels/forms/advance-config.tsx b/src/pages/llmodels/forms/advance-config.tsx index 099aa730..f2518655 100644 --- a/src/pages/llmodels/forms/advance-config.tsx +++ b/src/pages/llmodels/forms/advance-config.tsx @@ -1,4 +1,5 @@ import LabelSelector from '@/components/label-selector'; +import { LabelSelectorContext } from '@/components/label-selector/context'; import CheckboxField from '@/components/seal-form/checkbox-field'; import SealSelect from '@/components/seal-form/seal-select'; import TooltipList from '@/components/tooltip-list'; @@ -36,7 +37,7 @@ const AdvanceConfig = () => { const EnviromentVars = Form.useWatch('env', form); const scheduleType = Form.useWatch('scheduleType', form); const backend = Form.useWatch('backend', form); - const { onValuesChange } = useFormContext(); + const { onValuesChange, workerLabelOptions } = useFormContext(); const handleWorkerLabelsChange = useCallback( (labels: Record) => { @@ -104,52 +105,57 @@ const AdvanceConfig = () => { } > - - name="worker_selector" - rules={[ - ({ getFieldValue }) => ({ - validator(rule, value) { - if ( - getFieldValue('scheduleType') === ScheduleValueMap.Auto && - _.keys(value).length > 0 - ) { - if (_.some(_.keys(value), (k: string) => !value[k])) { - return Promise.reject( - intl.formatMessage( - { - id: 'common.validate.value' - }, - { - name: intl.formatMessage({ - id: 'models.form.selector' - }) - } - ) - ); - } - } - return Promise.resolve(); - } - }) - ]} + - - {intl.formatMessage({ - id: 'resources.form.workerSelector.description' - })} - - } - > - + + name="worker_selector" + rules={[ + ({ getFieldValue }) => ({ + validator(rule, value) { + if ( + getFieldValue('scheduleType') === ScheduleValueMap.Auto && + _.keys(value).length > 0 + ) { + if (_.some(_.keys(value), (k: string) => !value[k])) { + return Promise.reject( + intl.formatMessage( + { + id: 'common.validate.value' + }, + { + name: intl.formatMessage({ + id: 'models.form.selector' + }) + } + ) + ); + } + } + return Promise.resolve(); + } + }) + ]} + > + + {intl.formatMessage({ + id: 'resources.form.workerSelector.description' + })} + + } + > + + )} diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index 9d4baa1c..21267bea 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -54,7 +54,8 @@ const DataForm: React.FC = forwardRef((props, ref) => { onOk } = props; const { backendOptions, getBackendOptions } = useQueryBackends(); - const { getGPUOptionList, gpuOptions } = useGenerateGPUOptions(); + const { getGPUOptionList, gpuOptions, workerLabelOptions } = + useGenerateGPUOptions(); const [form] = Form.useForm(); const intl = useIntl(); const [activeKey, setActiveKey] = React.useState([]); @@ -208,6 +209,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { pageAction: action, gpuOptions: gpuOptions, backendOptions: backendOptions, + workerLabelOptions: workerLabelOptions, onValuesChange: onValuesChange, onBackendChange: handleBackendChange }} diff --git a/src/pages/llmodels/hooks/use-form-initial-values.ts b/src/pages/llmodels/hooks/use-form-initial-values.ts index 00f8a2fd..f0c2da8e 100644 --- a/src/pages/llmodels/hooks/use-form-initial-values.ts +++ b/src/pages/llmodels/hooks/use-form-initial-values.ts @@ -26,6 +26,9 @@ type CascaderOption = { export const useGenerateGPUOptions = () => { const [gpuOptions, setGpuOptions] = useState([]); + const [workerLabelOptions, setWorkerLabelOptions] = useState< + CascaderOption[] + >([]); const generateCascaderGPUOptions = ( gpuList: GPUListItem[], @@ -80,7 +83,34 @@ export const useGenerateGPUOptions = () => { return gpuSelectorList; }; - const generateWorkerSelectorOptions = (workerList: WorkerListItem[]) => {}; + const generateWorkerSelectorOptions = (workerList: WorkerListItem[]) => { + // each worker may have multiple labels,the labels is object as: {key: value, key2: value2} + // different workers may have a same label key but different values + // we need to extract a list as: [{label: key, value: key, children: [{label: value, value: value}]}] + const labelMap = new Map>(); + workerList.forEach((worker) => { + const labels = worker.labels || {}; + Object.entries(labels).forEach(([key, value]) => { + if (!labelMap.has(key)) { + labelMap.set(key, new Set()); + } + labelMap.get(key)!.add(value); + }); + }); + + const labelOptions: CascaderOption[] = Array.from(labelMap.entries()).map( + ([key, values]) => ({ + label: key, + value: key, + parent: true, + children: Array.from(values).map((value) => ({ + label: value, + value: value + })) + }) + ); + return labelOptions; + }; const getGPUOptionList = async (params?: { clusterId: number; @@ -99,13 +129,16 @@ export const useGenerateGPUOptions = () => { }) ]); const gpuList = generateCascaderGPUOptions(gpuData.items, workerData.items); + const labelOptions = generateWorkerSelectorOptions(workerData.items); setGpuOptions(gpuList); + setWorkerLabelOptions(labelOptions); return gpuList; }; return { getGPUOptionList, - gpuOptions + gpuOptions, + workerLabelOptions }; }; diff --git a/src/request-config.ts b/src/request-config.ts index 6951aaa0..377fe8fb 100644 --- a/src/request-config.ts +++ b/src/request-config.ts @@ -13,7 +13,10 @@ export const requestConfig: RequestConfig = { }, errorHandler: (error: any, opts: any) => { const { message: errorMessage, response } = error; - const errMsg = response?.data?.message || errorMessage; + const errMsg = + response?.data?.error?.message || + response?.data?.message || + errorMessage; if (!opts?.skipErrorHandler && response?.status) { message.error(errMsg);