fix: replicas, ready_replicas sorting

This commit is contained in:
jialin
2025-12-23 18:35:33 +08:00
parent 9e019a98f6
commit 530e8f7537
18 changed files with 106 additions and 77 deletions
+3 -1
View File
@@ -8,6 +8,7 @@ type UserSettings = {
colorPrimary: string; colorPrimary: string;
isDarkTheme: boolean; isDarkTheme: boolean;
collapsed: boolean; collapsed: boolean;
hideAddResourceModal?: boolean;
}; };
const defaultSettings: UserSettings = { const defaultSettings: UserSettings = {
@@ -15,7 +16,8 @@ const defaultSettings: UserSettings = {
mode: 'auto', mode: 'auto',
isDarkTheme: false, isDarkTheme: false,
colorPrimary: COLOR_PRIMARY, colorPrimary: COLOR_PRIMARY,
collapsed: false collapsed: false,
hideAddResourceModal: false
}; };
export const getStorageUserSettings = () => { export const getStorageUserSettings = () => {
+1
View File
@@ -9,6 +9,7 @@ export const clearStorageUserSettings = () => {
'userSettings', 'userSettings',
JSON.stringify({ JSON.stringify({
...savedSettings, ...savedSettings,
hideAddResourceModal: false,
colorPrimary: undefined colorPrimary: undefined
}) })
); );
+1
View File
@@ -244,6 +244,7 @@ export default function useTableFetch<T>(
setQueryParams((pre: any) => { setQueryParams((pre: any) => {
return { return {
...pre, ...pre,
page: 1,
sort_by: sortKeys.join(',') sort_by: sortKeys.join(',')
}; };
}); });
+23 -10
View File
@@ -28,7 +28,7 @@ import { Button, ConfigProvider, Modal, theme } from 'antd';
import 'driver.js/dist/driver.css'; import 'driver.js/dist/driver.css';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { PageContainerInner } from '../pages/_components/page-box'; import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception'; import Exception from './Exception';
import './Layout.css'; import './Layout.css';
@@ -113,12 +113,6 @@ export default (props: any) => {
const { initialize: initialize } = useOverlayScroller({ const { initialize: initialize } = useOverlayScroller({
defer: false defer: false
}); });
const {
setLoadingStatus,
fetchResourceData,
NoResourceModal,
loadingStatus
} = useAddResource();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { themeData, setUserSettings, userSettings } = useUserSettings(); const { themeData, setUserSettings, userSettings } = useUserSettings();
const [userInfo] = useAtom(userAtom); const [userInfo] = useAtom(userAtom);
@@ -128,6 +122,18 @@ export default (props: any) => {
const intl = useIntl(); const intl = useIntl();
const { clientRoutes } = useAppData(); const { clientRoutes } = useAppData();
const [version] = useAtom(GPUStackVersionAtom); const [version] = useAtom(GPUStackVersionAtom);
const requestResourceRef = useRef<boolean>(false);
const {
setLoadingStatus,
fetchResourceData,
NoResourceModal,
loadingStatus
} = useAddResource({
onCreate() {
requestResourceRef.current = false;
}
});
const initialInfo = (useModel && useModel('@@initialState')) || { const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined, initialState: undefined,
@@ -252,15 +258,18 @@ export default (props: any) => {
return <SiderMenu {...menuProps}></SiderMenu>; return <SiderMenu {...menuProps}></SiderMenu>;
}; };
const onPageChange = (route: any) => { const onPageChange = async (route: any) => {
const { location } = history; const { location } = history;
const { pathname } = location; const { pathname } = location;
if ( if (
!CHECK_RESOURCE_PATH.includes(pathname) && !CHECK_RESOURCE_PATH.includes(pathname) &&
initialState?.currentUser?.is_admin initialState?.currentUser?.is_admin &&
!requestResourceRef.current &&
!userSettings.hideAddResourceModal
) { ) {
fetchResourceData(); requestResourceRef.current = true;
await fetchResourceData();
} }
initRouteCacheValue(pathname); initRouteCacheValue(pathname);
@@ -294,6 +303,10 @@ export default (props: any) => {
}; };
const onCollapse = (value: boolean) => { const onCollapse = (value: boolean) => {
// only trigger by window resize
if (!value) {
return;
}
setUserSettings({ setUserSettings({
...userSettings, ...userSettings,
collapsed: value collapsed: value
+4 -4
View File
@@ -72,7 +72,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP', 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
'clusters.addworker.specifyWorkerAddress': 'Specify Worker External Address', 'clusters.addworker.specifyWorkerAddress': 'Specify Worker External Address',
'clusters.addworker.detectWorkerAddress': 'clusters.addworker.detectWorkerAddress':
'Specify Worker External Address (default uses Worker IP)', 'Specify Worker External Address (default to Worker IP)',
'clusters.addworker.externalIP.tips': 'clusters.addworker.externalIP.tips':
'If running in a VPC or private network, please specify the Worker external address reachable by the GPUStack Server.', 'If running in a VPC or private network, please specify the Worker external address reachable by the GPUStack Server.',
'clusters.addworker.enterWorkerIP': 'Enter worker IP', 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
@@ -83,10 +83,10 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'If you want to customize the model cache directory, you can specify the path to mount it.', 'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary', 'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor', 'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'clusters.addworker.workerIP': 'Worker IP',
'Worker IP <span class="text-tertiary">{type}</span>', 'clusters.addworker.workerExternalIP': 'Worker External Address',
'clusters.addworker.notSpecified': 'Not Specified', 'clusters.addworker.notSpecified': 'Not Specified',
'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.autoDetect': 'Auto',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', 'e.g. /data/models (path must start with /)',
'clusters.addworker.cacheVolume.holder': 'clusters.addworker.cacheVolume.holder':
+2 -1
View File
@@ -265,5 +265,6 @@ export default {
'common.filter.status': 'Filter by status', 'common.filter.status': 'Filter by status',
'common.form.rule.selectInput': 'Please select or enter a {name}', 'common.form.rule.selectInput': 'Please select or enter a {name}',
'common.tag.experimental': 'Experimental', 'common.tag.experimental': 'Experimental',
'common.title.example': 'Example' 'common.title.example': 'Example',
'common.button.dontshowagain': "Don't show again"
}; };
+6 -5
View File
@@ -83,10 +83,10 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'If you want to customize the model cache directory, you can specify the path to mount it.', 'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary', 'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor', 'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'clusters.addworker.workerIP': 'Worker IP',
'Worker IP <span class="text-tertiary">{type}</span>', 'clusters.addworker.workerExternalIP': 'Worker External Address',
'clusters.addworker.notSpecified': 'Not Specified', 'clusters.addworker.notSpecified': 'Not Specified',
'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.autoDetect': 'Auto',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', 'e.g. /data/models (path must start with /)',
'clusters.addworker.cacheVolume.holder': 'clusters.addworker.cacheVolume.holder':
@@ -177,9 +177,10 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 61. 'clusters.addworker.extraVolume': 'Additional Volume Mount', // 61. 'clusters.addworker.extraVolume': 'Additional Volume Mount',
// 62. 'clusters.addworker.configSummary': 'Configuration Summary', // 62. 'clusters.addworker.configSummary': 'Configuration Summary',
// 63. 'clusters.addworker.gpuVendor': 'GPU Vendor', // 63. 'clusters.addworker.gpuVendor': 'GPU Vendor',
// 64. 'clusters.addworker.workerIP': 'Worker IP <span class="text-tertiary">{type}</span>', // 64. 'clusters.addworker.workerIP': 'Worker IP',
// 65. 'clusters.addworker.workerExternalIP': 'Worker External Address',
// 65. 'clusters.addworker.notSpecified': 'Not Specified', // 65. 'clusters.addworker.notSpecified': 'Not Specified',
// 66. 'clusters.addworker.autoDetect': 'Auto-detect', // 66. 'clusters.addworker.autoDetect': 'Auto',
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)' // 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device', // 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
// 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.', // 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
+4 -2
View File
@@ -265,7 +265,8 @@ export default {
'common.filter.status': 'Filter by status', 'common.filter.status': 'Filter by status',
'common.form.rule.selectInput': 'Please select or enter a {name}', 'common.form.rule.selectInput': 'Please select or enter a {name}',
'common.tag.experimental': 'Experimental', 'common.tag.experimental': 'Experimental',
'common.title.example': 'Example' 'common.title.example': 'Example',
'common.button.dontshowagain': "Don't show again"
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -296,5 +297,6 @@ export default {
// 25. 'common.filter.status': 'Filter by status' // 25. 'common.filter.status': 'Filter by status'
// 26. 'common.form.rule.selectInput': 'Please select or enter a {name}', // 26. 'common.form.rule.selectInput': 'Please select or enter a {name}',
// 27. 'common.tag.experimental': 'Experimental', // 27. 'common.tag.experimental': 'Experimental',
// 28. 'common.title.example': 'Example' // 28. 'common.title.example': 'Example',
// 29. 'common.button.dontshowagain': "Don't show again"
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+2 -2
View File
@@ -83,8 +83,8 @@ export default {
'If you want to customize the model cache directory, you can specify the path to mount it.', 'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Сводка конфигурации', 'clusters.addworker.configSummary': 'Сводка конфигурации',
'clusters.addworker.gpuVendor': 'Производитель GPU', 'clusters.addworker.gpuVendor': 'Производитель GPU',
'clusters.addworker.workerIP': 'clusters.addworker.workerIP': 'IP воркера',
'IP воркера <span class="text-tertiary">{type}</span>', 'clusters.addworker.workerExternalIP': 'Внешний адрес воркера',
'clusters.addworker.notSpecified': 'Не указано', 'clusters.addworker.notSpecified': 'Не указано',
'clusters.addworker.autoDetect': 'Автоопределение', 'clusters.addworker.autoDetect': 'Автоопределение',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
+3 -2
View File
@@ -264,9 +264,10 @@ export default {
'common.filter.status': 'Фильтровать по статусу', 'common.filter.status': 'Фильтровать по статусу',
'common.form.rule.selectInput': 'Выберите или введите {name}', 'common.form.rule.selectInput': 'Выберите или введите {name}',
'common.tag.experimental': 'Экспериментальный', 'common.tag.experimental': 'Экспериментальный',
'common.title.example': 'Пример' 'common.title.example': 'Пример',
'common.button.dontshowagain': "Don't show again"
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.button.dontshowagain': "Don't show again"
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+7 -7
View File
@@ -66,13 +66,13 @@ export default {
'clusters.addworker.checkEnv': '检查环境', 'clusters.addworker.checkEnv': '检查环境',
'clusters.addworker.specifyArgs': '指定参数', 'clusters.addworker.specifyArgs': '指定参数',
'clusters.addworker.runCommand': '运行指令', 'clusters.addworker.runCommand': '运行指令',
'clusters.addworker.specifyWorkerIP': '指定 Worker IP', 'clusters.addworker.specifyWorkerIP': '指定节点 IP',
'clusters.addworker.detectWorkerIP': '自动检测 Worker IP', 'clusters.addworker.detectWorkerIP': '自动检测节点 IP',
'clusters.addworker.specifyWorkerAddress': '指定 Worker 外部地址', 'clusters.addworker.specifyWorkerAddress': '指定节点外部地址',
'clusters.addworker.detectWorkerAddress': 'clusters.addworker.detectWorkerAddress':
'指定 Worker 外部地址(默认使用Worker IP', '指定节点外部地址(默认使用节点 IP',
'clusters.addworker.externalIP.tips': 'clusters.addworker.externalIP.tips':
'如运行在 VPC 或私有网络时,请指定 GPUStack Server 可达的 Worker 外部地址。', '如运行在 VPC 或私有网络时,请指定 GPUStack Server 可达的节点外部地址。',
'clusters.addworker.enterWorkerIP': '输入节点 IP', 'clusters.addworker.enterWorkerIP': '输入节点 IP',
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP', 'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
'clusters.addworker.extraVolume': '额外卷挂载', 'clusters.addworker.extraVolume': '额外卷挂载',
@@ -81,8 +81,8 @@ export default {
'如果你想自定义模型缓存目录,可以指定路径进行挂载。', '如果你想自定义模型缓存目录,可以指定路径进行挂载。',
'clusters.addworker.configSummary': '配置摘要', 'clusters.addworker.configSummary': '配置摘要',
'clusters.addworker.gpuVendor': 'GPU 厂商', 'clusters.addworker.gpuVendor': 'GPU 厂商',
'clusters.addworker.workerIP': 'clusters.addworker.workerIP': '节点 IP',
'节点 IP <span class="text-tertiary">{type}</span>', 'clusters.addworker.workerExternalIP': '节点外部地址',
'clusters.addworker.notSpecified': '未指定', 'clusters.addworker.notSpecified': '未指定',
'clusters.addworker.autoDetect': '自动检测', 'clusters.addworker.autoDetect': '自动检测',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
+2 -1
View File
@@ -257,5 +257,6 @@ export default {
'common.filter.status': '按状态筛选', 'common.filter.status': '按状态筛选',
'common.form.rule.selectInput': '请选择或输入{name}', 'common.form.rule.selectInput': '请选择或输入{name}',
'common.tag.experimental': '实验性', 'common.tag.experimental': '实验性',
'common.title.example': '示例' 'common.title.example': '示例',
'common.button.dontshowagain': '不再提示'
}; };
@@ -126,16 +126,11 @@ const SummaryData: React.FC = () => {
<DataItem <DataItem
label={ label={
<span <span>
dangerouslySetInnerHTML={{ {intl.formatMessage({
__html: intl.formatMessage( id: 'clusters.addworker.workerIP'
{ id: 'clusters.addworker.workerIP' }, })}
{ </span>
type: `(${intl.formatMessage({ id: 'clusters.table.ip.internal' })})`
}
)
}}
></span>
} }
tips={ tips={
workerIPConfig.enable workerIPConfig.enable
@@ -151,16 +146,11 @@ const SummaryData: React.FC = () => {
<DataItem <DataItem
label={ label={
<span <span>
dangerouslySetInnerHTML={{ {intl.formatMessage({
__html: intl.formatMessage( id: 'clusters.addworker.workerExternalIP'
{ id: 'clusters.addworker.workerIP' }, })}
{ </span>
type: `(${intl.formatMessage({ id: 'clusters.table.ip.external' })})`
}
)
}}
></span>
} }
tips={ tips={
externalWorkerIPConfig.enable externalWorkerIPConfig.enable
+24 -12
View File
@@ -2,6 +2,7 @@ import { clusterSessionAtom } from '@/atoms/clusters';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import ScrollerModal from '@/components/scroller-modal/index'; import ScrollerModal from '@/components/scroller-modal/index';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import useUserSettings from '@/hooks/use-user-settings';
import useClusterList from '@/pages/cluster-management/hooks/use-cluster-list'; import useClusterList from '@/pages/cluster-management/hooks/use-cluster-list';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { Button } from 'antd'; import { Button } from 'antd';
@@ -53,14 +54,15 @@ const Content = styled.div`
} }
`; `;
export default function useAddResource() { export default function useAddResource(options?: { onCreated?: () => void }) {
const { onCreated } = options || {};
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const { setUserSettings, userSettings } = useUserSettings();
const [, setClusterSession] = useAtom(clusterSessionAtom); const [, setClusterSession] = useAtom(clusterSessionAtom);
const [hideModalTemporarily, setHideModalTemporarily] = useState(false);
const { fetchResource, resourceCount, resourceAtom } = useClusterList(); const { fetchResource, resourceCount, resourceAtom } = useClusterList();
const [hiddenModal, setHiddenModal] = useState(false);
const [loadingStatus, setLoadingStatus] = useState({ const [loadingStatus, setLoadingStatus] = useState({
loading: false, loading: false,
loadend: false loadend: false
@@ -73,7 +75,6 @@ export default function useAddResource() {
}, [resourceAtom, loadingStatus]); }, [resourceAtom, loadingStatus]);
const contentInfo = useMemo(() => { const contentInfo = useMemo(() => {
console.log('resourceCount=', resourceCount);
if (!resourceCount.cluster_count) { if (!resourceCount.cluster_count) {
return { return {
title: intl.formatMessage({ id: 'noresult.cluster.title' }), title: intl.formatMessage({ id: 'noresult.cluster.title' }),
@@ -89,11 +90,12 @@ export default function useAddResource() {
}, [resourceCount, intl]); }, [resourceCount, intl]);
const open: boolean = useMemo(() => { const open: boolean = useMemo(() => {
return isNoResource && !hiddenModal; return isNoResource && !userSettings.hideAddResourceModal;
}, [isNoResource, hiddenModal]); }, [isNoResource, userSettings.hideAddResourceModal]);
const handleCreate = () => { const handleCreate = () => {
setHiddenModal(true); setHideModalTemporarily(true);
onCreated?.();
if (!resourceCount.cluster_count) { if (!resourceCount.cluster_count) {
setClusterSession({ setClusterSession({
firstAddWorker: false, firstAddWorker: false,
@@ -116,23 +118,33 @@ export default function useAddResource() {
}; };
const handleCancel = () => { const handleCancel = () => {
setHiddenModal(true); setUserSettings({
...userSettings,
hideAddResourceModal: true
});
}; };
const fetchResourceData = async () => { const fetchResourceData = async () => {
setLoadingStatus({ loading: true, loadend: false }); setLoadingStatus({ loading: true, loadend: false });
setHiddenModal(false); const { hasClusters, hasWorkers } = await fetchResource();
await fetchResource();
setLoadingStatus({ loading: false, loadend: true }); setLoadingStatus({ loading: false, loadend: true });
setUserSettings({
...userSettings,
hideAddResourceModal: hasClusters && hasWorkers
});
}; };
const NoResourceModal = ( const NoResourceModal = (
<ScrollerModal <ScrollerModal
open={open} open={open && !hideModalTemporarily}
footer={null} footer={null}
maskClosable={false} maskClosable={false}
keyboard={false}
closeIcon={null} closeIcon={null}
destroyOnHidden={false} destroyOnHidden={false}
style={{
top: '25%'
}}
onCancel={handleCancel} onCancel={handleCancel}
> >
<Content> <Content>
@@ -145,7 +157,7 @@ export default function useAddResource() {
</div> </div>
<div className="btn-wrapper"> <div className="btn-wrapper">
<Button onClick={handleCancel} type="default" style={{ flex: 1 }}> <Button onClick={handleCancel} type="default" style={{ flex: 1 }}>
{intl.formatMessage({ id: 'clusters.create.skipfornow' })} {intl.formatMessage({ id: 'common.button.dontshowagain' })}
</Button> </Button>
<Button onClick={handleCreate} type="primary" style={{ flex: 1 }}> <Button onClick={handleCreate} type="primary" style={{ flex: 1 }}>
{contentInfo.btnText} {contentInfo.btnText}
@@ -105,9 +105,8 @@ const useModelsColumns = ({
<QuestionCircleOutlined className="m-l-5" /> <QuestionCircleOutlined className="m-l-5" />
</Tooltip> </Tooltip>
), ),
dataIndex: 'ready_replicas', dataIndex: 'replicas',
key: 'ready_replicas', key: 'replicas',
dataField: 'replicas',
align: 'left', align: 'left',
sorter: tableSorter(4), sorter: tableSorter(4),
span: 4, span: 4,
+8 -1
View File
@@ -316,7 +316,14 @@ const Models: React.FC = () => {
}; };
const handleOnSortChange = (order: TableOrder | Array<TableOrder>) => { const handleOnSortChange = (order: TableOrder | Array<TableOrder>) => {
const sortKeys = handleMultiSortChange(order); let orderList = Array.isArray(order) ? order : [order];
if (orderList[0].columnKey === 'replicas') {
orderList.push({
columnKey: 'ready_replicas',
order: orderList[0].order
});
}
const sortKeys = handleMultiSortChange(orderList);
setQueryParams((pre: any) => { setQueryParams((pre: any) => {
return { return {
...pre, ...pre,
@@ -142,9 +142,8 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
const params = { const params = {
..._.omitBy(finalParameters, (value: string) => !value), ..._.omitBy(finalParameters, (value: string) => !value),
seed: seed:
parameters.seed || parameters.random_seed parameters.seed ||
? generateRandomNumber() (parameters.random_seed ? generateRandomNumber() : null),
: null,
stream: false, stream: false,
prompt: currentPrompt prompt: currentPrompt
}; };
@@ -167,9 +167,8 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
const params = { const params = {
..._.omitBy(finalParameters, (value: string) => !value), ..._.omitBy(finalParameters, (value: string) => !value),
seed: seed:
parameters.seed || parameters.random_seed parameters.seed ||
? generateRandomNumber() (parameters.random_seed ? generateRandomNumber() : null),
: null,
stream: false, stream: false,
prompt: currentPrompt prompt: currentPrompt
}; };