chore: get started for add cluster

This commit is contained in:
jialin
2025-11-27 11:35:45 +08:00
parent 97601280d1
commit 962df859f1
26 changed files with 385 additions and 64 deletions
@@ -1,7 +1,9 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import PageBreadcrumb from '@/pages/_components/page-breadcrumb';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useAtom } from 'jotai';
import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
@@ -39,6 +41,7 @@ const ClusterCreate = () => {
const action =
(searchParams.get('action') as PageActionType) || PageAction.CREATE;
const navigate = useNavigate();
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const [credentialList, setCredentialList] = useState<
Global.BaseOption<number, { provider: ProviderType }>[]
>([]);
@@ -253,6 +256,14 @@ const ClusterCreate = () => {
};
const handleCancel = () => {
if (clusterSession?.firstAddCluster) {
setClusterSession({
firstAddCluster: false,
firstAddWorker: false
});
navigate(`/cluster-management/clusters/list`);
return;
}
navigate(-1);
};
+28 -2
View File
@@ -1,4 +1,4 @@
import { expandKeysAtom } from '@/atoms/clusters';
import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
@@ -33,7 +33,11 @@ import {
K8sStepsFromCluter
} from './components/add-worker/config';
import PoolRows from './components/pool-rows';
import { ProviderType, ProviderValueMap } from './config';
import {
ClusterStatusValueMap,
ProviderType,
ProviderValueMap
} from './config';
import {
ClusterListItem,
CredentialListItem,
@@ -65,6 +69,7 @@ const Clusters: React.FC = () => {
});
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
const [expandAtom] = useAtom(expandKeysAtom);
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom);
const navigate = useNavigate();
@@ -241,6 +246,27 @@ const Clusters: React.FC = () => {
fetchCredentialList();
}, []);
useEffect(() => {
if (
clusterSession?.firstAddWorker &&
dataSource.loadend &&
dataSource.dataList?.length > 0
) {
const targetCluster = dataSource.dataList.find(
(cluster) =>
cluster.state === ClusterStatusValueMap.Ready &&
!cluster.workers &&
!cluster.worker_pools?.length
);
if (targetCluster) {
handleAddWorker(targetCluster);
// reset session
setClusterSession(null);
}
}
}, [clusterSession, dataSource.loadend, dataSource.dataList]);
const renderChildren = (
list: any,
options: { parent?: any; [key: string]: any }
@@ -4,8 +4,9 @@ import SealInput from '@/components/seal-form/seal-input';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils';
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { Form, Typography } from 'antd';
import React, { useEffect } from 'react';
import { ProviderType, ProviderValueMap } from '../config';
import {
@@ -100,8 +101,21 @@ const AddModal: React.FC<AddModalProps> = ({
]}
>
<SealInput.Password
label={intl.formatMessage({ id: 'clusters.credential.token' })}
label={intl.formatMessage({
id: 'clusters.credential.token'
})}
required={action === PageAction.CREATE}
labelExtra={
<Typography.Link
href="https://cloud.digitalocean.com/account/api/tokens"
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 8, lineHeight: 1 }}
>
{intl.formatMessage({ id: 'clusters.button.genToken' })}{' '}
<ExportOutlined style={{ fontSize: 12 }} />
</Typography.Link>
}
></SealInput.Password>
</Form.Item>
</>
@@ -1,4 +1,5 @@
import AlertBlockInfo from '@/components/alert-info/block';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import TransferInner from '@/pages/_components/transfer';
@@ -29,7 +30,34 @@ import { AccessControlFormData, ListItem } from '../../config/types';
type TransferKey = string | number | bigint;
const accessScopeTips = [
{
title: {
text: 'models.accessSettings.authed',
locale: true
},
tips: 'models.accessSettings.authed.tips'
},
{
title: {
text: 'models.accessSettings.allowedUsers',
locale: true
},
tips: 'models.accessSettings.allowedUsers.tips'
},
{
title: {
text: 'models.accessSettings.public',
locale: true
},
tips: 'models.accessSettings.public.desc'
}
];
const Label = styled.div`
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
margin-block: 8px 12px;
font-size: 14px;
@@ -263,7 +291,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
access_policy: action === PageAction.CREATE ? 'authed' : undefined
}}
>
<Label>{intl.formatMessage({ id: 'models.table.accessScope' })}</Label>
<Label>
{intl.formatMessage({ id: 'models.table.accessScope' })}
<Tooltip title={<TooltipList list={accessScopeTips}></TooltipList>}>
<QuestionCircleOutlined />
</Tooltip>
</Label>
<Form.Item<AccessControlFormData> name="access_policy" noStyle>
<Radio.Group
onChange={handleOnPolicyChange}
@@ -307,7 +341,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
id: 'models.table.userSelection.tips'
})}
>
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
<QuestionCircleOutlined />
</Tooltip>
</Label>
<Form.Item<AccessControlFormData> name="users">
@@ -110,7 +110,6 @@ const AddModal: FC<AddModalProps> = (props) => {
const { checkOnlyAscendNPU } = useCheckBackend();
const {
handleShowCompatibleAlert,
setWarningStatus,
handleBackendChangeBefore,
cancelEvaluate,
@@ -429,6 +429,7 @@ const Models: React.FC<ModelsProps> = ({
data: row
});
}, []);
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
try {
if (val === 'edit') {
+27 -2
View File
@@ -1,14 +1,16 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { modelNameReg } from '@/config';
import { modelNameReg, PageAction } from '@/config';
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import useAppUtils from '@/hooks/use-app-utils';
import {
ClusterStatusLabelMap,
ClusterStatusValueMap
} from '@/pages/cluster-management/config';
import { useIntl } from '@umijs/max';
import { Link, useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useAtom } from 'jotai';
import { useMemo } from 'react';
import { sourceOptions } from '../config';
import { FormData } from '../config/types';
@@ -28,6 +30,26 @@ interface BasicFormProps {
onSourceChange?: (value: string) => void;
}
const NotFoundCredentialContent: React.FC = () => {
const [, setFromClusterCreation] = useAtom(clusterSessionAtom);
const intl = useIntl();
const handleOnClick = () => {
setFromClusterCreation({
firstAddWorker: false,
firstAddCluster: true
});
};
return (
<Link
to={`/cluster-management/clusters/create?action=${PageAction.CREATE}`}
onClick={handleOnClick}
>
{intl.formatMessage({ id: 'noresult.resources.gotocluster' })}
</Link>
);
};
const BasicForm: React.FC<BasicFormProps> = (props) => {
const {
fields = [],
@@ -118,6 +140,9 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
>
{
<SealSelect
notFoundContent={
<NotFoundCredentialContent></NotFoundCredentialContent>
}
onChange={handleClusterChange}
label={intl.formatMessage({ id: 'clusters.title' })}
options={clusterOptions}
+11
View File
@@ -173,6 +173,16 @@ export const useCheckCompatibility = () => {
const handleEvaluate = async (data: any) => {
try {
// when no cluster selected, show warning and prompt user to add cluster first
if (!data.cluster_id) {
setWarningStatus({
show: true,
title: '',
type: 'warning',
message: intl.formatMessage({ id: 'noresult.resources.cluster' })
});
return;
}
checkTokenRef.current?.cancel();
checkTokenRef.current = createAxiosToken();
setWarningStatus({
@@ -407,6 +417,7 @@ export const useCheckCompatibility = () => {
return;
}
// when custom backend, and no run_command or image_name, skip evaluate
if (
backendOptionsMap.custom === allValues.backend &&
(!allValues.run_command || !allValues.image_name)
@@ -0,0 +1,113 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import IconFont from '@/components/icon-font';
import { PageAction } from '@/config';
import NoResult from '@/pages/_components/no-result';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button } from 'antd';
import { useAtom } from 'jotai';
import React, { useMemo } from 'react';
const useNoResourceResult = (props: {
iconType: string;
loading?: boolean;
loadend?: boolean;
dataSource?: any[];
queryParams?: Record<string, any>;
title: React.ReactNode;
noClusters?: boolean;
noWorkers?: boolean;
subTitle?: React.ReactNode;
defaultContent?: {
subTitle: string;
noFoundText: string;
buttonText: string;
onClick: () => void;
};
}) => {
const intl = useIntl();
const navigate = useNavigate();
const {
noClusters,
noWorkers,
defaultContent,
loading,
loadend,
dataSource,
queryParams,
iconType,
title,
subTitle
} = props;
const [, setClusterSession] = useAtom(clusterSessionAtom);
const handleClick = useMemoizedFn(() => {
if (noClusters) {
setClusterSession({
firstAddWorker: false,
firstAddCluster: true
});
navigate(
`/cluster-management/clusters/create?action=${PageAction.CREATE}`
);
}
if (noWorkers) {
setClusterSession({
firstAddWorker: true,
firstAddCluster: false
});
navigate(`/cluster-management/clusters/list`);
}
});
const statusContent = useMemo(() => {
if (noClusters) {
return {
subTitle:
subTitle || intl.formatMessage({ id: 'noresult.resources.cluster' }),
noFoundText: defaultContent?.noFoundText || '',
buttonText: intl.formatMessage({
id: 'noresult.resources.gotocluster'
}),
onClick: handleClick
};
}
if (noWorkers) {
return {
subTitle:
subTitle || intl.formatMessage({ id: 'noresult.resources.worker' }),
noFoundText: defaultContent?.noFoundText || '',
buttonText: intl.formatMessage({ id: 'noresult.resources.gotoworker' }),
onClick: handleClick
};
}
return {
...defaultContent
};
}, [noClusters, noWorkers, intl]);
const noResourceResult = (
<NoResult
loading={loading}
loadend={loadend}
dataSource={dataSource}
image={<IconFont type={iconType} />}
filters={queryParams}
noFoundText={statusContent.noFoundText}
title={title}
subTitle={statusContent.subTitle}
>
<Button type="primary" iconPosition="end" onClick={statusContent.onClick}>
{statusContent.buttonText}
</Button>
</NoResult>
);
return {
noResourceResult
};
};
export default useNoResourceResult;
+9 -5
View File
@@ -156,11 +156,15 @@ const Playground: React.FC = () => {
activeKey={activeKey}
key="view-code-buttons"
/>,
<Divider
key="divider"
type="vertical"
style={{ height: 24, marginInline: 16 }}
/>,
<>
{activeKey === 'chat' && (
<Divider
key="divider"
type="vertical"
style={{ height: 24, marginInline: 16 }}
/>
)}
</>,
<ExtraContent key="extra-content" />
]}
>
+20 -19
View File
@@ -1,5 +1,4 @@
import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools';
import useTableFetch from '@/hooks/use-table-fetch';
import PageBox from '@/pages/_components/page-box';
@@ -11,11 +10,11 @@ import {
} from '@/pages/cluster-management/config';
import { ClusterListItem } from '@/pages/cluster-management/config/types';
import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker';
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, Table, message } from 'antd';
import { ConfigProvider, Table, message } from 'antd';
import React, { useEffect, useState } from 'react';
import NoResult from '../../_components/no-result';
import {
WORKERS_API,
deleteWorker,
@@ -191,24 +190,26 @@ const Workers: React.FC = () => {
handleAddWorker(currentData as ClusterListItem);
};
const { noResourceResult } = useNoResourceResult({
loadend: dataSource.loadend,
loading: dataSource.loading,
dataSource: dataSource.dataList,
queryParams: queryParams,
iconType: 'icon-resources',
title: intl.formatMessage({ id: 'noresult.workers.title' }),
noClusters: !clusterData.list.length,
noWorkers: dataSource.dataList.length === 0 && clusterData.list.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: handleOnAddWorker
}
});
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-resources" />}
filters={queryParams}
noFoundText={intl.formatMessage({ id: 'noresult.workers.nofound' })}
title={intl.formatMessage({ id: 'noresult.workers.title' })}
subTitle={intl.formatMessage({ id: 'noresult.workers.subTitle' })}
>
<Button type="primary" onClick={handleOnAddWorker}>
{intl.formatMessage({ id: 'noresult.workers.button.add' })}
</Button>
</NoResult>
);
return noResourceResult;
};
const handleClusterChange = (value: number) => {