Files
gpustack-ui/src/pages/resources/components/workers.tsx
T

485 lines
15 KiB
TypeScript

import AutoTooltip from '@/components/auto-tooltip';
import DeleteModal from '@/components/delete-modal';
import DropdownButtons from '@/components/drop-down-buttons';
import { FilterBar } from '@/components/page-tools';
import ProgressBar from '@/components/progress-bar';
import InfoColumn from '@/components/simple-table/info-column';
import StatusTag from '@/components/status-tag';
import Hotkeys from '@/config/hotkeys';
import useTableFetch from '@/hooks/use-table-fetch';
import { convertFileSize } from '@/utils';
import {
DeleteOutlined,
EditOutlined,
InfoCircleOutlined
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { ConfigProvider, Empty, Table, Tooltip, message } from 'antd';
import _ from 'lodash';
import React, { useCallback, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
WORKERS_API,
deleteWorker,
queryWorkersList,
updateWorker
} from '../apis';
import { WorkerStatusMapValue, status } from '../config';
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
import AddWorker from './add-worker';
import UpdateLabels from './update-labels';
const { Column } = Table;
const fieldList = [
{
label: 'resources.table.total',
key: 'total',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
},
{
label: 'resources.table.used',
key: 'used',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
},
{
label: 'resources.table.allocated',
key: 'allocated',
locale: true,
render: (val: any) => {
return convertFileSize(val, 0);
}
}
];
const ActionList = [
{
label: 'common.button.edit',
key: 'edit',
icon: <EditOutlined />
},
{
label: 'common.button.delete',
key: 'delete',
props: {
danger: true
},
icon: <DeleteOutlined />
}
];
const formateUtilazation = (val1: number, val2: number): number => {
if (!val2 || !val1) {
return 0;
}
return _.round((val1 / val2) * 100, 0);
};
const calcStorage = (files: Filesystem[]) => {
const mountRoot = _.find(
files,
(item: Filesystem) => item.mount_point === '/'
);
return mountRoot ? formateUtilazation(mountRoot.used, mountRoot.total) : 0;
};
const Workers: React.FC = () => {
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
fetchAPI: queryWorkersList,
deleteAPI: deleteWorker,
contentForDelete: 'worker',
watch: true,
API: WORKERS_API
});
const intl = useIntl();
const [open, setOpen] = useState(false);
const [updateLabelsData, setUpdateLabelsData] = useState<{
open: boolean;
data: ListItem;
}>({
open: false,
data: {} as ListItem
});
const handleAddWorker = () => {
setOpen(true);
};
const handleUpdateLabelsOk = useCallback(
async (values: Record<string, any>) => {
try {
console.log('updateLabelsData.data', updateLabelsData.data);
await updateWorker(updateLabelsData.data.id, {
...updateLabelsData.data,
labels: values.labels
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
setUpdateLabelsData({ open: false, data: {} as ListItem });
} catch (error) {
console.log('error', error);
}
},
[updateLabelsData, fetchData]
);
const handleCancelUpdateLabels = useCallback(() => {
setUpdateLabelsData({
...updateLabelsData,
open: false
});
}, []);
const handleUpdateLabels = (record: ListItem) => {
console.log('record', record);
setUpdateLabelsData({
open: true,
data: {
...record
}
});
};
const handleSelect = (val: any, record: ListItem) => {
if (val === 'edit') {
handleUpdateLabels(record);
return;
}
if (val === 'delete') {
handleDelete(record);
}
};
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 renderStorageTooltip = (files: Filesystem[]) => {
const mountRoot = _.find(
files,
(item: Filesystem) => item.mount_point === '/'
);
return mountRoot ? (
<InfoColumn
fieldList={fieldList.filter((item) => item.key !== 'allocated')}
data={mountRoot}
></InfoColumn>
) : (
0
);
};
useHotkeys(Hotkeys.CREATE.join(','), handleAddWorker, {
enabled: !open
});
return (
<>
<PageContainer
ghost
header={{
title: 'Workers',
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
},
breadcrumb: {}
}}
extra={[]}
>
<FilterBar
marginBottom={22}
marginTop={30}
buttonText={intl.formatMessage({ id: 'resources.button.create' })}
handleDeleteByBatch={handleDeleteBatch}
handleSearch={handleSearch}
handleClickPrimary={handleAddWorker}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
width={{ input: 300 }}
></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
tableLayout={dataSource.loadend ? 'auto' : 'fixed'}
style={{ width: '100%' }}
dataSource={dataSource.dataList}
loading={dataSource.loading}
rowKey="id"
onChange={handleTableChange}
rowSelection={rowSelection}
pagination={{
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
>
<Column
title={intl.formatMessage({ id: 'common.table.name' })}
dataIndex="name"
key="name"
width={100}
render={(text, record: ListItem) => {
return (
<AutoTooltip ghost maxWidth={240}>
<span>{record.name}</span>
</AutoTooltip>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'resources.table.labels' })}
dataIndex="labels"
key="labels"
width={200}
render={(text, record: ListItem) => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6
}}
>
{_.map(record.labels, (value: any, key: string) => {
return (
<AutoTooltip
key={key}
className="m-r-0"
maxWidth={155}
style={{
paddingInline: 8,
borderRadius: 12
}}
>
<span>{key}</span>
<span>:{value}</span>
</AutoTooltip>
);
})}
</div>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'common.table.status' })}
dataIndex="state"
key="state"
render={(text, record: ListItem) => {
return (
<StatusTag
maxTooltipWidth={400}
statusValue={{
status: status[record.state] as any,
text: WorkerStatusMapValue[record.state],
message: record.state_message
}}
></StatusTag>
);
}}
/>
<Column title="IP" dataIndex="ip" key="address" />
<Column
title="CPU"
dataIndex="CPU"
key="CPU"
render={(text, record: ListItem) => {
return (
<ProgressBar
percent={_.round(record?.status?.cpu?.utilization_rate, 0)}
></ProgressBar>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'resources.table.memory' })}
dataIndex="memory"
key="Memory"
render={(text, record: ListItem) => {
return (
<ProgressBar
percent={formateUtilazation(
record?.status?.memory?.used,
record?.status?.memory?.total
)}
label={
<InfoColumn
fieldList={fieldList}
data={record.status.memory}
></InfoColumn>
}
></ProgressBar>
);
}}
/>
<Column
title="GPU"
dataIndex="GPU"
key="GPU"
render={(text, record: ListItem) => {
return (
<span className="flex-column flex-gap-2">
{_.map(
_.sortBy(record?.status?.gpu_devices || [], ['index']),
(item: GPUDeviceItem, index: string) => {
return (
<span className="flex-center" key={index}>
<span
className="m-r-5"
style={{ display: 'flex', width: 25 }}
>
[{item.index}]
</span>
{item.core ? (
<ProgressBar
key={index}
percent={_.round(
item.core?.utilization_rate,
0
)}
></ProgressBar>
) : (
'-'
)}
</span>
);
}
)}
</span>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'resources.table.vram' })}
dataIndex="VRAM"
key="VRAM"
render={(text, record: ListItem) => {
return (
<span className="flex-column flex-gap-2">
{_.map(
_.sortBy(record?.status?.gpu_devices || [], ['index']),
(item: GPUDeviceItem, index: string) => {
return (
<span key={index}>
<span className="flex-center">
<span
className="m-r-5"
style={{ display: 'flex', width: 25 }}
>
[{item.index}]
</span>
<ProgressBar
key={index}
percent={
item.memory?.used
? _.round(item.memory?.utilization_rate, 0)
: _.round(
(item.memory?.allocated /
item.memory?.total) *
100,
0
)
}
label={
<InfoColumn
fieldList={fieldList}
data={item.memory}
></InfoColumn>
}
></ProgressBar>
{item.memory.is_unified_memory && (
<Tooltip
title={intl.formatMessage({
id: 'resources.table.unified'
})}
>
<InfoCircleOutlined
className="m-l-5"
style={{ color: 'var(--ant-blue-5)' }}
/>
</Tooltip>
)}
</span>
</span>
);
}
)}
</span>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'resources.table.disk' })}
dataIndex="storage"
key="storage"
render={(text, record: ListItem) => {
return (
<ProgressBar
percent={calcStorage(record.status?.filesystem)}
label={renderStorageTooltip(record.status.filesystem)}
></ProgressBar>
);
}}
/>
<Column
title={intl.formatMessage({ id: 'common.table.operation' })}
key="operation"
render={(text, record: ListItem) => {
return (
<DropdownButtons
items={ActionList}
onSelect={(val) => handleSelect(val, record)}
></DropdownButtons>
);
}}
/>
</Table>
</ConfigProvider>
<DeleteModal ref={modalRef}></DeleteModal>
<AddWorker open={open} onCancel={() => setOpen(false)}></AddWorker>
<UpdateLabels
open={updateLabelsData.open}
onOk={handleUpdateLabelsOk}
onCancel={handleCancelUpdateLabels}
data={{
name: updateLabelsData.data.name,
labels: updateLabelsData.data.labels
}}
></UpdateLabels>
</PageContainer>
</>
);
};
export default Workers;