feat: add instance view

This commit is contained in:
jialin
2026-03-26 18:20:55 +08:00
committed by jialin
parent 33fe180997
commit adde7fba37
31 changed files with 1171 additions and 702 deletions
+89 -11
View File
@@ -9,38 +9,71 @@ import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Table } from 'antd';
import _ from 'lodash';
import { forwardRef, useImperativeHandle } from 'react';
import {
deleteModelInstance,
MODEL_INSTANCE_API,
queryModelsInstances
} from '../apis';
import ViewLogsModal from '../components/view-logs-modal';
import { useDeploymentsContext } from '../config/deploments-context';
import { ModelInstanceListItem as ListItem } from '../config/types';
import useViewInstanceLogs from '../hooks/use-view-instance-logs';
import LeftFilters from './left-filters';
import useInstanceColumns from './use-instance-columns';
const InstanceView: React.FC = () => {
const InstanceView = forwardRef((props, ref) => {
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleTableChange,
cancelChunkRequest,
createTableListChunkRequest,
handleDelete,
handleDeleteBatch,
fetchData,
handleQueryChange,
handlePageChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: PaginationKey.Providers,
key: PaginationKey.Instances,
fetchAPI: queryModelsInstances,
deleteAPI: deleteModelInstance,
watch: false,
watch: true,
API: MODEL_INSTANCE_API,
contentForDelete: 'menu.models.providers'
contentForDelete: 'menu.models.instances'
});
const intl = useIntl();
const { clusterList, workerList } = useDeploymentsContext();
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
useViewInstanceLogs();
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {});
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {
if (val === 'delete') {
handleDelete(row);
}
if (val === 'viewlog') {
openViewLogsModal(row);
}
});
const cancelRequestsOnPageInactive = useMemoizedFn(() => {
cancelChunkRequest();
});
const resumeRequestsOnPageActive = useMemoizedFn(() => {
fetchData({} as any, true);
createTableListChunkRequest();
});
useImperativeHandle(ref, () => ({
resumeRequestsOnPageActive,
cancelRequestsOnPageInactive
}));
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
@@ -49,21 +82,46 @@ const InstanceView: React.FC = () => {
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-extension-outline" />}
image={<IconFont type="icon-instance-outline" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({
id: 'noresult.providers.nofound'
id: 'noresult.instances.nofound'
})}
title={intl.formatMessage({ id: 'noresult.providers.title' })}
title={intl.formatMessage({ id: 'noresult.instances.title' })}
subTitle={intl.formatMessage({
id: 'noresult.providers.subTitle'
id: 'noresult.instances.subTitle'
})}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
></NoResult>
);
};
const columns = useInstanceColumns(handleSelect);
const handleonClusterChange = (value: number) => {
handleQueryChange({
page: 1,
cluster_id: value
});
};
const handleStatusChange = (value: string) => {
handleQueryChange({
page: 1,
state: value
});
};
const handleOnWorkerChange = (value: number) => {
handleQueryChange({
page: 1,
worker_id: value
});
};
const columns = useInstanceColumns({
handleSelect,
clusterList,
workerList
});
return (
<>
@@ -77,6 +135,17 @@ const InstanceView: React.FC = () => {
handleInputChange={handleNameChange}
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
left={
<LeftFilters
handleNameChange={handleNameChange}
handleClusterChange={handleonClusterChange}
handleStatusChange={handleStatusChange}
handleWorkerChange={handleOnWorkerChange}
handleSearch={handleSearch}
clusterList={clusterList}
workerList={workerList}
></LeftFilters>
}
></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
@@ -103,9 +172,18 @@ const InstanceView: React.FC = () => {
></Table>
</ConfigProvider>
</PageBox>
<ViewLogsModal
status={openViewLogsModalStatus.currentData.status}
url={openViewLogsModalStatus.currentData.url}
tail={openViewLogsModalStatus.currentData.tail}
id={openViewLogsModalStatus.currentData.id}
modelId={openViewLogsModalStatus.currentData.modelId}
open={openViewLogsModalStatus.open}
onCancel={closeViewLogsModal}
></ViewLogsModal>
<DeleteModal ref={modalRef}></DeleteModal>
</>
);
};
});
export default InstanceView;
@@ -0,0 +1,113 @@
import BaseSelect from '@/components/seal-form/base/select';
import { ListItem as workerListItem } from '@/pages/resources/config/types';
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Space } from 'antd';
import React from 'react';
import useFilterStatus from '../hooks/use-filter-status';
interface LeftFiltersProps {
handleNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleClusterChange: (value: number) => void;
handleStatusChange: (value: string) => void;
handleWorkerChange: (value: number) => void;
handleSearch: () => void;
clusterList: Global.BaseOption<number>[];
workerList: workerListItem[];
}
const LeftFilters: React.FC<LeftFiltersProps> = (props) => {
const {
handleNameChange,
handleClusterChange,
handleStatusChange,
handleWorkerChange,
handleSearch,
clusterList,
workerList
} = props;
const { labelRender, optionRender, statusOptions } = useFilterStatus({
optionList: [
{
label: 'Running',
value: 'running',
color: 'var(--ant-color-success)'
},
{
label: 'Error',
value: 'error',
color: 'var(--ant-color-error)'
},
{
label: 'Provisioning',
value: 'provisioning',
color: 'var(--ant-blue-5)'
}
]
});
const intl = useIntl();
return (
<Space>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
style={{ width: 200 }}
size="large"
allowClear
onChange={handleNameChange}
></Input>
<BaseSelect
allowClear
showSearch={false}
placeholder={intl.formatMessage({
id: 'clusters.filterBy.cluster'
})}
style={{ width: 160 }}
size="large"
maxTagCount={1}
onChange={handleClusterChange}
options={clusterList}
></BaseSelect>
<BaseSelect
allowClear
showSearch={false}
placeholder={intl.formatMessage({
id: 'resources.filter.worker'
})}
style={{ width: 160 }}
size="large"
maxTagCount={1}
onChange={handleWorkerChange}
options={workerList.map((worker) => ({
label: worker.name,
value: worker.id
}))}
></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)' }}
onClick={handleSearch}
icon={<SyncOutlined></SyncOutlined>}
></Button>
</Space>
);
};
export default LeftFilters;
@@ -1,16 +1,39 @@
// columns.ts
import DropdownButtons from '@/components/drop-down-buttons';
import AutoTooltip from '@/components/auto-tooltip';
import { tableSorter } from '@/config/settings';
import { ListItem as workerListItem } from '@/pages/resources/config/types';
import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max';
import { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo } from 'react';
import ActionsCell from '../components/instance-cells/actions-cell';
import DistributeInfoCell from '../components/instance-cells/distribute-info-cell';
import DownloadingStatusCell from '../components/instance-cells/downloading-status-cell';
import InstanceStatusCell from '../components/instance-cells/instance-status-cell';
import NameCell from '../components/instance-cells/name-cell';
import { ModelInstanceListItem as ListItem } from '../config/types';
const useProviderColumns = (
handleSelect: (val: string, record: ListItem) => void,
onCellClick?: (record: ListItem, dataIndex: string) => void
): ColumnsType<ListItem> => {
const calcTotalVram = (vram: Record<string, number>) => {
return _.sum(_.values(vram));
};
const useProviderColumns = (options: {
workerList: workerListItem[];
clusterList: Global.BaseOption<
number,
{
provider: string;
state: string;
is_default: boolean;
}
>[];
handleSelect: (val: string, record: ListItem) => void;
onCellClick?: (record: ListItem, dataIndex: string) => void;
}): ColumnsType<ListItem> => {
const intl = useIntl();
const { workerList, clusterList, handleSelect, onCellClick } = options;
return useMemo(() => {
return [
@@ -19,30 +42,87 @@ const useProviderColumns = (
dataIndex: 'name',
sorter: tableSorter(1),
minWidth: 160,
span: 5,
render: () => '--'
render: (value: string, record: ListItem) => (
<NameCell
record={record}
modelData={{
backend: record.backend,
backend_version: record.backend_version
}}
></NameCell>
)
},
{
title: intl.formatMessage({ id: 'providers.table.providerName' }),
dataIndex: ['config', 'type'],
sorter: tableSorter(2),
span: 4,
minWidth: 160,
render: () => '--'
},
{
title: intl.formatMessage({ id: 'providers.table.models' }),
dataIndex: 'models',
span: 3,
title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: 'cluster_id',
minWidth: 200,
render: () => '--'
render: (text: number) => (
<AutoTooltip ghost>
{text
? clusterList.find((cluster) => cluster.value === text)?.label
: ''}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'resources.worker' }),
dataIndex: 'worker_id',
minWidth: 200,
render: (text, record) => (
<AutoTooltip ghost>
{text &&
(!record.distributed_servers?.subordinate_workers ||
!record.distributed_servers?.subordinate_workers.length)
? workerList.find((worker) => worker.id === text)?.name
: ''}
<DistributeInfoCell
record={record}
workerList={workerList}
></DistributeInfoCell>
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'models.table.vram.allocated' }),
dataIndex: 'allocated_vram',
sorter: tableSorter(6),
render: (text, record) => (
<AutoTooltip ghost>
{convertFileSize(
record.computed_resource_claim?.vram
? calcTotalVram(record.computed_resource_claim?.vram)
: 0,
1
)}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: 'state',
width: 160,
render: (text: string, record: ListItem) => (
<span style={{ gap: 4 }} className="flex-center">
<InstanceStatusCell record={record} onSelect={() => {}} />
<DownloadingStatusCell
backend={record?.backend}
distributed_servers={record.distributed_servers}
workerList={[]}
record={record}
></DownloadingStatusCell>
</span>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at',
sorter: tableSorter(6),
span: 3,
render: () => '--'
render: (text, record) => (
<AutoTooltip ghost>
{dayjs(record.created_at).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.operation' }),
@@ -50,14 +130,17 @@ const useProviderColumns = (
span: 3,
minWidth: 120,
render: (value: string, record: ListItem) => (
<DropdownButtons
items={[]}
onSelect={(val) => handleSelect(val, record)}
></DropdownButtons>
<ActionsCell
record={record}
modelData={{
categories: record.categories || []
}}
onSelect={handleSelect}
></ActionsCell>
)
}
];
}, [handleSelect, onCellClick]);
}, [handleSelect, onCellClick, workerList, clusterList]);
};
export default useProviderColumns;