From adde7fba37e23727b9dbe5df3c52841985f53155 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 20 Mar 2026 16:54:08 +0800 Subject: [PATCH] feat: add instance view --- config/routes.ts | 2 +- src/components/icon-font/index.tsx | 2 +- .../logs-viewer/virtual-log-list.tsx | 16 +- src/config/settings.ts | 3 +- src/hooks/use-table-fetch.ts | 8 +- src/locales/en-US/menu.ts | 1 + src/locales/en-US/models.ts | 4 +- src/locales/en-US/no-result.ts | 6 +- src/locales/ja-JP/menu.ts | 1 + src/locales/ja-JP/models.ts | 6 +- src/locales/ja-JP/no-result.ts | 6 +- src/locales/ru-RU/menu.ts | 2 + src/locales/ru-RU/models.ts | 6 +- src/locales/ru-RU/no-result.ts | 9 +- src/locales/zh-CN/menu.ts | 1 + src/locales/zh-CN/models.ts | 4 +- src/locales/zh-CN/no-result.ts | 6 +- .../cluster-management/cluster-detail.tsx | 2 +- .../llmodels/components/left-filters.tsx | 90 +++ src/pages/llmodels/components/table-list.tsx | 149 +---- .../llmodels/components/view-logs-modal.tsx | 3 +- .../llmodels/config/deploments-context.ts | 35 ++ src/pages/llmodels/config/types.ts | 1 + src/pages/llmodels/deployments.tsx | 526 ++++++++++++++-- .../llmodels/hooks/use-filter-status.tsx | 9 +- .../llmodels/hooks/use-view-instance-logs.ts | 62 ++ src/pages/llmodels/index.tsx | 567 ++++-------------- .../instance-view/cells/actions-cell.tsx | 0 src/pages/llmodels/instance-view/index.tsx | 100 ++- .../llmodels/instance-view/left-filters.tsx | 113 ++++ .../instance-view/use-instance-columns.tsx | 133 +++- 31 files changed, 1171 insertions(+), 702 deletions(-) create mode 100644 src/pages/llmodels/components/left-filters.tsx create mode 100644 src/pages/llmodels/config/deploments-context.ts create mode 100644 src/pages/llmodels/hooks/use-view-instance-logs.ts delete mode 100644 src/pages/llmodels/instance-view/cells/actions-cell.tsx create mode 100644 src/pages/llmodels/instance-view/left-filters.tsx diff --git a/config/routes.ts b/config/routes.ts index 7898e241..a62e8b42 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -114,7 +114,7 @@ export default [ selectedIcon: 'icon-rocket-launch-fill', defaultIcon: 'icon-rocket-launch1', access: 'canSeeAdmin', - component: './llmodels/deployments' + component: './llmodels/index' }, { name: 'routes', diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 2c4fbc7b..c126bdbc 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_7zu9s75km1r.js' }); export default IconFont; diff --git a/src/components/logs-viewer/virtual-log-list.tsx b/src/components/logs-viewer/virtual-log-list.tsx index 12ad231f..c84d7b2a 100644 --- a/src/components/logs-viewer/virtual-log-list.tsx +++ b/src/components/logs-viewer/virtual-log-list.tsx @@ -318,13 +318,15 @@ const LogsViewer: React.FC = forwardRef((props, ref) => { onScroll={handleOnScroll} > - + {loading && ( + + )} {totalPage > 1 && (
( }); }; + const cancelChunkRequest = () => { + chunkRequestRef.current?.current?.cancel?.(); + }; + const createTableListChunkRequest = async (params?: any) => { if (!API || !watch) return; chunkRequestRef.current?.current?.cancel?.(); @@ -289,7 +293,7 @@ export default function useTableFetch( const handleQueryChange = async ( params: any, 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 @@ -459,6 +463,8 @@ export default function useTableFetch( handleSearch, handleQueryChange, loadMore, + cancelChunkRequest, + createTableListChunkRequest, handleNameChange }; } diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 19ceafa5..71ef09bd 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -17,6 +17,7 @@ export default { 'menu.models.benchmark': 'Benchmarks', 'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.providers': 'Providers', + 'menu.models.instances': 'Instances', 'menu.models.routes': 'Routes', 'menu.modelCatalog': 'Catalog', 'menu.resources': 'Resources', diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 86639a85..0a0ace2b 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -279,5 +279,7 @@ export default { 'models.form.maxContextLength': 'Maximum Context Length', 'models.form.backend.helperText': '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' }; diff --git a/src/locales/en-US/no-result.ts b/src/locales/en-US/no-result.ts index a9b6db85..87ae927d 100644 --- a/src/locales/en-US/no-result.ts +++ b/src/locales/en-US/no-result.ts @@ -48,5 +48,9 @@ export default { 'noresult.providers.nofound': 'No matching providers found.', 'noresult.routes.title': 'No Routes', '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.' }; diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index 1e6e396d..baae2f30 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -17,6 +17,7 @@ export default { 'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.modelCatalog': 'カタログ', 'menu.models.providers': 'Providers', + 'menu.models.instances': 'Instances', 'menu.models.routes': 'Routes', 'menu.resources': 'リソース', 'menu.apikeys': 'APIキー', diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index 13b21fa8..c444daf2 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -279,7 +279,9 @@ export default { 'models.form.maxContextLength': 'Maximum Context Length', 'models.form.backend.helperText': '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) ========== @@ -380,4 +382,6 @@ export default { // 78. 'models.table.instance.benchmark': 'Run Benchmark' // 77. 'models.form.enableModelRoute': '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 ========== diff --git a/src/locales/ja-JP/no-result.ts b/src/locales/ja-JP/no-result.ts index a9b6db85..87ae927d 100644 --- a/src/locales/ja-JP/no-result.ts +++ b/src/locales/ja-JP/no-result.ts @@ -48,5 +48,9 @@ export default { 'noresult.providers.nofound': 'No matching providers found.', 'noresult.routes.title': 'No Routes', '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.' }; diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index ea624995..b3bd6bab 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -16,6 +16,7 @@ export default { 'menu.models.benchmark': 'Benchmarks', 'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.providers': 'Providers', + 'menu.models.instances': 'Instances', 'menu.models.routes': 'Routes', 'menu.modelCatalog': 'Каталог', 'menu.resources': 'Ресурсы', @@ -45,4 +46,5 @@ export default { // 2. 'menu.models.benchmarkDetail': 'Benchmark Details', // 3. 'menu.models.providers': 'Providers', // 4. 'menu.models.routes': 'Routes', +// 5. 'menu.models.instances': 'Instances', // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index 97591147..0a0d2727 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -283,7 +283,9 @@ export default { 'models.form.maxContextLength': 'Maximum Context Length', 'models.form.backend.helperText': '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) ========== @@ -294,4 +296,6 @@ export default { // 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.', // 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 ========== diff --git a/src/locales/ru-RU/no-result.ts b/src/locales/ru-RU/no-result.ts index b801f553..5f4e43d4 100644 --- a/src/locales/ru-RU/no-result.ts +++ b/src/locales/ru-RU/no-result.ts @@ -49,7 +49,11 @@ export default { 'noresult.providers.nofound': 'No matching providers found.', 'noresult.routes.title': 'No Routes', '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) ========== @@ -66,4 +70,7 @@ export default { // 8. 'noresult.routes.title': 'No Routes', // 9. 'noresult.routes.subTitle': 'No routes have been added yet.', // 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.' // ======================================================================== diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 39fbf680..bc20da52 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -16,6 +16,7 @@ export default { 'menu.models.benchmark': '基准测试', 'menu.models.benchmarkDetail': '基准测试详情', 'menu.models.providers': '提供商', + 'menu.models.instances': '实例', 'menu.models.routes': '路由', 'menu.modelCatalog': '模型库', 'menu.models.catalog': '模型库', diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index be78ec8f..f9e1eb61 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -263,5 +263,7 @@ export default { 'models.form.readyWorkers': '节点就绪', 'models.form.maxContextLength': '最大上下文长度', 'models.form.backend.helperText': '该社区后端暂未启用,部署后将自动启用', - 'models.table.instance.benchmark': '运行基准测试' + 'models.table.instance.benchmark': '运行基准测试', + 'models.table.modelView': '模型视图', + 'models.table.instanceView': '实例视图' }; diff --git a/src/locales/zh-CN/no-result.ts b/src/locales/zh-CN/no-result.ts index 5d6be0b4..bf437984 100644 --- a/src/locales/zh-CN/no-result.ts +++ b/src/locales/zh-CN/no-result.ts @@ -46,5 +46,9 @@ export default { 'noresult.providers.nofound': '未找到匹配的提供商', 'noresult.routes.title': '暂无路由', 'noresult.routes.subTitle': '尚未添加任何路由。', - 'noresult.routes.nofound': '未找到匹配的路由' + 'noresult.routes.nofound': '未找到匹配的路由', + 'noresult.instances.title': '暂无实例', + 'noresult.instances.subTitle': + '当前没有运行中的模型实例,部署模型后将自动创建。', + 'noresult.instances.nofound': '未找到匹配的实例' }; diff --git a/src/pages/cluster-management/cluster-detail.tsx b/src/pages/cluster-management/cluster-detail.tsx index 3389ba42..777ac896 100644 --- a/src/pages/cluster-management/cluster-detail.tsx +++ b/src/pages/cluster-management/cluster-detail.tsx @@ -1,6 +1,6 @@ import { clusterDetailAtom } from '@/atoms/clusters'; 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 WorkerList from '@/pages/resources/components/workers'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; diff --git a/src/pages/llmodels/components/left-filters.tsx b/src/pages/llmodels/components/left-filters.tsx new file mode 100644 index 00000000..65386408 --- /dev/null +++ b/src/pages/llmodels/components/left-filters.tsx @@ -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) => void; + handleClusterChange: (value: number) => void; + handleStatusChange: (value: string) => void; + handleSearch: () => void; + handleCategoryChange: (value: string) => void; + clusterList: Global.BaseOption[]; +} + +const LeftFilters: React.FC = (props) => { + const { + handleNameChange, + handleClusterChange, + handleStatusChange, + handleSearch, + handleCategoryChange, + clusterList + } = props; + const { labelRender, optionRender, statusOptions } = useFilterStatus(); + const intl = useIntl(); + + return ( + + + } + placeholder={intl.formatMessage({ id: 'common.filter.name' })} + style={{ width: 200 }} + size="large" + allowClear + onChange={handleNameChange} + > + item.value)} + > + + + + + ); +}; + +export default LeftFilters; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index dd8e8faa..e1a5597d 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -2,9 +2,7 @@ import { modelsExpandKeysAtom, modelsSessionAtom } from '@/atoms/models'; import DeleteModal from '@/components/delete-modal'; import DropDownActions from '@/components/drop-down-actions'; import DropdownButtons from '@/components/drop-down-buttons'; -import { PageSize } from '@/components/logs-viewer/config'; import PageTools from '@/components/page-tools'; -import BaseSelect from '@/components/seal-form/base/select'; import SealTable from '@/components/seal-table'; import { TableOrder } from '@/components/seal-table/types'; 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 useGranfanaLink from '@/pages/resources/hooks/use-grafana-link'; 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 { useMemoizedFn } from 'ahooks'; -import { Button, Input, Space, message } from 'antd'; +import { Button, Space, message } from 'antd'; import { useAtom } from 'jotai'; import _ from 'lodash'; import React, { @@ -36,23 +34,19 @@ import React, { } from 'react'; import { MODELS_API, - MODEL_INSTANCE_API, createModel, deleteModel, deleteModelInstance, queryModelInstancesList, updateModel } from '../apis'; -import { - InstanceRealtimeLogStatus, - modelCategories, - modelSourceMap -} from '../config'; +import { modelSourceMap } from '../config'; import { ButtonList, modalConfig, sourceOptions } from '../config/button-actions'; +import { useDeploymentsContext } from '../config/deploments-context'; import { FormData, ListItem, @@ -60,12 +54,12 @@ import { SourceType } from '../config/types'; 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 useViewInstanceLogs from '../hooks/use-view-instance-logs'; import DeployModal from './deployment/deploy-modal'; import UpdateModelModal from './deployment/update-modal'; import Instances from './instance/instances'; +import LeftFilters from './left-filters'; import ViewLogsModal from './view-logs-modal'; interface ModelsProps { handleSearch: (params?: any) => void; @@ -136,13 +130,8 @@ const Models: React.FC = ({ loadend, total }) => { - const { - generateFormValues, - clusterList, - getClusterList, - getWorkerList, - workerList - } = useFormInitialValues(); + const { generateFormValues, clusterList, workerList } = + useDeploymentsContext(); const [searchParams] = useSearchParams(); const page = searchParams.get('page'); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); @@ -165,11 +154,9 @@ const Models: React.FC = ({ expandedRowKeys } = useExpandedRowKeys(expandAtom); const { handleOpenPlayGround } = useOpenPlayground(); - const { labelRender, optionRender, handleStatusChange, statusOptions } = - useFilterStatus({ - onStatusChange: onStatusChange - }); const { watchDataList: targetList } = useWatchList(MODEL_ROUTE_TARGETS); + const { openViewLogsModal, openViewLogsModalStatus, closeViewLogsModal } = + useViewInstanceLogs(); const { goToGrafana, ActionButton } = useGranfanaLink({ type: 'model' @@ -189,16 +176,6 @@ const Models: React.FC = ({ isGGUF: false, 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(null); useEffect(() => { @@ -208,10 +185,6 @@ const Models: React.FC = ({ }, [deleteIds]); useEffect(() => { - const getData = async () => { - await Promise.all([getClusterList(), getWorkerList()]); - }; - getData(); return () => { setExpandAtom([]); }; @@ -308,11 +281,10 @@ const Models: React.FC = ({ } catch (error) {} }; - const handleLogModalCancel = useCallback(() => { - setOpenLogModal(false); + const handleLogModalCancel = () => { onCancelViewLogs(); - restoreScrollHeight(); - }, [onCancelViewLogs]); + closeViewLogsModal(); + }; const handleDelete = async (row: any) => { modalRef.current?.show({ @@ -352,22 +324,8 @@ const Models: React.FC = ({ }; const handleViewLogs = async (row: any) => { - try { - setCurrentInstance({ - 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); - } + openViewLogsModal(row); + onViewLogs(); }; const handleDeleteInstace = (row: any) => { @@ -600,62 +558,14 @@ const Models: React.FC = ({ marginBottom={22} marginTop={0} left={ - - - } - placeholder={intl.formatMessage({ id: 'common.filter.name' })} - style={{ width: 200 }} - size="large" - allowClear - onChange={handleNameChange} - > - item.value)} - > - - - - + } right={ @@ -720,6 +630,7 @@ const Models: React.FC = ({ pageSize: queryParams.perPage, current: queryParams.page, total: total, + size: 'middle', hideOnSinglePage: queryParams.perPage === 10, onChange: handlePageChange }} @@ -747,12 +658,12 @@ const Models: React.FC = ({ onOk={handleCreateModel} > diff --git a/src/pages/llmodels/components/view-logs-modal.tsx b/src/pages/llmodels/components/view-logs-modal.tsx index 628867af..f6b44e23 100644 --- a/src/pages/llmodels/components/view-logs-modal.tsx +++ b/src/pages/llmodels/components/view-logs-modal.tsx @@ -69,8 +69,7 @@ const ViewLogsModal: React.FC = (props) => { }, [open]); useEffect(() => { - if (!props.id) return; - if (open) { + if (open && props.id) { requestRef.current?.current?.cancel?.(); requestRef.current = setChunkRequest({ url: `${MODELS_API}/${props.modelId}/instances`, diff --git a/src/pages/llmodels/config/deploments-context.ts b/src/pages/llmodels/config/deploments-context.ts new file mode 100644 index 00000000..0bf4566b --- /dev/null +++ b/src/pages/llmodels/config/deploments-context.ts @@ -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; +} + +export const DeploymentsContext = createContext({ + clusterList: [], + workerList: [], + generateFormValues: () => ({}) +}); + +export const useDeploymentsContext = () => { + const context = useContext(DeploymentsContext); + if (!context) { + throw new Error( + 'useDeploymentsContext must be used within a DeploymentsProvider' + ); + } + return context; +}; diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index 2cdb5b32..a7430117 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -119,6 +119,7 @@ export interface ModelInstanceListItem { cluster_id: number; backend_version?: string; source: string; + categories?: string[]; huggingface_repo_id: string; huggingface_filename: string; ollama_library_model_name: string; diff --git a/src/pages/llmodels/deployments.tsx b/src/pages/llmodels/deployments.tsx index 62764e33..b97667a5 100644 --- a/src/pages/llmodels/deployments.tsx +++ b/src/pages/llmodels/deployments.tsx @@ -1,70 +1,472 @@ -import IconFont from '@/components/icon-font'; -import { PageContainerInner } from '@/pages/_components/page-box'; -import { useIntl } from '@umijs/max'; -import { Segmented, Tabs } from 'antd'; -import { useMemo, useState } from 'react'; -import ModelView from './index'; -import InstanceView from './instance-view'; +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 { 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 = { - ModelView: 'modelView', - InstanceView: 'instanceView' -}; +const Models = forwardRef((props, ref) => { + const { pagination, setPagination } = usePaginationStatus( + PaginationKey.Deployments + ); + const { sortOrder, handleMultiSortChange } = useTableMultiSort(); + const { setChunkRequest, createAxiosToken } = useSetChunkRequest(); + const { setChunkRequest: setModelInstanceChunkRequest } = + useSetChunkRequest(); + const [modelInstances, setModelInstances] = useState([]); + 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(); + const chunkInstanceRequedtRef = useRef(); + const isPageHidden = useRef(false); + const instancesToken = useRef(); + 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 intl = useIntl(); - const [activeKey, setActiveKey] = useState(TabsValueMap.ModelView); + 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 header = useMemo(() => { - return { - title: ( -
- - {intl.formatMessage({ id: 'menu.models.deployment' })} - - - }, - { - label: 'Instance View', - value: TabsValueMap.InstanceView, - icon: - } - ]} - size="middle" - className="m-l-24 font-600" - value={activeKey} - onChange={(key) => setActiveKey(key)} - > -
- ) + 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 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) => { + 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 ( - - <>} - items={[ - { - key: TabsValueMap.ModelView, - label: 'Model View', - children: - }, - { - key: TabsValueMap.InstanceView, - label: 'Instance View', - children: - } - ]} - activeKey={activeKey} - > - + + + ); -}; +}); -export default Playground; +export default Models; diff --git a/src/pages/llmodels/hooks/use-filter-status.tsx b/src/pages/llmodels/hooks/use-filter-status.tsx index 28de1650..be2e736a 100644 --- a/src/pages/llmodels/hooks/use-filter-status.tsx +++ b/src/pages/llmodels/hooks/use-filter-status.tsx @@ -17,8 +17,9 @@ const Dot = ({ color }: { color: string }) => { const useFilterStatus = (options?: { onStatusChange?: (value?: any) => void; + optionList?: (Global.BaseOption & { color: string })[]; }) => { - const { onStatusChange } = options || {}; + const { onStatusChange, optionList } = options || {}; const intl = useIntl(); const statusOptions = [ @@ -45,8 +46,10 @@ const useFilterStatus = (options?: { } ]; + const mergedOptions = optionList || statusOptions; + const labelRender = (item: any) => { - const current = statusOptions.find((option) => option.value === item.value); + const current = mergedOptions.find((option) => option.value === item.value); return ( {current && } @@ -69,7 +72,7 @@ const useFilterStatus = (options?: { }; return { - statusOptions, + statusOptions: mergedOptions, labelRender, optionRender, handleStatusChange diff --git a/src/pages/llmodels/hooks/use-view-instance-logs.ts b/src/pages/llmodels/hooks/use-view-instance-logs.ts new file mode 100644 index 00000000..d7b03d9a --- /dev/null +++ b/src/pages/llmodels/hooks/use-view-instance-logs.ts @@ -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; diff --git a/src/pages/llmodels/index.tsx b/src/pages/llmodels/index.tsx index 2e6c2afe..378230fd 100644 --- a/src/pages/llmodels/index.tsx +++ b/src/pages/llmodels/index.tsx @@ -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([]); - 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(); - const chunkInstanceRequedtRef = useRef(); - const isPageHidden = useRef(false); - const instancesToken = useRef(); - 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) => { - 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(null); + const instanceViewRef = useRef(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: ( +
+ + {intl.formatMessage({ id: 'menu.models.deployment' })} + + + }, + { + label: intl.formatMessage({ id: 'models.table.instanceView' }), + value: TabsValueMap.InstanceView, + icon: + } + ]} + size="middle" + className="m-l-24 font-600" + value={activeKey} + onChange={handleTabChange} + > +
+ ) + }; + }, [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 ( - - - + + + <>} + items={[ + { + key: TabsValueMap.ModelView, + label: intl.formatMessage({ id: 'models.table.modelView' }), + children: + }, + { + key: TabsValueMap.InstanceView, + label: intl.formatMessage({ id: 'models.table.instanceView' }), + children: + } + ]} + activeKey={activeKey} + > + + ); }; -export default Models; +export default LLModels; diff --git a/src/pages/llmodels/instance-view/cells/actions-cell.tsx b/src/pages/llmodels/instance-view/cells/actions-cell.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/llmodels/instance-view/index.tsx b/src/pages/llmodels/instance-view/index.tsx index dec5d73e..eacd35dd 100644 --- a/src/pages/llmodels/instance-view/index.tsx +++ b/src/pages/llmodels/instance-view/index.tsx @@ -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({ - 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={} + image={} 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' })} > ); }; - 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={ + + } > { >
+ ); -}; +}); export default InstanceView; diff --git a/src/pages/llmodels/instance-view/left-filters.tsx b/src/pages/llmodels/instance-view/left-filters.tsx new file mode 100644 index 00000000..1504c8ea --- /dev/null +++ b/src/pages/llmodels/instance-view/left-filters.tsx @@ -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) => void; + handleClusterChange: (value: number) => void; + handleStatusChange: (value: string) => void; + handleWorkerChange: (value: number) => void; + handleSearch: () => void; + clusterList: Global.BaseOption[]; + workerList: workerListItem[]; +} + +const LeftFilters: React.FC = (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 ( + + + } + placeholder={intl.formatMessage({ id: 'common.filter.name' })} + style={{ width: 200 }} + size="large" + allowClear + onChange={handleNameChange} + > + + ({ + label: worker.name, + value: worker.id + }))} + > + + + + ); +}; + +export default LeftFilters; diff --git a/src/pages/llmodels/instance-view/use-instance-columns.tsx b/src/pages/llmodels/instance-view/use-instance-columns.tsx index 8c670345..cbe94e34 100644 --- a/src/pages/llmodels/instance-view/use-instance-columns.tsx +++ b/src/pages/llmodels/instance-view/use-instance-columns.tsx @@ -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 => { +const calcTotalVram = (vram: Record) => { + 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 => { 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) => ( + + ) }, { - 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) => ( + + {text + ? clusterList.find((cluster) => cluster.value === text)?.label + : ''} + + ) + }, + { + title: intl.formatMessage({ id: 'resources.worker' }), + dataIndex: 'worker_id', + minWidth: 200, + render: (text, record) => ( + + {text && + (!record.distributed_servers?.subordinate_workers || + !record.distributed_servers?.subordinate_workers.length) + ? workerList.find((worker) => worker.id === text)?.name + : ''} + + + ) + }, + { + title: intl.formatMessage({ id: 'models.table.vram.allocated' }), + dataIndex: 'allocated_vram', + sorter: tableSorter(6), + render: (text, record) => ( + + {convertFileSize( + record.computed_resource_claim?.vram + ? calcTotalVram(record.computed_resource_claim?.vram) + : 0, + 1 + )} + + ) + }, + { + title: intl.formatMessage({ id: 'common.table.status' }), + dataIndex: 'state', + width: 160, + render: (text: string, record: ListItem) => ( + + {}} /> + + + ) }, { title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: 'created_at', sorter: tableSorter(6), span: 3, - render: () => '--' + render: (text, record) => ( + + {dayjs(record.created_at).format('YYYY-MM-DD HH:mm:ss')} + + ) }, { title: intl.formatMessage({ id: 'common.table.operation' }), @@ -50,14 +130,17 @@ const useProviderColumns = ( span: 3, minWidth: 120, render: (value: string, record: ListItem) => ( - handleSelect(val, record)} - > + ) } ]; - }, [handleSelect, onCellClick]); + }, [handleSelect, onCellClick, workerList, clusterList]); }; export default useProviderColumns;