feat: instances on worker
This commit is contained in:
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 |
@@ -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
|
||||
|
||||
Vendored
+5
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }} />;
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -17,7 +17,7 @@ interface MenuItem {
|
||||
interface SiderMenuProps {
|
||||
menuData: MenuItem[];
|
||||
collapsed?: boolean;
|
||||
initialState: Global.InitialState;
|
||||
initialState: Global.InitialStateType;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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': 'Фильтрация по пути',
|
||||
|
||||
@@ -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': '路径查询',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user