fix: ui issues

This commit is contained in:
jialin
2026-02-06 11:13:49 +08:00
parent d54722609c
commit 7f3d6bb55f
35 changed files with 364 additions and 141 deletions
+2 -1
View File
@@ -29,6 +29,7 @@ const SealAutoComplete: React.FC<
extra,
style,
addAfter,
suffixIcon,
loading,
allowClear,
clearSpaceOnBlur,
@@ -103,7 +104,7 @@ const SealAutoComplete: React.FC<
</Link>
);
}
return null;
return suffixIcon || null;
};
const popupRender = (originNode: React.ReactElement): React.ReactElement => {
+1 -1
View File
@@ -22,7 +22,7 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
let status = '';
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
status = props.status || statusData?.status || '';
} else {
status = props.status || '';
}
+1 -1
View File
@@ -51,8 +51,8 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
e.target.value = e.target.value?.trim?.();
if (!inputRef.current?.input?.value) {
setIsFocus(false);
props.onBlur?.(e);
}
props.onBlur?.(e);
};
return (
+3 -1
View File
@@ -106,7 +106,9 @@ const SealCascader: React.FC<
// the status can be controlled by Form.Item
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
status = props.status || statusData?.status || '';
} else {
status = props.status || '';
}
const _options = useMemo(() => {
+1 -1
View File
@@ -30,7 +30,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
let status = '';
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
status = props.status || statusData?.status || '';
} else {
status = props.status || '';
}
+5 -2
View File
@@ -181,8 +181,11 @@ const SelectWrapper = styled.div`
}
&.validate-status-error {
&.dropdown-visible {
.ant-select-dropdown {
.ant-select-dropdown {
border-color: var(--ant-color-error) !important;
}
.__wrapper__ {
.ant-cascader.ant-select.ant-select-outlined {
border-color: var(--ant-color-error) !important;
}
}
+1 -1
View File
@@ -283,7 +283,7 @@ export default function useTableFetch<T>(
}
) => {
loadendRef.current = false;
const newQueryParams = { ...queryParams, ...params };
const newQueryParams = { ...queryParams, ...params, page: 1 };
setQueryParams(newQueryParams);
await fetchData({ query: newQueryParams });
if (watch && !options?.paginate) {
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver Version',
'benchmark.env.cpuCounts': 'CPU Counts'
'benchmark.env.cpuCounts': 'CPU Cores',
'benchmark.table.export.results': 'Export Results',
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models'
};
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models',
'providers.form.fallback.token': 'Fallback API Keys'
'providers.form.fallback.token': 'Fallback API Keys',
'providers.form.custombeckendUrl': 'Custom Base URL',
'providers.form.more': '+{count} more',
'providers.form.addAll': 'Add All',
'providers.form.rules.models': 'Please select at least one model',
'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model'
};
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver Version',
'benchmark.env.cpuCounts': 'CPU Counts'
'benchmark.env.cpuCounts': 'CPU Cores',
'benchmark.table.export.results': 'Export Results',
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models'
};
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models',
'providers.form.fallback.token': 'Fallback API Keys'
'providers.form.fallback.token': 'Fallback API Keys',
'providers.form.custombeckendUrl': 'Custom Base URL',
'providers.form.more': '+{count} more',
'providers.form.addAll': 'Add All',
'providers.form.rules.models': 'Please select at least one model',
'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model'
};
+4 -1
View File
@@ -80,5 +80,8 @@ export default {
'benchmark.env.system': 'System',
'benchmark.env.runtimeVersion': 'Runtime Version',
'benchmark.env.driverVersion': 'Driver Version',
'benchmark.env.cpuCounts': 'CPU Counts'
'benchmark.env.cpuCounts': 'CPU Cores',
'benchmark.table.export.results': 'Export Results',
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models'
};
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection',
'providers.form.target.placeholder': 'provider/models',
'providers.form.fallback.token': 'Fallback API Keys'
'providers.form.fallback.token': 'Fallback API Keys',
'providers.form.custombeckendUrl': 'Custom Base URL',
'providers.form.more': '+{count} more',
'providers.form.addAll': 'Add All',
'providers.form.rules.models': 'Please select at least one model',
'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model'
};
+3 -1
View File
@@ -80,5 +80,7 @@ export default {
'benchmark.env.system': '系统',
'benchmark.env.runtimeVersion': '运行时版本',
'benchmark.env.driverVersion': '驱动版本',
'benchmark.env.cpuCounts': 'CPU 核心数'
'benchmark.env.cpuCounts': 'CPU 核心数',
'benchmark.table.export.results': '导出结果',
'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型'
};
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
'providers.form.model.test': '测试',
'providers.form.model.test.tips': '测试连接',
'providers.form.target.placeholder': '提供商/模型',
'providers.form.fallback.token': '备用 API Key'
'providers.form.fallback.token': '备用 API Key',
'providers.form.custombeckendUrl': '自定义 Base URL',
'providers.form.more': '+{count} 更多',
'providers.form.addAll': '添加全部',
'providers.form.rules.models': '请选择至少一个模型',
'providers.form.rules.tokens': '请输入有效的 API Key',
'providers.form.rules.model': '请选择模型'
};
@@ -25,7 +25,7 @@ const RightActions: React.FC<RightActionsProps> = ({
}) => {
const ButtonList = [
{
label: 'common.button.export',
label: 'benchmark.table.export.results',
key: 'export',
icon: (
<IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} />
+12 -12
View File
@@ -8,17 +8,6 @@ import { BenchmarkStatusValueMap } from '../config';
import { BenchmarkListItem as ListItem } from '../config/types';
const actionList = [
{
key: 'edit',
label: 'common.button.edit',
icon: icons.EditOutlined
},
{
label: 'common.button.stop',
key: 'stop',
icon: icons.Stop,
status: [BenchmarkStatusValueMap.QUEUED, BenchmarkStatusValueMap.Running]
},
{
label: 'common.button.viewlog',
key: 'viewlog',
@@ -30,9 +19,20 @@ const actionList = [
],
icon: <IconFont type="icon-logs" />
},
{
key: 'edit',
label: 'common.button.edit',
icon: icons.EditOutlined
},
{
label: 'common.button.stop',
key: 'stop',
icon: icons.Stop,
status: [BenchmarkStatusValueMap.QUEUED, BenchmarkStatusValueMap.Running]
},
{
key: 'export',
label: 'common.button.export',
label: 'benchmark.table.export.results',
icon: (
<IconFont type="icon-export" style={{ lineHeight: 1, fontSize: 16 }} />
)
+27 -8
View File
@@ -3,19 +3,22 @@ import { PageAction } from '@/config';
import useAppUtils from '@/hooks/use-app-utils';
import {
InstanceStatusMap,
InstanceStatusMapValue
InstanceStatusMapValue,
modelCategoriesMap
} from '@/pages/llmodels/config';
import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark';
import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-model-instances';
import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { Form, Tooltip } from 'antd';
import React, { useEffect } from 'react';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
// benchmark.form.nonLlmModel.tips
const InstanceNode = (props: any) => {
const { data: instance } = props;
const intl = useIntl();
return instance.isLeaf ? (
<span className="flex-center">
{instance.label}
@@ -24,7 +27,17 @@ const InstanceNode = (props: any) => {
)}
</span>
) : (
<span>{instance.label}</span>
<>
{instance.disabled ? (
<Tooltip
title={intl.formatMessage({ id: 'benchmark.form.nonLlmModel.tips' })}
>
<span>{instance.label}</span>
</Tooltip>
) : (
<span>{instance.label}</span>
)}
</>
);
};
@@ -99,6 +112,7 @@ const ModelInstanceForm: React.FC = () => {
.map((model: any) => ({
label: model.name,
value: model.name,
disabled: modelCategoriesMap.llm !== model.categories?.[0],
id: model.id,
isLeaf: false,
children: []
@@ -109,12 +123,17 @@ const ModelInstanceForm: React.FC = () => {
}
// preload instances for the first model
const instanceList = await fetchInstanceList({ id: modelOptions[0]?.id });
const selectedllmModel = modelOptions.find((model) => !model.disabled);
if (!selectedllmModel) {
setModelList(modelOptions);
return;
}
const instanceList = await fetchInstanceList({ id: selectedllmModel.id });
const instanceOptions = instanceList.map((instance: any) =>
renderInstance(instance)
);
if (modelOptions[0]) {
modelOptions[0].children = [...instanceOptions] as never[];
if (selectedllmModel) {
selectedllmModel.children = [...instanceOptions] as never[];
}
// init form value for model instance
@@ -127,8 +146,8 @@ const ModelInstanceForm: React.FC = () => {
});
} else {
handleOnChange(
[modelOptions[0].value, instanceOptions[0]?.value],
[modelOptions[0], instanceOptions[0]]
[selectedllmModel.value, instanceOptions[0]?.value],
[selectedllmModel, instanceOptions[0]]
);
}
+6 -2
View File
@@ -147,7 +147,7 @@ const Benchmark: React.FC = () => {
} else if (val === 'stop') {
handleStopBenchmark(row.id);
} else if (val === 'export') {
exportData([row.id]);
exportData([row.id], row.name);
}
});
@@ -192,7 +192,11 @@ const Benchmark: React.FC = () => {
});
const handleExportData = () => {
exportData(rowSelection.selectedRowKeys);
const firstSelectedRow = dataSource.dataList.find(
(item) => item.id === rowSelection.selectedRowKeys[0]
);
const name = firstSelectedRow ? firstSelectedRow.name : 'benchmark';
exportData(rowSelection.selectedRowKeys, name);
};
return (
@@ -1,6 +1,7 @@
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import { downloadFile } from '@/utils/download-stream';
import { message } from 'antd';
import dayjs from 'dayjs';
import { EXPORT_BENCHMARK_LIST } from '../apis';
const matchFilename = (disposition: string | null): string | undefined => {
@@ -12,7 +13,9 @@ const matchFilename = (disposition: string | null): string | undefined => {
};
export function useExportBenchmark() {
const exportData = async (data: any[]) => {
const exportData = async (data: any[], name: string) => {
const date = dayjs().format('YYYYMMDD_HHmmss');
const fileName = `${name || 'benchmark'}_${date}`;
try {
const res = await fetch(
`${GPUSTACK_API_BASE_URL}${EXPORT_BENCHMARK_LIST}`,
@@ -26,8 +29,7 @@ export function useExportBenchmark() {
);
// header
const contentDispostion = res.headers.get('content-Disposition');
const filename =
matchFilename(contentDispostion) || `benchmark-export.yml`;
const filename = matchFilename(contentDispostion) || `${fileName}.yml`;
if (res.ok) {
const blob = await res.blob();
downloadFile(blob, filename);
@@ -15,7 +15,8 @@ const CloudOptions: React.FC<{
const { disabled } = props;
const intl = useIntl();
const form = Form.useFormInstance();
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
const volumes =
Form.useWatch(['cloud_options', volumeOptions.name], form) || [];
const [validateStatusList, setValidateStatusList] = useState<
{ [key: string]: statusType }[]
>([]);
@@ -45,10 +46,15 @@ const CloudOptions: React.FC<{
form.setFieldValue(['cloud_options', volumeOptions.name], data);
toggleValidation(true);
};
console.log('disabled=====', disabled);
return (
<Form.Item
name={['cloud_options', volumeOptions.name as string]}
style={{
backgroundColor:
disabled && volumes?.length === 0
? 'var(--ant-color-bg-container-disabled)'
: ''
}}
rules={[
{
validator: listMapValidator
@@ -1,10 +1,8 @@
import AutoTooltip from '@/components/auto-tooltip';
import {
CheckCircleOutlined,
CloseCircleOutlined,
WarningOutlined
} from '@ant-design/icons';
import { Flex, Tag } from 'antd';
import OverlayScroller from '@/components/overlay-scroller';
import { CheckCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Flex, Popover, Tag } from 'antd';
import _ from 'lodash';
import React from 'react';
import { categoryConfig } from '../../_components/model-tag';
@@ -17,47 +15,101 @@ interface ProviderModelProps {
const ProviderModels: React.FC<ProviderModelProps> = ({ dataList }) => {
const iconsMap = {
accessible: <CheckCircleOutlined />,
inaccessible: <CloseCircleOutlined />,
none: <WarningOutlined />
inaccessible: null,
none: null
};
const intl = useIntl();
const head12Items = dataList.slice(0, 8);
const restItems = dataList.slice(8);
const renderModels = (dataList: ProviderModel[]) => {
return (
<>
{dataList.map((model, index) => (
<Tag
key={index}
icon={
_.isBoolean(model.accessible)
? iconsMap[model.accessible ? 'accessible' : 'inaccessible']
: iconsMap['none']
}
variant="outlined"
styles={{
root: {
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'center',
borderRadius: 4
}
}}
color={
model.accessible === true ? 'var(--ant-color-success)' : 'default'
}
>
<span className="flex-center">
<AutoTooltip ghost maxWidth={'120px'}>
{model.name}
</AutoTooltip>
<span style={{ marginLeft: 8 }}>
{categoryConfig[model.category]?.icon}
</span>
</span>
</Tag>
))}
</>
);
};
return (
<Flex gap="8px" wrap="wrap">
{dataList.map((model) => (
<Tag
key={model.name}
icon={
_.isBoolean(model.accessible)
? iconsMap[model.accessible ? 'accessible' : 'inaccessible']
: iconsMap['none']
}
variant="outlined"
styles={{
root: {
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'center',
borderRadius: 4
<div>
<Flex gap="8px" wrap="wrap">
{renderModels(head12Items)}
{restItems.length > 0 && (
<Popover
placement="right"
content={
<OverlayScroller
maxHeight={420}
styles={{
wrapper: {
paddingInlineStart: 0
}
}}
>
<Flex gap="8px" wrap="wrap">
{renderModels(restItems)}
</Flex>
</OverlayScroller>
}
}}
color={
model.accessible === true
? 'success'
: model.accessible === false
? 'error'
: 'warning'
}
>
<span className="flex-center">
<AutoTooltip ghost maxWidth={'120px'}>
{model.name}
</AutoTooltip>
<span style={{ marginLeft: 8 }}>
{categoryConfig[model.category]?.icon}
</span>
</span>
</Tag>
))}
</Flex>
styles={{
root: { maxWidth: '400px' },
container: {
paddingInlineEnd: 4
}
}}
>
<Tag
variant="outlined"
styles={{
root: {
width: 'fit-content',
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'center',
borderRadius: 4
}
}}
>
{intl.formatMessage(
{ id: 'providers.form.more' },
{ count: restItems.length }
)}
</Tag>
</Popover>
)}
</Flex>
</div>
);
};
+1
View File
@@ -12,6 +12,7 @@ export interface FormData {
api_key: string;
config: {
type: maasProviderType;
openaiCustomUrl?: string;
[key: string]: any;
};
}
+1 -4
View File
@@ -42,10 +42,7 @@ const AccessToken = () => {
rules={[
{
required: false,
message: getRuleMessage(
'input',
intl.formatMessage({ id: 'providers.form.tokens.title' })
)
message: getRuleMessage('input', 'providers.form.tokens.title')
}
]}
>
+28 -7
View File
@@ -4,12 +4,15 @@ import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import ProviderLogo from '../components/provider-logo';
import { maasProviderOptions } from '../config/providers';
import { maasProviderOptions, ProviderEnum } from '../config/providers';
import { FormData } from '../config/types';
const Basic = () => {
const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void;
}> = ({ onAPIKeyBlur }) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const providerType = Form.useWatch(['config', 'type'], form);
const { getRuleMessage } = useAppUtils();
const optionRender = (option: any) => {
@@ -30,7 +33,17 @@ const Basic = () => {
return (
<>
<Form.Item<FormData> name="name" data-field="name">
<Form.Item<FormData>
name="name"
data-field="name"
required
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<SealInput.Input
required
label={intl.formatMessage({
@@ -43,10 +56,7 @@ const Basic = () => {
rules={[
{
required: true,
message: getRuleMessage(
'select',
intl.formatMessage({ id: 'common.table.type' })
)
message: getRuleMessage('select', 'common.table.type')
}
]}
>
@@ -63,6 +73,16 @@ const Basic = () => {
})}
/>
</Form.Item>
{providerType === ProviderEnum.OPENAI && (
<Form.Item<FormData> name={['config', 'openaiCustomUrl']}>
<SealInput.Input
placeholder="http://<your-inference-server>/v1"
label={intl.formatMessage({
id: 'providers.form.custombeckendUrl'
})}
/>
</Form.Item>
)}
<Form.Item<FormData>
name="api_key"
rules={[
@@ -74,6 +94,7 @@ const Basic = () => {
>
<SealInput.Password
required
onBlur={onAPIKeyBlur}
label={intl.formatMessage({
id: 'providers.form.tokens.title'
})}
+3 -1
View File
@@ -107,7 +107,9 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
api_key: currentData.api_tokens?.[0] || '',
api_tokens: currentData.api_tokens?.slice(1) || [],
proxy_enabled: !!currentData.proxy_url,
custom_config: json2Yaml(_.omit(currentData.config, ['type']) || {})
custom_config: json2Yaml(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {}
)
});
}
}, [form, currentData, action]);
+21 -10
View File
@@ -3,8 +3,8 @@ import SealSelect from '@/components/seal-form/seal-select';
import { categoryOptions } from '@/pages/llmodels/config';
import {
CheckCircleFilled,
CloseCircleFilled,
LoadingOutlined
LoadingOutlined,
WarningFilled
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form, Tooltip } from 'antd';
@@ -27,9 +27,15 @@ const SelectWrapper = styled.div`
`;
interface ModelItemProps {
onOpenChange: (open: boolean) => void;
onOpenChange?: (open: boolean) => void;
onChange: (data: ProviderModel) => void;
providerModelList: Global.BaseOption<string>[];
providerModelList: Global.BaseOption<
string,
{
category: string;
accessible: boolean;
}
>[];
selectedModelList: ProviderModel[];
item: ProviderModel;
loading?: boolean;
@@ -63,10 +69,9 @@ const ModelItem: React.FC<ModelItemProps> = ({
});
};
const handleOnChange = (value: string) => {
const handleOnChange = (value: string, option: any) => {
onChange({
...item,
accessible: null,
...option,
name: value
});
};
@@ -79,20 +84,26 @@ const ModelItem: React.FC<ModelItemProps> = ({
};
const renderSuffixIcon = () => {
console.log(
'testLoading',
testLoading,
item.accessible,
item.accessible === false
);
if (testLoading) {
return <LoadingOutlined />;
}
if (item.accessible === true) {
return (
<CheckCircleFilled
style={{ color: 'var(--ant-color-success)', fontSize: 16 }}
style={{ color: 'var(--ant-color-success)', fontSize: 14 }}
/>
);
}
if (item.accessible === false) {
return (
<CloseCircleFilled
style={{ color: 'var(--ant-color-error)', fontSize: 16 }}
<WarningFilled
style={{ color: 'var(--ant-color-error)', fontSize: 14 }}
/>
);
}
@@ -1,6 +1,8 @@
import MetadataList from '@/components/metadata-list';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useRef } from 'react';
import { FormData, ProviderModel } from '../config/types';
import { useQueryProviderModels } from '../hooks/use-query-provider-models';
import ModelItem from './model-item';
@@ -11,15 +13,26 @@ const SupportedModels = () => {
useQueryProviderModels();
const form = Form.useFormInstance<FormData>();
const modelList = Form.useWatch('models', form) || [];
const prevAPIKeyRef = useRef<string>('');
const { getRuleMessage } = useAppUtils();
const handleOpenChange = async (open: boolean) => {
try {
await form.validateFields(['api_key']);
if (open && providerModelList.length === 0) {
const currentAPIKey = form.getFieldValue('api_key') || '';
// Avoid repeated requests with the same API key
if (
open &&
providerModelList.length === 0 &&
prevAPIKeyRef.current !== currentAPIKey &&
currentAPIKey
) {
prevAPIKeyRef.current = currentAPIKey;
fetchProviderModels({
data: {
api_token: form.getFieldValue('api_key') || '',
api_token: currentAPIKey,
config: {
type: form.getFieldValue(['config', 'type']) || ''
}
@@ -55,7 +68,33 @@ const SupportedModels = () => {
return (
<>
<Form.Item name="models" data-field="supportedModels">
<Form.Item
name="models"
data-field="supportedModels"
rules={[
{
required: true,
validator: async (_, value) => {
if (!value || value.length === 0) {
return Promise.reject(
new Error(
getRuleMessage('input', 'providers.form.rules.models')
)
);
}
if (value.some((item: ProviderModel) => !item.name)) {
return Promise.reject(
new Error(
getRuleMessage('select', 'providers.form.rules.model')
)
);
}
return Promise.resolve();
}
}
]}
>
<MetadataList
styles={{
wrapper: {
@@ -33,7 +33,9 @@ export const useQueryProviderModels = () => {
setProviderModelList(
response.data?.map((item: any) => ({
label: item.id,
value: item.id
value: item.id,
accessible: item.accessible,
category: item.categories?.[0] || ''
})) || []
);
},
+1 -1
View File
@@ -14,7 +14,7 @@ export const TargetStatusLabelMap = {
export const TargetStatus: Record<string, StatusType> = {
[TargetStatusValueMap.Active]: StatusMaps.success,
[TargetStatusValueMap.Unavailable]: StatusMaps.error
[TargetStatusValueMap.Unavailable]: StatusMaps.warning
};
// actions for each row
+3
View File
@@ -43,5 +43,8 @@ export interface RouteTarget {
name: string;
route_name: string;
route_id: number;
provider_id: number;
provider_model_name: string;
fallback_status_codes: string[];
state: string;
}
+40 -25
View File
@@ -1,6 +1,7 @@
import MetadataList from '@/components/metadata-list';
import SealCascader from '@/components/seal-form/seal-cascader';
import SealInput from '@/components/seal-form/seal-input';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
@@ -10,6 +11,7 @@ import useTargetSourceModels from '../hooks/use-target-source-models';
const TargetsForm = forwardRef((props, ref) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const { sourceModels, loading, fetchSourceModels } = useTargetSourceModels();
const form = Form.useFormInstance<FormData>();
const targets = Form.useWatch('targets', form) || [];
@@ -38,19 +40,11 @@ const TargetsForm = forwardRef((props, ref) => {
}));
const handleTargetsChange = (value: any[], index: number, options: any[]) => {
console.log(
'handleTargetsChange:',
value,
index,
options,
dataList,
sourceModels
);
const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {};
const targetList = [...targets];
targetList[index] = {
weight: targetList[index]?.weight || null,
weight: targetList[index]?.weight,
...selectedOption?.data
};
@@ -68,11 +62,19 @@ const TargetsForm = forwardRef((props, ref) => {
const newDataList = [
...dataList,
{
weight: null,
weight: 100,
value: []
}
];
setDataList(newDataList);
const newTargets = [
...targets,
{
weight: 100,
value: []
}
];
form.setFieldValue('targets', newTargets);
};
const handleOnDelete = (index: number, item: any) => {
@@ -85,13 +87,19 @@ const TargetsForm = forwardRef((props, ref) => {
};
const handleFallbackChange = (value: any[], options?: any[]) => {
if (!value || value.length === 0) {
form.setFieldValue('fallback_target', null);
setFallbackValues({
value: []
});
return;
}
const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {};
form.setFieldValue('fallback_target', {
...selectedOption?.data
});
console.log('handleFallbackChange:', value, dataList);
setFallbackValues({
value: value
});
@@ -163,27 +171,29 @@ const TargetsForm = forwardRef((props, ref) => {
name="targets"
data-field="targets"
rules={[
({ getFieldValue }) => ({
{
validator(rule, value) {
if (_.keys(value).length > 0) {
if (_.some(_.keys(value), (k: string) => !value[k])) {
if (value && value?.length > 0) {
if (_.some(value, (item: any) => !item.weight)) {
return Promise.reject(
intl.formatMessage(
{
id: 'common.validate.value'
},
{
name: intl.formatMessage({
id: 'models.form.selector'
})
}
)
getRuleMessage('input', 'routes.form.target.weight')
);
}
if (
_.some(
dataList,
(item: any) => !item.value || item.value.length === 0
)
) {
return Promise.reject(
getRuleMessage('input', 'providers.form.target.placeholder')
);
}
}
return Promise.resolve();
}
})
}
]}
>
<MetadataList
@@ -203,6 +213,9 @@ const TargetsForm = forwardRef((props, ref) => {
<SealCascader
required
showSearch
status={
!item.value || item.value.length === 0 ? 'error' : 'success'
}
expandTrigger="hover"
multiple={false}
alwaysFocus={true}
@@ -235,6 +248,8 @@ const TargetsForm = forwardRef((props, ref) => {
<SealInput.Number
style={{ flex: 100 }}
min={0}
step={1}
status={item.weight === null ? 'error' : 'success'}
value={item.weight}
onChange={(value) => handleOnWeightChange(value, index)}
placeholder={intl.formatMessage({
@@ -1,9 +1,11 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types';
const useAccessControl = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
@@ -27,6 +29,7 @@ const useAccessControl = () => {
currentData,
title: title
});
saveScrollHeight();
};
const closeModal = () => {
@@ -36,6 +39,7 @@ const useAccessControl = () => {
currentData: undefined,
title: ''
});
restoreScrollHeight();
};
return {
@@ -1,9 +1,11 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { RouteItem as ListItem } from '../config/types';
const useCreateRoute = (options?: { refresh: () => void }) => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
@@ -27,6 +29,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => {
currentData,
title: title
});
saveScrollHeight();
};
const closeModal = () => {
@@ -36,6 +39,7 @@ const useCreateRoute = (options?: { refresh: () => void }) => {
currentData: undefined,
title: ''
});
restoreScrollHeight();
};
return {
+1 -1
View File
@@ -275,7 +275,7 @@ const ModelRoutes: React.FC = () => {
loadend={dataSource.loadend}
rowSelection={rowSelection}
columns={columns}
childParentKey="access_id"
childParentKey="route_id"
expandable={true}
empty={
<NoResult