chore: worker selector

This commit is contained in:
jialin
2025-10-21 15:59:48 +08:00
parent f017d609fb
commit 1e4018350f
10 changed files with 319 additions and 70 deletions
@@ -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<string, any>;
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<LabelItemProps> = ({
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<string>) => 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 (
<div className="label-item">
<div className="label-key">
{keyAddon ?? (
<Tooltip
open={open}
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
>
<AutoComplete
options={keyOptions}
disabled={disabled}
checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
></AutoComplete>
</Tooltip>
)}
</div>
{seperator && <span className="seprator">{seperator}</span>}
<div className="label-value">
{valueAddon ?? (
<AutoComplete
options={valueOptions}
disabled={disabled}
checkStatus={label.value ? 'success' : ''}
label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value}
onChange={handleOnValueChange}
onBlur={(e: any) => onBlur?.(e, 'value')}
></AutoComplete>
)}
</div>
{!disabled && (
<Button
size="small"
className="btn"
type="default"
shape="circle"
onClick={onDelete}
>
<MinusOutlined />
</Button>
)}
</div>
);
};
export default LabelItem;
+25
View File
@@ -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<string, any>;
}
export const LabelSelectorContext =
React.createContext<LabelSelectorContextProps>(
{} as LabelSelectorContextProps
);
export const useLabelSelectorContext = () => {
const context = React.useContext(LabelSelectorContext);
if (!context) {
throw new Error(
'useLabelSelectorContext must be used within a LabelSelectorProvider'
);
}
return context;
};
+4 -1
View File
@@ -9,6 +9,7 @@ interface LabelSelectorProps {
btnText?: string;
description?: React.ReactNode;
disabled?: boolean;
isAutoComplete?: boolean;
onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void;
@@ -22,7 +23,8 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
disabled,
label,
btnText,
description
description,
isAutoComplete
}) => {
const intl = useIntl();
const [labelsData, setLabelsData] = useState({});
@@ -89,6 +91,7 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
description={
description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' })
}
isAutoComplete={isAutoComplete}
labels={labelsData}
labelList={labelList}
onChange={handleLabelsChange}
+41 -17
View File
@@ -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<string, any>;
label?: string;
btnText?: string;
isAutoComplete?: boolean;
labelList: Array<{ key: string; value: string }>;
onLabelListChange: (list: { key: string; value: string }[]) => void;
onChange?: (labels: Record<string, any>) => void;
@@ -28,10 +30,15 @@ const Inner: React.FC<LabelSelectorProps> = ({
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<LabelSelectorProps> = ({
btnText={btnText}
>
<>
{labelList?.map((item: any, index: number) => {
return (
<LabelItem
disabled={disabled}
key={index}
label={item}
seperator=":"
labelList={labelList}
onDelete={() => 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 (
<AutoCompleteItem
disabled={disabled}
key={index}
label={item}
seperator=":"
labels={labels}
labelList={labelList}
onDelete={() => 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 (
<LabelItem
disabled={disabled}
key={index}
label={item}
seperator=":"
labelList={labelList}
onDelete={() => handleOnDelete(index)}
onChange={(obj) => handleOnChange(index, obj)}
onPaste={(e) => onPaste?.(e, index)}
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
/>
);
})}
</>
</Wrapper>
);
+4 -1
View File
@@ -17,6 +17,7 @@ const SealAutoComplete: React.FC<
trim = true,
onSelect,
onBlur,
checkStatus,
extra,
style,
addAfter,
@@ -83,7 +84,7 @@ const SealAutoComplete: React.FC<
<SelectWrapper style={style}>
<Wrapper
className="seal-select-wrapper"
status={status}
status={checkStatus || status}
extra={extra}
label={label}
isFocus={isFocus}
@@ -103,6 +104,8 @@ const SealAutoComplete: React.FC<
''
)
}
// @ts-ignore
status={checkStatus || status}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
+13 -1
View File
@@ -2,12 +2,24 @@ import { PageActionType } from '@/config/types';
import React from 'react';
import { BackendOption, DeployFormKey } from './types';
type EmptyObject = Record<never, never>;
type CascaderOption<T extends object = EmptyObject> = {
label: string;
value: string | number;
parent?: boolean;
disabled?: boolean;
index?: number;
children?: CascaderOption<T>[];
} & Partial<T>;
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;
+52 -46
View File
@@ -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<string, any>) => {
@@ -104,52 +105,57 @@ const AdvanceConfig = () => {
}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
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();
}
})
]}
<LabelSelectorContext.Provider
value={{ options: workerLabelOptions }}
>
<LabelSelector
label={intl.formatMessage({
id: 'resources.form.workerSelector'
})}
labels={wokerSelector}
onChange={handleWorkerLabelsChange}
onBlur={handleSelectorOnBlur}
onDelete={handleDeleteWorkerSelector}
description={
<span>
{intl.formatMessage({
id: 'resources.form.workerSelector.description'
})}
</span>
}
></LabelSelector>
</Form.Item>
<Form.Item<FormData>
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();
}
})
]}
>
<LabelSelector
isAutoComplete
label={intl.formatMessage({
id: 'resources.form.workerSelector'
})}
labels={wokerSelector}
onChange={handleWorkerLabelsChange}
onBlur={handleSelectorOnBlur}
onDelete={handleDeleteWorkerSelector}
description={
<span>
{intl.formatMessage({
id: 'resources.form.workerSelector.description'
})}
</span>
}
></LabelSelector>
</Form.Item>
</LabelSelectorContext.Provider>
</>
)}
+3 -1
View File
@@ -54,7 +54,8 @@ const DataForm: React.FC<DataFormProps> = 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<string[]>([]);
@@ -208,6 +209,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
pageAction: action,
gpuOptions: gpuOptions,
backendOptions: backendOptions,
workerLabelOptions: workerLabelOptions,
onValuesChange: onValuesChange,
onBackendChange: handleBackendChange
}}
@@ -26,6 +26,9 @@ type CascaderOption<T extends object = EmptyObject> = {
export const useGenerateGPUOptions = () => {
const [gpuOptions, setGpuOptions] = useState<CascaderOption[]>([]);
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<string, Set<string>>();
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
};
};
+4 -1
View File
@@ -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);