diff --git a/src/components/auto-tooltip/index.tsx b/src/components/auto-tooltip/index.tsx index c72c44ed..1fffa8b0 100644 --- a/src/components/auto-tooltip/index.tsx +++ b/src/components/auto-tooltip/index.tsx @@ -124,7 +124,7 @@ const AutoTooltip: React.FC = ({ style={{ position: 'absolute', right: 8, - top: 8 + top: 7 }} /> ) : ( diff --git a/src/components/seal-form/seal-cascader.tsx b/src/components/seal-form/seal-cascader.tsx new file mode 100644 index 00000000..2f4835ae --- /dev/null +++ b/src/components/seal-form/seal-cascader.tsx @@ -0,0 +1,107 @@ +import { isNotEmptyValue } from '@/utils/index'; +import { useIntl } from '@umijs/max'; +import type { CascaderAutoProps } from 'antd'; +import { Cascader, Form } from 'antd'; +import { cloneDeep } from 'lodash'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import Wrapper from './components/wrapper'; +import { SealFormItemProps } from './types'; + +const SealCascader: React.FC = ( + props +) => { + const { + label, + placeholder, + children, + required, + description, + options, + allowNull, + isInFormItems = true, + ...rest + } = props; + const intl = useIntl(); + const [isFocus, setIsFocus] = useState(false); + const inputRef = useRef(null); + let status = ''; + + // the status can be controlled by Form.Item + if (isInFormItems) { + const statusData = Form?.Item?.useStatus?.(); + status = statusData?.status || ''; + } + + const _options = useMemo(() => { + if (!options?.length) { + return []; + } + const list = cloneDeep(options); + return list.map((item: any) => { + if (item.locale) { + item.label = intl.formatMessage({ id: item.label as string }); + } + return item; + }); + }, [options, intl]); + + useEffect(() => { + if (isNotEmptyValue(props.value) || (allowNull && props.value === null)) { + setIsFocus(true); + } + }, [props.value, allowNull]); + + const handleClickWrapper = () => { + if (!props.disabled && !isFocus) { + inputRef.current?.focus?.(); + setIsFocus(true); + } + }; + + const handleChange = (val: any, options: any) => { + if (isNotEmptyValue(val) || (allowNull && val === null)) { + setIsFocus(true); + } else { + setIsFocus(false); + } + props.onChange?.(val, options); + }; + + const handleOnFocus = (e: any) => { + setIsFocus(true); + props.onFocus?.(e); + }; + + const handleOnBlur = (e: any) => { + if (allowNull && props.value === null) { + setIsFocus(true); + } else if (!props.value) { + setIsFocus(false); + } + props.onBlur?.(e); + }; + + return ( + + + + ); +}; + +export default SealCascader; diff --git a/src/global.less b/src/global.less index dc10115b..182cf1b5 100644 --- a/src/global.less +++ b/src/global.less @@ -777,6 +777,40 @@ body { font-weight: var(--font-weight-bold); } +.ant-cascader-menu-item.ant-cascader-menu-item-active { + background-color: var(--ant-select-option-selected-bg) !important; +} + +.cascader-popup-wrapper { + .ant-cascader-menu { + overflow-x: hidden; + + &::-webkit-scrollbar { + width: var(--scrollbar-size); + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 4px; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &:hover { + &::-webkit-scrollbar-thumb { + background-color: var(--color-scrollbar-thumb); + border-radius: 4px; + } + } + + .ant-cascader-checkbox.ant-cascader-checkbox-disabled { + display: none; + } + } +} + .ant-tooltip-content { .ant-tooltip-inner { a { diff --git a/src/pages/llmodels/components/advance-config.tsx b/src/pages/llmodels/components/advance-config.tsx index eb74224f..ac67abb8 100644 --- a/src/pages/llmodels/components/advance-config.tsx +++ b/src/pages/llmodels/components/advance-config.tsx @@ -1,6 +1,7 @@ import AutoTooltip from '@/components/auto-tooltip'; import LabelSelector from '@/components/label-selector'; import ListInput from '@/components/list-input'; +import SealCascader from '@/components/seal-form/seal-cascader'; import SealInput from '@/components/seal-form/seal-input'; import SealSelect from '@/components/seal-form/seal-select'; import TooltipList from '@/components/tooltip-list'; @@ -122,6 +123,24 @@ const AdvanceConfig: React.FC = (props) => { form.setFieldValue('backend_parameters', list); }, []); + const gpuOptionRender = (data: any) => { + let width = { + maxWidth: 180, + minWidth: 180 + }; + if (!data.parent) { + width = { maxWidth: 180, minWidth: 180 }; + } + if (data.parent) { + return ( + + {data.label} + + ); + } + return ; + }; + const collapseItems = useMemo(() => { const children = ( <> @@ -214,47 +233,94 @@ const AdvanceConfig: React.FC = (props) => { )} {scheduleType === 'manual' && ( - - name={['gpu_selector', 'gpu_ids']} - rules={[ - { - required: true, - message: intl.formatMessage( - { - id: 'common.form.rule.select' - }, - { - name: intl.formatMessage({ id: 'models.form.gpuselector' }) - } - ) - } - ]} - > - { - return ( - - {props.label} - - ); - }} - options={gpuOptions} - optionRender={(props) => { - return ; - }} - > - + <> + {/* + name={['gpu_selector', 'gpu_ids']} + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.select' + }, + { + name: intl.formatMessage({ + id: 'models.form.gpuselector' + }) + } + ) + } + ]} + > + { + return ( + + {props.label} + + ); + }} + options={gpuOptions} + optionRender={(props) => { + return ; + }} + > + */} + + { + item.disableCheckbox = backend !== backendOptionsMap.llamaBox; + return item; + })} + tagRender={(props) => { + return ( + + {props.label} + + ); + }} + optionRender={gpuOptionRender} + > + + )} + = forwardRef((props, ref) => { } = props; const [form] = Form.useForm(); const intl = useIntl(); - const [gpuOptions, setGpuOptions] = useState< - Array - >([]); + const [gpuOptions, setGpuOptions] = useState>([]); const [modelTask, setModelTask] = useState>({ type: '', value: '', @@ -114,17 +119,37 @@ const DataForm: React.FC = forwardRef((props, ref) => { } ]; - const getGPUList = async () => { - const data = await queryGPUList(); - const list = _.map(data.items, (item: GPUListItem) => { + const generateCascaderOptions = (list: GPUListItem[]) => { + const workerFields = ['worker_name', 'worker_id', 'worker_ip']; + + const workers = _.groupBy(list, 'worker_name'); + + const workerList = _.map(workers, (value: GPUListItem[]) => { return { - ...item, - title: '', - label: ` ${item.name}(${item.worker_name}) [${intl.formatMessage({ id: 'resources.table.index' })}:${item.index}]`, - value: item.id + label: `${value[0].worker_name}`, + value: value[0].worker_name, + parent: true, + disableCheckbox: true, + ..._.pick(value[0], workerFields), + children: _.map(value, (item: GPUListItem) => { + return { + label: `[ + ${intl.formatMessage({ id: 'resources.table.index' })}:${item.index}] ${item.name}`, + value: item.id, + disableCheckbox: false, + ..._.omit(item, workerFields) + }; + }) }; }); - setGpuOptions(list); + + return workerList; + }; + + const getGPUList = async () => { + const data = await queryGPUList(); + const gpuList = generateCascaderOptions(data.items); + setGpuOptions(gpuList); }; useEffect(() => { @@ -470,6 +495,34 @@ const DataForm: React.FC = forwardRef((props, ref) => { props.onBackendChange?.(val); }, []); + const generateGPUIds = (data: FormData) => { + const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); + if (!gpu_ids.length) { + return {}; + } + const result: string[] = []; + + _.each(gpu_ids, (item: string[]) => { + if (item.length > 1) { + result.push(..._.tail(item)); + } + if (item.length === 1) { + const worker = _.find(gpuOptions, { value: item[0] }); + const gpus = _.map(worker?.children, 'value'); + result.push(...gpus); + } + }); + + if (result.length) { + return { + gpu_selector: { + gpu_ids: result + } + }; + } + return {}; + }; + const handleOk = (formdata: FormData) => { let data = _.cloneDeep(formdata); if (data.categories) { @@ -477,8 +530,10 @@ const DataForm: React.FC = forwardRef((props, ref) => { } else { data.categories = []; } + const gpuSelector = generateGPUIds(data); onOk({ - ..._.omit(data, ['scheduleType']) + ..._.omit(data, ['scheduleType']), + ...gpuSelector }); }; diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index a8a8eb1c..1614490f 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -75,18 +75,59 @@ const UpdateModal: React.FC = (props) => { const [loading, setLoading] = useState(false); const localPathCache = useRef(''); - const getGPUList = async () => { - const data = await queryGPUList(); - const list = _.map(data.items, (item: GPUListItem) => { + const generateCascaderOptions = (list: GPUListItem[]) => { + const workerFields = ['worker_name', 'worker_id', 'worker_ip']; + + const workers = _.groupBy(list, 'worker_name'); + + const workerList = _.map(workers, (value: GPUListItem[]) => { return { - ...item, - title: '', - label: ` ${item.name}(${item.worker_name})[ - ${intl.formatMessage({ id: 'resources.table.index' })}:${item.index}]`, - value: item.id + label: `${value[0].worker_name}`, + value: value[0].worker_name, + parent: true, + ..._.pick(value[0], workerFields), + children: _.map(value, (item: GPUListItem) => { + return { + label: `[ + ${intl.formatMessage({ id: 'resources.table.index' })}:${item.index}] ${item.name}`, + value: item.id, + ..._.omit(item, workerFields) + }; + }) }; }); - setGpuOptions(list); + + return workerList; + }; + + const getGPUList = async () => { + const data = await queryGPUList(); + const gpuList = generateCascaderOptions(data.items); + setGpuOptions(gpuList); + }; + + const generateGPUSelector = (data: any) => { + const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); + if (gpu_ids.length === 0) { + return []; + } + const dataList = _.reduce( + gpuOptions, + (list: string[], item: any) => { + const gpuIds = _.filter( + gpu_ids, + (id: string) => id.indexOf(item.worker_name) > -1 + ); + if (gpuIds.length && gpuIds.length === item.children.length) { + list.push(item.worker_name); + } else if (gpuIds.length) { + list.push(item.worker_name, ...gpuIds); + } + return list; + }, + {} + ); + return dataList; }; useEffect(() => { @@ -96,6 +137,10 @@ const UpdateModal: React.FC = (props) => { props.data ); + const gpu_ids = generateGPUSelector(props.data); + + console.log('gpu_ids+++++++++', gpu_ids); + const formData = { ...result.values, ..._.omit(props.data, result.omits), @@ -104,7 +149,7 @@ const UpdateModal: React.FC = (props) => { : null, scheduleType: props.data?.gpu_selector ? 'manual' : 'auto', gpu_selector: props.data?.gpu_selector || { - gpu_ids: [] + gpu_ids: gpu_ids } }; form.setFieldsValue(formData); diff --git a/src/pages/llmodels/style/gpu-card.less b/src/pages/llmodels/style/gpu-card.less index 99301df1..644b2e70 100644 --- a/src/pages/llmodels/style/gpu-card.less +++ b/src/pages/llmodels/style/gpu-card.less @@ -13,8 +13,8 @@ .info { display: flex; - align-items: center; - flex-wrap: wrap; + flex-direction: column; + align-items: flex-start; gap: 10px; color: var(--ant-color-text-tertiary); }