From 3dbb0de683af26ac04ccbe9799e63fad6a3a3f54 Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 11 Dec 2025 17:39:13 +0800 Subject: [PATCH] feat: add resource for first login --- src/atoms/models.ts | 7 ++ .../hooks/use-cluster-list.ts | 92 +++++++++++++++++++ .../dashboard/components/system-load.tsx | 22 +---- .../dashboard/config/dashboard-context.ts | 8 ++ src/pages/dashboard/index.tsx | 57 ++++++++++-- .../components/deploy-builtin-modal.tsx | 3 +- src/pages/llmodels/components/table-list.tsx | 55 ++++++----- src/pages/llmodels/hooks/index.ts | 10 +- .../llmodels/hooks/use-form-initial-values.ts | 27 +++++- .../llmodels/hooks/use-no-resource-result.tsx | 7 ++ src/pages/llmodels/index.tsx | 21 +---- 11 files changed, 227 insertions(+), 82 deletions(-) create mode 100644 src/pages/cluster-management/hooks/use-cluster-list.ts diff --git a/src/atoms/models.ts b/src/atoms/models.ts index c5242c8b..edb74c1d 100644 --- a/src/atoms/models.ts +++ b/src/atoms/models.ts @@ -27,4 +27,11 @@ export const clusterListAtom = atom< }[] >([]); +export const workerListAtom = atom< + { + label: string; + value: number; + }[] +>([]); + export const backendOptionsAtom = atom([]); diff --git a/src/pages/cluster-management/hooks/use-cluster-list.ts b/src/pages/cluster-management/hooks/use-cluster-list.ts new file mode 100644 index 00000000..162918aa --- /dev/null +++ b/src/pages/cluster-management/hooks/use-cluster-list.ts @@ -0,0 +1,92 @@ +import { clusterListAtom, workerListAtom } from '@/atoms/models'; +import { queryClusterList } from '@/pages/cluster-management/apis'; +import { queryWorkersList } from '@/pages/resources/apis'; +import { useAtom } from 'jotai'; +import { useState } from 'react'; + +/** + * Currently fetch the cluster list and worker list for checking resource existence. + * If there is no cluster or no worker, introduct the use to create/add them. + * @returns + */ +export default function useClusterList() { + const [clusterList, setClusterList] = useState< + { + label: string; + value: number; + id: number; + state: string; + provider: string; + }[] + >([]); + const [workerList, setWorkerList] = useState< + { + cluster_id: number; + state: string; + label: string; + value: number; + id: number; + labels: { [key: string]: string }; + name: string; + }[] + >([]); + const [, setClusterListAtom] = useAtom(clusterListAtom); + const [, setWorkerListAtom] = useAtom(workerListAtom); + + const fetchClusterList = async () => { + try { + const res = await queryClusterList({ page: -1 }); + const list = res?.items?.map((item: any) => ({ + label: item.name, + value: item.id, + id: item.id, + state: item.state, + provider: item.provider + })); + return list; + } catch (error) { + return []; + } + }; + + const fetchWorkerList = async () => { + try { + const res = await queryWorkersList({ page: -1 }); + const list = res?.items?.map((item: any) => ({ + cluster_id: item.cluster_id, + state: item.state, + label: item.name, + value: item.id, + id: item.id, + labels: item.labels || {}, + name: item.name + })); + return list; + } catch (error) { + return []; + } + }; + + const fetchAll = async () => { + const [clusters, workers] = await Promise.all([ + fetchClusterList(), + fetchWorkerList() + ]); + setClusterList(clusters); + setWorkerList(workers); + setClusterListAtom(clusters); + setWorkerListAtom(workers); + return { + hasClusters: clusters.length > 0, + hasWorkers: workers.length > 0 + }; + }; + + return { + clusterList, + workerList, + fetchAll, + fetchWorkerList, + fetchClusterList + }; +} diff --git a/src/pages/dashboard/components/system-load.tsx b/src/pages/dashboard/components/system-load.tsx index d2e90e52..9a058301 100644 --- a/src/pages/dashboard/components/system-load.tsx +++ b/src/pages/dashboard/components/system-load.tsx @@ -2,7 +2,6 @@ import CardWrapper from '@/components/card-wrapper'; import GaugeChart from '@/components/echarts/gauge'; import PageTools from '@/components/page-tools'; import BaseSelect from '@/components/seal-form/base/select'; -import { queryClusterList } from '@/pages/cluster-management/apis'; import { useIntl } from '@umijs/max'; import { Col, Row } from 'antd'; import _ from 'lodash'; @@ -16,11 +15,8 @@ const resourceChartHeight = 400; const SystemLoad = () => { const intl = useIntl(); - const { system_load, fetchData } = useContext(DashboardContext); + const { system_load, fetchData, clusterList } = useContext(DashboardContext); const [systemLoadData, setSystemLoadData] = useState(system_load || {}); - const [clusterList, setClusterList] = useState[]>( - [] - ); const chartData = useMemo(() => { const data = systemLoadData?.current || {}; @@ -53,22 +49,6 @@ const SystemLoad = () => { } }; - useEffect(() => { - const fetchClusters = async () => { - try { - const res = await queryClusterList({ page: -1 }); - const options = res.items.map((cluster: any) => ({ - label: cluster.name, - value: cluster.id - })); - setClusterList(options); - } catch (error) { - setClusterList([]); - } - }; - fetchClusters(); - }, []); - return (
diff --git a/src/pages/dashboard/config/dashboard-context.ts b/src/pages/dashboard/config/dashboard-context.ts index 8192e0e7..8942eb20 100644 --- a/src/pages/dashboard/config/dashboard-context.ts +++ b/src/pages/dashboard/config/dashboard-context.ts @@ -4,10 +4,18 @@ import { DashboardProps } from './types'; export const DashboardContext = createContext< DashboardProps & { fetchData: (params?: { [key: string]: any }) => Promise; + clusterList: Global.BaseOption< + number, + { provider: string; state: string | number } + >[]; } >( {} as DashboardProps & { fetchData: (params?: { [key: string]: any }) => Promise; + clusterList: Global.BaseOption< + number, + { provider: string; state: string | number } + >[]; } ); diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 747d8d60..74036c33 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -1,3 +1,6 @@ +import useClusterList from '@/pages/cluster-management/hooks/use-cluster-list'; +import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result'; +import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { Spin } from 'antd'; import { useEffect, useState } from 'react'; @@ -8,32 +11,70 @@ import DashboardContext from './config/dashboard-context'; import { DashboardProps } from './config/types'; const Dashboard: React.FC = () => { + const intl = useIntl(); const [loading, setLoading] = useState(false); const [data, setData] = useState({} as DashboardProps); + const { fetchAll, clusterList, workerList } = useClusterList(); - const getDashboardData = useMemoizedFn(async () => { + const { noResourceResult } = useNoResourceResult({ + loadend: true, + loading: false, + dataSource: clusterList.length > 0 ? workerList : [], + queryParams: {}, + iconType: 'icon-dashboard', + title: + clusterList.length > 0 + ? intl.formatMessage({ id: 'noresult.workers.title' }) + : intl.formatMessage({ id: 'noresult.cluster.title' }), + noClusters: !clusterList.length, + noWorkers: workerList.length === 0 && clusterList.length > 0, + defaultContent: { + subTitle: intl.formatMessage({ id: 'noresult.workers.subTitle' }), + noFoundText: intl.formatMessage({ id: 'noresult.workers.nofound' }), + buttonText: intl.formatMessage({ id: 'noresult.workers.button.add' }), + onClick: () => {} + } + }); + + const fetchDashboardData = useMemoizedFn(async () => { try { - setLoading(true); const res = await queryDashboardData(); setData(res); - setLoading(false); } catch (error) { - setLoading(false); setData({} as DashboardProps); } }); + const initData = useMemoizedFn(async () => { + try { + setLoading(true); + const { hasClusters, hasWorkers } = await fetchAll(); + if (!hasClusters || !hasWorkers) { + setLoading(false); + return; + } + await fetchDashboardData(); + setLoading(false); + } catch (error) { + setLoading(false); + } + }); + useEffect(() => { - getDashboardData(); + initData(); }, []); return ( - - + + {(!clusterList.length || !workerList.length) && !loading ? ( + noResourceResult + ) : loading ? null : ( + + )} diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index 84c5b86e..4c7a207c 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -83,7 +83,7 @@ const AddModal: React.FC = (props) => { handleOnValuesChange, warningStatus } = useCheckCompatibility(); - const { getClusterList, clusterList } = useFormInitialValues(); + const { getClusterList, getWorkerList, clusterList } = useFormInitialValues(); const intl = useIntl(); const form = useRef({}); const [isGGUF, setIsGGUF] = useState(false); @@ -295,6 +295,7 @@ const AddModal: React.FC = (props) => { useEffect(() => { getClusterList(); + getWorkerList(); }, []); useEffect(() => { diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 30381f72..79a3c78b 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -2,7 +2,6 @@ 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 IconFont from '@/components/icon-font'; import { PageSize } from '@/components/logs-viewer/config'; import PageTools from '@/components/page-tools'; import BaseSelect from '@/components/seal-form/base/select'; @@ -12,9 +11,8 @@ import { PageActionType } from '@/config/types'; import useBodyScroll from '@/hooks/use-body-scroll'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useTableRowSelection from '@/hooks/use-table-row-selection'; -import NoResult from '@/pages/_components/no-result'; import PageBox from '@/pages/_components/page-box'; -import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; +import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result'; import { handleBatchRequest } from '@/utils'; import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { useIntl, useNavigate } from '@umijs/max'; @@ -84,7 +82,6 @@ interface ModelsProps { categories?: string[]; }; deleteIds?: number[]; - workerList: WorkerListItem[]; dataSource: ListItem[]; loading: boolean; loadend: boolean; @@ -119,14 +116,18 @@ const Models: React.FC = ({ onStart, deleteIds, dataSource, - workerList, queryParams, loading, loadend, total }) => { - const { generateFormValues, clusterList, getClusterList } = - useFormInitialValues(); + const { + generateFormValues, + clusterList, + getClusterList, + getWorkerList, + workerList + } = useFormInitialValues(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const [updateFormInitials, setUpdateFormInitials] = useState<{ data: any; @@ -206,7 +207,7 @@ const Models: React.FC = ({ useEffect(() => { const getData = async () => { - await getClusterList(); + await Promise.all([getClusterList(), getWorkerList()]); }; getData(); return () => { @@ -594,6 +595,23 @@ const Models: React.FC = ({ }); }; + const { noResourceResult } = useNoResourceResult({ + loadend: loadend, + loading: loading, + dataSource: dataSource, + queryParams: queryParams, + iconType: 'icon-resources', + title: intl.formatMessage({ id: 'noresult.deployments.title' }), + noClusters: !clusterList.length, + noWorkers: workerList.length === 0 && clusterList.length > 0, + defaultContent: { + subTitle: intl.formatMessage({ id: 'noresult.deployments.subTitle' }), + noFoundText: intl.formatMessage({ id: 'noresult.mymodels.nofound' }), + buttonText: intl.formatMessage({ id: 'models.table.button.deploy' }), + onClick: () => handleClickDropdown({ key: 'catalog' }) + } + }); + useEffect(() => { if (modelsSession.source && loadend) { handleClickDropdown({ @@ -709,26 +727,7 @@ const Models: React.FC = ({ loadChildren={getModelInstances} loadChildrenAPI={generateChildrenRequestAPI} renderChildren={renderChildren} - empty={ - } - filters={queryParams} - noFoundText={intl.formatMessage({ - id: 'noresult.mymodels.nofound' - })} - title={intl.formatMessage({ id: 'noresult.deployments.title' })} - subTitle={intl.formatMessage({ - id: 'noresult.deployments.subTitle' - })} - onClick={() => handleClickDropdown({ key: 'catalog' })} - buttonText={intl?.formatMessage?.({ - id: 'models.table.button.deploy' - })} - > - } + empty={noResourceResult} pagination={{ showSizeChanger: true, pageSize: queryParams.perPage, diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index f5962342..dca99982 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -1,4 +1,4 @@ -import { clusterListAtom } from '@/atoms/models'; +import { clusterListAtom, workerListAtom } from '@/atoms/models'; import { createAxiosToken } from '@/hooks/use-chunk-request'; import { queryModelFilesList } from '@/pages/resources/apis'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; @@ -132,6 +132,7 @@ export const useCheckCompatibility = () => { const updateStatusTimer = useRef(null); const isLockWarningStatus = useRef(false); const clusterList = useAtomValue(clusterListAtom); + const workerList = useAtomValue(workerListAtom); const [warningStatus, setWarningStatus] = useState({ show: false, title: '', @@ -175,12 +176,15 @@ export const useCheckCompatibility = () => { try { // when no cluster selected, show warning and prompt user to add cluster first console.log('handleEvaluate', data); - if (!data.cluster_id) { + + if (!data.cluster_id || workerList.length === 0) { setWarningStatus({ show: true, title: '', type: 'warning', - message: intl.formatMessage({ id: 'noresult.resources.cluster' }) + message: !data.cluster_id + ? intl.formatMessage({ id: 'noresult.resources.cluster' }) + : intl.formatMessage({ id: 'noresult.resources.worker' }) }); return; } diff --git a/src/pages/llmodels/hooks/use-form-initial-values.ts b/src/pages/llmodels/hooks/use-form-initial-values.ts index 1a1c23e0..482d9773 100644 --- a/src/pages/llmodels/hooks/use-form-initial-values.ts +++ b/src/pages/llmodels/hooks/use-form-initial-values.ts @@ -1,4 +1,4 @@ -import { clusterListAtom } from '@/atoms/models'; +import { clusterListAtom, workerListAtom } from '@/atoms/models'; import { queryClusterList } from '@/pages/cluster-management/apis'; import { ClusterListItem } from '@/pages/cluster-management/config/types'; import { queryWorkersList } from '@/pages/resources/apis'; @@ -232,11 +232,14 @@ export const useGenerateWorkerOptions = () => { export default function useFormInitialValues() { const { getGPUOptionList, gpuOptions } = useGenerateGPUOptions(); const [, setClusterListAtom] = useAtom(clusterListAtom); + const [, setWorkerListAtom] = useAtom(workerListAtom); const [clusterList, setClusterList] = useState< Global.BaseOption[] >([]); + const [workerList, setWorkerList] = useState([]); + const getClusterList = async (): Promise[]> => { try { const response = await queryClusterList({ @@ -259,6 +262,26 @@ export default function useFormInitialValues() { } }; + // get worker list + const getWorkerList = async (): Promise => { + try { + const data = await queryWorkersList({ page: -1 }); + const list = + data.items?.map((item) => ({ + label: item.name, + value: item.id + })) || []; + setWorkerList(data.items); + setWorkerListAtom(list); + return data; + } catch (error) { + // ingore + setWorkerList([]); + setWorkerListAtom([]); + return {}; + } + }; + /** * before set the form initial values, generate the form values * @param data @@ -280,6 +303,8 @@ export default function useFormInitialValues() { getGPUOptionList, generateFormValues, getClusterList, + getWorkerList, + workerList, clusterList, gpuOptions }; diff --git a/src/pages/llmodels/hooks/use-no-resource-result.tsx b/src/pages/llmodels/hooks/use-no-resource-result.tsx index 3f685c73..9a7f7140 100644 --- a/src/pages/llmodels/hooks/use-no-resource-result.tsx +++ b/src/pages/llmodels/hooks/use-no-resource-result.tsx @@ -7,6 +7,12 @@ import { useMemoizedFn } from 'ahooks'; import { useAtom } from 'jotai'; import React, { useMemo } from 'react'; +/**Title: Generally, this is from the activation page. + * DefaultContent: This content from the activate page, for example, the activate page is Deployments page, + * then the defaultContent is for deployment. + * @param props + * @returns + */ const useNoResourceResult = (props: { iconType: string; loading?: boolean; @@ -84,6 +90,7 @@ const useNoResourceResult = (props: { onClick: handleClick }; } + return { ...defaultContent }; diff --git a/src/pages/llmodels/index.tsx b/src/pages/llmodels/index.tsx index 5a0dcda0..b19c4068 100644 --- a/src/pages/llmodels/index.tsx +++ b/src/pages/llmodels/index.tsx @@ -1,8 +1,6 @@ import TableContext from '@/components/seal-table/table-context'; import useSetChunkRequest from '@/hooks/use-chunk-request'; import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; -import { queryWorkersList } from '@/pages/resources/apis'; -import { ListItem as WokerListItem } from '@/pages/resources/config/types'; import { useMemoizedFn } from 'ahooks'; import _ from 'lodash'; import qs from 'query-string'; @@ -35,7 +33,6 @@ const Models: React.FC = () => { total: 0 }); - const [workerList, setWorkerList] = useState([]); const chunkRequedtRef = useRef(); const chunkInstanceRequedtRef = useRef(); const isPageHidden = useRef(false); @@ -320,22 +317,8 @@ const Models: React.FC = () => { } }; - // get worker list - const getWorkerList = async (): Promise => { - try { - const data = await queryWorkersList({ page: -1 }); - return data; - } catch (error) { - // ingore - return {}; - } - }; - const init = async () => { - const [modelRes, workerRes] = await Promise.all([ - getTableData(), - getWorkerList() - ]); + const [modelRes] = await Promise.all([getTableData()]); setDataSource({ dataList: modelRes.items || [], @@ -344,7 +327,6 @@ const Models: React.FC = () => { total: modelRes.pagination?.total || 0, deletedIds: [] }); - setWorkerList(workerRes.items || []); clearTimeout(timer); timer = setTimeout(() => { @@ -420,7 +402,6 @@ const Models: React.FC = () => { loadend={dataSource.loadend} total={dataSource.total} deleteIds={dataSource.deletedIds} - workerList={workerList} > );