fix: my models status

This commit is contained in:
jialin
2025-10-24 16:11:12 +08:00
parent 0401cf5fb9
commit ba80ade643
15 changed files with 277 additions and 61 deletions
+14 -4
View File
@@ -13,6 +13,7 @@ import {
writeState writeState
} from '@/utils/localstore/index'; } from '@/utils/localstore/index';
import { RequestConfig, history } from '@umijs/max'; import { RequestConfig, history } from '@umijs/max';
import { message } from 'antd';
const loginPath = '/login'; const loginPath = '/login';
@@ -53,13 +54,24 @@ export async function getInitialState(): Promise<{
}): Promise<Global.UserInfo> => { }): Promise<Global.UserInfo> => {
try { try {
const data = await queryCurrentUserState({ const data = await queryCurrentUserState({
skipErrorHandler: config?.skipErrorHandler ?? false skipErrorHandler: true
}); });
if (data.is_admin) { if (data.is_admin) {
getUpdateCheck(); getUpdateCheck();
} }
return data; return data;
} catch (error: any) { } catch (error: any) {
const data = error?.response?.data;
if (data?.code === 401 && data?.message.includes('deactivate')) {
message.error({
content: (
<div>
<span>{data?.message}</span>
</div>
),
duration: 5
});
}
history.push(loginPath); history.push(loginPath);
} }
return {} as Global.UserInfo; return {} as Global.UserInfo;
@@ -81,9 +93,7 @@ export async function getInitialState(): Promise<{
getAppVersionInfo(); getAppVersionInfo();
if (![loginPath].includes(location.pathname)) { if (![loginPath].includes(location.pathname)) {
const userInfo = await fetchUserInfo({ const userInfo = await fetchUserInfo();
skipErrorHandler: location.pathname === '/'
});
checkDefaultPage(userInfo); checkDefaultPage(userInfo);
return { return {
fetchUserInfo, fetchUserInfo,
+3 -3
View File
@@ -8,7 +8,7 @@ import './index.less';
interface TagsWrapperProps { interface TagsWrapperProps {
gap?: number; gap?: number;
dataList: any[]; dataList: any[];
renderTag: (item: any) => React.ReactNode; renderTag: (item: any, index?: number) => React.ReactNode;
} }
const TagsWrapper: React.FC<TagsWrapperProps> = (props) => { const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
@@ -94,7 +94,7 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
{_.map( {_.map(
_.slice(dataList, hiddenIndices.start, hiddenIndices.end), _.slice(dataList, hiddenIndices.start, hiddenIndices.end),
(item: any, index: number) => { (item: any, index: number) => {
return <span key={index}>{renderTag?.(item)}</span>; return <span key={index}>{renderTag?.(item, index)}</span>;
} }
)} )}
</div> </div>
@@ -108,7 +108,7 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
_.slice(dataList, hiddenIndices.end), _.slice(dataList, hiddenIndices.end),
(item: any, index: number) => { (item: any, index: number) => {
return { return {
label: renderTag?.(item), label: renderTag?.(item, index),
key: index, key: index,
onClick: handleClick onClick: handleClick
}; };
+2 -1
View File
@@ -261,5 +261,6 @@ export default {
'common.button.edit.item': 'Edit {name}', 'common.button.edit.item': 'Edit {name}',
'common.button.terminal': 'Terminal', 'common.button.terminal': 'Terminal',
'common.button.addItem': 'Add Item', 'common.button.addItem': 'Add Item',
'common.help.default': 'Default: {content}' 'common.help.default': 'Default: {content}',
'common.filter.status': 'Filter by status'
}; };
+4 -1
View File
@@ -203,5 +203,8 @@ export default {
'models.form.gpusAllocationType.auto.tips': 'models.form.gpusAllocationType.auto.tips':
'System calculates GPUs per replica automatically.', 'System calculates GPUs per replica automatically.',
'models.form.gpusAllocationType.custom.tips': 'models.form.gpusAllocationType.custom.tips':
'You can specify the exact number of GPUs per replica.' 'You can specify the exact number of GPUs per replica.',
'models.mymodels.status.inactive': 'Stopped',
'models.mymodels.status.degrade': 'Abnormal',
'models.mymodels.status.active': 'Active'
}; };
+4 -2
View File
@@ -261,7 +261,8 @@ export default {
'common.button.edit.item': 'Edit {name}', 'common.button.edit.item': 'Edit {name}',
'common.button.terminal': 'Terminal', 'common.button.terminal': 'Terminal',
'common.button.addItem': 'Add Item', 'common.button.addItem': 'Add Item',
'common.help.default': 'Default: {content}' 'common.help.default': 'Default: {content}',
'common.filter.status': 'Filter by status'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -288,5 +289,6 @@ export default {
// 21. 'common.button.edit.item': 'Edit {name}', // 21. 'common.button.edit.item': 'Edit {name}',
// 22. 'common.button.terminal': 'Terminal', // 22. 'common.button.terminal': 'Terminal',
// 23. 'common.button.addItem': 'Add Item', // 23. 'common.button.addItem': 'Add Item',
// 24. 'common.help.default': 'Default: {content}' // 24. 'common.help.default': 'Default: {content}',
// 25. 'common.filter.status': 'Filter by status'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+8 -2
View File
@@ -200,7 +200,10 @@ export default {
'models.form.gpusAllocationType.auto.tips': 'models.form.gpusAllocationType.auto.tips':
'System calculates GPUs per replica automatically.', 'System calculates GPUs per replica automatically.',
'models.form.gpusAllocationType.custom.tips': 'models.form.gpusAllocationType.custom.tips':
'You can specify the exact number of GPUs per replica.' 'You can specify the exact number of GPUs per replica.',
'models.mymodels.status.inactive': 'Stopped',
'models.mymodels.status.degrade': 'Abnormal',
'models.mymodels.status.active': 'Active'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -240,6 +243,9 @@ export default {
// 35. 'models.form.gpusAllocationType.auto': 'Auto', // 35. 'models.form.gpusAllocationType.auto': 'Auto',
// 36. 'models.form.gpusAllocationType.custom': 'Custom', // 36. 'models.form.gpusAllocationType.custom': 'Custom',
// 37. 'models.form.gpusAllocationType.auto.tips': 'System calculates GPUs per replica automatically.', // 37. 'models.form.gpusAllocationType.auto.tips': 'System calculates GPUs per replica automatically.',
// 38. 'models.form.gpusAllocationType.custom.tips': 'You can specify the exact number of GPUs per replica.' // 38. 'models.form.gpusAllocationType.custom.tips': 'You can specify the exact number of GPUs per replica.',
// 39. 'models.mymodels.status.inactive': 'Stopped',
// 41. 'models.mymodels.status.degrade': 'Abnormal',
// 42. 'models.mymodels.status.active': 'Active'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+3 -2
View File
@@ -260,9 +260,10 @@ export default {
'common.button.edit.item': 'Редактировать {name}', 'common.button.edit.item': 'Редактировать {name}',
'common.button.terminal': 'Терминал', 'common.button.terminal': 'Терминал',
'common.button.addItem': 'Добавить элемент', 'common.button.addItem': 'Добавить элемент',
'common.help.default': 'По умолчанию: {content}' 'common.help.default': 'По умолчанию: {content}',
'common.filter.status': 'Filter by status'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.filter.status': 'Filter by status'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+15 -5
View File
@@ -187,20 +187,30 @@ export default {
'models.form.chunkSize': 'Размер чанков кэша', 'models.form.chunkSize': 'Размер чанков кэша',
'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)', 'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)',
'models.form.remoteURL': 'URL удаленного хранилища', 'models.form.remoteURL': 'URL удаленного хранилища',
'models.form.runCommandPlaceholder': 'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct', 'models.form.runCommandPlaceholder':
'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct',
'models.accessSettings.public': 'Публичный', 'models.accessSettings.public': 'Публичный',
'models.accessSettings.authed': 'Аутентифицированный', 'models.accessSettings.authed': 'Аутентифицированный',
'models.accessSettings.allowedUsers': 'Разрешенные пользователи', 'models.accessSettings.allowedUsers': 'Разрешенные пользователи',
'models.accessSettings.public.tips': 'При публичном доступе любой пользователь может получить доступ к модели без аутентификации, что может привести к риску утечки данных.', 'models.accessSettings.public.tips':
'При публичном доступе любой пользователь может получить доступ к модели без аутентификации, что может привести к риску утечки данных.',
'models.table.button.deploy': 'Развернуть сейчас', 'models.table.button.deploy': 'Развернуть сейчас',
'models.form.backendVersion.holder': 'Введите или выберите версию', 'models.form.backendVersion.holder': 'Введите или выберите версию',
'models.form.gpusperreplica': 'GPU на реплику', 'models.form.gpusperreplica': 'GPU на реплику',
'models.form.gpusAllocationType': 'Тип распределения GPU', 'models.form.gpusAllocationType': 'Тип распределения GPU',
'models.form.gpusAllocationType.auto': 'Авто', 'models.form.gpusAllocationType.auto': 'Авто',
'models.form.gpusAllocationType.custom': 'Вручную', 'models.form.gpusAllocationType.custom': 'Вручную',
'models.form.gpusAllocationType.auto.tips': 'Система автоматически рассчитывает количество GPU на реплику.', 'models.form.gpusAllocationType.auto.tips':
'models.form.gpusAllocationType.custom.tips': 'Вы можете указать точное количество GPU на реплику.' 'Система автоматически рассчитывает количество GPU на реплику.',
'models.form.gpusAllocationType.custom.tips':
'Вы можете указать точное количество GPU на реплику.',
'models.mymodels.status.inactive': 'Stopped',
'models.mymodels.status.degrade': 'Abnormal',
'models.mymodels.status.active': 'Active'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.mymodels.status.inactive': 'Stopped',
// 3. 'models.mymodels.status.degrade': 'Abnormal',
// 4. 'models.mymodels.status.active': 'Active'
// ========== End of To-Do List ==========
+2 -1
View File
@@ -253,5 +253,6 @@ export default {
'common.button.edit.item': '编辑 {name}', 'common.button.edit.item': '编辑 {name}',
'common.button.terminal': '终端', 'common.button.terminal': '终端',
'common.button.addItem': '添加项', 'common.button.addItem': '添加项',
'common.help.default': '默认: {content}' 'common.help.default': '默认: {content}',
'common.filter.status': '按状态筛选'
}; };
+4 -1
View File
@@ -191,5 +191,8 @@ export default {
'models.form.gpusAllocationType.custom': '自定义', 'models.form.gpusAllocationType.custom': '自定义',
'models.form.gpusAllocationType.auto.tips': 'models.form.gpusAllocationType.auto.tips':
'系统自动计算每个副本的 GPU 数量。', '系统自动计算每个副本的 GPU 数量。',
'models.form.gpusAllocationType.custom.tips': '您可以指定每个副本的 GPU 数量' 'models.form.gpusAllocationType.custom.tips': '您可以指定每个副本的 GPU 数量',
'models.mymodels.status.inactive': '已停止',
'models.mymodels.status.degrade': '异常',
'models.mymodels.status.active': '可用'
}; };
+53 -31
View File
@@ -1,31 +1,24 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import StatusTag from '@/components/status-tag'; import StatusTag from '@/components/status-tag';
import TagWrapper from '@/components/tags-wrapper';
import ThemeTag from '@/components/tags-wrapper/theme-tag'; import ThemeTag from '@/components/tags-wrapper/theme-tag';
import Card from '@/components/templates/card'; import Card from '@/components/templates/card';
import { StatusMaps } from '@/config';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { Button } from 'antd'; import { Button } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { modelCategories, modelCategoriesMap, modelSourceMap } from '../config'; import {
modelCategories,
modelCategoriesMap,
modelSourceMap,
MyModelsStatusLabelMap,
MyModelsStatusMap,
MyModelsStatusValueMap
} from '../config';
import { categoryToPathMap } from '../config/button-actions'; import { categoryToPathMap } from '../config/button-actions';
import { categoryConfig } from './model-tag'; import { categoryConfig } from './model-tag';
const MyModelsStatusValueMap = {
Inactive: 'Inactive',
Starting: 'Starting',
Degrade: 'Degrade',
Active: 'Active'
};
const MyModelsStatusMap = {
[MyModelsStatusValueMap.Inactive]: StatusMaps.inactive,
[MyModelsStatusValueMap.Starting]: StatusMaps.transitioning,
[MyModelsStatusValueMap.Degrade]: StatusMaps.warning,
[MyModelsStatusValueMap.Active]: StatusMaps.success
};
const CardWrapper = styled.div` const CardWrapper = styled.div`
&:hover { &:hover {
.content { .content {
@@ -36,6 +29,14 @@ const CardWrapper = styled.div`
} }
`; `;
const Dot = styled.span`
background-color: var(--ant-color-text-quaternary);
border-radius: 50%;
flex: none;
height: 3px;
width: 3px;
`;
const ModelItemContent = styled.div` const ModelItemContent = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -63,6 +64,7 @@ const ModelItemContent = styled.div`
font-weight: 400; font-weight: 400;
} }
.extra-info { .extra-info {
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -105,6 +107,14 @@ const sourceIconMap = {
[modelSourceMap.modelscope_value]: 'icon-tu2' [modelSourceMap.modelscope_value]: 'icon-tu2'
}; };
const renderTag = (item: any, index = 0) => {
return (
<ThemeTag key={item} className="tag-item" color="purple">
{item}
</ThemeTag>
);
};
const ModelItem: React.FC<{ const ModelItem: React.FC<{
model: Record<string, any>; model: Record<string, any>;
onClick: (model: any) => void; onClick: (model: any) => void;
@@ -155,16 +165,15 @@ const ModelItem: React.FC<{
if (!model.replicas && !model.ready_replicas) { if (!model.replicas && !model.ready_replicas) {
return MyModelsStatusValueMap.Inactive; return MyModelsStatusValueMap.Inactive;
} }
if (model.replicas > 0 && !model.ready_replicas) { if (model.replicas > 0 && !model.ready_replicas) {
return MyModelsStatusValueMap.Starting;
}
if (model.replicas > 1 && model.replicas !== model.ready_replicas) {
return MyModelsStatusValueMap.Degrade; return MyModelsStatusValueMap.Degrade;
} }
if (model.replicas === model.ready_replicas && model.replicas > 0) { if (model.replicas === model.ready_replicas && model.replicas > 0) {
return MyModelsStatusValueMap.Active; return MyModelsStatusValueMap.Active;
} }
return MyModelsStatusValueMap.Inactive; return MyModelsStatusValueMap.Degrade;
}, [model]); }, [model]);
return ( return (
@@ -177,18 +186,20 @@ const ModelItem: React.FC<{
ghost ghost
header={ header={
<Header> <Header>
<span className="text"> <span className="text gap-8">
<IconFont <IconFont
type={sourceIconMap[model.source]} type={sourceIconMap[model.source]}
style={{ marginRight: 8, fontSize: 24 }} style={{ fontSize: 24 }}
/> />
<span>{model.name}</span> <span>{model.name}</span>
</span> </span>
<StatusTag <StatusTag
maxTooltipWidth={400} maxTooltipWidth={400}
statusValue={{ statusValue={{
status: MyModelsStatusMap[status], status: MyModelsStatusMap[model.status],
text: status, text: intl.formatMessage({
id: MyModelsStatusLabelMap[model.status]
}),
message: model.state_message message: model.state_message
}} }}
></StatusTag> ></StatusTag>
@@ -214,16 +225,27 @@ const ModelItem: React.FC<{
</ThemeTag> </ThemeTag>
); );
})} })}
{maxToken > 0 && ( {maxToken > 0 && (
<ThemeTag className="tag-item" color="purple"> <>
{maxToken}K context <Dot></Dot>
</ThemeTag> <ThemeTag className="tag-item" color="purple">
{maxToken}K context
</ThemeTag>
</>
)}
{model.meta?.voices?.length > 0 && (
<>
<Dot></Dot>
<TagWrapper
gap={8}
dataList={model.meta?.voices}
renderTag={renderTag}
></TagWrapper>
</>
)} )}
</div> </div>
{[ {[MyModelsStatusValueMap.Active].includes(model.status) && (
MyModelsStatusValueMap.Active,
MyModelsStatusValueMap.Degrade
].includes(status) && (
<Button <Button
size="middle" size="middle"
className="btn" className="btn"
+22 -2
View File
@@ -188,6 +188,24 @@ export const status: any = {
[InstanceStatusMap.Unreachable]: StatusMaps.error [InstanceStatusMap.Unreachable]: StatusMaps.error
}; };
export const MyModelsStatusValueMap = {
Inactive: 'Inactive',
Degrade: 'Degrade',
Active: 'Active'
};
export const MyModelsStatusMap = {
[MyModelsStatusValueMap.Inactive]: StatusMaps.inactive,
[MyModelsStatusValueMap.Degrade]: StatusMaps.error,
[MyModelsStatusValueMap.Active]: StatusMaps.success
};
export const MyModelsStatusLabelMap = {
[MyModelsStatusValueMap.Inactive]: 'models.mymodels.status.inactive',
[MyModelsStatusValueMap.Degrade]: 'models.mymodels.status.degrade',
[MyModelsStatusValueMap.Active]: 'models.mymodels.status.active'
};
export const ScheduleValueMap = { export const ScheduleValueMap = {
Auto: 'auto', Auto: 'auto',
Manual: 'manual', Manual: 'manual',
@@ -334,7 +352,8 @@ export const excludeFields = [
'backend', 'backend',
'gpu_selector', 'gpu_selector',
'run_command', 'run_command',
'image_name' 'image_name',
'extended_kv_cache.enabled'
]; ];
// ingore fields when compare old and new data // ingore fields when compare old and new data
@@ -357,7 +376,8 @@ export const updateExcludeFields = [
'gpu_selector', 'gpu_selector',
'categories', 'categories',
'env', 'env',
'replicas' 'replicas',
'extended_kv_cache.enabled'
]; ];
export const formFields = [ export const formFields = [
+12 -1
View File
@@ -214,8 +214,19 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
}; };
const getFieldPaths = (obj: Record<string, any>, prefix = ''): string => {
const result = Object.entries(obj).flatMap(([key, value]) => {
const path = prefix ? `${prefix}.${key}` : key;
return typeof value === 'object' && value !== null
? getFieldPaths(value, path)
: [path];
});
return result[0] || '';
};
const handleOnValuesChange = async (changedValues: any, allValues: any) => { const handleOnValuesChange = async (changedValues: any, allValues: any) => {
const fieldName = Object.keys(changedValues)[0]; const fieldName = getFieldPaths(changedValues);
if (excludeFields.includes(fieldName)) { if (excludeFields.includes(fieldName)) {
return; return;
+20
View File
@@ -3,13 +3,32 @@ import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
const KVCacheForm = () => { const KVCacheForm = () => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const { onValuesChange } = useFormContext();
const kvCacheEnabled = Form.useWatch(['extended_kv_cache', 'enabled'], form); const kvCacheEnabled = Form.useWatch(['extended_kv_cache', 'enabled'], form);
const handleOnChange = async (e: any) => {
if (e.target.checked) {
form.setFieldsValue({
extended_kv_cache: {
enabled: true,
chunk_size: 256,
max_local_cpu_size: 10,
remote_url: ''
}
});
}
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
onValuesChange?.({}, form.getFieldsValue());
};
return ( return (
<> <>
<div style={{ paddingBottom: 22 }}> <div style={{ paddingBottom: 22 }}>
@@ -20,6 +39,7 @@ const KVCacheForm = () => {
style={{ padding: '0 10px', marginBottom: 0 }} style={{ padding: '0 10px', marginBottom: 0 }}
> >
<CheckboxField <CheckboxField
onChange={handleOnChange}
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })} label={intl.formatMessage({ id: 'models.form.extendedkvcache' })}
></CheckboxField> ></CheckboxField>
</Form.Item> </Form.Item>
+111 -5
View File
@@ -9,14 +9,41 @@ import { PageContainer } from '@ant-design/pro-components';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Button, Input, Space } from 'antd'; import { Button, Input, Space } from 'antd';
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import { ScrollerContext } from '../_components/infinite-scroller/use-scroller-context'; import { ScrollerContext } from '../_components/infinite-scroller/use-scroller-context';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
import { MY_MODELS_API, queryMyModels } from './apis'; import { MY_MODELS_API, queryMyModels } from './apis';
import ModelItem from './components/model-item'; import ModelItem from './components/model-item';
import { categoryOptions, modelCategoriesMap } from './config'; import {
categoryOptions,
modelCategoriesMap,
MyModelsStatusValueMap
} from './config';
import { categoryToPathMap } from './config/button-actions'; import { categoryToPathMap } from './config/button-actions';
const Dot = ({ color }: { color: string }) => {
return (
<span
style={{
backgroundColor: color,
borderRadius: '50%',
height: 8,
width: 8,
display: 'flex'
}}
></span>
);
};
const optionRender = (item: any) => {
return (
<span className="flex-center gap-8">
<Dot color={item.data?.color}></Dot>
{item.label}
</span>
);
};
const UserModels: React.FC = () => { const UserModels: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
@@ -36,6 +63,33 @@ const UserModels: React.FC = () => {
} }
}); });
const intl = useIntl(); const intl = useIntl();
const [status, setStatus] = React.useState<string | undefined>(undefined);
const statusOptions = useMemo(() => {
return [
{
value: MyModelsStatusValueMap.Active,
color: 'var(--ant-color-success)',
label: intl.formatMessage({
id: 'models.mymodels.status.active'
})
},
{
value: MyModelsStatusValueMap.Inactive,
color: 'var(--ant-color-fill)',
label: intl.formatMessage({
id: 'models.mymodels.status.inactive'
})
},
{
value: MyModelsStatusValueMap.Degrade,
color: 'var(--ant-color-error)',
label: intl.formatMessage({
id: 'models.mymodels.status.degrade'
})
}
];
}, [intl]);
const handleCategoryChange = (value: string) => { const handleCategoryChange = (value: string) => {
handleQueryChange({ handleQueryChange({
@@ -77,6 +131,46 @@ const UserModels: React.FC = () => {
}); });
}); });
const handleStatusChange = (value: string) => {
setStatus(value);
};
const labelRender = (item: any) => {
const current = statusOptions.find((option) => option.value === item.value);
return (
<span className="flex-center gap-8">
<Dot color={current!.color}></Dot>
{item.label}
</span>
);
};
const getStatus = useCallback((model: any) => {
if (!model.replicas && !model.ready_replicas) {
return MyModelsStatusValueMap.Inactive;
}
if (model.replicas > 0 && !model.ready_replicas) {
return MyModelsStatusValueMap.Degrade;
}
if (model.replicas === model.ready_replicas && model.replicas > 0) {
return MyModelsStatusValueMap.Active;
}
return MyModelsStatusValueMap.Degrade;
}, []);
const dataList = useMemo(() => {
const result = dataSource.dataList.map((item) => {
return {
...item,
status: getStatus(item)
};
});
if (!status) return result;
return result.filter((item) => item.status === status);
}, [dataSource.dataList, status]);
return ( return (
<PageContainer <PageContainer
ghost ghost
@@ -117,6 +211,18 @@ const UserModels: React.FC = () => {
options={categoryOptions} options={categoryOptions}
onChange={handleCategoryChange} onChange={handleCategoryChange}
></BaseSelect> ></BaseSelect>
<BaseSelect
allowClear
showSearch={false}
placeholder={intl.formatMessage({ id: 'common.filter.status' })}
style={{ width: 180 }}
size="large"
maxTagCount={1}
optionRender={optionRender}
labelRender={labelRender}
options={statusOptions}
onChange={handleStatusChange}
></BaseSelect>
<Button <Button
type="text" type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }} style={{ color: 'var(--ant-color-text-tertiary)' }}
@@ -135,7 +241,7 @@ const UserModels: React.FC = () => {
}} }}
> >
<CardList <CardList
dataList={dataSource.dataList} dataList={dataList}
loading={dataSource.loading} loading={dataSource.loading}
activeId={false} activeId={false}
isFirst={!dataSource.loadend} isFirst={!dataSource.loadend}
@@ -145,9 +251,9 @@ const UserModels: React.FC = () => {
<NoResult <NoResult
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
dataSource={dataSource.dataList} dataSource={dataList}
image={<IconFont type="icon-models" />} image={<IconFont type="icon-models" />}
filters={queryParams} filters={{ ...queryParams, status: status }}
noFoundText={intl.formatMessage({ noFoundText={intl.formatMessage({
id: 'noresult.mymodels.nofound' id: 'noresult.mymodels.nofound'
})} })}