style: subdrawer width

This commit is contained in:
jialin
2026-05-27 12:14:15 +08:00
committed by jialin
parent 1878753cde
commit 7287f8c81b
10 changed files with 176 additions and 68 deletions
-4
View File
@@ -4,7 +4,3 @@ import { atom } from 'jotai';
export const currentClusterAtom = atom<
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
>(null);
export const addSSHKeyPageAtom = atom<{ create: boolean }>({
create: false
});
@@ -14,7 +14,7 @@ export const InstanceStatusValueMap = {
Preparing: 'Preparing',
NotReady: 'NotReady',
Ready: 'Ready',
Staring: 'Staring'
Starting: 'Starting'
};
export const InstanceStatusLabelMap: Record<string, string> = {
@@ -27,7 +27,7 @@ export const InstanceStatusLabelMap: Record<string, string> = {
[InstanceStatusValueMap.Preparing]: 'Preparing',
[InstanceStatusValueMap.NotReady]: 'NotReady',
[InstanceStatusValueMap.Ready]: 'Ready',
[InstanceStatusValueMap.Staring]: 'Staring'
[InstanceStatusValueMap.Starting]: 'Starting'
};
export const status: Record<string, StatusType> = {
@@ -40,7 +40,7 @@ export const status: Record<string, StatusType> = {
[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
+53 -48
View File
@@ -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<InstanceFormProps> = forwardRef(
onFinish
} = props;
const intl = useIntl();
const navigate = useNavigate();
const setAddSSHKeyPage = useSetAtom(addSSHKeyPageAtom);
const { getRuleMessage } = useAppUtils();
const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null);
@@ -133,6 +132,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = 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<InstanceFormProps> = 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<InstanceFormProps> = forwardRef(
]}
/>
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
<Flex align="center" justify="space-between">
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
})}
></CheckboxField>
</Form.Item>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
></CheckboxField>
</Form.Item>
</Button>
</Flex>
{
<div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData>
@@ -515,38 +542,16 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}
}}
options={sshkeyOptions}
notFoundContent={
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={intl.formatMessage({
id: 'noresult.gpuservice.sshkey.title'
})}
styles={{
image: {
height: 32
},
root: {
margin: 0
}
}}
>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
</Button>
</Empty>
}
></MultipleSelect>
</Form.Item>
</div>
}
</Form>
<PublicKeyOverlay
open={sshOverlayOpen}
onCancel={() => setSshOverlayOpen(false)}
onSubmit={handleCreateSSHKey}
/>
</ScrollSpyTabs>
);
}
@@ -162,7 +162,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
]}
>
<NumberSelection
min={0}
min={1}
onChange={handleOnGPUCountChange}
max={maxGpuCount}
step={1}
@@ -0,0 +1,77 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useRef, useState } from 'react';
import { FormData as PublicKeyFormData } from '../../public-keys/config/types';
import GPUServicePublicKeyForm from '../../public-keys/forms';
import useCreateSshkey from '../../public-keys/services/use-create-sshkey';
import useOverlayLayout from '../hooks/use-overlay-layout';
interface PublicKeyOverlayProps {
open: boolean;
onCancel: () => void;
onSubmit: (values: PublicKeyFormData) => Promise<void> | void;
}
const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
open,
onCancel,
onSubmit
}) => {
const intl = useIntl();
const [loading, setLoading] = useState(false);
const formRef = useRef<any>(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 (
<FormOverlayView
title={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
open={open}
width={drawerWidth}
onCancel={handleCancel}
getContainer={getOverlayContainer}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
loading={loading}
style={{
padding: '16px 24px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServicePublicKeyForm
ref={formRef}
action={PageAction.CREATE}
open={open}
onFinish={handleFinish}
/>
</FormOverlayView>
);
};
export default PublicKeyOverlay;
@@ -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<StorageOverlayProps> = ({
useQueryStorageClass();
const [loading, setLoading] = useState(false);
const formRef = useRef<any>(null);
const { drawerWidth, getOverlayContainer } = useOverlayLayout(open);
useEffect(() => {
if (open) {
@@ -49,19 +51,11 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
}
};
const getOverlayContainer = useCallback(() => {
const containers = document.querySelectorAll<HTMLElement>(
'.ant-layout-content'
);
return containers[containers.length - 1] ?? null;
}, []);
return (
<FormOverlayView
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open}
width={600}
width={drawerWidth}
onCancel={handleCancel}
getContainer={getOverlayContainer}
footer={
@@ -1,4 +1,5 @@
import { PageAction } from '@/config';
import { PlusOutlined } from '@ant-design/icons';
import {
Input as CInput,
InputNumber as CInputNumber,
@@ -135,6 +136,7 @@ const StorageVolume = ({
<Button
type="link"
size="small"
icon={<PlusOutlined />}
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
@@ -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<ListItem> => {
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) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{text || record.displayName}</span>
<span className="text-primary">{record.displayName || text}</span>
</AutoTooltip>
)
},
@@ -247,6 +248,7 @@ const useInstancesColumns = ({
{
title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: 'clusterId',
hidden: !access.canSeeAdmin,
render: (id: number) => (
<AutoTooltip ghost maxWidth={240}>
{_.find(clusterList, { value: id })?.label || id || '-'}
@@ -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<number | string>(
FALLBACK_WIDTH
);
useEffect(() => {
if (!open) return;
const formWrapper = document.querySelector<HTMLElement>(
`.${instanceStyles.formWrapper}`
);
if (formWrapper) {
setDrawerWidth(formWrapper.clientWidth);
}
}, [open]);
const getOverlayContainer = useCallback(() => {
const containers = document.querySelectorAll<HTMLElement>(
'.ant-layout-content'
);
return containers[containers.length - 1] ?? null;
}, []);
return { drawerWidth, getOverlayContainer };
};
export default useOverlayLayout;
@@ -67,6 +67,7 @@ const usePublicKeyColumns = ({
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
width: 200,
render: (_text, record) => (
<DropdownButtons
items={rowActionList}