import useTableFetch from '@/hooks/use-table-fetch'; import { SyncOutlined } from '@ant-design/icons'; import { BaseSelect, InfiniteScrollerProvider, PageTools, TemplateCardList } from '@gpustack/core-ui'; import { useAccess, useIntl, useNavigate } from '@umijs/max'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import { Button, Input, Space } from 'antd'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PageBox from '../_components/page-box'; import { MY_MODELS_API, queryMyModels } from './apis'; import APIAccessInfoModal from './components/api-access-info'; import ModelItem from './components/model-item'; import { categoryOptions, MyModelsStatusValueMap } from './config'; import useFormInitialValues from './hooks/use-form-initial-values'; import useNoResourceResult from './hooks/use-no-resource-result'; import useViewApIInfo from './hooks/use-view-api-info'; const Dot = ({ color }: { color: string }) => { return ( ); }; const optionRender = (item: any) => { return ( {item.label} ); }; const UserModels: React.FC = () => { const { dataSource, queryParams, fetchData, handleSearch, handleQueryChange, handleNameChange } = useTableFetch({ fetchAPI: queryMyModels, API: MY_MODELS_API, watch: false, isInfiniteScroll: true, defaultQueryParams: { perPage: 24, state: MyModelsStatusValueMap.Ready } }); const intl = useIntl(); const access = useAccess(); const navigate = useNavigate(); const { apiAccessInfo, openViewAPIInfo, closeViewAPIInfo } = useViewApIInfo(); // Only managers (platform admin or org owner) can see / manage // clusters and workers, so only they hit those endpoints. A plain // user falls straight through to the default "no models" empty state // without the infra-guidance queries firing. const canManageResources = access?.canSeeAdmin || access?.canSeeOrgAdmin; const { getClusterList, getWorkerList, clusterList, workerList } = useFormInitialValues(); // Managers start in a loading state so the empty state waits for the // infra queries to resolve — otherwise the initially-empty // cluster/worker lists briefly flash the "no clusters" / "no workers" // guidance before the real data lands. const [infraLoading, setInfraLoading] = useState(!!canManageResources); useEffect(() => { if (canManageResources) { setInfraLoading(true); Promise.all([getClusterList(), getWorkerList()]).finally(() => { setInfraLoading(false); }); } }, [canManageResources]); const statusOptions = useMemo(() => { return [ { value: MyModelsStatusValueMap.Ready, color: 'var(--ant-color-success)', label: intl.formatMessage({ id: 'models.mymodels.status.active' }) }, { value: MyModelsStatusValueMap.Stopped, color: 'var(--ant-color-fill)', label: intl.formatMessage({ id: 'models.mymodels.status.inactive' }) }, { value: MyModelsStatusValueMap.NotReady, color: 'var(--ant-color-warning)', label: intl.formatMessage({ id: 'models.mymodels.status.degrade' }) } ]; }, [intl]); const handleCategoryChange = (value: string) => { handleQueryChange({ categories: value }); }; const renderCard = (data: any) => { return ; }; const loadMore = useMemoizedFn((nextPage: number) => { fetchData({ query: { ...queryParams, page: nextPage }, loadmore: true }); }); const handleStatusChange = (value: string) => { handleQueryChange({ state: value }); }; const labelRender = (item: any) => { const current = statusOptions.find((option) => option.value === item.value); return ( {item.label} ); }; const getStatus = useCallback((model: any) => { if (!model.targets && !model.ready_targets) { return MyModelsStatusValueMap.Stopped; } if (model.targets > 0 && !model.ready_targets) { return MyModelsStatusValueMap.NotReady; } if (model.ready_targets > 0 && model.targets > 0) { return MyModelsStatusValueMap.Ready; } return MyModelsStatusValueMap.NotReady; }, []); const dataList = useMemo(() => { const result = dataSource.dataList.map((item) => { return { ...item, status: getStatus(item) }; }); return result; }, [dataSource.dataList]); const handleRefresh = () => { fetchData({ query: { ...queryParams, page: 1 } }); }; const { noResourceResult } = useNoResourceResult({ // Hold the empty state until infra queries resolve so managers don't // see a "no clusters/workers" flash before the real data arrives. loading: dataSource.loading || infraLoading, loadend: dataSource.loadend, dataSource: dataList, // Preserve the original filters heuristic: only treat the current // query as an active filter when there is data across pages, so a // truly empty account still shows the full empty state (CTA). queryParams: dataSource.totalPage > 0 ? queryParams : {}, iconType: 'icon-models', title: intl.formatMessage({ id: 'noresult.mymodels.title' }), noClusters: !!canManageResources && !clusterList.length, noWorkers: !!canManageResources && workerList.length === 0 && clusterList.length > 0, defaultContent: { // Infra is in place but no models yet: guide managers to deploy // one, reusing the deployments-page copy so the two empty states // read consistently. Plain users can't deploy (and skip the infra // queries), so they keep the consumer-facing "ask an admin" copy // and a button-less empty state. subTitle: canManageResources ? intl.formatMessage({ id: 'noresult.deployments.subTitle' }) : intl.formatMessage({ id: 'noresult.mymodels.subTitle' }), noFoundText: intl.formatMessage({ id: 'noresult.mymodels.nofound' }), buttonText: canManageResources ? intl.formatMessage({ id: 'models.table.button.deploy' }) : '', onClick: canManageResources ? () => navigate('/models/catalog') : () => {} } }); return ( <> handleNameChange({ target: { value: '' } }) } onChange={handleNameChange} > } > {noResourceResult} ); }; export default UserModels;