From 95ffe0350eca46949580d9bb18077e092e3283f5 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 1 Jun 2026 21:00:40 +0800 Subject: [PATCH] fix(style): k8s form --- package.json | 2 +- src/locales/zh-CN/menu.ts | 2 +- src/pages/backends/forms/versions-config.tsx | 4 +- .../components/cluster-form.tsx | 189 ++++++++++-------- .../components/credential-item.tsx | 81 ++++++++ .../components/image-credential.tsx | 98 +++++++++ .../components/k8s-pod-spec.tsx | 151 +------------- .../components/k8s-volume-mount.tsx | 4 +- .../cluster-management/config/form-context.ts | 12 ++ .../styles/img-credential.less | 11 + .../gpu-service/public-keys/forms/basic.tsx | 1 + .../components/download/target-form.tsx | 22 +- 12 files changed, 328 insertions(+), 249 deletions(-) create mode 100644 src/pages/cluster-management/components/credential-item.tsx create mode 100644 src/pages/cluster-management/components/image-credential.tsx create mode 100644 src/pages/cluster-management/config/form-context.ts create mode 100644 src/pages/cluster-management/styles/img-credential.less diff --git a/package.json b/package.json index cf9e3310..7adc191b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@ant-design/pro-components": "3.1.0-0", "@antv/g6": "^5.0.51", "@braintree/sanitize-url": "^7.1.1", - "@gpustack/core-ui": "^1.0.20", + "@gpustack/core-ui": "^1.0.21", "@huggingface/gguf": "^0.1.7", "@huggingface/hub": "^0.15.1", "@huggingface/tasks": "^0.11.6", diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index c7994bd6..fe2f9129 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -30,7 +30,7 @@ export default { 'menu.usage': '使用量', 'menu.404': '404', 'menu.resources.workers': '节点', - 'menu.resources.gpus': 'GPUs', + 'menu.resources.gpus': 'GPU', 'menu.models.modelfiles': '模型文件', 'menu.accessControl': '访问控制', 'menu.accessControl.apikeys': 'API 密钥', diff --git a/src/pages/backends/forms/versions-config.tsx b/src/pages/backends/forms/versions-config.tsx index 27683788..9501a493 100644 --- a/src/pages/backends/forms/versions-config.tsx +++ b/src/pages/backends/forms/versions-config.tsx @@ -276,7 +276,9 @@ const VersionsForm: React.FC = ({ key={name} defaultOpen styles={{ - body: collapseKey.has(name) ? { padding: 16 } : {}, + body: collapseKey.has(name) + ? { paddingInline: 16, paddingBlock: '16px 0' } + : {}, content: { paddingTop: 0 }, header: { backgroundColor: 'unset' diff --git a/src/pages/cluster-management/components/cluster-form.tsx b/src/pages/cluster-management/components/cluster-form.tsx index b6e62763..07782374 100644 --- a/src/pages/cluster-management/components/cluster-form.tsx +++ b/src/pages/cluster-management/components/cluster-form.tsx @@ -11,8 +11,14 @@ import { import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import { useAtomValue } from 'jotai'; -import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useState +} from 'react'; import { ProviderType, ProviderValueMap } from '../config'; +import { FormContext } from '../config/form-context'; import { ClusterFormData as FormData, ClusterListItem as ListItem @@ -37,6 +43,7 @@ const ClusterForm: React.FC = forwardRef( const [k8sActiveKey, setK8sActiveKey] = React.useState([ 'k8sOptions' ]); + const [submitAttempted, setSubmitAttempted] = useState(false); const advanceConfigRef = React.useRef(null); const systemConfig = useAtomValue(systemConfigAtom); @@ -171,7 +178,12 @@ const ClusterForm: React.FC = forwardRef( }; }, validateFields: async () => { - await form.validateFields(); + try { + await form.validateFields(); + } catch (e) { + setSubmitAttempted(true); + throw e; + } const values = form.getFieldsValue(true); const workerConfig = yaml2Json( @@ -187,103 +199,112 @@ const ClusterForm: React.FC = forwardRef( } })); + const handleOnFinishFailed = () => { + setSubmitAttempted(true); + }; + return ( -
- - name="name" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, + + + + name="name" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: intl.formatMessage({ id: 'common.table.name' }) + } + ) + } + ]} + > + + + + {provider === ProviderValueMap.DigitalOcean && ( + + )} + + + name="description" + rules={[{ required: false }]} + style={{ marginBottom: 8 }} + > + + + + {provider === ProviderValueMap.Kubernetes && ( + + setK8sActiveKey(Array.isArray(keys) ? keys : [keys]) + } + items={[ { - name: intl.formatMessage({ id: 'common.table.name' }) + key: 'k8sOptions', + label: intl.formatMessage({ + id: 'clusters.k8sOptions.title' + }), + forceRender: true, + children: ( + + ) } - ) - } - ]} - > - - - - {provider === ProviderValueMap.DigitalOcean && ( - - )} + ]} + > + )} - - name="description" - rules={[{ required: false }]} - style={{ marginBottom: 8 }} - > - - - - {provider === ProviderValueMap.Kubernetes && ( - setK8sActiveKey(Array.isArray(keys) ? keys : [keys]) - } + activeKey={activeKey} + onChange={handleOnCollapseChange} items={[ { - key: 'k8sOptions', - label: intl.formatMessage({ id: 'clusters.k8sOptions.title' }), + key: 'advanceConfig', + label: intl.formatMessage({ id: 'resources.form.advanced' }), forceRender: true, children: ( - + provider={provider} + currentData={currentData} + ref={advanceConfigRef} + > ) } ]} > - )} - - - ) - } - ]} - > - + + ); } ); diff --git a/src/pages/cluster-management/components/credential-item.tsx b/src/pages/cluster-management/components/credential-item.tsx new file mode 100644 index 00000000..27e8633c --- /dev/null +++ b/src/pages/cluster-management/components/credential-item.tsx @@ -0,0 +1,81 @@ +import { Input as CInput } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Divider } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { ImageCredential } from '../config/types'; + +const Wrapper = styled.div` + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 12px; + + .row { + display: flex; + gap: 12px; + + > * { + flex: 1; + min-width: 0; + } + } +`; + +interface CredentialItemProps { + item: ImageCredential; + validated: boolean; + index: number; + onChange: (partial: Partial) => void; +} + +const CredentialItem: React.FC = ({ + item, + index, + validated, + onChange +}) => { + const intl = useIntl(); + + const registryEmpty = !item.registry?.trim(); + const registryStatus = + validated && registryEmpty ? ('error' as const) : ('success' as const); + + return ( + + {index !== 0 && ( + + )} + onChange({ registry: e.target.value })} + label={intl.formatMessage({ + id: 'clusters.imageCredentials.registry' + })} + /> +
+ onChange({ username: e.target.value })} + label={intl.formatMessage({ + id: 'clusters.imageCredentials.username' + })} + /> + onChange({ password: e.target.value })} + label={intl.formatMessage({ + id: 'clusters.imageCredentials.password' + })} + /> +
+
+ ); +}; + +export default CredentialItem; diff --git a/src/pages/cluster-management/components/image-credential.tsx b/src/pages/cluster-management/components/image-credential.tsx new file mode 100644 index 00000000..bb2d3e47 --- /dev/null +++ b/src/pages/cluster-management/components/image-credential.tsx @@ -0,0 +1,98 @@ +import { MetadataList, useAppUtils } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React, { useEffect } from 'react'; +import { useFormContext } from '../config/form-context'; +import { ImageCredential as ImageCredentialType } from '../config/types'; +import imgCredentialStyle from '../styles/img-credential.less'; +import CredentialItem from './credential-item'; + +const FIELD_PATH = ['k8s_options', 'imageCredentials']; + +const ImageCredential: React.FC = () => { + const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); + const { submitAttempted } = useFormContext(); + const validated = !!submitAttempted; + const form = Form.useFormInstance(); + const credentials: ImageCredentialType[] = + Form.useWatch(FIELD_PATH, form) || []; + + const updateList = (list: ImageCredentialType[]) => { + form.setFieldValue(FIELD_PATH, list); + }; + + useEffect(() => { + if (validated) { + form.validateFields([FIELD_PATH]).catch(() => {}); + } + }, [credentials, validated]); + + const handleAdd = () => { + updateList([...credentials, { registry: '', username: '', password: '' }]); + }; + + const handleDelete = (index: number) => { + const next = [...credentials]; + next.splice(index, 1); + updateList(next); + }; + + const handleChange = ( + index: number, + partial: Partial + ) => { + const next = [...credentials]; + next[index] = { ...next[index], ...partial }; + updateList(next); + }; + + return ( +
+ { + if (!value?.length) return; + const hasMissingRegistry = value.some( + (item) => !item?.registry?.trim() + ); + if (hasMissingRegistry) { + throw new Error( + getRuleMessage('input', 'clusters.imageCredentials.registry') + ); + } + } + } + ]} + > + + {(item: ImageCredentialType, index: number) => ( + handleChange(index, partial)} + /> + )} + + +
+ ); +}; + +export default ImageCredential; diff --git a/src/pages/cluster-management/components/k8s-pod-spec.tsx b/src/pages/cluster-management/components/k8s-pod-spec.tsx index 69e2ccf6..90db91ba 100644 --- a/src/pages/cluster-management/components/k8s-pod-spec.tsx +++ b/src/pages/cluster-management/components/k8s-pod-spec.tsx @@ -1,20 +1,11 @@ import { PageActionType } from '@/config/types'; -import { - MinusOutlined, - PlusOutlined, - QuestionCircleOutlined -} from '@ant-design/icons'; -import { - Input as CInput, - LabelSelector, - SwitchCard, - useAppUtils -} from '@gpustack/core-ui'; +import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Button, Form, Tooltip } from 'antd'; +import { Form } from 'antd'; import React, { useState } from 'react'; import styled from 'styled-components'; import { GpuInstanceOptions } from '../config/types'; +import ImageCredential from './image-credential'; import K8SVolumeMount from './k8s-volume-mount'; const Title = styled.div` @@ -32,151 +23,21 @@ const SectionWrap = styled.div` margin-bottom: 16px; `; -const ImageCredentialsForm: React.FC = () => { - const intl = useIntl(); - const { getRuleMessage } = useAppUtils(); - - return ( - - - {(fields, { add, remove }) => ( - <> - - <div className="flex-center gap-8"> - <span> - {intl.formatMessage({ - id: 'clusters.imageCredentials.title' - })} - </span> - <Button - type="link" - onClick={() => - add({ registry: '', username: '', password: '' }) - } - > - <PlusOutlined />{' '} - {intl.formatMessage({ - id: 'clusters.imageCredentials.add' - })} - </Button> - </div> - -
- {fields.map(({ key, name }) => ( -
-
- - - -
-
- - - -
-
- - - -
-
-
- -
- ))} -
- - )} -
-
- ); -}; - const NodeSelectorForm: React.FC = () => { const intl = useIntl(); return ( - - <span className="flex-center gap-4"> - <span> - {intl.formatMessage({ id: 'clusters.nodeSelector.title' })} - </span> - <Tooltip - title={intl.formatMessage({ id: 'clusters.nodeSelector.tip' })} - > - <QuestionCircleOutlined - style={{ color: 'var(--ant-color-text-secondary)' }} - /> - </Tooltip> - </span> - ); }; -// Render namespace. Kept as the first field of the section so the most -// fundamental K8s deployment knob is set before the rest. const NamespaceForm: React.FC = () => { const intl = useIntl(); @@ -197,8 +58,6 @@ const NamespaceForm: React.FC = () => { ); }; -// Operator-image override. A plain string knob that used to ride along inside -// worker_config; it now lives directly on k8s_options. const OperatorImageForm: React.FC = () => { const intl = useIntl(); @@ -273,7 +132,7 @@ const K8sPodSpec: React.FC<{ - + diff --git a/src/pages/cluster-management/components/k8s-volume-mount.tsx b/src/pages/cluster-management/components/k8s-volume-mount.tsx index a6154df0..6efe245a 100644 --- a/src/pages/cluster-management/components/k8s-volume-mount.tsx +++ b/src/pages/cluster-management/components/k8s-volume-mount.tsx @@ -169,7 +169,9 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => { open={collapseKey.has(name)} onToggle={(open: boolean) => onToggle(open, name)} styles={{ - body: collapseKey.has(name) ? { padding: 16 } : {}, + body: collapseKey.has(name) + ? { paddingBlock: '16px 0', paddingInline: 16 } + : {}, content: { paddingTop: 0 }, header: { backgroundColor: 'unset' diff --git a/src/pages/cluster-management/config/form-context.ts b/src/pages/cluster-management/config/form-context.ts new file mode 100644 index 00000000..1f799778 --- /dev/null +++ b/src/pages/cluster-management/config/form-context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; +import { ClusterListItem } from './types'; + +// for cluster form +interface FormContextProps { + currentData?: ClusterListItem; + submitAttempted?: boolean; +} + +export const FormContext = createContext({}); + +export const useFormContext = () => useContext(FormContext); diff --git a/src/pages/cluster-management/styles/img-credential.less b/src/pages/cluster-management/styles/img-credential.less new file mode 100644 index 00000000..17dbd50d --- /dev/null +++ b/src/pages/cluster-management/styles/img-credential.less @@ -0,0 +1,11 @@ +.container { + :global(.item-container) { + align-items: flex-start; + + &:nth-child(2) { + :global(.btn) { + margin-top: 16px !important; + } + } + } +} diff --git a/src/pages/gpu-service/public-keys/forms/basic.tsx b/src/pages/gpu-service/public-keys/forms/basic.tsx index 52afb933..35b5a30e 100644 --- a/src/pages/gpu-service/public-keys/forms/basic.tsx +++ b/src/pages/gpu-service/public-keys/forms/basic.tsx @@ -33,6 +33,7 @@ const Basic = ({ action }: { action: string }) => { name="displayName"> diff --git a/src/pages/llmodels/components/download/target-form.tsx b/src/pages/llmodels/components/download/target-form.tsx index 8423f050..dc392282 100644 --- a/src/pages/llmodels/components/download/target-form.tsx +++ b/src/pages/llmodels/components/download/target-form.tsx @@ -1,6 +1,5 @@ import { ModelFileFormData as FormData } from '@/pages/resources/config/types'; import { - CheckboxField, Input as CInput, Cascader as SealCascader, Select as SealSelect, @@ -204,6 +203,13 @@ const TargetForm: React.FC = forwardRef((props, ref) => { root: 'cascader-popup-wrapper gpu-selector' } }} + styles={{ + popup: { + listItem: { + maxWidth: '100%' + } + } + }} maxTagCount={1} label={intl.formatMessage({ id: 'resources.worker' })} options={workerOptions} @@ -241,20 +247,6 @@ const TargetForm: React.FC = forwardRef((props, ref) => { > )} - {source === modelSourceMap.local_path_value && ( - - name="is_lora" - key="is_lora" - valuePropName="checked" - noStyle - > - - - )} );