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
+107 -460
View File
@@ -1,445 +1,97 @@
import TableContext from '@/components/seal-table/table-context';
import { TableOrder } from '@/components/seal-table/types';
import { PaginationKey } from '@/config/settings';
import useSetChunkRequest from '@/hooks/use-chunk-request';
import { usePaginationStatus } from '@/hooks/use-pagination-status';
import { useTableMultiSort } from '@/hooks/use-table-sort';
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import IconFont from '@/components/icon-font';
import { PageContainerInner } from '@/pages/_components/page-box';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import qs from 'query-string';
import { useEffect, useRef, useState } from 'react';
import {
MODELS_API,
MODEL_INSTANCE_API,
queryModelsInstances,
queryModelsList
} from './apis';
import TableList from './components/table-list';
import { ListItem } from './config/types';
import { Segmented, Tabs } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DeploymentsContext } from './config/deploments-context';
import ModelView from './deployments';
import useFormInitialValues from './hooks/use-form-initial-values';
import InstanceView from './instance-view';
const Models = () => {
const { pagination, setPagination } = usePaginationStatus(
PaginationKey.Deployments
);
const { sortOrder, handleMultiSortChange } = useTableMultiSort();
const { setChunkRequest, createAxiosToken } = useSetChunkRequest();
const { setChunkRequest: setModelInstanceChunkRequest } =
useSetChunkRequest();
const [modelInstances, setModelInstances] = useState<any[]>([]);
const [dataSource, setDataSource] = useState<{
dataList: ListItem[];
deletedIds: number[];
loading: boolean;
loadend: boolean;
total: number;
}>({
dataList: [],
deletedIds: [],
loading: false,
loadend: false,
total: 0
});
const chunkRequedtRef = useRef<any>();
const chunkInstanceRequedtRef = useRef<any>();
const isPageHidden = useRef(false);
const instancesToken = useRef<any>();
let axiosToken = createAxiosToken();
const [queryParams, setQueryParams] = useState({
page: 1,
perPage: 10,
search: '',
cluster_id: 0,
categories: [],
state: '',
sort_by: '',
...pagination
});
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
events: ['UPDATE'],
dataList: dataSource.dataList,
setDataList(list, opts?: any) {
setDataSource((pre) => {
return {
total: pre.total,
loading: false,
loadend: true,
dataList: list,
deletedIds: opts?.deletedIds || []
};
});
}
});
const TabsValueMap = {
ModelView: 'modelView',
InstanceView: 'instanceView'
};
const LLModels: React.FC = () => {
const intl = useIntl();
const {
updateChunkedList: updateInstanceChunkedList,
cacheDataListRef: cacheInsDataListRef
} = useUpdateChunkedList({
dataList: modelInstances,
limit: 100,
setDataList: setModelInstances
});
const getAllModelInstances = useMemoizedFn(async () => {
try {
instancesToken.current?.cancel?.();
instancesToken.current = createAxiosToken();
const params = {
page: -1
};
const res: any = await queryModelsInstances(params, {
token: instancesToken.current.token
});
cacheInsDataListRef.current = res.items || [];
setModelInstances(res.items || []);
} catch (error) {
// ignore
}
});
const fetchData = useMemoizedFn(
async (params?: {
loadingVal?: boolean;
query?: {
page: number;
perPage: number;
search: string;
categories: any[];
sort_by: string;
};
}) => {
const { loadingVal, query } = params || {};
axiosToken?.cancel?.();
axiosToken = createAxiosToken();
setDataSource((pre) => {
pre.loading = loadingVal ?? true;
return { ...pre };
});
try {
const params = {
..._.pickBy(query || queryParams, (val: any) => !!val)
};
const res: any = await queryModelsList(params, {
cancelToken: axiosToken.token
});
// if the current page is beyond total page, fetch again
if (
!res.items.length &&
params.page > res.pagination.totalPage &&
res.pagination.totalPage > 0
) {
const newParams = {
...params,
page: res.pagination.totalPage
};
const newRes: any = await queryModelsList(newParams, {
cancelToken: axiosToken.token
});
setDataSource({
dataList: newRes.items || [],
loading: false,
loadend: true,
total: newRes.pagination.total,
deletedIds: []
});
return;
}
setDataSource({
dataList: res.items || [],
loading: false,
loadend: true,
total: res.pagination.total,
deletedIds: []
});
} catch (error) {
if (!isPageHidden.current) {
setDataSource({
dataList: [],
loading: false,
loadend: true,
total: dataSource.total,
deletedIds: []
});
}
}
}
);
const handleQueryChange = (params: any) => {
setQueryParams({
...queryParams,
...params
});
fetchData({ query: { ...queryParams, ...params } });
};
const handlePageChange = useMemoizedFn(
(page: number, pageSize: number | undefined) => {
console.log('page change', page, pageSize);
handleQueryChange({
page: page,
perPage: pageSize || 10
});
setPagination({
perPage: pageSize || 10
});
}
);
const updateHandler = (list: any) => {
_.each(list, (data: any) => {
updateChunkedList(data);
});
};
const updateInstanceHandler = (list: any) => {
// filter the data
_.each(list, (data: any) => {
updateInstanceChunkedList(data);
});
};
const createModelsChunkRequest = useMemoizedFn(
async (params?: {
search: string;
categories: any[];
cluster_id: number;
state?: string;
}) => {
const search = params?.search || queryParams.search;
const categories = params?.categories || queryParams.categories;
chunkRequedtRef.current?.current?.cancel?.();
try {
const query = {
search: search,
categories: categories
};
chunkRequedtRef.current = setChunkRequest({
url: `${MODELS_API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`,
handler: updateHandler
});
} catch (error) {
// ignore
}
}
);
const createModelsInstanceChunkRequest = useMemoizedFn(async () => {
chunkInstanceRequedtRef.current?.current?.cancel?.();
cacheInsDataListRef.current = [];
try {
chunkInstanceRequedtRef.current = setModelInstanceChunkRequest({
url: `${MODEL_INSTANCE_API}`,
params: {},
handler: updateInstanceHandler
});
} catch (error) {
// ignore
}
});
const handleOnViewLogs = useMemoizedFn(() => {
isPageHidden.current = true;
chunkRequedtRef.current?.current?.cancel?.();
cacheDataListRef.current = [];
cacheInsDataListRef.current = [];
chunkInstanceRequedtRef.current?.current?.cancel?.();
instancesToken.current?.cancel?.();
});
const handleOnCancelViewLogs = useMemoizedFn(async () => {
isPageHidden.current = false;
fetchData({
loadingVal: false
});
await getAllModelInstances();
await createModelsInstanceChunkRequest();
await createModelsChunkRequest();
});
const handleSearchBySilent = useMemoizedFn(async () => {
await new Promise((resolve) => {
setTimeout(resolve, 300);
});
fetchData({
loadingVal: false
});
});
const handleSearch = useMemoizedFn(async (params?: any) => {
await fetchData(params);
});
const debounceUpdateFilter = _.debounce((e: any) => {
handleQueryChange({
page: 1,
search: e.target.value
});
createModelsChunkRequest({
search: e.target.value,
categories: queryParams.categories,
cluster_id: queryParams.cluster_id || 0
});
}, 350);
const handleNameChange = useMemoizedFn(debounceUpdateFilter);
const handleCategoryChange = async (value: any) => {
handleQueryChange({
page: 1,
categories: value
});
createModelsChunkRequest({
search: queryParams.search,
cluster_id: queryParams.cluster_id || 0,
categories: value
});
};
const handleOnStatusChange = async (value: string | undefined) => {
handleQueryChange({
page: 1,
state: value
});
createModelsChunkRequest({
search: queryParams.search,
categories: queryParams.categories,
cluster_id: queryParams.cluster_id || 0,
state: value
});
};
const handleClusterChange = async (value: number) => {
handleQueryChange({
page: 1,
cluster_id: value
});
createModelsChunkRequest({
search: queryParams.search,
categories: queryParams.categories,
cluster_id: value
});
};
const handleOnSortChange = (order: TableOrder | Array<TableOrder>) => {
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) => {
return {
...pre,
page: 1,
sort_by: sortKeys.join(',')
};
});
fetchData({
query: {
...queryParams,
page: 1,
sort_by: sortKeys.join(',')
}
});
};
const handleOnFilterChange = (filters: any) => {
handleQueryChange({
page: 1,
...filters
});
createModelsChunkRequest({
search: queryParams.search,
...filters
});
};
const handleDeleteInstanceFromCache = (id: number) => {
cacheInsDataListRef.current = cacheInsDataListRef.current.filter(
(item) => item.id !== id
);
setModelInstances(cacheInsDataListRef.current);
};
generateFormValues,
clusterList,
getClusterList,
getWorkerList,
workerList
} = useFormInitialValues();
const [activeKey, setActiveKey] = useState(TabsValueMap.ModelView);
const modelViewRef = useRef<any>(null);
const instanceViewRef = useRef<any>(null);
useEffect(() => {
let timer: any = null;
// fetch data first time
const getTableData = async (loadingVal?: boolean) => {
axiosToken?.cancel?.();
axiosToken = createAxiosToken();
setDataSource((pre) => {
pre.loading = loadingVal ?? true;
return { ...pre };
});
try {
const params = {
..._.pickBy(queryParams, (val: any) => !!val)
};
const res: any = await queryModelsList(params, {
cancelToken: axiosToken.token
});
return res;
} catch (error) {
return {};
}
};
const init = async () => {
const [modelRes] = await Promise.all([getTableData()]);
setDataSource({
dataList: modelRes.items || [],
loading: false,
loadend: true,
total: modelRes.pagination?.total || 0,
deletedIds: []
});
clearTimeout(timer);
timer = setTimeout(() => {
createModelsInstanceChunkRequest();
createModelsChunkRequest();
}, 1000);
};
init();
return () => {
clearTimeout(timer);
axiosToken?.cancel?.();
chunkRequedtRef.current?.current?.cancel?.();
cacheDataListRef.current = [];
cacheInsDataListRef.current = [];
chunkInstanceRequedtRef.current?.current?.cancel?.();
instancesToken.current?.cancel?.();
const getData = async () => {
await Promise.all([getClusterList(), getWorkerList()]);
};
getData();
}, []);
const setDisableExpand = useMemoizedFn((record: any) => {
return !record?.replicas;
const handleTabChange = useMemoizedFn((key: string) => {
if (key === TabsValueMap.ModelView) {
modelViewRef.current?.resumeRequestsOnPageActive();
instanceViewRef.current?.cancelRequestsOnPageInactive();
}
if (key === TabsValueMap.InstanceView) {
instanceViewRef.current?.resumeRequestsOnPageActive();
modelViewRef.current?.cancelRequestsOnPageInactive();
}
setActiveKey(key);
});
const header = useMemo(() => {
return {
title: (
<div className="flex items-center">
<span className="font-600">
{intl.formatMessage({ id: 'menu.models.deployment' })}
</span>
<Segmented
options={[
{
label: intl.formatMessage({ id: 'models.table.modelView' }),
value: TabsValueMap.ModelView,
icon: <IconFont type={'icon-models'}></IconFont>
},
{
label: intl.formatMessage({ id: 'models.table.instanceView' }),
value: TabsValueMap.InstanceView,
icon: <IconFont type={'icon-instance-outline'}></IconFont>
}
]}
size="middle"
className="m-l-24 font-600"
value={activeKey}
onChange={handleTabChange}
></Segmented>
</div>
)
};
}, [activeKey, intl]);
useEffect(() => {
const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible') {
isPageHidden.current = false;
fetchData({
loadingVal: false
});
await getAllModelInstances();
await createModelsInstanceChunkRequest();
await createModelsChunkRequest();
if (activeKey === TabsValueMap.ModelView) {
modelViewRef.current?.resumeRequestsOnPageActive();
}
if (activeKey === TabsValueMap.InstanceView) {
instanceViewRef.current?.resumeRequestsOnPageActive();
}
} else {
isPageHidden.current = true;
chunkRequedtRef.current?.current?.cancel?.();
cacheDataListRef.current = [];
cacheInsDataListRef.current = [];
chunkInstanceRequedtRef.current?.current?.cancel?.();
instancesToken.current?.cancel?.();
if (activeKey === TabsValueMap.ModelView) {
modelViewRef.current?.cancelRequestsOnPageInactive();
}
if (activeKey === TabsValueMap.InstanceView) {
instanceViewRef.current?.cancelRequestsOnPageInactive();
}
}
};
@@ -448,41 +100,36 @@ const Models = () => {
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [fetchData, createModelsChunkRequest, createModelsInstanceChunkRequest]);
}, [activeKey]);
return (
<TableContext.Provider
value={{
allChildren: modelInstances,
setDisableExpand: setDisableExpand
}}
>
<TableList
dataSource={dataSource.dataList}
onStatusChange={handleOnStatusChange}
handleNameChange={handleNameChange}
handleCategoryChange={handleCategoryChange}
handleClusterChange={handleClusterChange}
handleSearch={handleSearch}
handlePageChange={handlePageChange}
handleDeleteSuccess={fetchData}
handleOnToggleExpandAll={createModelsInstanceChunkRequest}
onViewLogs={handleOnViewLogs}
onCancelViewLogs={handleOnCancelViewLogs}
onStop={handleSearchBySilent}
onStart={handleSearchBySilent}
onTableSort={handleOnSortChange}
onFilterChange={handleOnFilterChange}
onDeleteInstanceFromCache={handleDeleteInstanceFromCache}
sortOrder={sortOrder}
queryParams={queryParams}
loading={dataSource.loading}
loadend={dataSource.loadend}
total={dataSource.total}
deleteIds={dataSource.deletedIds}
></TableList>
</TableContext.Provider>
<PageContainerInner header={header}>
<DeploymentsContext.Provider
value={{
generateFormValues,
clusterList,
workerList
}}
>
<Tabs
renderTabBar={() => <></>}
items={[
{
key: TabsValueMap.ModelView,
label: intl.formatMessage({ id: 'models.table.modelView' }),
children: <ModelView ref={modelViewRef}></ModelView>
},
{
key: TabsValueMap.InstanceView,
label: intl.formatMessage({ id: 'models.table.instanceView' }),
children: <InstanceView ref={instanceViewRef}></InstanceView>
}
]}
activeKey={activeKey}
></Tabs>
</DeploymentsContext.Provider>
</PageContainerInner>
);
};
export default Models;
export default LLModels;