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
} from '@/utils/localstore/index';
import { RequestConfig, history } from '@umijs/max';
import { message } from 'antd';
const loginPath = '/login';
@@ -53,13 +54,24 @@ export async function getInitialState(): Promise<{
}): Promise<Global.UserInfo> => {
try {
const data = await queryCurrentUserState({
skipErrorHandler: config?.skipErrorHandler ?? false
skipErrorHandler: true
});
if (data.is_admin) {
getUpdateCheck();
}
return data;
} 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);
}
return {} as Global.UserInfo;
@@ -81,9 +93,7 @@ export async function getInitialState(): Promise<{
getAppVersionInfo();
if (![loginPath].includes(location.pathname)) {
const userInfo = await fetchUserInfo({
skipErrorHandler: location.pathname === '/'
});
const userInfo = await fetchUserInfo();
checkDefaultPage(userInfo);
return {
fetchUserInfo,
+3 -3
View File
@@ -8,7 +8,7 @@ import './index.less';
interface TagsWrapperProps {
gap?: number;
dataList: any[];
renderTag: (item: any) => React.ReactNode;
renderTag: (item: any, index?: number) => React.ReactNode;
}
const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
@@ -94,7 +94,7 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
{_.map(
_.slice(dataList, hiddenIndices.start, hiddenIndices.end),
(item: any, index: number) => {
return <span key={index}>{renderTag?.(item)}</span>;
return <span key={index}>{renderTag?.(item, index)}</span>;
}
)}
</div>
@@ -108,7 +108,7 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
_.slice(dataList, hiddenIndices.end),
(item: any, index: number) => {
return {
label: renderTag?.(item),
label: renderTag?.(item, index),
key: index,
onClick: handleClick
};
+2 -1
View File
@@ -261,5 +261,6 @@ export default {
'common.button.edit.item': 'Edit {name}',
'common.button.terminal': 'Terminal',
'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':
'System calculates GPUs per replica automatically.',
'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.terminal': 'Terminal',
'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) ==========
@@ -288,5 +289,6 @@ export default {
// 21. 'common.button.edit.item': 'Edit {name}',
// 22. 'common.button.terminal': 'Terminal',
// 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 ==========
+8 -2
View File
@@ -200,7 +200,10 @@ export default {
'models.form.gpusAllocationType.auto.tips':
'System calculates GPUs per replica automatically.',
'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) ==========
@@ -240,6 +243,9 @@ export default {
// 35. 'models.form.gpusAllocationType.auto': 'Auto',
// 36. 'models.form.gpusAllocationType.custom': 'Custom',
// 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 ==========
+3 -2
View File
@@ -260,9 +260,10 @@ export default {
'common.button.edit.item': 'Редактировать {name}',
'common.button.terminal': 'Терминал',
'common.button.addItem': 'Добавить элемент',
'common.help.default': 'По умолчанию: {content}'
'common.help.default': 'По умолчанию: {content}',
'common.filter.status': 'Filter by status'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.filter.status': 'Filter by status'
// ========== End of To-Do List ==========
+15 -5
View File
@@ -187,20 +187,30 @@ export default {
'models.form.chunkSize': 'Размер чанков кэша',
'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)',
'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.authed': 'Аутентифицированный',
'models.accessSettings.allowedUsers': 'Разрешенные пользователи',
'models.accessSettings.public.tips': 'При публичном доступе любой пользователь может получить доступ к модели без аутентификации, что может привести к риску утечки данных.',
'models.accessSettings.public.tips':
'При публичном доступе любой пользователь может получить доступ к модели без аутентификации, что может привести к риску утечки данных.',
'models.table.button.deploy': 'Развернуть сейчас',
'models.form.backendVersion.holder': 'Введите или выберите версию',
'models.form.gpusperreplica': 'GPU на реплику',
'models.form.gpusAllocationType': 'Тип распределения GPU',
'models.form.gpusAllocationType.auto': 'Авто',
'models.form.gpusAllocationType.custom': 'Вручную',
'models.form.gpusAllocationType.auto.tips': 'Система автоматически рассчитывает количество GPU на реплику.',
'models.form.gpusAllocationType.custom.tips': 'Вы можете указать точное количество GPU на реплику.'
'models.form.gpusAllocationType.auto.tips':
'Система автоматически рассчитывает количество 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) ==========
// 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.terminal': '终端',
'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.auto.tips':
'系统自动计算每个副本的 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 StatusTag from '@/components/status-tag';
import TagWrapper from '@/components/tags-wrapper';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import Card from '@/components/templates/card';
import { StatusMaps } from '@/config';
import { useIntl, useNavigate } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
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 { 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`
&:hover {
.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`
display: flex;
flex-direction: column;
@@ -63,6 +64,7 @@ const ModelItemContent = styled.div`
font-weight: 400;
}
.extra-info {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
@@ -105,6 +107,14 @@ const sourceIconMap = {
[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<{
model: Record<string, any>;
onClick: (model: any) => void;
@@ -155,16 +165,15 @@ const ModelItem: React.FC<{
if (!model.replicas && !model.ready_replicas) {
return MyModelsStatusValueMap.Inactive;
}
if (model.replicas > 0 && !model.ready_replicas) {
return MyModelsStatusValueMap.Starting;
}
if (model.replicas > 1 && model.replicas !== model.ready_replicas) {
return MyModelsStatusValueMap.Degrade;
}
if (model.replicas === model.ready_replicas && model.replicas > 0) {
return MyModelsStatusValueMap.Active;
}
return MyModelsStatusValueMap.Inactive;
return MyModelsStatusValueMap.Degrade;
}, [model]);
return (
@@ -177,18 +186,20 @@ const ModelItem: React.FC<{
ghost
header={
<Header>
<span className="text">
<span className="text gap-8">
<IconFont
type={sourceIconMap[model.source]}
style={{ marginRight: 8, fontSize: 24 }}
style={{ fontSize: 24 }}
/>
<span>{model.name}</span>
</span>
<StatusTag
maxTooltipWidth={400}
statusValue={{
status: MyModelsStatusMap[status],
text: status,
status: MyModelsStatusMap[model.status],
text: intl.formatMessage({
id: MyModelsStatusLabelMap[model.status]
}),
message: model.state_message
}}
></StatusTag>
@@ -214,16 +225,27 @@ const ModelItem: React.FC<{
</ThemeTag>
);
})}
{maxToken > 0 && (
<ThemeTag className="tag-item" color="purple">
{maxToken}K context
</ThemeTag>
<>
<Dot></Dot>
<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>
{[
MyModelsStatusValueMap.Active,
MyModelsStatusValueMap.Degrade
].includes(status) && (
{[MyModelsStatusValueMap.Active].includes(model.status) && (
<Button
size="middle"
className="btn"
+22 -2
View File
@@ -188,6 +188,24 @@ export const status: any = {
[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 = {
Auto: 'auto',
Manual: 'manual',
@@ -334,7 +352,8 @@ export const excludeFields = [
'backend',
'gpu_selector',
'run_command',
'image_name'
'image_name',
'extended_kv_cache.enabled'
];
// ingore fields when compare old and new data
@@ -357,7 +376,8 @@ export const updateExcludeFields = [
'gpu_selector',
'categories',
'env',
'replicas'
'replicas',
'extended_kv_cache.enabled'
];
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 fieldName = Object.keys(changedValues)[0];
const fieldName = getFieldPaths(changedValues);
if (excludeFields.includes(fieldName)) {
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 { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const KVCacheForm = () => {
const intl = useIntl();
const form = Form.useFormInstance();
const { onValuesChange } = useFormContext();
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 (
<>
<div style={{ paddingBottom: 22 }}>
@@ -20,6 +39,7 @@ const KVCacheForm = () => {
style={{ padding: '0 10px', marginBottom: 0 }}
>
<CheckboxField
onChange={handleOnChange}
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })}
></CheckboxField>
</Form.Item>
+111 -5
View File
@@ -9,14 +9,41 @@ import { PageContainer } from '@ant-design/pro-components';
import { useIntl, useNavigate } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
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 NoResult from '../_components/no-result';
import { MY_MODELS_API, queryMyModels } from './apis';
import ModelItem from './components/model-item';
import { categoryOptions, modelCategoriesMap } from './config';
import {
categoryOptions,
modelCategoriesMap,
MyModelsStatusValueMap
} from './config';
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 navigate = useNavigate();
const {
@@ -36,6 +63,33 @@ const UserModels: React.FC = () => {
}
});
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) => {
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 (
<PageContainer
ghost
@@ -117,6 +211,18 @@ const UserModels: React.FC = () => {
options={categoryOptions}
onChange={handleCategoryChange}
></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
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
@@ -135,7 +241,7 @@ const UserModels: React.FC = () => {
}}
>
<CardList
dataList={dataSource.dataList}
dataList={dataList}
loading={dataSource.loading}
activeId={false}
isFirst={!dataSource.loadend}
@@ -145,9 +251,9 @@ const UserModels: React.FC = () => {
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
dataSource={dataList}
image={<IconFont type="icon-models" />}
filters={queryParams}
filters={{ ...queryParams, status: status }}
noFoundText={intl.formatMessage({
id: 'noresult.mymodels.nofound'
})}