diff --git a/src/atoms/gpuservice.ts b/src/atoms/gpuservice.ts index 2b33c050..f7f9df55 100644 --- a/src/atoms/gpuservice.ts +++ b/src/atoms/gpuservice.ts @@ -4,7 +4,3 @@ import { atom } from 'jotai'; export const currentClusterAtom = atom< (Partial & { label?: string; value?: number }) | null >(null); - -export const addSSHKeyPageAtom = atom<{ create: boolean }>({ - create: false -}); diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts index 82aa0735..5781142e 100644 --- a/src/pages/gpu-service/instances/config/index.ts +++ b/src/pages/gpu-service/instances/config/index.ts @@ -14,7 +14,7 @@ export const InstanceStatusValueMap = { Preparing: 'Preparing', NotReady: 'NotReady', Ready: 'Ready', - Staring: 'Staring' + Starting: 'Starting' }; export const InstanceStatusLabelMap: Record = { @@ -27,7 +27,7 @@ export const InstanceStatusLabelMap: Record = { [InstanceStatusValueMap.Preparing]: 'Preparing', [InstanceStatusValueMap.NotReady]: 'NotReady', [InstanceStatusValueMap.Ready]: 'Ready', - [InstanceStatusValueMap.Staring]: 'Staring' + [InstanceStatusValueMap.Starting]: 'Starting' }; export const status: Record = { @@ -40,7 +40,7 @@ export const status: Record = { [InstanceStatusValueMap.Preparing]: StatusMaps.transitioning, [InstanceStatusValueMap.NotReady]: StatusMaps.error, [InstanceStatusValueMap.Ready]: StatusMaps.success, - [InstanceStatusValueMap.Staring]: StatusMaps.transitioning + [InstanceStatusValueMap.Starting]: StatusMaps.transitioning }; // Lifecycle actions (logs/events/start/stop) require K8s proxy endpoints that diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 477d8b28..b972954a 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -1,4 +1,3 @@ -import { addSSHKeyPageAtom } from '@/atoms/gpuservice'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { PlusOutlined } from '@ant-design/icons'; @@ -14,9 +13,8 @@ import { useScrollActiveChange, useWrapperContext } from '@gpustack/core-ui'; -import { useIntl, useNavigate } from '@umijs/max'; -import { Button, Empty, Form } from 'antd'; -import { useSetAtom } from 'jotai'; +import { useIntl } from '@umijs/max'; +import { Button, Flex, Form } from 'antd'; import _ from 'lodash'; import { forwardRef, @@ -26,6 +24,8 @@ import { useRef, useState } from 'react'; +import { FormData as PublicKeyFormData } from '../../public-keys/config/types'; +import useCreateSshkey from '../../public-keys/services/use-create-sshkey'; import useQuerySshkeys from '../../public-keys/services/use-query-sshkeys'; import { DefaultImagePullPolicy } from '../../templates/config'; import TemplateBasicForm, { @@ -36,6 +36,7 @@ import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import instanceStyles from '../styles/instances.module.less'; import Basic from './basic'; import InstanceTypeFormItem from './instance-type'; +import PublicKeyOverlay from './public-key-overlay'; import StorageVolume from './storage-volume'; const SSH_PORT = 22; @@ -124,8 +125,6 @@ const GPUServiceInstanceForm: React.FC = forwardRef( onFinish } = props; const intl = useIntl(); - const navigate = useNavigate(); - const setAddSSHKeyPage = useSetAtom(addSSHKeyPageAtom); const { getRuleMessage } = useAppUtils(); const [form] = Form.useForm(); const scrollTabsRef = useRef(null); @@ -133,6 +132,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( realAction === PageAction.CREATE ? PageAction.CREATE : action; const sshEnabled = Form.useWatch('enable_ssh', form); const { sshkeyOptions, fetchData: fetchSSHData } = useQuerySshkeys(); + const { fetchData: createSshkey } = useCreateSshkey(); + const [sshOverlayOpen, setSshOverlayOpen] = useState(false); const { getScrollElementScrollableHeight } = useWrapperContext(); const { activeKey, @@ -352,9 +353,23 @@ const GPUServiceInstanceForm: React.FC = forwardRef( })); const handleAddSSHKey = () => { - setAddSSHKeyPage({ create: true }); - // to go ssh key page - navigate('/gpu-service/public-keys'); + setSshOverlayOpen(true); + }; + + const handleCreateSSHKey = async (values: PublicKeyFormData) => { + try { + await fetchSSHData({ page: -1 }); + const current: Array<{ name: string } | string> = + form.getFieldValue(['spec', 'sshPublicKeys']) || []; + form.setFieldValue( + ['spec', 'sshPublicKeys'], + [...current, { name: values.name }] + ); + + setSshOverlayOpen(false); + } catch (error) { + // it's handled in interceptor + } }; const handleOnEnableSSHChange = (e: any) => { @@ -463,19 +478,31 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ]} /> - - name="enable_ssh" - valuePropName="checked" - style={{ marginBottom: 8 }} - > - + + name="enable_ssh" + valuePropName="checked" + style={{ marginBottom: 8 }} + > + + + + {
@@ -515,38 +542,16 @@ const GPUServiceInstanceForm: React.FC = forwardRef( } }} options={sshkeyOptions} - notFoundContent={ - - - - } >
} + setSshOverlayOpen(false)} + onSubmit={handleCreateSSHKey} + /> ); } diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 3b1d7f37..c963655e 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -162,7 +162,7 @@ const InstanceTypeFormItem: React.FC = ({ ]} > void; + onSubmit: (values: PublicKeyFormData) => Promise | void; +} + +const PublicKeyOverlay: React.FC = ({ + open, + onCancel, + onSubmit +}) => { + const intl = useIntl(); + const [loading, setLoading] = useState(false); + const formRef = useRef(null); + const { fetchData: createSshkey } = useCreateSshkey(); + const { drawerWidth, getOverlayContainer } = useOverlayLayout(open); + + const handleSubmit = () => { + formRef.current?.submit(); + }; + + const handleCancel = () => { + formRef.current?.resetFields(); + onCancel(); + }; + + const handleFinish = async (values: PublicKeyFormData) => { + setLoading(true); + try { + await createSshkey({ data: values }); + await onSubmit(values); + } finally { + setLoading(false); + } + }; + + return ( + + } + > + + + ); +}; + +export default PublicKeyOverlay; diff --git a/src/pages/gpu-service/instances/forms/storage-overlay.tsx b/src/pages/gpu-service/instances/forms/storage-overlay.tsx index a6bb897d..c889612f 100644 --- a/src/pages/gpu-service/instances/forms/storage-overlay.tsx +++ b/src/pages/gpu-service/instances/forms/storage-overlay.tsx @@ -4,9 +4,10 @@ import { FormContext } from '@/pages/gpu-service/storage/config/form-context'; import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class'; import { ModalFooter } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { FormData as StorageFormData } from '../../storage/config/types'; import GPUServiceStorageForm from '../../storage/forms'; +import useOverlayLayout from '../hooks/use-overlay-layout'; interface StorageOverlayProps { open: boolean; @@ -24,6 +25,7 @@ const StorageOverlay: React.FC = ({ useQueryStorageClass(); const [loading, setLoading] = useState(false); const formRef = useRef(null); + const { drawerWidth, getOverlayContainer } = useOverlayLayout(open); useEffect(() => { if (open) { @@ -49,19 +51,11 @@ const StorageOverlay: React.FC = ({ } }; - const getOverlayContainer = useCallback(() => { - const containers = document.querySelectorAll( - '.ant-layout-content' - ); - - return containers[containers.length - 1] ?? null; - }, []); - return ( } onClick={() => setOverlayOpen(true)} > {intl.formatMessage({ id: 'gpuservice.storage.add' })} diff --git a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx index e0e2bd00..9f080289 100644 --- a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx +++ b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx @@ -5,7 +5,7 @@ import { DropdownButtons, StatusTag } from '@gpustack/core-ui'; -import { useIntl } from '@umijs/max'; +import { useAccess, useIntl } from '@umijs/max'; import { Button, Flex } from 'antd'; import type { ColumnsType } from 'antd/lib/table'; import dayjs from 'dayjs'; @@ -80,6 +80,7 @@ const useInstancesColumns = ({ sortOrder }: ColumnsHookProps): ColumnsType => { const intl = useIntl(); + const access = useAccess(); const renderInstanceType = (record: ListItem) => { const description = JSON.parse(record?.description || '{}').spec || {}; @@ -122,7 +123,7 @@ const useInstancesColumns = ({ }, render: (text: string, record: ListItem) => ( - {text || record.displayName} + {record.displayName || text} ) }, @@ -247,6 +248,7 @@ const useInstancesColumns = ({ { title: intl.formatMessage({ id: 'clusters.title' }), dataIndex: 'clusterId', + hidden: !access.canSeeAdmin, render: (id: number) => ( {_.find(clusterList, { value: id })?.label || id || '-'} diff --git a/src/pages/gpu-service/instances/hooks/use-overlay-layout.ts b/src/pages/gpu-service/instances/hooks/use-overlay-layout.ts new file mode 100644 index 00000000..1a93dc2f --- /dev/null +++ b/src/pages/gpu-service/instances/hooks/use-overlay-layout.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useState } from 'react'; +import instanceStyles from '../styles/instances.module.less'; + +const FALLBACK_WIDTH = `calc((100vw - 220px) / 3 + 16px)`; + +const useOverlayLayout = (open: boolean) => { + const [drawerWidth, setDrawerWidth] = useState( + FALLBACK_WIDTH + ); + + useEffect(() => { + if (!open) return; + const formWrapper = document.querySelector( + `.${instanceStyles.formWrapper}` + ); + if (formWrapper) { + setDrawerWidth(formWrapper.clientWidth); + } + }, [open]); + + const getOverlayContainer = useCallback(() => { + const containers = document.querySelectorAll( + '.ant-layout-content' + ); + return containers[containers.length - 1] ?? null; + }, []); + + return { drawerWidth, getOverlayContainer }; +}; + +export default useOverlayLayout; diff --git a/src/pages/gpu-service/public-keys/hooks/use-public-key-columns.tsx b/src/pages/gpu-service/public-keys/hooks/use-public-key-columns.tsx index 06cee93d..2de8aa5c 100644 --- a/src/pages/gpu-service/public-keys/hooks/use-public-key-columns.tsx +++ b/src/pages/gpu-service/public-keys/hooks/use-public-key-columns.tsx @@ -67,6 +67,7 @@ const usePublicKeyColumns = ({ title: intl.formatMessage({ id: 'common.table.operation' }), key: 'operation', dataIndex: 'operation', + width: 200, render: (_text, record) => (