From ad048a92edfbe98b42480fbd9b49ef3b36a00214 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 19 Dec 2025 14:39:28 +0800 Subject: [PATCH] feat: backend entrypoint --- src/components/alert-info/block.tsx | 19 ++++- src/components/seal-form/seal-textarea.tsx | 2 +- src/components/seal-form/switch-input.tsx | 75 +++++++++++++++++++ src/global.less | 2 +- src/locales/en-US/backends.ts | 6 +- src/locales/en-US/clusters.ts | 6 +- src/locales/ja-JP/backends.ts | 6 +- src/locales/ja-JP/clusters.ts | 17 +++-- src/locales/ru-RU/backends.ts | 12 ++- src/locales/ru-RU/clusters.ts | 21 +++--- src/locales/zh-CN/backends.ts | 6 +- src/locales/zh-CN/clusters.ts | 5 +- src/pages/backends/components/add-modal.tsx | 3 +- src/pages/backends/config/types.ts | 1 + src/pages/backends/forms/versions-config.tsx | 27 ++++++- .../add-worker/add-worker-context.ts | 2 + .../components/add-worker/add-worker-step.tsx | 42 ++++++++--- .../components/add-worker/index.tsx | 54 +++++++------ .../components/add-worker/step-collapse.tsx | 39 ++++++++-- .../step-forms/advance-config.tsx | 2 +- .../llmodels/components/compatible-alert.tsx | 18 +---- 21 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 src/components/seal-form/switch-input.tsx diff --git a/src/components/alert-info/block.tsx b/src/components/alert-info/block.tsx index 38cd4891..cec53470 100644 --- a/src/components/alert-info/block.tsx +++ b/src/components/alert-info/block.tsx @@ -1,4 +1,8 @@ -import { WarningFilled } from '@ant-design/icons'; +import { + CheckCircleFilled, + LoadingOutlined, + WarningFilled +} from '@ant-design/icons'; import { Typography } from 'antd'; import { createStyles } from 'antd-style'; import classNames from 'classnames'; @@ -122,6 +126,17 @@ const AlertInfo: React.FC = (props) => { overlayScrollerProps = {} } = props; const { styles } = useStyles(); + + const renderIcon = () => { + if (type === 'transition') { + return ; + } + if (type === 'success') { + return ; + } + return ; + }; + return ( <> {message ? ( @@ -139,7 +154,7 @@ const AlertInfo: React.FC = (props) => { >
- {icon ?? } + {icon ?? renderIcon()}
{title && ( diff --git a/src/components/seal-form/seal-textarea.tsx b/src/components/seal-form/seal-textarea.tsx index 428c2880..214445c4 100644 --- a/src/components/seal-form/seal-textarea.tsx +++ b/src/components/seal-form/seal-textarea.tsx @@ -102,7 +102,7 @@ const SealTextArea: React.FC = ( {label}} + label={label && {label}} isFocus={alwaysFocus || isFocus} required={required} description={description} diff --git a/src/components/seal-form/switch-input.tsx b/src/components/seal-form/switch-input.tsx new file mode 100644 index 00000000..76b85652 --- /dev/null +++ b/src/components/seal-form/switch-input.tsx @@ -0,0 +1,75 @@ +import { Switch, Tooltip } from 'antd'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import LabelInfo from '../seal-form/components/label-info'; + +const SwitchContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + border: 1px solid var(--ant-color-border); + border-radius: var(--ant-border-radius); + padding: 12px 14px; + min-height: 54px; +`; + +const LabelContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +interface SwitchInputProps { + label?: React.ReactNode; + description?: string; + checked?: boolean; + defaultChecked?: boolean; + onChange?: (checked: boolean) => void; + children?: React.ReactNode; + style?: React.CSSProperties; + alwaysShowChildren?: boolean; + btnTips?: React.ReactNode; + size?: 'small' | 'default'; +} + +const SwitchInput: React.FC = (props) => { + const { + label, + description, + checked, + defaultChecked, + onChange, + children, + style, + alwaysShowChildren = true, + size, + btnTips + } = props; + const [internalChecked, setInternalChecked] = useState( + defaultChecked || false + ); + + const handleChange = (checked: boolean) => { + setInternalChecked(checked); + onChange?.(checked); + }; + + return ( + + + + + + + + {(alwaysShowChildren || internalChecked) && children} + + ); +}; + +export default SwitchInput; diff --git a/src/global.less b/src/global.less index c3912e89..bd4f72df 100644 --- a/src/global.less +++ b/src/global.less @@ -203,7 +203,7 @@ body { // form item help .ant-form-item-with-help .ant-form-item-explain { - padding-left: 16px; + // padding-left: 16px; } // icon diff --git a/src/locales/en-US/backends.ts b/src/locales/en-US/backends.ts index 42e8a246..45701e14 100644 --- a/src/locales/en-US/backends.ts +++ b/src/locales/en-US/backends.ts @@ -34,5 +34,9 @@ export default { 'The custom backend name must end with "-custom".', 'backend.quickConfig': 'Quick Config', 'backend.version.default.not.exists': - 'The default version does not exist in {versions}.' + 'The default version does not exist in {versions}.', + 'backend.replaceEntrypoint': 'Override Image Entrypoint', + 'backend.entrypoint': 'Image Entrypoint', + 'backend.entrypoint.tips': + 'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.' }; diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index 9fa5829d..5de8a34c 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -89,8 +89,10 @@ Same applies to the /opt/dtk directory.`, 'clusters.button.genToken': 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, - 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.', + 'clusters.addworker.message.success_single': + '{count} new worker has been added to the cluster.', + 'clusters.addworker.message.success_multiple': + '{count} new workers have been added to the cluster.', 'clusters.create.serverUrl': 'Server URL', 'clusters.create.workerConfig': 'Worker Configuration', 'clusters.addworker.containerName': 'Worker Container Name', diff --git a/src/locales/ja-JP/backends.ts b/src/locales/ja-JP/backends.ts index 42e8a246..45701e14 100644 --- a/src/locales/ja-JP/backends.ts +++ b/src/locales/ja-JP/backends.ts @@ -34,5 +34,9 @@ export default { 'The custom backend name must end with "-custom".', 'backend.quickConfig': 'Quick Config', 'backend.version.default.not.exists': - 'The default version does not exist in {versions}.' + 'The default version does not exist in {versions}.', + 'backend.replaceEntrypoint': 'Override Image Entrypoint', + 'backend.entrypoint': 'Image Entrypoint', + 'backend.entrypoint.tips': + 'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 3b81a430..b9ba21fd 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -89,8 +89,10 @@ Same applies to the /opt/dtk directory.`, 'clusters.button.genToken': 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, - 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.', + 'clusters.addworker.message.success_single': + '{count} new worker has been added to the cluster.', + 'clusters.addworker.message.success_multiple': + '{count} new workers have been added to the cluster.', 'clusters.create.serverUrl': 'Server URL', 'clusters.create.workerConfig': 'Worker Configuration', 'clusters.addworker.containerName': 'Worker Container Name', @@ -179,11 +181,12 @@ Same applies to the /opt/dtk directory.`, // 71. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount', // 72. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.', // 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)', -// 74. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.', -// 75. 'clusters.create.serverUrl': 'Server URL', -// 76. 'clusters.create.workerConfig': 'Worker Configuration' -// 75. 'clusters.addworker.containerName': 'Worker Container Name', -// 76. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', +// 74. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.', +// 75. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.', +// 76. 'clusters.create.serverUrl': 'Server URL', +// 77. 'clusters.create.workerConfig': 'Worker Configuration' +// 78. 'clusters.addworker.containerName': 'Worker Container Name', +// 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', // 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', // 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 79. 'clusters.table.ip.internal': 'Internal', diff --git a/src/locales/ru-RU/backends.ts b/src/locales/ru-RU/backends.ts index 48ba3985..93f9aad3 100644 --- a/src/locales/ru-RU/backends.ts +++ b/src/locales/ru-RU/backends.ts @@ -34,9 +34,17 @@ export default { 'Пользовательское название бэкенда должно оканчиваться на «-custom».', 'backend.quickConfig': 'Быстрая настройка', 'backend.version.default.not.exists': - 'The default version does not exist in {versions}.' + 'The default version does not exist in {versions}.', + 'backend.replaceEntrypoint': 'Override Image Entrypoint', + 'backend.entrypoint': 'Image Entrypoint', + 'backend.entrypoint.tips': + 'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== -// 1. 'backend.version.default.not.exists': 'The default version does not exist in {versions}.' +// 1. 'backend.version.default.not.exists': 'The default version does not exist in {versions}.', +// 2. 'backend.replaceEntrypoint': 'Override Image Entrypoint', +// 3. 'backend.entrypoint': 'Image Entrypoint', +// 4. 'backend.entrypoint.tips': +// 'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index c66367ff..b66f94a5 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -89,8 +89,10 @@ export default { 'clusters.button.genToken': 'Need to create a new token? Click here.', 'clusters.addworker.amdNotes-01': `If the /opt/rocm directory does not exist, please create a symbolic link pointing to the ROCm installed path: ln -s /path/to/rocm /opt/rocm.`, - 'clusters.addworker.message.success': - '{count} workers have been added to the cluster.', + 'clusters.addworker.message.success_single': + '{count} new worker has been added to the cluster.', + 'clusters.addworker.message.success_multiple': + '{count} new workers have been added to the cluster.', 'clusters.create.serverUrl': 'Server URL', 'clusters.create.workerConfig': 'Worker Configuration', 'clusters.addworker.containerName': 'Worker Container Name', @@ -110,13 +112,14 @@ export default { // 2. 'clusters.button.genToken': 'Need to create a new token? Click here.', // 3. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount', // 4. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.', -// 5. 'clusters.addworker.message.success': '{count} workers have been added to the cluster.', -// 6. 'clusters.create.serverUrl': 'Server URL', -// 7. 'clusters.create.workerConfig': 'Worker Configuration' -// 6. 'clusters.addworker.containerName': 'Worker Container Name', -// 7. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', -// 8. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', -// 9. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', +// 5. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.', +// 6. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.', +// 7. 'clusters.create.serverUrl': 'Server URL', +// 8. 'clusters.create.workerConfig': 'Worker Configuration' +// 9. 'clusters.addworker.containerName': 'Worker Container Name', +// 10. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', +// 11. 'clusters.addworker.dataVolume': 'GPUStack Data Volume', +// 12. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 10. 'clusters.table.ip.internal': 'Internal', // 11. 'clusters.table.ip.external': 'External', // 12. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.' diff --git a/src/locales/zh-CN/backends.ts b/src/locales/zh-CN/backends.ts index 28b07ce3..4ada3466 100644 --- a/src/locales/zh-CN/backends.ts +++ b/src/locales/zh-CN/backends.ts @@ -31,5 +31,9 @@ export default { 'backend.version.no.tips': '内置后端自定义版本名称必须以 "-custom" 结尾。', 'backend.backend.rules.custom': '自定义后端名称须以 "-custom" 结尾。', 'backend.quickConfig': '快速配置', - 'backend.version.default.not.exists': '默认版本不存在于 {versions}。' + 'backend.version.default.not.exists': '默认版本不存在于 {versions}。', + 'backend.replaceEntrypoint': '覆盖镜像入口命令', + 'backend.entrypoint': '镜像入口命令', + 'backend.entrypoint.tips': + '如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。' }; diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 5aeffa4d..324f72a9 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -88,7 +88,10 @@ export default { '需要创建令牌?点击这里。', 'clusters.addworker.amdNotes-01': '如果 /opt/rocm 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:ln -s /path/to/rocm /opt/rocm。', - 'clusters.addworker.message.success': '已将 {count} 个工作节点添加到集群中。', + 'clusters.addworker.message.success_single': + '已将 {count} 个新节点添加到集群中。', + 'clusters.addworker.message.success_multiple': + '已将 {count} 个新节点添加到集群中。', 'clusters.create.serverUrl': '服务器地址', 'clusters.create.workerConfig': '节点配置', 'clusters.addworker.containerName': '节点容器名称', diff --git a/src/pages/backends/components/add-modal.tsx b/src/pages/backends/components/add-modal.tsx index eb8dbe4f..0e14bce0 100644 --- a/src/pages/backends/components/add-modal.tsx +++ b/src/pages/backends/components/add-modal.tsx @@ -84,7 +84,8 @@ const AddModal: React.FC = (props) => { acc[curr.version_no] = { custom_framework: curr.custom_framework, image_name: curr.image_name, - run_command: curr.run_command + run_command: curr.run_command, + entrypoint: curr.entrypoint }; } return acc; diff --git a/src/pages/backends/config/types.ts b/src/pages/backends/config/types.ts index 14e5b175..08562b50 100644 --- a/src/pages/backends/config/types.ts +++ b/src/pages/backends/config/types.ts @@ -4,6 +4,7 @@ export interface VersionConfigs { is_default: boolean; built_in_frameworks?: string[]; custom_framework?: string; + entrypoint?: string; version_no?: string; is_built_in?: boolean; } diff --git a/src/pages/backends/forms/versions-config.tsx b/src/pages/backends/forms/versions-config.tsx index 5817a263..734d5c81 100644 --- a/src/pages/backends/forms/versions-config.tsx +++ b/src/pages/backends/forms/versions-config.tsx @@ -2,6 +2,7 @@ import CollapsibleContainer from '@/components/collapse-container'; import BaseSelect from '@/components/seal-form/base/select'; import SealInput from '@/components/seal-form/seal-input'; import SealSelect from '@/components/seal-form/seal-select'; +import SealTextArea from '@/components/seal-form/seal-textarea'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; @@ -342,14 +343,36 @@ const VersionsForm: React.FC = ({ required /> + + + - + > diff --git a/src/pages/cluster-management/components/add-worker/add-worker-context.ts b/src/pages/cluster-management/components/add-worker/add-worker-context.ts index 5d96e9de..98b626fd 100644 --- a/src/pages/cluster-management/components/add-worker/add-worker-context.ts +++ b/src/pages/cluster-management/components/add-worker/add-worker-context.ts @@ -8,6 +8,8 @@ interface AddWorkerContextProps { clusterLoading?: boolean; provider: ProviderType; stepList: string[]; + actionSource?: 'modal' | 'page'; + onCancel?: () => void; onClusterChange?: (value: number, row?: any) => void; collapseKey: Set; onToggle: (open: boolean, key: string) => void; diff --git a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx index 4e622a55..3ca44dc7 100644 --- a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx +++ b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx @@ -1,4 +1,4 @@ -import AlertInfoBlock from '@/components/alert-info/block'; +import AlertBlockInfo from '@/components/alert-info/block'; import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message'; import { ExclamationCircleFilled } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; @@ -36,6 +36,7 @@ type AddWorkerProps = { clusterLoading?: boolean; stepList: StepName[]; onClusterChange?: (value: number, row?: any) => void; + onCancel?: () => void; registrationInfo: { token: string; image: string; @@ -57,6 +58,7 @@ const AddWorkerSteps: React.FC = (props) => { clusterList, clusterLoading, stepList = [], + onCancel, onClusterChange } = props || {}; const intl = useIntl(); @@ -82,12 +84,28 @@ const AddWorkerSteps: React.FC = (props) => { React.useEffect(() => { // this effect is only triggered when used in cluster create page - console.log('actionSource:', actionSource); if (actionSource === 'page') { createModelsChunkRequest(); } }, [actionSource]); + const renderMessage = (count: number) => { + if (count === 1) { + return intl.formatMessage( + { + id: 'clusters.addworker.message.success_single' + }, + { count: addedCount } + ); + } + return intl.formatMessage( + { + id: 'clusters.addworker.message.success_multiple' + }, + { count: addedCount } + ); + }; + return ( = (props) => { provider, stepList: stepList, collapseKey, + actionSource, onToggle, + onCancel, onClusterChange: handleOnClusterChange, registrationInfo, summary, @@ -110,7 +130,7 @@ const AddWorkerSteps: React.FC = (props) => { )} {stepList.includes(StepNamesMap.SelectCluster) && !clusterList?.length && ( - = (props) => { message={intl.formatMessage({ id: 'resources.worker.noCluster.tips' })} - > + > )} {/* render the steps only when there is at least one cluster available or cluster selection is not required */} @@ -142,14 +162,14 @@ const AddWorkerSteps: React.FC = (props) => { )} {addedCount > 0 && ( )} diff --git a/src/pages/cluster-management/components/add-worker/index.tsx b/src/pages/cluster-management/components/add-worker/index.tsx index 26e5ed5e..4b5b6f1a 100644 --- a/src/pages/cluster-management/components/add-worker/index.tsx +++ b/src/pages/cluster-management/components/add-worker/index.tsx @@ -1,7 +1,7 @@ import ScrollerModal from '@/components/scroller-modal'; import useAddWorkerMessage from '@/pages/cluster-management/hooks/use-add-worker-message'; import { useIntl } from '@umijs/max'; -import { Alert, Button } from 'antd'; +import { Alert } from 'antd'; import React, { useEffect } from 'react'; import styled from 'styled-components'; import { queryClusterToken } from '../../apis'; @@ -100,6 +100,23 @@ const AddWorker: React.FC = (props) => { } }, [open]); + const renderMessage = (count: number) => { + if (count === 1) { + return intl.formatMessage( + { + id: 'clusters.addworker.message.success_single' + }, + { count: addedCount } + ); + } + return intl.formatMessage( + { + id: 'clusters.addworker.message.success_multiple' + }, + { count: addedCount } + ); + }; + return ( = (props) => { maxContentHeight={'max(calc(100vh - 200px), 600px)'} footer={
- - {addedCount > 0 && ( - - )} - - + {addedCount > 0 && ( + + )}
} > = ({ ...rest }) => { const intl = useIntl(); - const { collapseKey, onToggle, stepList } = useAddWorkerContext(); + const { collapseKey, onToggle, stepList, actionSource, onCancel } = + useAddWorkerContext(); const handleOnNext = async () => { const res = await beforeNext?.(); @@ -51,8 +54,16 @@ const StepCollapse: React.FC = ({ onToggle(true, nextName); }; + const handleOnPrevious = () => { + // find the previous step and open it + const previousName = stepList[stepList.indexOf(name) - 1]; + onToggle(true, previousName); + }; + const isLastStep = stepList.indexOf(name) === stepList.length - 1; + const isFirstStep = stepList.indexOf(name) === 0; + return ( = ({ {...rest} > {children} - {!isLastStep && ( - + + + {!isFirstStep && ( + + )} + + {!isLastStep && ( - - )} + )} + {isLastStep && actionSource === 'modal' && ( + + )} + ); diff --git a/src/pages/cluster-management/step-forms/advance-config.tsx b/src/pages/cluster-management/step-forms/advance-config.tsx index 18ca652e..7a256f70 100644 --- a/src/pages/cluster-management/step-forms/advance-config.tsx +++ b/src/pages/cluster-management/step-forms/advance-config.tsx @@ -25,7 +25,7 @@ const ClusterAdvanceConfig: React.FC<{ return editorRef.current?.getValue(); }, setYamlValue: (values: any) => { - editorRef.current?.setValue(values); + editorRef.current?.setValue(values || yamlTemplate); } })); diff --git a/src/pages/llmodels/components/compatible-alert.tsx b/src/pages/llmodels/components/compatible-alert.tsx index 3ce8825e..379fa0d4 100644 --- a/src/pages/llmodels/components/compatible-alert.tsx +++ b/src/pages/llmodels/components/compatible-alert.tsx @@ -1,10 +1,5 @@ import AlertBlockInfo from '@/components/alert-info/block'; -import { - CheckCircleFilled, - CloseOutlined, - LoadingOutlined, - WarningFilled -} from '@ant-design/icons'; +import { CloseOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { isArray } from 'lodash'; import React, { useCallback, useMemo } from 'react'; @@ -98,16 +93,6 @@ const CompatibilityAlert: React.FC = (props) => { return ''; }, [message, show, handleLinkMessage]); - const renderIcon = useMemo(() => { - if (type === 'transition') { - return ; - } - if (type === 'success') { - return ; - } - return ; - }, [type]); - return ( show && !!message && ( @@ -118,7 +103,6 @@ const CompatibilityAlert: React.FC = (props) => { title={title} contentStyle={contentStyle} type={type || 'warning'} - icon={renderIcon} style={{ paddingInlineEnd: showClose ? 20 : 16 }}