feat: instances on worker

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent 949eded4ee
commit 43936e21d9
21 changed files with 589 additions and 184 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+2 -1
View File
@@ -3,7 +3,7 @@ import { CloseOutlined } from '@ant-design/icons';
import { Button, Drawer, type DrawerProps } from 'antd';
const ScrollerModal = (props: DrawerProps) => {
const { title, closable = true, ...restProps } = props;
const { title, closable = true, maskClosable = false, ...restProps } = props;
const { EscHint } = useEscHint({
enabled: !props.keyboard && props.open
});
@@ -15,6 +15,7 @@ const ScrollerModal = (props: DrawerProps) => {
<Drawer
{...restProps}
closable={false}
maskClosable={maskClosable}
title={
<div className="flex-between flex-center">
<span
+5
View File
@@ -44,6 +44,11 @@ declare namespace Global {
opts?: Array<BaseOption<string | number>>;
}
interface InitialStateType {
fetchUserInfo: () => Promise<UserInfo>;
currentUser?: UserInfo;
}
type SearchParams = Pagination & { search?: string };
type MessageType = 'transition' | 'warning' | 'danger' | 'success' | 'info';
+5 -2
View File
@@ -21,6 +21,7 @@ export default function useTableFetch<ListItem>(
contentForDelete?: string;
defaultData?: any[];
events?: EventsType[];
defaultQueryParams?: Record<string, any>;
} & WatchConfig
) {
const {
@@ -31,7 +32,8 @@ export default function useTableFetch<ListItem>(
polling = false,
watch,
defaultData = [],
events = ['UPDATE', 'DELETE']
events = ['UPDATE', 'DELETE'],
defaultQueryParams = {}
} = options;
const pollingRef = useRef<any>(null);
const chunkRequedtRef = useRef<any>(null);
@@ -60,7 +62,8 @@ export default function useTableFetch<ListItem>(
const [queryParams, setQueryParams] = useState<any>({
page: 1,
perPage: 10,
search: ''
search: '',
...defaultQueryParams
});
const { setChunkRequest } = useSetChunkRequest();
+1 -1
View File
@@ -4,7 +4,7 @@ import SmallLogo from '@/assets/images/small-logo-200x200.png';
import React from 'react';
const LogoIcon: React.FC = () => {
return <img src={GpustackLogo} alt="logo" style={{ height: 16 }} />;
return <img src={GpustackLogo} alt="logo" style={{ height: 24 }} />;
};
const SLogoIcon: React.FC = () => {
return <img src={SmallLogo} alt="logo" style={{ height: 24 }} />;
-5
View File
@@ -180,11 +180,6 @@ export default (props: any) => {
showShortcuts: () => {
return showShortcuts();
},
setTheme: setTheme,
toggleTheme: () => {
const newTheme = userSettings.theme === 'realDark' ? 'light' : 'realDark';
setTheme(newTheme);
},
notFound: <span>404 not found</span>
};
+2 -63
View File
@@ -2,20 +2,17 @@
import avatarImg from '@/assets/images/avatar.png';
import IconFont from '@/components/icon-font';
import externalLinks from '@/constants/external-links';
import langConfigMap from '@/locales/lang-config-map';
import {
DiscordOutlined,
GithubOutlined,
HomeOutlined,
InfoCircleOutlined,
LogoutOutlined,
MoonOutlined,
MoreOutlined,
ReadOutlined,
SettingOutlined,
SunOutlined
SettingOutlined
} from '@ant-design/icons';
import { getAllLocales, history, setLocale } from '@umijs/max';
import { getAllLocales, history } from '@umijs/max';
import { Avatar, Menu, Spin } from 'antd';
import _ from 'lodash';
import React from 'react';
@@ -191,64 +188,6 @@ export const getRightRenderContent = (opts: {
]
};
const langMenu = {
selectedKeys: [],
className: collapsed
? 'user-menu-container user-menu-collapsed'
: 'user-menu-container',
mode: 'vertical',
expandIcon: false,
// inlineCollapsed: collapsed,
triggerSubMenuAction: 'hover',
items: [
{
key: 'lang',
icon: <IconFont type="icon-language" />,
label: (
<span className="sub-title">
{intl?.formatMessage?.({ id: 'common.settings.language' })}
</span>
),
children: allLocals.map((key) => ({
key,
label: (
<span className="flex flex-center">
<span>{_.get(langConfigMap, [key, 'label'])}</span>
</span>
),
onClick: () => {
setLocale(key, false);
}
}))
}
]
};
const themeMenu = {
selectedKeys: [],
className: collapsed
? 'user-menu-container user-menu-collapsed'
: 'user-menu-container',
mode: 'vertical',
expandIcon: false,
// inlineCollapsed: collapsed,
triggerSubMenuAction: 'click',
items: [
{
key: 'theme',
icon: isDarkTheme ? <MoonOutlined /> : <SunOutlined />,
label: (
<span className="sub-title">
{intl?.formatMessage?.({ id: 'common.appearance' })}
</span>
),
onClick: () => {
opts.runtimeConfig.toggleTheme();
}
}
]
};
const userMenu = {
selectedKeys: [],
className: collapsed
+1 -1
View File
@@ -17,7 +17,7 @@ interface MenuItem {
interface SiderMenuProps {
menuData: MenuItem[];
collapsed?: boolean;
initialState: Global.InitialState;
initialState: Global.InitialStateType;
}
const useStyles = createStyles(({ css, token }) => {
+1
View File
@@ -69,6 +69,7 @@ export default {
'Waiting for the download to complete...',
'resources.filter.worker': 'Filter by worker',
'resources.filter.source': 'Filter by Source',
'resources.filter.status': 'Filter by Status',
'resources.modelfiles.delete.tips': 'Also delete the file from disk',
'resources.modelfiles.copy.tips': 'Copy Full Path',
'resources.filter.path': 'Filter by path',
+3 -1
View File
@@ -70,6 +70,7 @@ export default {
'ダウンロード完了を待っています...',
'resources.filter.worker': 'ワーカーでフィルタ',
'resources.filter.source': 'ソースでフィルタ',
'resources.filter.status': 'Filter by Status',
'resources.modelfiles.delete.tips': 'ディスクからファイルも削除します',
'resources.modelfiles.copy.tips': 'フルパスをコピー',
'resources.filter.path': 'パスでフィルタ',
@@ -105,5 +106,6 @@ export default {
// 8. 'resources.register.install.title': 'Install GPUStack on {os}',
// 9. 'resources.register.download':'Download and install the <a>installer</a>. Only supported: {versions}.',
// 10. 'resource.register.maos.support': 'Apple Silicon (M series), macOS 14+',
// 11. 'resource.register.windows.support': 'win 10, win 11'
// 11. 'resource.register.windows.support': 'win 10, win 11',
// 12. 'resources.filter.status': 'Filter by Status',
// ========== End of To-Do List ==========
+1
View File
@@ -68,6 +68,7 @@ export default {
'resources.modelfiles.storagePath.holder': 'Ожидание завершения загрузки...',
'resources.filter.worker': 'Фильтровать по узлу',
'resources.filter.source': 'Фильтровать по источнику',
'resources.filter.status': 'Фильтровать по статусу',
'resources.modelfiles.delete.tips': 'Также удалить файл с диска',
'resources.modelfiles.copy.tips': 'Скопировать полный путь',
'resources.filter.path': 'Фильтрация по пути',
+1
View File
@@ -67,6 +67,7 @@ export default {
'resources.modelfiles.storagePath.holder': '等待下载完成...',
'resources.filter.worker': '按 Worker 筛选',
'resources.filter.source': '按来源筛选',
'resources.filter.status': '按状态筛选',
'resources.modelfiles.delete.tips': '同时从磁盘删除文件',
'resources.modelfiles.copy.tips': '复制完整路径',
'resources.filter.path': '路径查询',
+1 -1
View File
@@ -73,7 +73,7 @@ export const clusterActionList = [
},
{
key: 'details',
label: 'common.button.view',
label: 'common.button.detail',
icon: icons.DetailInfo
},
{
@@ -39,6 +39,14 @@ import {
} from '../config/types';
import '../style/instance-item.less';
interface InstanceItemProps {
instanceData: ModelInstanceListItem;
workerList: WorkerListItem[];
modelData?: any;
defaultOpenId: string;
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
}
const fieldList = [
{
label: 'CPU',
@@ -250,14 +258,6 @@ const InstanceStatusTag = (
);
};
interface InstanceItemProps {
instanceData: ModelInstanceListItem;
workerList: WorkerListItem[];
modelData?: any;
defaultOpenId: string;
handleChildSelect: (val: string, item: ModelInstanceListItem) => void;
}
const childActionList = [
{
label: 'common.button.viewlog',
-80
View File
@@ -2,86 +2,6 @@ import { StatusMaps } from '@/config';
import { EditOutlined } from '@ant-design/icons';
import _ from 'lodash';
export const ollamaModelOptions = [
{
label: 'qwen2.5-coder',
value: 'qwen2.5-coder',
name: 'qwen2.5-coder',
id: 'qwen2.5-coder',
tags: []
},
{
label: 'llama3.2',
value: 'llama3.2',
name: 'llama3.2',
id: 'llama3.2',
tags: ['Tools', '1B', '3B']
},
{
label: 'nomic-embed-text',
value: 'nomic-embed-text',
name: 'nomic-embed-text',
id: 'nomic-embed-text',
tags: []
},
{
label: 'qwen2.5',
value: 'qwen2.5',
name: 'qwen2.5',
tags: ['Tools', '0.5B', '1.5B', '3B', '7B', '14B', '32B', '72B'],
id: 'gemma2'
},
{
label: 'gemma2',
value: 'gemma2',
name: 'gemma2',
tags: ['2B', '9B', '27B'],
id: 'gemma2'
},
{
label: 'mistral-nemo',
value: 'mistral-nemo',
name: 'mistral-nemo',
tags: ['12B'],
id: 'mistral-nemo'
},
{
label: 'mistral-large',
value: 'mistral-large',
name: 'mistral-large',
tags: ['123B'],
id: 'mistral-large'
},
{
label: 'mistral',
value: 'mistral',
name: 'mistral',
tags: ['7B'],
id: 'mistral'
},
{
label: 'phi3.5',
value: 'phi3.5',
name: 'phi3.5',
tags: ['3B'],
id: 'phi3.5'
},
{
label: 'codellama',
value: 'codellama',
name: 'codellama',
tags: ['7B', '13B', '34B', '70B'],
id: 'codellama'
},
{
label: 'deepseek-coder-v2',
value: 'deepseek-coder-v2',
name: 'deepseek-coder-v2',
tags: ['16B', '236B'],
id: 'deepseek-coder-v2'
}
];
export const backendTipsList = [
{
title: 'vLLM',
@@ -0,0 +1,125 @@
import { FilterBar } from '@/components/page-tools';
import useTableFetch from '@/hooks/use-table-fetch';
import { queryClusterList } from '@/pages/cluster-management/apis';
import {
MODEL_INSTANCE_API,
queryModelsInstances
} from '@/pages/llmodels/apis';
import { InstanceStatusMap } from '@/pages/llmodels/config';
import { ModelInstanceListItem } from '@/pages/llmodels/config/types';
import { useIntl } from '@umijs/max';
import { ConfigProvider, Empty, Table } from 'antd';
import React, { useEffect, useState } from 'react';
import useInstanceColumns from '../hooks/use-instances-columns';
const statusList = Object.entries(InstanceStatusMap).map(([key, value]) => ({
label: key,
value: value
}));
const WorkerDetailContent: React.FC<{ worker_id: number | undefined }> = ({
worker_id
}) => {
const {
dataSource,
queryParams,
handlePageChange,
handleTableChange,
handleSearch,
handleQueryChange,
handleNameChange
} = useTableFetch<ModelInstanceListItem>({
fetchAPI: queryModelsInstances,
polling: false,
API: MODEL_INSTANCE_API,
defaultQueryParams: {
worker_id
}
});
const intl = useIntl();
const [clusterList, setClusterList] = useState<Global.BaseOption<number>[]>(
[]
);
const getClusterList = async () => {
try {
const res = await queryClusterList({ page: 1, perPage: 100 });
const list = res.items?.map((item) => ({
label: item.name,
value: item.id
}));
setClusterList(list || []);
} catch (error) {
setClusterList([]);
}
};
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
if (
!dataSource.loading &&
dataSource.loadend &&
!dataSource.dataList.length
) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>;
}
return <div></div>;
};
const handleSelectChange = (value: string) => {
handleQueryChange({ state: value });
};
const columns = useInstanceColumns({
clusterList
});
useEffect(() => {
console.log('columns changed!');
}, [columns]);
useEffect(() => {
getClusterList();
}, []);
return (
<>
<FilterBar
marginBottom={22}
marginTop={0}
buttonText={intl.formatMessage({ id: 'resources.button.create' })}
handleSearch={handleSearch}
handleSelectChange={handleSelectChange}
handleInputChange={handleNameChange}
showDeleteButton={false}
showPrimaryButton={false}
selectHolder={intl.formatMessage({ id: 'resources.filter.status' })}
showSelect={true}
selectOptions={statusList}
width={{ input: 300 }}
></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
style={{ width: '100%' }}
tableLayout={dataSource.loadend ? 'auto' : 'fixed'}
dataSource={dataSource.dataList}
loading={dataSource.loading}
rowKey="id"
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
></Table>
</ConfigProvider>
</>
);
};
export default WorkerDetailContent;
@@ -0,0 +1,34 @@
import GSDrawer from '@/components/scroller-modal/gs-drawer';
import React from 'react';
import { ListItem } from '../config/types';
import WorkerDetailContent from './worker-detail-content';
interface WorkerDetailModalProps {
open: boolean;
onClose?: () => void;
currentData: ListItem | null;
}
const WorkerDetailModal: React.FC<WorkerDetailModalProps> = ({
open,
onClose,
currentData
}) => {
const handleOnClose = () => {
onClose?.();
};
return (
<GSDrawer
width={'calc(100vw - 200px)'}
title={`${currentData?.name}`}
open={open}
destroyOnClose={true}
onClose={handleOnClose}
>
<WorkerDetailContent worker_id={currentData?.id} />
</GSDrawer>
);
};
export default WorkerDetailModal;
+27 -2
View File
@@ -16,6 +16,7 @@ import {
import { ListItem } from '../config/types';
import useWorkerColumns from '../hooks/use-worker-columns';
import UpdateLabels from './update-labels';
import WorkerDetailModal from './worker-detail-modal';
const Workers: React.FC = () => {
const {
@@ -55,6 +56,13 @@ const Workers: React.FC = () => {
list: [],
data: {}
});
const [workerDetailStatus, setWorkerDetailStatus] = useState<{
open: boolean;
currentData: ListItem | null;
}>({
open: false,
currentData: null
});
const getClusterList = async () => {
try {
@@ -118,6 +126,13 @@ const Workers: React.FC = () => {
});
};
const handleViewDetail = (record: ListItem) => {
setWorkerDetailStatus({
open: true,
currentData: record
});
};
const handleSelect = useMemoizedFn((val: any, record: ListItem) => {
if (val === 'edit') {
handleUpdateLabels(record);
@@ -126,6 +141,9 @@ const Workers: React.FC = () => {
if (val === 'delete') {
handleDelete(record);
}
if (val === 'details') {
handleViewDetail(record);
}
});
const renderEmpty = (type?: string) => {
@@ -149,8 +167,8 @@ const Workers: React.FC = () => {
const columns = useWorkerColumns({
clusterData,
dataSource,
extraStatus,
loadend: dataSource.loadend,
firstLoad: extraStatus.firstLoad,
handleSelect
});
@@ -216,6 +234,13 @@ const Workers: React.FC = () => {
labels: updateLabelsData.data.labels
}}
></UpdateLabels>
<WorkerDetailModal
open={workerDetailStatus.open}
currentData={workerDetailStatus.currentData}
onClose={() =>
setWorkerDetailStatus({ currentData: null, open: false })
}
/>
</PageContainer>
</>
);
@@ -0,0 +1,344 @@
import AutoTooltip from '@/components/auto-tooltip';
import { TooltipOverlayScroller } from '@/components/overlay-scroller';
import SimpleTabel, { ColumnProps } from '@/components/simple-table';
import InfoColumn from '@/components/simple-table/info-column';
import StatusTag from '@/components/status-tag';
import ThemeTag from '@/components/tags-wrapper/theme-tag';
import {
InstanceStatusMap,
InstanceStatusMapValue,
status
} from '@/pages/llmodels/config';
import {
DistributedServerItem,
ModelInstanceListItem as ListItem,
ModelInstanceListItem
} from '@/pages/llmodels/config/types';
import { convertFileSize } from '@/utils';
import { InfoCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import _ from 'lodash';
import { useMemo } from 'react';
import styled from 'styled-components';
const GPUIndexWrapper = styled.span`
display: flex;
flex-direction: column;
gap: 2px;
`;
const StatusWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
`;
const fieldList = [
{
label: 'CPU',
key: 'cpuoffload',
locale: false
},
{
label: 'GPU',
key: 'gpuoffload',
locale: false
}
];
const renderGpuIndexs = (gpuIndexes: number[]) => {
return (
<GPUIndexWrapper>
{_.chunk(gpuIndexes, 8).map((item: number[], index: number) => {
return <span key={index}>{item.join(',')}</span>;
})}
</GPUIndexWrapper>
);
};
const distributeCols: ColumnProps[] = [
{
title: 'Worker',
key: 'worker_name',
style: {
wordBreak: 'break-word'
}
},
{
title: 'IP',
key: 'worker_ip',
render: ({ row }) => {
return row.port ? `${row.worker_ip}:${row.port}` : row.worker_ip;
}
},
{
title: 'models.table.gpuindex',
locale: true,
key: 'gpu_index',
render: ({ row }) => {
const list = _.sortBy(row.gpu_index, (item: number) => item);
return row.is_main ? (
<>
{renderGpuIndexs(list)}
<span>(main)</span>
</>
) : (
renderGpuIndexs(list)
);
}
},
{
title: 'models.table.vram.allocated',
locale: true,
key: 'vram',
render: ({ rowIndex, row, dataList }) => {
return convertFileSize(row.vram, 1);
}
}
];
const calcTotalVram = (vram: Record<string, number>) => {
return _.sum(_.values(vram));
};
const renderDistributedServer = (
severList: any[],
instanceData: ModelInstanceListItem
) => {
// const list = _.map(severList, (item: any) => {
// const data = _.find(workerList, { id: item.worker_id });
// return {
// worker_name: data?.name,
// worker_ip: data?.ip,
// port: '',
// is_main: false,
// vram: calcTotalVram(item.computed_resource_claim?.vram || {}),
// gpu_index: _.keys(item.computed_resource_claim?.vram)
// };
// });
const mainWorker = [
{
worker_name: `${instanceData.worker_name}`,
worker_ip: `${instanceData.worker_ip}`,
port: '',
vram: calcTotalVram(instanceData.computed_resource_claim?.vram || {}),
is_main: true,
gpu_index: instanceData.gpu_indexes
}
];
return (
<div>
<SimpleTabel
rowKey="worker_name"
columns={distributeCols}
dataSource={[...mainWorker]}
></SimpleTabel>
</div>
);
};
const DistributionInfo: React.FC<{
instanceData: ModelInstanceListItem;
}> = ({ instanceData }) => {
const intl = useIntl();
const distributed_servers = instanceData.distributed_servers;
const severList: DistributedServerItem[] =
distributed_servers?.subordinate_workers || [];
if (!severList.length) {
return null;
}
return (
<TooltipOverlayScroller
toolTipProps={{
trigger: 'hover',
overlayInnerStyle: {
width: 'max-content',
maxWidth: '520px',
minWidth: '400px'
}
}}
title={renderDistributedServer(severList, instanceData)}
>
<ThemeTag
opacity={0.75}
color="processing"
style={{
marginRight: 0,
display: 'flex',
alignItems: 'center',
maxWidth: '100%',
minWidth: 50,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
borderRadius: 12
}}
>
<InfoCircleOutlined className="m-r-5" />
{intl.formatMessage({
id: 'models.table.acrossworker'
})}
</ThemeTag>
</TooltipOverlayScroller>
);
};
const OffloadInfo: React.FC<{
instanceData: ListItem;
}> = (props) => {
const intl = useIntl();
const { instanceData } = props;
const total_layers = instanceData.computed_resource_claim?.total_layers;
const offload_layers = instanceData.computed_resource_claim?.offload_layers;
if (total_layers === offload_layers || !total_layers) {
return null;
}
const offloadData = {
cpuoffload: `${
_.subtract(
instanceData.computed_resource_claim?.total_layers,
instanceData.computed_resource_claim?.offload_layers
) || 0
} ${intl.formatMessage({
id: 'models.table.layers'
})}`,
gpuoffload: `${instanceData.computed_resource_claim?.offload_layers} ${intl.formatMessage(
{
id: 'models.table.layers'
}
)}`
};
return (
<Tooltip
overlayInnerStyle={{ paddingInline: 12 }}
title={<InfoColumn fieldList={fieldList} data={offloadData}></InfoColumn>}
>
<ThemeTag
opacity={0.75}
color="cyan"
style={{
display: 'flex',
alignItems: 'center',
maxWidth: '100%',
minWidth: 50,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
borderRadius: 12
}}
>
<InfoCircleOutlined className="m-r-5" />
{intl.formatMessage({
id: 'models.table.cpuoffload'
})}
</ThemeTag>
</Tooltip>
);
};
const InstanceStatusTag: React.FC<{
instanceData: ListItem;
}> = (props) => {
const intl = useIntl();
const { instanceData } = props;
if (!instanceData.state) {
return null;
}
return (
<>
<StatusTag
download={
instanceData.state === InstanceStatusMap.Downloading
? { percent: instanceData.download_progress }
: undefined
}
extra={
instanceData.state === InstanceStatusMap.Error &&
instanceData.worker_id ? (
<Button type="link" size="small" style={{ paddingLeft: 0 }}>
{intl.formatMessage({ id: 'models.list.more.logs' })}
</Button>
) : null
}
statusValue={{
status:
instanceData.state === InstanceStatusMap.Downloading &&
instanceData.download_progress === 100
? status[InstanceStatusMap.Running]
: status[instanceData.state],
text: InstanceStatusMapValue[instanceData.state],
message:
instanceData.state === InstanceStatusMap.Downloading &&
instanceData.download_progress === 100
? ''
: instanceData.state_message
}}
/>
</>
);
};
const useInstanceColumns = (props: {
clusterList: Global.BaseOption<number>[];
}): ColumnsType<ListItem> => {
const { clusterList } = props;
const intl = useIntl();
return useMemo(() => {
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: 'name',
width: 240,
render: (text: string, record: ListItem) => (
<AutoTooltip ghost maxWidth={240}>
{text}
</AutoTooltip>
)
},
{
title: 'Worker',
dataIndex: 'worker_name',
render: (text: string, record: ListItem) => (
<AutoTooltip ghost>{text}</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'models.title' }),
dataIndex: 'model_name',
render: (text: string, record: ListItem) => (
<AutoTooltip ghost>{text}</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: 'cluster_id',
render: (text: number, record: ListItem) => (
<AutoTooltip ghost>
{clusterList.find((item) => item.value === text)?.label}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: 'state',
render: (text: string, record: ListItem) => (
<StatusWrapper>
<OffloadInfo instanceData={record} />
<DistributionInfo instanceData={record} />
<InstanceStatusTag instanceData={record} />
</StatusWrapper>
)
}
];
}, [clusterList]);
};
export default useInstanceColumns;
@@ -17,7 +17,7 @@ import { ColumnsType } from 'antd/lib/table';
import _ from 'lodash';
import { useMemo } from 'react';
import styled from 'styled-components';
import { WorkerStatusMapValue, status } from '../config';
import { WorkerStatusMap, WorkerStatusMapValue, status } from '../config';
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
const LabelsWrapper = styled.div`
@@ -28,6 +28,11 @@ const LabelsWrapper = styled.div`
const ActionList = [
{ label: 'common.button.edit', key: 'edit', icon: <EditOutlined /> },
{
label: 'common.button.detail',
key: 'details',
icon: <IconFont type="icon-detail-info" />
},
{
label: 'common.button.logs',
locale: false,
@@ -43,6 +48,15 @@ const ActionList = [
}
];
const setActions = (row: ListItem) => {
return ActionList.filter((action) => {
if (action.key === 'details' && row.state === WorkerStatusMap.ready) {
return true;
}
return true;
});
};
const fieldList = [
{
label: 'resources.table.total',
@@ -115,14 +129,14 @@ const VRAMCell = ({
devices,
intl,
rIndex,
dataSource,
extraStatus
loadend,
firstLoad
}: {
devices: GPUDeviceItem[];
intl: any;
rIndex: number;
dataSource: any;
extraStatus: any;
loadend: boolean;
firstLoad: boolean;
}) => (
<span className="flex-column flex-gap-2">
{_.map(
@@ -133,12 +147,7 @@ const VRAMCell = ({
[{item.index}]
</span>
<ProgressBar
defaultOpen={
rIndex === 0 &&
index === 0 &&
dataSource.loadend &&
extraStatus.firstLoad
}
defaultOpen={rIndex === 0 && index === 0 && loadend && firstLoad}
percent={
item.memory?.used
? _.round(item.memory?.utilization_rate, 0)
@@ -189,16 +198,16 @@ const StorageCell = ({ files }: { files: Filesystem[] }) => {
const useWorkerColumns = ({
clusterData,
dataSource,
extraStatus,
loadend,
firstLoad,
handleSelect
}: {
clusterData: {
list: Global.BaseOption<number>[];
data: Record<number, string>;
};
dataSource: any;
extraStatus: any;
loadend: boolean;
firstLoad: boolean;
handleSelect: (action: string, record: ListItem) => void;
}): ColumnsType<ListItem> => {
const intl = useIntl();
@@ -290,8 +299,8 @@ const useWorkerColumns = ({
devices={record?.status?.gpu_devices}
intl={intl}
rIndex={rIndex}
dataSource={dataSource}
extraStatus={extraStatus}
loadend={loadend}
firstLoad={firstLoad}
/>
)
},
@@ -305,13 +314,13 @@ const useWorkerColumns = ({
key: 'operation',
render: (_, record) => (
<DropdownButtons
items={ActionList}
items={setActions(record)}
onSelect={(val) => handleSelect(val, record)}
/>
)
}
],
[intl, clusterData, dataSource, extraStatus, handleSelect]
[intl, clusterData, loadend, firstLoad, handleSelect]
);
};