chore: worker selector
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user