import { clusterSessionAtom } from '@/atoms/clusters'; import { PageAction } from '@/config'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import useTableFetch from '@/hooks/use-table-fetch'; import { ProviderValueMap } from '@/pages/cluster-management/config'; import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui'; import { useAccess, useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { ConfigProvider, message, Modal, Table } from 'antd'; import { useSetAtom } from 'jotai'; import _ from 'lodash'; import { useEffect, useMemo } from 'react'; import PageBox from '../../_components/page-box'; import { deleteGPUServiceInstance, GPU_SERVICE_INSTANCES_API, queryGPUServiceInstances } from './apis'; import AddModal from './components/add-modal'; import ViewEventsModal from './components/view-events-modal'; import ViewLogsModal from './components/view-logs-modal'; import { FormData, ListItem } from './config/types'; import useCreateInstance from './hooks/use-create-instance'; import useInstancesColumns from './hooks/use-instances-columns'; import useViewEvents from './hooks/use-view-events'; import useViewLogs from './hooks/use-view-logs'; import useCreateInstanceRequest from './services/use-create-instance'; import useUpdateInstance from './services/use-update-instance'; const GPUService: React.FC = () => { const intl = useIntl(); const navigate = useNavigate(); const access = useAccess(); const setClusterSession = useSetAtom(clusterSessionAtom); const [, modalContextHolder] = Modal.useModal(); // Empty-state CTA: send the user to the cluster-create flow with // Kubernetes preselected so the GPU Service feature is reachable // in one step instead of having to pick the provider themselves. const handleAddK8sCluster = () => { setClusterSession({ firstAddWorker: false, firstAddCluster: true, providerHint: ProviderValueMap.Kubernetes }); navigate('/cluster-management/clusters/list'); }; const { dataSource, rowSelection, queryParams, sortOrder, modalRef, handleDelete, handleDeleteBatch, fetchData, handlePageChange, handleTableChange, handleSearch, handleNameChange } = useTableFetch({ key: PaginationKey.Instances, fetchAPI: queryGPUServiceInstances, deleteAPI: deleteGPUServiceInstance, watch: true, API: GPU_SERVICE_INSTANCES_API, contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' }) }); const { fetchData: createInstance } = useCreateInstanceRequest(); const { fetchData: updateInstance } = useUpdateInstance(); const { openInstanceModalStatus, openCreateInstanceModal, openEditInstanceModal, openViewInstanceModal, openRecreateInstanceModal, closeInstanceModal } = useCreateInstance(); const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } = useViewLogs(); const { openViewEventsModal, closeViewEventsModal, openViewEventsModalStatus } = useViewEvents(); const { fetchClusterList, cancelRequest: cancelClusterRequest, clusterList, loading: clusterLoading } = useQueryClusterList(); useEffect(() => { fetchClusterList({ page: -1 }); }, []); // GPU Service today is Kubernetes-only — Docker / cloud clusters // can't host the CRDs. Filter so the page reflects scheduling // reality even when the caller owns non-K8s clusters. const k8sClusterList = useMemo( () => clusterList.filter((c) => c.provider === ProviderValueMap.Kubernetes), [clusterList] ); const hasK8sCluster = k8sClusterList.length > 0; const handleModalOk = async (data: FormData) => { try { if (openInstanceModalStatus.realAction === PageAction.CREATE) { await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id); await new Promise((resolve) => { setTimeout(resolve, 300); }); await createInstance({ data }); } else if (openInstanceModalStatus.action === PageAction.EDIT) { await updateInstance({ id: openInstanceModalStatus.currentData!.id, data }); } else { await createInstance({ data }); } fetchData(); closeInstanceModal(); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) { // ignore } }; const handleSelect = useMemoizedFn((val: string, row: ListItem) => { if (val === 'view') { openViewInstanceModal(row); } else if (val === 'edit') { openEditInstanceModal(row); } else if (val === 'delete') { handleDelete({ ...row, name: row.name }); } else if (val === 'recreate') { openRecreateInstanceModal(row); } else if (val === 'viewlog') { openViewLogsModal(row); } else if (val === 'viewevent') { openViewEventsModal(row); } // start / stop are disabled until backend endpoints exist }); const handleBatchActionSelect = useMemoizedFn((val: string) => { if (val === 'delete') { handleDeleteBatch(); } // start / stop are disabled until backend endpoints exist }); const renderEmpty = (type?: string) => { if (type !== 'Table') return; // No K8s cluster the caller can schedule on — replace the "no // instances" empty state with a cluster-bootstrap prompt. The // "Add cluster" CTA is reserved for callers who can actually // create one (platform admin / Org owner); members see the // explanation without a misleading button. if (!clusterLoading && !hasK8sCluster) { return ( } title={intl.formatMessage({ id: 'noresult.gpuservice.instance.title' })} subTitle={intl.formatMessage({ id: 'noresult.resources.k8sCluster' })} {...(access.canSeeOrgAdmin ? { buttonText: intl.formatMessage({ id: 'noresult.resources.addk8scluster' }), onClick: handleAddK8sCluster } : {})} /> ); } return ( } filters={_.pick(queryParams, ['search'])} noFoundText={intl.formatMessage({ id: 'noresult.gpuservice.instance.nofound' })} title={intl.formatMessage({ id: 'noresult.gpuservice.instance.title' })} subTitle={intl.formatMessage({ id: 'noresult.gpuservice.instance.subTitle' })} onClick={openCreateInstanceModal} buttonText={intl.formatMessage({ id: 'noresult.button.add' })} /> ); }; const columns = useInstancesColumns({ handleSelect, clusterList, sortOrder }); return ( <> record.id} onChange={handleTableChange} pagination={{ showSizeChanger: true, pageSize: queryParams.perPage, current: queryParams.page, total: dataSource.total, hideOnSinglePage: queryParams.perPage === 10, onChange: handlePageChange }} /> {modalContextHolder} ); }; export default GPUService;