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
+1 -1
View File
@@ -114,7 +114,7 @@ export default [
selectedIcon: 'icon-rocket-launch-fill', selectedIcon: 'icon-rocket-launch-fill',
defaultIcon: 'icon-rocket-launch1', defaultIcon: 'icon-rocket-launch1',
access: 'canSeeAdmin', access: 'canSeeAdmin',
component: './llmodels/deployments' component: './llmodels/index'
}, },
{ {
name: 'routes', name: 'routes',
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
import './iconfont/iconfont.js'; import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: '' scriptUrl: '//at.alicdn.com/t/c/font_4613488_7zu9s75km1r.js'
}); });
export default IconFont; export default IconFont;
@@ -318,13 +318,15 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
onScroll={handleOnScroll} onScroll={handleOnScroll}
></LogsList> ></LogsList>
</div> </div>
<Spin {loading && (
size="middle" <Spin
spinning={loading} size="middle"
className={classNames({ spinning={loading}
loading: loading className={classNames({
})} loading: loading
></Spin> })}
></Spin>
)}
{totalPage > 1 && ( {totalPage > 1 && (
<div className="pg"> <div className="pg">
<div <div
+2 -1
View File
@@ -42,5 +42,6 @@ export const PaginationKey = {
ModelFiles: 'ModelFiles', ModelFiles: 'ModelFiles',
Users: 'Users', Users: 'Users',
APIKeys: 'APIKeys', APIKeys: 'APIKeys',
Credentials: 'Credentials' Credentials: 'Credentials',
Instances: 'Instances'
}; };
+7 -1
View File
@@ -249,6 +249,10 @@ export default function useTableFetch<T>(
}); });
}; };
const cancelChunkRequest = () => {
chunkRequestRef.current?.current?.cancel?.();
};
const createTableListChunkRequest = async (params?: any) => { const createTableListChunkRequest = async (params?: any) => {
if (!API || !watch) return; if (!API || !watch) return;
chunkRequestRef.current?.current?.cancel?.(); chunkRequestRef.current?.current?.cancel?.();
@@ -289,7 +293,7 @@ export default function useTableFetch<T>(
const handleQueryChange = async ( const handleQueryChange = async (
params: any, params: any,
options?: { options?: {
paginate?: boolean; paginate?: boolean; // it's change for pagination
} }
) => { ) => {
// cancel previous chunk request so that we can create a new one with updated params // cancel previous chunk request so that we can create a new one with updated params
@@ -459,6 +463,8 @@ export default function useTableFetch<T>(
handleSearch, handleSearch,
handleQueryChange, handleQueryChange,
loadMore, loadMore,
cancelChunkRequest,
createTableListChunkRequest,
handleNameChange handleNameChange
}; };
} }
+1
View File
@@ -17,6 +17,7 @@ export default {
'menu.models.benchmark': 'Benchmarks', 'menu.models.benchmark': 'Benchmarks',
'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.benchmarkDetail': 'Benchmark Details',
'menu.models.providers': 'Providers', 'menu.models.providers': 'Providers',
'menu.models.instances': 'Instances',
'menu.models.routes': 'Routes', 'menu.models.routes': 'Routes',
'menu.modelCatalog': 'Catalog', 'menu.modelCatalog': 'Catalog',
'menu.resources': 'Resources', 'menu.resources': 'Resources',
+3 -1
View File
@@ -279,5 +279,7 @@ export default {
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
'Not enabled yet. Will be enabled after deployment. ', 'Not enabled yet. Will be enabled after deployment. ',
'models.table.instance.benchmark': 'Run Benchmark' 'models.table.instance.benchmark': 'Run Benchmark',
'models.table.modelView': 'Model View',
'models.table.instanceView': 'Instance View'
}; };
+5 -1
View File
@@ -48,5 +48,9 @@ export default {
'noresult.providers.nofound': 'No matching providers found.', 'noresult.providers.nofound': 'No matching providers found.',
'noresult.routes.title': 'No Routes', 'noresult.routes.title': 'No Routes',
'noresult.routes.subTitle': 'No routes have been added yet.', 'noresult.routes.subTitle': 'No routes have been added yet.',
'noresult.routes.nofound': 'No matching routes found.' 'noresult.routes.nofound': 'No matching routes found.',
'noresult.instances.title': 'No instances',
'noresult.instances.subTitle':
'There are no running model instances. Deploy a model to create one automatically.',
'noresult.instances.nofound': 'No matching instances found.'
}; };
+1
View File
@@ -17,6 +17,7 @@ export default {
'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.benchmarkDetail': 'Benchmark Details',
'menu.modelCatalog': 'カタログ', 'menu.modelCatalog': 'カタログ',
'menu.models.providers': 'Providers', 'menu.models.providers': 'Providers',
'menu.models.instances': 'Instances',
'menu.models.routes': 'Routes', 'menu.models.routes': 'Routes',
'menu.resources': 'リソース', 'menu.resources': 'リソース',
'menu.apikeys': 'APIキー', 'menu.apikeys': 'APIキー',
+5 -1
View File
@@ -279,7 +279,9 @@ export default {
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
'Not enabled yet. Will be enabled after deployment. ', 'Not enabled yet. Will be enabled after deployment. ',
'models.table.instance.benchmark': 'Run Benchmark' 'models.table.instance.benchmark': 'Run Benchmark',
'models.table.modelView': 'Model View',
'models.table.instanceView': 'Instance View'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -380,4 +382,6 @@ export default {
// 78. 'models.table.instance.benchmark': 'Run Benchmark' // 78. 'models.table.instance.benchmark': 'Run Benchmark'
// 77. 'models.form.enableModelRoute': 'Enable Model Route', // 77. 'models.form.enableModelRoute': 'Enable Model Route',
// 78. 'models.form.enableModelRoute.tips': 'Enable Model Route', // 78. 'models.form.enableModelRoute.tips': 'Enable Model Route',
// 79. 'models.table.modelView': 'Model View',
// 80. 'models.table.instanceView': 'Instance View'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+5 -1
View File
@@ -48,5 +48,9 @@ export default {
'noresult.providers.nofound': 'No matching providers found.', 'noresult.providers.nofound': 'No matching providers found.',
'noresult.routes.title': 'No Routes', 'noresult.routes.title': 'No Routes',
'noresult.routes.subTitle': 'No routes have been added yet.', 'noresult.routes.subTitle': 'No routes have been added yet.',
'noresult.routes.nofound': 'No matching routes found.' 'noresult.routes.nofound': 'No matching routes found.',
'noresult.instances.title': 'No instances',
'noresult.instances.subTitle':
'There are no running model instances. Deploy a model to create one automatically.',
'noresult.instances.nofound': 'No matching instances found.'
}; };
+2
View File
@@ -16,6 +16,7 @@ export default {
'menu.models.benchmark': 'Benchmarks', 'menu.models.benchmark': 'Benchmarks',
'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.benchmarkDetail': 'Benchmark Details',
'menu.models.providers': 'Providers', 'menu.models.providers': 'Providers',
'menu.models.instances': 'Instances',
'menu.models.routes': 'Routes', 'menu.models.routes': 'Routes',
'menu.modelCatalog': 'Каталог', 'menu.modelCatalog': 'Каталог',
'menu.resources': 'Ресурсы', 'menu.resources': 'Ресурсы',
@@ -45,4 +46,5 @@ export default {
// 2. 'menu.models.benchmarkDetail': 'Benchmark Details', // 2. 'menu.models.benchmarkDetail': 'Benchmark Details',
// 3. 'menu.models.providers': 'Providers', // 3. 'menu.models.providers': 'Providers',
// 4. 'menu.models.routes': 'Routes', // 4. 'menu.models.routes': 'Routes',
// 5. 'menu.models.instances': 'Instances',
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+5 -1
View File
@@ -283,7 +283,9 @@ export default {
'models.form.maxContextLength': 'Maximum Context Length', 'models.form.maxContextLength': 'Maximum Context Length',
'models.form.backend.helperText': 'models.form.backend.helperText':
'Not enabled yet. Will be enabled after deployment. ', 'Not enabled yet. Will be enabled after deployment. ',
'models.table.instance.benchmark': 'Run Benchmark' 'models.table.instance.benchmark': 'Run Benchmark',
'models.table.modelView': 'Model View',
'models.table.instanceView': 'Instance View'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -294,4 +296,6 @@ export default {
// 3. 'models.form.enableModelRoute.tips': 'Enable Model Route', // 3. 'models.form.enableModelRoute.tips': 'Enable Model Route',
// 4. 'models.form.backend.vllm': 'Built-in support for NVIDIA, AMD, Ascend, Hygon, Moore Threads, Iluvatar, MetaX, T-Head PPU devices.', // 4. 'models.form.backend.vllm': 'Built-in support for NVIDIA, AMD, Ascend, Hygon, Moore Threads, Iluvatar, MetaX, T-Head PPU devices.',
// 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.', // 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.',
// 6. 'models.table.modelView': 'Model View',
// 7. 'models.table.instanceView': 'Instance View'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+8 -1
View File
@@ -49,7 +49,11 @@ export default {
'noresult.providers.nofound': 'No matching providers found.', 'noresult.providers.nofound': 'No matching providers found.',
'noresult.routes.title': 'No Routes', 'noresult.routes.title': 'No Routes',
'noresult.routes.subTitle': 'No routes have been added yet.', 'noresult.routes.subTitle': 'No routes have been added yet.',
'noresult.routes.nofound': 'No matching routes found.' 'noresult.routes.nofound': 'No matching routes found.',
'noresult.instances.title': 'No instances',
'noresult.instances.subTitle':
'There are no running model instances. Deploy a model to create one automatically.',
'noresult.instances.nofound': 'No matching instances found.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -66,4 +70,7 @@ export default {
// 8. 'noresult.routes.title': 'No Routes', // 8. 'noresult.routes.title': 'No Routes',
// 9. 'noresult.routes.subTitle': 'No routes have been added yet.', // 9. 'noresult.routes.subTitle': 'No routes have been added yet.',
// 10. 'noresult.routes.nofound': 'No matching routes found.' // 10. 'noresult.routes.nofound': 'No matching routes found.'
// 11. 'noresult.instances.title': 'No instances',
// 12. 'noresult.instances.subTitle': 'There are no running model instances. Deploy a model to create one automatically.',
// 13. 'noresult.instances.nofound': 'No matching instances found.'
// ======================================================================== // ========================================================================
+1
View File
@@ -16,6 +16,7 @@ export default {
'menu.models.benchmark': '基准测试', 'menu.models.benchmark': '基准测试',
'menu.models.benchmarkDetail': '基准测试详情', 'menu.models.benchmarkDetail': '基准测试详情',
'menu.models.providers': '提供商', 'menu.models.providers': '提供商',
'menu.models.instances': '实例',
'menu.models.routes': '路由', 'menu.models.routes': '路由',
'menu.modelCatalog': '模型库', 'menu.modelCatalog': '模型库',
'menu.models.catalog': '模型库', 'menu.models.catalog': '模型库',
+3 -1
View File
@@ -263,5 +263,7 @@ export default {
'models.form.readyWorkers': '节点就绪', 'models.form.readyWorkers': '节点就绪',
'models.form.maxContextLength': '最大上下文长度', 'models.form.maxContextLength': '最大上下文长度',
'models.form.backend.helperText': '该社区后端暂未启用,部署后将自动启用', 'models.form.backend.helperText': '该社区后端暂未启用,部署后将自动启用',
'models.table.instance.benchmark': '运行基准测试' 'models.table.instance.benchmark': '运行基准测试',
'models.table.modelView': '模型视图',
'models.table.instanceView': '实例视图'
}; };
+5 -1
View File
@@ -46,5 +46,9 @@ export default {
'noresult.providers.nofound': '未找到匹配的提供商', 'noresult.providers.nofound': '未找到匹配的提供商',
'noresult.routes.title': '暂无路由', 'noresult.routes.title': '暂无路由',
'noresult.routes.subTitle': '尚未添加任何路由。', 'noresult.routes.subTitle': '尚未添加任何路由。',
'noresult.routes.nofound': '未找到匹配的路由' 'noresult.routes.nofound': '未找到匹配的路由',
'noresult.instances.title': '暂无实例',
'noresult.instances.subTitle':
'当前没有运行中的模型实例,部署模型后将自动创建。',
'noresult.instances.nofound': '未找到匹配的实例'
}; };
@@ -1,6 +1,6 @@
import { clusterDetailAtom } from '@/atoms/clusters'; import { clusterDetailAtom } from '@/atoms/clusters';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import Deployments from '@/pages/llmodels/index'; import Deployments from '@/pages/llmodels/deployments';
import GPUList from '@/pages/resources/components/gpus'; import GPUList from '@/pages/resources/components/gpus';
import WorkerList from '@/pages/resources/components/workers'; import WorkerList from '@/pages/resources/components/workers';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
@@ -0,0 +1,90 @@
import BaseSelect from '@/components/seal-form/base/select';
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Space } from 'antd';
import React from 'react';
import { modelCategories } from '../config';
import useFilterStatus from '../hooks/use-filter-status';
interface LeftFiltersProps {
handleNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleClusterChange: (value: number) => void;
handleStatusChange: (value: string) => void;
handleSearch: () => void;
handleCategoryChange: (value: string) => void;
clusterList: Global.BaseOption<number>[];
}
const LeftFilters: React.FC<LeftFiltersProps> = (props) => {
const {
handleNameChange,
handleClusterChange,
handleStatusChange,
handleSearch,
handleCategoryChange,
clusterList
} = props;
const { labelRender, optionRender, statusOptions } = useFilterStatus();
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: 'models.filter.category'
})}
style={{ width: 160 }}
size="large"
maxTagCount={1}
onChange={handleCategoryChange}
options={modelCategories.filter((item) => item.value)}
></BaseSelect>
<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: '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;
+30 -119
View File
@@ -2,9 +2,7 @@ import { modelsExpandKeysAtom, modelsSessionAtom } from '@/atoms/models';
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import DropDownActions from '@/components/drop-down-actions'; import DropDownActions from '@/components/drop-down-actions';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import { PageSize } from '@/components/logs-viewer/config';
import PageTools from '@/components/page-tools'; import PageTools from '@/components/page-tools';
import BaseSelect from '@/components/seal-form/base/select';
import SealTable from '@/components/seal-table'; import SealTable from '@/components/seal-table';
import { TableOrder } from '@/components/seal-table/types'; import { TableOrder } from '@/components/seal-table/types';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
@@ -21,10 +19,10 @@ import { TargetStatusValueMap } from '@/pages/model-routes/config';
import useOpenPlayground from '@/pages/model-routes/hooks/use-open-playground'; import useOpenPlayground from '@/pages/model-routes/hooks/use-open-playground';
import useGranfanaLink from '@/pages/resources/hooks/use-grafana-link'; import useGranfanaLink from '@/pages/resources/hooks/use-grafana-link';
import { handleBatchRequest } from '@/utils'; import { handleBatchRequest } from '@/utils';
import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, Input, Space, message } from 'antd'; import { Button, Space, message } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { import React, {
@@ -36,23 +34,19 @@ import React, {
} from 'react'; } from 'react';
import { import {
MODELS_API, MODELS_API,
MODEL_INSTANCE_API,
createModel, createModel,
deleteModel, deleteModel,
deleteModelInstance, deleteModelInstance,
queryModelInstancesList, queryModelInstancesList,
updateModel updateModel
} from '../apis'; } from '../apis';
import { import { modelSourceMap } from '../config';
InstanceRealtimeLogStatus,
modelCategories,
modelSourceMap
} from '../config';
import { import {
ButtonList, ButtonList,
modalConfig, modalConfig,
sourceOptions sourceOptions
} from '../config/button-actions'; } from '../config/button-actions';
import { useDeploymentsContext } from '../config/deploments-context';
import { import {
FormData, FormData,
ListItem, ListItem,
@@ -60,12 +54,12 @@ import {
SourceType SourceType
} from '../config/types'; } from '../config/types';
import useEditDeployment from '../hooks/use-edit-deployment'; import useEditDeployment from '../hooks/use-edit-deployment';
import useFilterStatus from '../hooks/use-filter-status';
import useFormInitialValues from '../hooks/use-form-initial-values';
import useModelsColumns from '../hooks/use-models-columns'; import useModelsColumns from '../hooks/use-models-columns';
import useViewInstanceLogs from '../hooks/use-view-instance-logs';
import DeployModal from './deployment/deploy-modal'; import DeployModal from './deployment/deploy-modal';
import UpdateModelModal from './deployment/update-modal'; import UpdateModelModal from './deployment/update-modal';
import Instances from './instance/instances'; import Instances from './instance/instances';
import LeftFilters from './left-filters';
import ViewLogsModal from './view-logs-modal'; import ViewLogsModal from './view-logs-modal';
interface ModelsProps { interface ModelsProps {
handleSearch: (params?: any) => void; handleSearch: (params?: any) => void;
@@ -136,13 +130,8 @@ const Models: React.FC<ModelsProps> = ({
loadend, loadend,
total total
}) => { }) => {
const { const { generateFormValues, clusterList, workerList } =
generateFormValues, useDeploymentsContext();
clusterList,
getClusterList,
getWorkerList,
workerList
} = useFormInitialValues();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const page = searchParams.get('page'); const page = searchParams.get('page');
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
@@ -165,11 +154,9 @@ const Models: React.FC<ModelsProps> = ({
expandedRowKeys expandedRowKeys
} = useExpandedRowKeys(expandAtom); } = useExpandedRowKeys(expandAtom);
const { handleOpenPlayGround } = useOpenPlayground(); const { handleOpenPlayGround } = useOpenPlayground();
const { labelRender, optionRender, handleStatusChange, statusOptions } =
useFilterStatus({
onStatusChange: onStatusChange
});
const { watchDataList: targetList } = useWatchList(MODEL_ROUTE_TARGETS); const { watchDataList: targetList } = useWatchList(MODEL_ROUTE_TARGETS);
const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } =
useViewInstanceLogs();
const { goToGrafana, ActionButton } = useGranfanaLink({ const { goToGrafana, ActionButton } = useGranfanaLink({
type: 'model' type: 'model'
@@ -189,16 +176,6 @@ const Models: React.FC<ModelsProps> = ({
isGGUF: false, isGGUF: false,
source: modelSourceMap.huggingface_value as SourceType source: modelSourceMap.huggingface_value as SourceType
}); });
const [currentInstance, setCurrentInstance] = useState<{
url: string;
status: string;
id?: number | string;
modelId?: number | string;
tail?: number;
}>({
url: '',
status: ''
});
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
@@ -208,10 +185,6 @@ const Models: React.FC<ModelsProps> = ({
}, [deleteIds]); }, [deleteIds]);
useEffect(() => { useEffect(() => {
const getData = async () => {
await Promise.all([getClusterList(), getWorkerList()]);
};
getData();
return () => { return () => {
setExpandAtom([]); setExpandAtom([]);
}; };
@@ -308,11 +281,10 @@ const Models: React.FC<ModelsProps> = ({
} catch (error) {} } catch (error) {}
}; };
const handleLogModalCancel = useCallback(() => { const handleLogModalCancel = () => {
setOpenLogModal(false);
onCancelViewLogs(); onCancelViewLogs();
restoreScrollHeight(); closeViewLogsModal();
}, [onCancelViewLogs]); };
const handleDelete = async (row: any) => { const handleDelete = async (row: any) => {
modalRef.current?.show({ modalRef.current?.show({
@@ -352,22 +324,8 @@ const Models: React.FC<ModelsProps> = ({
}; };
const handleViewLogs = async (row: any) => { const handleViewLogs = async (row: any) => {
try { openViewLogsModal(row);
setCurrentInstance({ onViewLogs();
url: `${MODEL_INSTANCE_API}/${row.id}/logs`,
status: row.state,
id: row.id,
modelId: row.model_id,
tail: InstanceRealtimeLogStatus.includes(row.state)
? undefined
: PageSize - 1
});
setOpenLogModal(true);
onViewLogs();
saveScrollHeight();
} catch (error) {
console.log('error:', error);
}
}; };
const handleDeleteInstace = (row: any) => { const handleDeleteInstace = (row: any) => {
@@ -600,62 +558,14 @@ const Models: React.FC<ModelsProps> = ({
marginBottom={22} marginBottom={22}
marginTop={0} marginTop={0}
left={ left={
<Space> <LeftFilters
<Input handleNameChange={handleNameChange}
prefix={ handleClusterChange={handleClusterChange}
<SearchOutlined handleCategoryChange={handleCategoryChange}
style={{ color: 'var(--ant-color-text-placeholder)' }} handleStatusChange={onStatusChange}
></SearchOutlined> handleSearch={handleSearch}
} clusterList={clusterList}
placeholder={intl.formatMessage({ id: 'common.filter.name' })} ></LeftFilters>
style={{ width: 200 }}
size="large"
allowClear
onChange={handleNameChange}
></Input>
<BaseSelect
allowClear
showSearch={false}
placeholder={intl.formatMessage({
id: 'models.filter.category'
})}
style={{ width: 160 }}
size="large"
maxTagCount={1}
onChange={handleCategoryChange}
options={modelCategories.filter((item) => item.value)}
></BaseSelect>
<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: '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>
} }
right={ right={
<Space size={16}> <Space size={16}>
@@ -720,6 +630,7 @@ const Models: React.FC<ModelsProps> = ({
pageSize: queryParams.perPage, pageSize: queryParams.perPage,
current: queryParams.page, current: queryParams.page,
total: total, total: total,
size: 'middle',
hideOnSinglePage: queryParams.perPage === 10, hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange onChange: handlePageChange
}} }}
@@ -747,12 +658,12 @@ const Models: React.FC<ModelsProps> = ({
onOk={handleCreateModel} onOk={handleCreateModel}
></DeployModal> ></DeployModal>
<ViewLogsModal <ViewLogsModal
status={currentInstance.status} status={openViewLogsModalStatus.currentData.status}
url={currentInstance.url} url={openViewLogsModalStatus.currentData.url}
tail={currentInstance.tail} tail={openViewLogsModalStatus.currentData.tail}
id={currentInstance.id} id={openViewLogsModalStatus.currentData.id}
modelId={currentInstance.modelId} modelId={openViewLogsModalStatus.currentData.modelId}
open={openLogModal} open={openViewLogsModalStatus.open}
onCancel={handleLogModalCancel} onCancel={handleLogModalCancel}
></ViewLogsModal> ></ViewLogsModal>
<DeleteModal ref={modalRef}></DeleteModal> <DeleteModal ref={modalRef}></DeleteModal>
@@ -69,8 +69,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
if (!props.id) return; if (open && props.id) {
if (open) {
requestRef.current?.current?.cancel?.(); requestRef.current?.current?.cancel?.();
requestRef.current = setChunkRequest({ requestRef.current = setChunkRequest({
url: `${MODELS_API}/${props.modelId}/instances`, url: `${MODELS_API}/${props.modelId}/instances`,
@@ -0,0 +1,35 @@
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { createContext, useContext } from 'react';
import { ListItem as ModelListItem } from '../config/types';
interface DeploymentsContextProps {
clusterList: Global.BaseOption<
number,
{
provider: string;
state: string;
is_default: boolean;
}
>[];
workerList: WorkerListItem[];
generateFormValues: (
data: ModelListItem,
gpuOptions: any
) => Record<string, any>;
}
export const DeploymentsContext = createContext<DeploymentsContextProps>({
clusterList: [],
workerList: [],
generateFormValues: () => ({})
});
export const useDeploymentsContext = () => {
const context = useContext(DeploymentsContext);
if (!context) {
throw new Error(
'useDeploymentsContext must be used within a DeploymentsProvider'
);
}
return context;
};
+1
View File
@@ -119,6 +119,7 @@ export interface ModelInstanceListItem {
cluster_id: number; cluster_id: number;
backend_version?: string; backend_version?: string;
source: string; source: string;
categories?: string[];
huggingface_repo_id: string; huggingface_repo_id: string;
huggingface_filename: string; huggingface_filename: string;
ollama_library_model_name: string; ollama_library_model_name: string;
+464 -62
View File
@@ -1,70 +1,472 @@
import IconFont from '@/components/icon-font'; import TableContext from '@/components/seal-table/table-context';
import { PageContainerInner } from '@/pages/_components/page-box'; import { TableOrder } from '@/components/seal-table/types';
import { useIntl } from '@umijs/max'; import { PaginationKey } from '@/config/settings';
import { Segmented, Tabs } from 'antd'; import useSetChunkRequest from '@/hooks/use-chunk-request';
import { useMemo, useState } from 'react'; import { usePaginationStatus } from '@/hooks/use-pagination-status';
import ModelView from './index'; import { useTableMultiSort } from '@/hooks/use-table-sort';
import InstanceView from './instance-view'; import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import qs from 'query-string';
import {
forwardRef,
useEffect,
useImperativeHandle,
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';
const TabsValueMap = { const Models = forwardRef((props, ref) => {
ModelView: 'modelView', const { pagination, setPagination } = usePaginationStatus(
InstanceView: 'instanceView' 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 Playground: React.FC = () => { const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
const intl = useIntl(); events: ['UPDATE'],
const [activeKey, setActiveKey] = useState(TabsValueMap.ModelView); dataList: dataSource.dataList,
setDataList(list, opts?: any) {
setDataSource((pre) => {
return {
total: pre.total,
loading: false,
loadend: true,
dataList: list,
deletedIds: opts?.deletedIds || []
};
});
}
});
const header = useMemo(() => { const {
return { updateChunkedList: updateInstanceChunkedList,
title: ( cacheDataListRef: cacheInsDataListRef
<div className="flex items-center"> } = useUpdateChunkedList({
<span className="font-600"> dataList: modelInstances,
{intl.formatMessage({ id: 'menu.models.deployment' })} limit: 100,
</span> setDataList: setModelInstances
<Segmented });
options={[
{ const getAllModelInstances = useMemoizedFn(async () => {
label: 'Model View', try {
value: TabsValueMap.ModelView, instancesToken.current?.cancel?.();
icon: <IconFont type={'icon-models'}></IconFont> instancesToken.current = createAxiosToken();
}, const params = {
{ page: -1
label: 'Instance View', };
value: TabsValueMap.InstanceView, const res: any = await queryModelsInstances(params, {
icon: <IconFont type={'icon-database'}></IconFont> token: instancesToken.current.token
} });
]} cacheInsDataListRef.current = res.items || [];
size="middle" setModelInstances(res.items || []);
className="m-l-24 font-600" } catch (error) {
value={activeKey} // ignore
onChange={(key) => setActiveKey(key)} }
></Segmented> });
</div>
) 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 cancelRequestsOnPageInactive = useMemoizedFn(() => {
isPageHidden.current = true;
chunkRequedtRef.current?.current?.cancel?.();
cacheDataListRef.current = [];
cacheInsDataListRef.current = [];
chunkInstanceRequedtRef.current?.current?.cancel?.();
instancesToken.current?.cancel?.();
});
const resumeRequestsOnPageActive = useMemoizedFn(async () => {
isPageHidden.current = false;
fetchData({
loadingVal: false
});
await getAllModelInstances();
await createModelsInstanceChunkRequest();
await createModelsChunkRequest();
});
const handleOnCancelViewLogs = useMemoizedFn(async () => {
resumeRequestsOnPageActive();
});
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);
};
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 {};
}
}; };
}, [activeKey, intl]);
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?.();
cancelRequestsOnPageInactive();
};
}, []);
const setDisableExpand = useMemoizedFn((record: any) => {
return !record?.replicas;
});
useImperativeHandle(ref, () => ({
cancelRequestsOnPageInactive,
resumeRequestsOnPageActive
}));
return ( return (
<PageContainerInner header={header}> <TableContext.Provider
<Tabs value={{
renderTabBar={() => <></>} allChildren: modelInstances,
items={[ setDisableExpand: setDisableExpand
{ }}
key: TabsValueMap.ModelView, >
label: 'Model View', <TableList
children: <ModelView></ModelView> dataSource={dataSource.dataList}
}, onStatusChange={handleOnStatusChange}
{ handleNameChange={handleNameChange}
key: TabsValueMap.InstanceView, handleCategoryChange={handleCategoryChange}
label: 'Instance View', handleClusterChange={handleClusterChange}
children: <InstanceView></InstanceView> handleSearch={handleSearch}
} handlePageChange={handlePageChange}
]} handleDeleteSuccess={fetchData}
activeKey={activeKey} handleOnToggleExpandAll={createModelsInstanceChunkRequest}
></Tabs> onViewLogs={cancelRequestsOnPageInactive}
</PageContainerInner> 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>
); );
}; });
export default Playground; export default Models;
@@ -17,8 +17,9 @@ const Dot = ({ color }: { color: string }) => {
const useFilterStatus = (options?: { const useFilterStatus = (options?: {
onStatusChange?: (value?: any) => void; onStatusChange?: (value?: any) => void;
optionList?: (Global.BaseOption<string> & { color: string })[];
}) => { }) => {
const { onStatusChange } = options || {}; const { onStatusChange, optionList } = options || {};
const intl = useIntl(); const intl = useIntl();
const statusOptions = [ const statusOptions = [
@@ -45,8 +46,10 @@ const useFilterStatus = (options?: {
} }
]; ];
const mergedOptions = optionList || statusOptions;
const labelRender = (item: any) => { const labelRender = (item: any) => {
const current = statusOptions.find((option) => option.value === item.value); const current = mergedOptions.find((option) => option.value === item.value);
return ( return (
<span className="flex-center gap-8"> <span className="flex-center gap-8">
{current && <Dot color={current.color}></Dot>} {current && <Dot color={current.color}></Dot>}
@@ -69,7 +72,7 @@ const useFilterStatus = (options?: {
}; };
return { return {
statusOptions, statusOptions: mergedOptions,
labelRender, labelRender,
optionRender, optionRender,
handleStatusChange handleStatusChange
@@ -0,0 +1,62 @@
import { PageSize } from '@/components/logs-viewer/config';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { MODEL_INSTANCE_API } from '../apis';
import { InstanceRealtimeLogStatus } from '../config';
import { ModelInstanceListItem as ListItem } from '../config/types';
const useViewInstanceLogs = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
currentData: {
url: string;
status: string;
id?: number | string;
modelId?: number | string;
tail?: number;
};
}>({
open: false,
currentData: {
url: '',
status: ''
}
});
const openModal = (row: ListItem) => {
setOpenModalStatus({
open: true,
currentData: {
url: `${MODEL_INSTANCE_API}/${row.id}/logs`,
status: row.state,
id: row.id,
modelId: row.model_id,
tail: InstanceRealtimeLogStatus.includes(row.state)
? undefined
: PageSize - 1
}
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
currentData: {
url: '',
status: ''
}
});
restoreScrollHeight();
};
return {
openViewLogsModalStatus: openModalStatus,
setOpenViewLogsModalStatus: setOpenModalStatus,
openViewLogsModal: openModal,
closeViewLogsModal: closeModal
};
};
export default useViewInstanceLogs;
+107 -460
View File
@@ -1,445 +1,97 @@
import TableContext from '@/components/seal-table/table-context'; import IconFont from '@/components/icon-font';
import { TableOrder } from '@/components/seal-table/types'; import { PageContainerInner } from '@/pages/_components/page-box';
import { PaginationKey } from '@/config/settings'; import { useIntl } from '@umijs/max';
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 { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import _ from 'lodash'; import { Segmented, Tabs } from 'antd';
import qs from 'query-string'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react'; import { DeploymentsContext } from './config/deploments-context';
import { import ModelView from './deployments';
MODELS_API, import useFormInitialValues from './hooks/use-form-initial-values';
MODEL_INSTANCE_API, import InstanceView from './instance-view';
queryModelsInstances,
queryModelsList
} from './apis';
import TableList from './components/table-list';
import { ListItem } from './config/types';
const Models = () => { const TabsValueMap = {
const { pagination, setPagination } = usePaginationStatus( ModelView: 'modelView',
PaginationKey.Deployments InstanceView: 'instanceView'
); };
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 LLModels: React.FC = () => {
const intl = useIntl();
const { const {
updateChunkedList: updateInstanceChunkedList, generateFormValues,
cacheDataListRef: cacheInsDataListRef clusterList,
} = useUpdateChunkedList({ getClusterList,
dataList: modelInstances, getWorkerList,
limit: 100, workerList
setDataList: setModelInstances } = useFormInitialValues();
}); const [activeKey, setActiveKey] = useState(TabsValueMap.ModelView);
const modelViewRef = useRef<any>(null);
const getAllModelInstances = useMemoizedFn(async () => { const instanceViewRef = useRef<any>(null);
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);
};
useEffect(() => { useEffect(() => {
let timer: any = null; const getData = async () => {
// fetch data first time await Promise.all([getClusterList(), getWorkerList()]);
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?.();
}; };
getData();
}, []); }, []);
const setDisableExpand = useMemoizedFn((record: any) => { const handleTabChange = useMemoizedFn((key: string) => {
return !record?.replicas; 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(() => { useEffect(() => {
const handleVisibilityChange = async () => { const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
isPageHidden.current = false; if (activeKey === TabsValueMap.ModelView) {
fetchData({ modelViewRef.current?.resumeRequestsOnPageActive();
loadingVal: false }
}); if (activeKey === TabsValueMap.InstanceView) {
await getAllModelInstances(); instanceViewRef.current?.resumeRequestsOnPageActive();
await createModelsInstanceChunkRequest(); }
await createModelsChunkRequest();
} else { } else {
isPageHidden.current = true; if (activeKey === TabsValueMap.ModelView) {
chunkRequedtRef.current?.current?.cancel?.(); modelViewRef.current?.cancelRequestsOnPageInactive();
cacheDataListRef.current = []; }
cacheInsDataListRef.current = []; if (activeKey === TabsValueMap.InstanceView) {
chunkInstanceRequedtRef.current?.current?.cancel?.(); instanceViewRef.current?.cancelRequestsOnPageInactive();
instancesToken.current?.cancel?.(); }
} }
}; };
@@ -448,41 +100,36 @@ const Models = () => {
return () => { return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('visibilitychange', handleVisibilityChange);
}; };
}, [fetchData, createModelsChunkRequest, createModelsInstanceChunkRequest]); }, [activeKey]);
return ( return (
<TableContext.Provider <PageContainerInner header={header}>
value={{ <DeploymentsContext.Provider
allChildren: modelInstances, value={{
setDisableExpand: setDisableExpand generateFormValues,
}} clusterList,
> workerList
<TableList }}
dataSource={dataSource.dataList} >
onStatusChange={handleOnStatusChange} <Tabs
handleNameChange={handleNameChange} renderTabBar={() => <></>}
handleCategoryChange={handleCategoryChange} items={[
handleClusterChange={handleClusterChange} {
handleSearch={handleSearch} key: TabsValueMap.ModelView,
handlePageChange={handlePageChange} label: intl.formatMessage({ id: 'models.table.modelView' }),
handleDeleteSuccess={fetchData} children: <ModelView ref={modelViewRef}></ModelView>
handleOnToggleExpandAll={createModelsInstanceChunkRequest} },
onViewLogs={handleOnViewLogs} {
onCancelViewLogs={handleOnCancelViewLogs} key: TabsValueMap.InstanceView,
onStop={handleSearchBySilent} label: intl.formatMessage({ id: 'models.table.instanceView' }),
onStart={handleSearchBySilent} children: <InstanceView ref={instanceViewRef}></InstanceView>
onTableSort={handleOnSortChange} }
onFilterChange={handleOnFilterChange} ]}
onDeleteInstanceFromCache={handleDeleteInstanceFromCache} activeKey={activeKey}
sortOrder={sortOrder} ></Tabs>
queryParams={queryParams} </DeploymentsContext.Provider>
loading={dataSource.loading} </PageContainerInner>
loadend={dataSource.loadend}
total={dataSource.total}
deleteIds={dataSource.deletedIds}
></TableList>
</TableContext.Provider>
); );
}; };
export default Models; export default LLModels;
+89 -11
View File
@@ -9,38 +9,71 @@ import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Table } from 'antd'; import { ConfigProvider, Table } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { forwardRef, useImperativeHandle } from 'react';
import { import {
deleteModelInstance, deleteModelInstance,
MODEL_INSTANCE_API, MODEL_INSTANCE_API,
queryModelsInstances queryModelsInstances
} from '../apis'; } from '../apis';
import ViewLogsModal from '../components/view-logs-modal';
import { useDeploymentsContext } from '../config/deploments-context';
import { ModelInstanceListItem as ListItem } from '../config/types'; 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'; import useInstanceColumns from './use-instance-columns';
const InstanceView: React.FC = () => { const InstanceView = forwardRef((props, ref) => {
const { const {
dataSource, dataSource,
rowSelection, rowSelection,
queryParams, queryParams,
modalRef, modalRef,
handleTableChange, handleTableChange,
cancelChunkRequest,
createTableListChunkRequest,
handleDelete, handleDelete,
handleDeleteBatch, handleDeleteBatch,
fetchData, fetchData,
handleQueryChange,
handlePageChange, handlePageChange,
handleSearch, handleSearch,
handleNameChange handleNameChange
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
key: PaginationKey.Providers, key: PaginationKey.Instances,
fetchAPI: queryModelsInstances, fetchAPI: queryModelsInstances,
deleteAPI: deleteModelInstance, deleteAPI: deleteModelInstance,
watch: false, watch: true,
API: MODEL_INSTANCE_API, API: MODEL_INSTANCE_API,
contentForDelete: 'menu.models.providers' contentForDelete: 'menu.models.instances'
}); });
const intl = useIntl(); 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) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
@@ -49,21 +82,46 @@ const InstanceView: React.FC = () => {
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
image={<IconFont type="icon-extension-outline" />} image={<IconFont type="icon-instance-outline" />}
filters={_.omit(queryParams, ['sort_by'])} filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({ 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({ subTitle={intl.formatMessage({
id: 'noresult.providers.subTitle' id: 'noresult.instances.subTitle'
})} })}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })} buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
></NoResult> ></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 ( return (
<> <>
@@ -77,6 +135,17 @@ const InstanceView: React.FC = () => {
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
handleSearch={handleSearch} handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
left={
<LeftFilters
handleNameChange={handleNameChange}
handleClusterChange={handleonClusterChange}
handleStatusChange={handleStatusChange}
handleWorkerChange={handleOnWorkerChange}
handleSearch={handleSearch}
clusterList={clusterList}
workerList={workerList}
></LeftFilters>
}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
@@ -103,9 +172,18 @@ const InstanceView: React.FC = () => {
></Table> ></Table>
</ConfigProvider> </ConfigProvider>
</PageBox> </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> <DeleteModal ref={modalRef}></DeleteModal>
</> </>
); );
}; });
export default InstanceView; 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 // columns.ts
import DropdownButtons from '@/components/drop-down-buttons'; import AutoTooltip from '@/components/auto-tooltip';
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import { ListItem as workerListItem } from '@/pages/resources/config/types';
import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo } from 'react'; 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'; import { ModelInstanceListItem as ListItem } from '../config/types';
const useProviderColumns = ( const calcTotalVram = (vram: Record<string, number>) => {
handleSelect: (val: string, record: ListItem) => void, return _.sum(_.values(vram));
onCellClick?: (record: ListItem, dataIndex: string) => void };
): ColumnsType<ListItem> => {
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 intl = useIntl();
const { workerList, clusterList, handleSelect, onCellClick } = options;
return useMemo(() => { return useMemo(() => {
return [ return [
@@ -19,30 +42,87 @@ const useProviderColumns = (
dataIndex: 'name', dataIndex: 'name',
sorter: tableSorter(1), sorter: tableSorter(1),
minWidth: 160, minWidth: 160,
span: 5, render: (value: string, record: ListItem) => (
render: () => '--' <NameCell
record={record}
modelData={{
backend: record.backend,
backend_version: record.backend_version
}}
></NameCell>
)
}, },
{ {
title: intl.formatMessage({ id: 'providers.table.providerName' }), title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: ['config', 'type'], dataIndex: 'cluster_id',
sorter: tableSorter(2),
span: 4,
minWidth: 160,
render: () => '--'
},
{
title: intl.formatMessage({ id: 'providers.table.models' }),
dataIndex: 'models',
span: 3,
minWidth: 200, 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' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: 'created_at', dataIndex: 'created_at',
sorter: tableSorter(6), sorter: tableSorter(6),
span: 3, 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' }), title: intl.formatMessage({ id: 'common.table.operation' }),
@@ -50,14 +130,17 @@ const useProviderColumns = (
span: 3, span: 3,
minWidth: 120, minWidth: 120,
render: (value: string, record: ListItem) => ( render: (value: string, record: ListItem) => (
<DropdownButtons <ActionsCell
items={[]} record={record}
onSelect={(val) => handleSelect(val, record)} modelData={{
></DropdownButtons> categories: record.categories || []
}}
onSelect={handleSelect}
></ActionsCell>
) )
} }
]; ];
}, [handleSelect, onCellClick]); }, [handleSelect, onCellClick, workerList, clusterList]);
}; };
export default useProviderColumns; export default useProviderColumns;