From d9d5ca95df6bb8873c25f4e3d1a20504b5bb96c7 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 15 Sep 2025 09:55:52 +0800 Subject: [PATCH] style: add worker ux --- src/atoms/clusters.ts | 12 ++ src/components/echarts/line-chart.tsx | 4 +- src/components/icon-font/index.tsx | 2 +- src/components/seal-form/auto-complete.tsx | 40 +++++++ .../seal-form/wrapper/auto-complete-label.ts | 28 +++++ src/components/seal-form/wrapper/select.ts | 2 +- src/components/templates/card.tsx | 27 +++-- src/config/index.ts | 3 +- src/config/types.ts | 2 +- src/locales/en-US/clusters.ts | 6 +- src/locales/ja-JP/clusters.ts | 10 +- src/locales/ru-RU/clusters.ts | 8 +- src/locales/zh-CN/clusters.ts | 12 +- .../cluster-management/cluster-create.tsx | 2 +- .../components/add-worker-command.tsx | 2 +- .../components/add-worker-step.tsx | 91 ++++++++++++-- .../components/cloud-provider-form.tsx | 37 ++++-- .../components/pool-form.tsx | 113 ++++++++++++------ .../components/provider-catalog.tsx | 30 +++-- .../components/support-hardware.tsx | 72 +++++++++-- src/pages/cluster-management/credentials.tsx | 10 +- .../hooks/use-pools-columns.tsx | 9 +- .../hooks/use-provider-regions.ts | 23 ++-- .../step-forms/basic-form.tsx | 2 +- src/pages/llmodels/components/gpu-card.tsx | 4 +- src/pages/resources/config/index.ts | 4 +- 26 files changed, 436 insertions(+), 119 deletions(-) create mode 100644 src/components/seal-form/wrapper/auto-complete-label.ts diff --git a/src/atoms/clusters.ts b/src/atoms/clusters.ts index 6a80eca2..ad8c2c76 100644 --- a/src/atoms/clusters.ts +++ b/src/atoms/clusters.ts @@ -17,6 +17,11 @@ export const regionInstanceTypeListAtom = atom< { label: string; value: string; + description: string; + specInfo: Record; + vendor: string; + available: boolean; + regions: string[]; }[] >([]); @@ -24,6 +29,11 @@ export const regionOSImageListAtom = atom< { label: string; value: string; + name: string; + description: string; + vendor: string; + specInfo: Record; + regions: string[]; }[] >([]); @@ -42,3 +52,5 @@ export const allRegionInstanceTypeListAtom = atom< regions: string[]; }[] >([]); + +export const fromClusterCreationAtom = atom(false); diff --git a/src/components/echarts/line-chart.tsx b/src/components/echarts/line-chart.tsx index bf922f8b..efe68816 100644 --- a/src/components/echarts/line-chart.tsx +++ b/src/components/echarts/line-chart.tsx @@ -23,7 +23,7 @@ const LineChart: React.FC = (props) => { legendOptions, gridOptions, titleOptions, - showArea + showArea = 0.25 } = props; const { grid, @@ -87,7 +87,7 @@ const LineChart: React.FC = (props) => { const data = _.map(seriesData, (item: any) => { const colors = genColors({ color: item.color, - alpha1: 0.5, + alpha1: 0.25, alpha2: 0.1 }); return { diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 1884f752..c0f7c035 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_5jrvj6qs39.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_84hy7h371ty.js' }); export default IconFont; diff --git a/src/components/seal-form/auto-complete.tsx b/src/components/seal-form/auto-complete.tsx index b08dd006..e236e33d 100644 --- a/src/components/seal-form/auto-complete.tsx +++ b/src/components/seal-form/auto-complete.tsx @@ -1,8 +1,10 @@ import { AutoComplete, Form, Spin } from 'antd'; import type { AutoCompleteProps } from 'antd/lib'; +import classNames from 'classnames'; import React, { useEffect, useRef, useState } from 'react'; import { SealFormItemProps } from './types'; import Wrapper from './wrapper'; +import AutoCompleteLabel from './wrapper/auto-complete-label'; import SelectWrapper from './wrapper/select'; const SealAutoComplete: React.FC< @@ -24,6 +26,7 @@ const SealAutoComplete: React.FC< ...rest } = props; const [isFocus, setIsFocus] = useState(false); + const [_isFocus_, _setIsFocus_] = useState(false); const inputRef = useRef(null); let status = ''; if (isInFormItems) { @@ -41,6 +44,7 @@ const SealAutoComplete: React.FC< if (!props.disabled && !isFocus) { inputRef.current?.focus?.(); setIsFocus(true); + _setIsFocus_(true); } }; @@ -49,17 +53,20 @@ const SealAutoComplete: React.FC< if (trim) { value = value?.trim?.(); } + _setIsFocus_(false); props.onChange?.(value, option); }; const handleOnFocus = (e: any) => { setIsFocus(true); + _setIsFocus_(true); props.onFocus?.(e); }; const handleOnBlur = (e: any) => { if (!props.value) { setIsFocus(false); + _setIsFocus_(false); } e.target.value = e.target.value?.trim?.(); props.onBlur?.(e); @@ -79,6 +86,39 @@ const SealAutoComplete: React.FC< return addAfter; }; + const renderAutoCompleteLabel = () => { + console.log( + 'renderAutoCompleteLabel===', + props.value, + props.showSearch, + _isFocus_, + isFocus + ); + if (!props.showSearch || _isFocus_) { + return null; + } + let selectItem: { + label: React.ReactNode; + value: string | number; + } = props.options?.find((item: any) => item.value === props.value) as { + label: React.ReactNode; + value: string | number; + }; + + if (!selectItem) { + selectItem = { label: props.value, value: props.value }; + } + return ( + + {props.labelRender ? props.labelRender(selectItem) : selectItem.label} + + ); + }; + return ( void; } -const CardWrapper = styled.div` +const CardWrapper = styled.div.attrs({ + className: 'template-card-wrapper' +})` overflow: hidden; display: flex; padding: 22px; @@ -43,37 +45,44 @@ const CardWrapper = styled.div` cursor: pointer; } &.disabled { - cursor: not-allowed; + cursor: default; opacity: 0.6; pointer-events: none; - background-color: var(--ant-color-fill-quaternary); + border-style: dashed; } `; -const CardContent = styled.div` +const CardContent = styled.div.attrs({ + className: 'template-card-content' +})` width: 100%; flex: 1; color: var(--ant-color-text-tertiary); `; -const Inner = styled.div` +const Inner = styled.div.attrs({ + className: 'template-card-inner' +})` display: flex; width: 100%; - height: 100%; flex-direction: column; justify-content: flex-start; gap: 8px; line-height: 1.5; `; -const Icon = styled.div` +const Icon = styled.div.attrs({ + className: 'template-card-icon' +})` display: flex; align-items: center; margin-right: 16px; font-size: 32px; `; -const Header = styled.div` +const Header = styled.div.attrs({ + className: 'template-card-header' +})` font-weight: bold; font-size: var(--font-size-base); display: flex; @@ -110,7 +119,7 @@ const Card: React.FC = (props) => { {icon && {icon}} {header &&
{header}
} - {children} + {children && {children}} {footer}
diff --git a/src/config/index.ts b/src/config/index.ts index 32b8a77c..0e6c1294 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,7 +3,8 @@ import { PageActionType, StatusType } from './types'; export const PageAction: Record = { CREATE: 'create', UPDATE: 'update', - VIEW: 'view' + VIEW: 'view', + EDIT: 'edit' }; export const StatusColorMap: Record< diff --git a/src/config/types.ts b/src/config/types.ts index 8d600a9b..9bcf0c38 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -6,7 +6,7 @@ export interface DropDownItem { iconfont?: boolean; } -export type PageActionType = 'create' | 'update' | 'view'; +export type PageActionType = 'create' | 'update' | 'view' | 'edit'; export type StatusType = | 'error' diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index ce390aa9..9407b93c 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -37,5 +37,9 @@ export default { 'clusters.create.noInstanceTypes': 'No instance types available', 'clusters.create.noRegions': 'No regions available', 'clusters.workerpool.batchSize.desc': - 'Number of workers created simultaneously in the Worker pool' + 'Number of workers created simultaneously in the Worker pool', + 'clusters.create.addworker.tips': + ' Please make sure the prerequisites for {label} are met before executing the following command.', + 'clusters.create.addCommand.tips': + ' On the Worker that needs to be added, run the following command to join it to the cluster.' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 14f9efff..9b447279 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -37,7 +37,11 @@ export default { 'clusters.create.noInstanceTypes': 'No instance types available', 'clusters.create.noRegions': 'No regions available', 'clusters.workerpool.batchSize.desc': - 'Number of workers created simultaneously in the Worker pool' + 'Number of workers created simultaneously in the Worker pool', + 'clusters.create.addworker.tips': + ' Please make sure the prerequisites for {label} are met before executing the following command.', + 'clusters.create.addCommand.tips': + ' On the Worker that needs to be added, run the following command to join it to the cluster.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -78,6 +82,8 @@ export default { // 35. 'clusters.create.noImages': 'No images available', // 36. 'clusters.create.noInstanceTypes': 'No instance types available', // 37. 'clusters.create.noRegions': 'No regions available', -// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool' +// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool', +// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for {label} are met before executing the following command.', +// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index e767460b..dbbce08a 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -37,7 +37,11 @@ export default { 'clusters.create.noInstanceTypes': 'No instance types available', 'clusters.create.noRegions': 'No regions available', 'clusters.workerpool.batchSize.desc': - 'Number of workers created simultaneously in the Worker pool' + 'Number of workers created simultaneously in the Worker pool', + 'clusters.create.addworker.tips': + ' Please make sure the prerequisites for {label} are met before executing the following command.', + 'clusters.create.addCommand.tips': + ' On the Worker that needs to be added, run the following command to join it to the cluster.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -79,5 +83,7 @@ export default { // 36. 'clusters.create.noInstanceTypes': 'No instance types available', // 37. 'clusters.create.noRegions': 'No regions available', // 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool' +// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for {label} are met before executing the following command.', +// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 518dcfdd..116f9d97 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -33,8 +33,12 @@ export default { 'clusters.create.execCommand': '执行命令', 'clusters.create.supportedGpu': '支持的 GPU', 'clusters.create.skipfornow': '暂时跳过', - 'clusters.create.noImages': '没有可用的镜像', - 'clusters.create.noInstanceTypes': '没有可用的实例类型', - 'clusters.create.noRegions': '没有可用的区域', - 'clusters.workerpool.batchSize.desc': 'Worker池中同时创建的worker数量' + 'clusters.create.noImages': '无可用的镜像', + 'clusters.create.noInstanceTypes': '无可用的实例类型', + 'clusters.create.noRegions': '无可用的区域', + 'clusters.workerpool.batchSize.desc': 'Worker 池中同时创建的 worker 数量', + 'clusters.create.addworker.tips': + '在执行以下命令之前,请确保已满足 {label} 的先决条件。', + 'clusters.create.addCommand.tips': + ' 在需要添加的 Worker 上运行以下命令,将其加入到集群中' }; diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index d614c1b0..5163b1a6 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -187,7 +187,7 @@ const ClusterCreate = () => { } }; - const handleSelectProvider = (value: ProviderType) => { + const handleSelectProvider = (value: string) => { if (value === extraData.provider) { onNext(); return; diff --git a/src/pages/cluster-management/components/add-worker-command.tsx b/src/pages/cluster-management/components/add-worker-command.tsx index 8fbf4170..90a5716e 100644 --- a/src/pages/cluster-management/components/add-worker-command.tsx +++ b/src/pages/cluster-management/components/add-worker-command.tsx @@ -25,7 +25,7 @@ const AddWorkerCommand: React.FC = ({ registrationInfo }) => { return ( diff --git a/src/pages/cluster-management/components/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker-step.tsx index f0e8b8b1..c9a85c79 100644 --- a/src/pages/cluster-management/components/add-worker-step.tsx +++ b/src/pages/cluster-management/components/add-worker-step.tsx @@ -1,4 +1,6 @@ +import { BulbOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; +import { Alert } from 'antd'; import React from 'react'; import styled from 'styled-components'; import { ProviderType, ProviderValueMap } from '../config'; @@ -9,7 +11,42 @@ import SupportedHardware from './support-hardware'; const Title = styled.div` font-size: 16px; font-weight: 600; - margin-bottom: 24px; + margin-bottom: 16px; +`; + +const Line = styled.div` + position: relative; + margin: 24px 0; + width: 100%; + border-top: 1px solid var(--ant-color-border); + &::before { + content: ''; + position: absolute; + top: -9px; + left: 24px; + width: 16px; + height: 16px; + transform: rotate(45deg); + background-color: var(--ant-color-bg-container); + border-top: 1px solid var(--ant-color-border); + border-left: 1px solid var(--ant-color-border); + } +`; + +const Container = styled.div` + width: 800px; + margin: 0 auto; + .command-info { + margin-top: 24px; + margin-bottom: 8px; + // font-weight: 500; + color: var(--ant-color-text-secondary); + } +`; + +const Content = styled.div` + margin-top: 24px; + margin-bottom: 16px; `; type AddModalProps = { @@ -26,19 +63,57 @@ const AddWorkerStep: React.FC = ({ registrationInfo }) => { const intl = useIntl(); + const [currentProvider, setCurrentProvider] = React.useState('cuda'); + const [workerCommand, setWorkerCommand] = React.useState>( + { + label: 'NVIDIA CUDA', + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda' + } + ); + + const handleSelectProvider = (provider: string, item: any) => { + setCurrentProvider(provider); + setWorkerCommand(item); + }; + return ( -
- {intl.formatMessage({ id: 'clusters.create.execCommand' })} + + + {intl.formatMessage({ id: 'clusters.create.supportedGpu' })} + + + {provider === ProviderValueMap.Custom && ( + + + } + message={ + + } + > + + )} +
+ {intl.formatMessage({ id: 'clusters.create.addCommand.tips' })} +
{provider === ProviderValueMap.Kubernetes ? ( ) : ( )} - - {intl.formatMessage({ id: 'clusters.create.supportedGpu' })} - - -
+ ); }; diff --git a/src/pages/cluster-management/components/cloud-provider-form.tsx b/src/pages/cluster-management/components/cloud-provider-form.tsx index 65103fc5..fa7ca274 100644 --- a/src/pages/cluster-management/components/cloud-provider-form.tsx +++ b/src/pages/cluster-management/components/cloud-provider-form.tsx @@ -1,10 +1,12 @@ +import { fromClusterCreationAtom } from '@/atoms/clusters'; import SealSelect from '@/components/seal-form/seal-select'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; import { LoadingOutlined } from '@ant-design/icons'; -import { useIntl } from '@umijs/max'; +import { Link, useIntl } from '@umijs/max'; import { Form } from 'antd'; +import { useAtom } from 'jotai'; import _ from 'lodash'; import React, { useEffect } from 'react'; import styled from 'styled-components'; @@ -14,7 +16,7 @@ import { useProviderRegions } from '../hooks/use-provider-regions'; type OptionData = { label: string; datacenter: string; - value: string; + value: string | number; icon: string; }; @@ -70,14 +72,24 @@ const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => { ); }; -const optionRender = ( - option: Global.BaseOption< - number, - { - data: OptionData; - } - > -): React.ReactNode => { +const NotFoundCredentialContent: React.FC = () => { + const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom); + const intl = useIntl(); + const handleOnClick = () => { + console.log('click add credential'); + setFromClusterCreation(true); + }; + + return ( + + + {intl.formatMessage({ id: 'clusters.button.addCredential' })} + + + ); +}; + +const optionRender = (option: any): React.ReactNode => { const { value } = option; const data = option.data!; @@ -127,8 +139,8 @@ const CloudProvider: React.FC = (props) => { }, [credentialID]); const labelRender = (props: { - label: string; - value: string; + label: React.ReactNode; + value: string | number; }): React.ReactNode => { const data = regions.find((item) => item.value === props.value); if (!data) return props.label; @@ -164,6 +176,7 @@ const CloudProvider: React.FC = (props) => { ]} > } disabled={action === PageAction.EDIT} label={intl.formatMessage({ id: 'clusters.credential.title' })} required diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index f344c8ac..a3e8c7c4 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -72,7 +72,7 @@ const OptionItem = styled.div.attrs({ const DescriptionWrapper = styled.div` display: grid; grid-template-columns: 1fr 1fr 1fr; - gap: 8px; + gap: 4px 8px; font-weight: 400; `; @@ -94,34 +94,48 @@ const NotFoundImageContent = () => { ); }; -export const RenderInstanceOption = (option: any) => { - const { data, styles } = option; +const RenderLabel = (data: { + label: React.ReactNode; + vendor: string; + style?: React.CSSProperties; +}) => { + const { label, vendor, style } = data; + return ( +
+ + {label} +
+ ); +}; +export const RenderOption = (option: any) => { + const { data = {}, styles } = option; + const entries = Object.entries(data?.specInfo || {}); return ( - - {data.description} - + } description={ - - {Object.entries(data?.specInfo) - .filter(([key, value]) => value) - .map(([key, value]) => ( - - - {_.get(instanceTypeFieldMap, key, key)}: - - {value as string} - - ))} - + entries.length > 0 && ( + + {Object.entries(data?.specInfo) + .filter(([key, value]) => value) + .map(([key, value]) => ( + + + {_.get(instanceTypeFieldMap, key, key)}: + + {value as string} + + ))} + + ) } /> ); @@ -186,19 +200,45 @@ const PoolForm: React.FC = forwardRef((props, ref) => { } }, [currentData]); - const imageLabelRender = (data: { label: string; value: string }) => { - console.log('imageLabelRender========', data); + const imageLabelRender = (data: { + label: React.ReactNode; + value: string | number; + }) => { if (action === PageAction.EDIT) { - return currentData?.image_name || currentData?.os_image; + console.log('imageLabelRender========::', action, currentData, data); + // return currentData?.image_name || currentData?.os_image; + const vendor = _.split(currentData?.image_name || '', ' ')[0]; + const iconType = _.get(vendorIconMap, vendor.toLowerCase()); + return ( +
+ {iconType && } + {currentData?.image_name || currentData?.os_image} +
+ ); } - return data.label; + const selectImage = osImageList.find((item) => item.value === data.value); + console.log('imageLabelRender========::', selectImage); + if (selectImage) { + return ( + + ); + } + return data.value; }; - const instanceLabelRender = (data: { label: string; value: string }) => { - if (action === PageAction.EDIT) { - return currentData?.instance_spec?.label || currentData?.instance_type; - } - return data.label; + const instanceLabelRender = (data: { + label: React.ReactNode; + value: string | number; + }) => { + const currentInstanceSpec = + instanceTypeList.find((item) => item.value === data.value) || + instanceSpec; + return ( + + ); }; const handleOsImageChange = (value: string) => { @@ -329,7 +369,7 @@ const PoolForm: React.FC = forwardRef((props, ref) => { required options={instanceTypeList} disabled={action === PageAction.EDIT} - optionRender={RenderInstanceOption} + optionRender={RenderOption} onChange={handleInstanceTypeChange} >
@@ -383,11 +423,16 @@ const PoolForm: React.FC = forwardRef((props, ref) => { > } onChange={handleOsImageChange} filterOption={filterImageOption} - optionRender={RenderInstanceOption} + optionRender={(option) => + RenderOption({ + ...option, + styles: { header: { marginBlock: 5 } } + }) + } labelRender={imageLabelRender} + placeholder={currentData?.image_name} options={osImageList} disabled={action === PageAction.EDIT} label={intl.formatMessage({ diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 444b6b40..431b9035 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -4,14 +4,20 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { ProviderType } from '../config'; -const Wrapper = styled.div` +const Wrapper = styled.div<{ $cols?: number }>` + width: 100%; display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 22px; + grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr); + gap: 16px; + .template-card-wrapper { + padding: 16px; + } `; const Container = styled.div` display: flex; + width: 800px; + margin: 0 auto; flex-direction: column; gap: 16px; `; @@ -22,19 +28,20 @@ const Title = styled.span` justify-content: space-between; font-weight: 700; font-size: 16px; - margin-block: 16px 24px; + margin-block: 20px 16px; `; interface ProviderCatalogProps { - onSelect?: (provider: ProviderType) => void; - currentProvider?: ProviderType; + onSelect?: (provider: string, item: any) => void; + cols?: number; + currentProvider?: ProviderType | string; clickable?: boolean; dataList: { label: string; key: string; locale?: boolean; disabled?: boolean; - icon: React.ReactNode; + icon?: React.ReactNode; description?: string; group?: string; }[]; @@ -42,6 +49,7 @@ interface ProviderCatalogProps { const ProviderCatalog: React.FC = ({ onSelect, + cols = 3, dataList, clickable, currentProvider @@ -66,12 +74,12 @@ const ProviderCatalog: React.FC = ({ {groupName !== 'default' && ( {intl.formatMessage({ id: groupName })} )} - + {items?.map((action) => ( onSelect?.(action.key as ProviderType)} + onClick={() => onSelect?.(action.key as string, action)} active={currentProvider === action.key} disabled={action.disabled} clickable={clickable} @@ -82,7 +90,7 @@ const ProviderCatalog: React.FC = ({ } icon={action.icon} > - {action.description || 'This is a description'} + {action.description} ))} diff --git a/src/pages/cluster-management/components/support-hardware.tsx b/src/pages/cluster-management/components/support-hardware.tsx index 0d96d2db..6fdea611 100644 --- a/src/pages/cluster-management/components/support-hardware.tsx +++ b/src/pages/cluster-management/components/support-hardware.tsx @@ -4,8 +4,8 @@ import hyponPNG from '@/assets/logo/hygon.png'; import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; import metaxLogo from '@/assets/logo/metax.png'; import moorePNG from '@/assets/logo/moore _threads.png'; -import nvidiaLogo from '@/assets/logo/nvidia.png'; import IconFont from '@/components/icon-font'; +import styled from 'styled-components'; import ProviderCatalog from './provider-catalog'; const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => { @@ -24,15 +24,19 @@ const supportedHardPlatforms = [ { label: 'NVIDIA CUDA', value: 'cuda', - key: 'nvidia', + description: '', + key: 'cuda', locale: false, - icon: + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda', + icon: }, { label: 'AMD ROCm', + description: '', value: 'rocm', - key: 'amd', + key: 'rocm', locale: false, + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#amd-rocm', icon: ( }, { label: 'Hygon DTK', + description: '', value: 'dcu', - key: 'hygon', + key: 'dcu', locale: false, + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#hygon-dtk', icon: }, { - label: 'Moore Threads MUSA', + label: 'Moore Threads', + description: '', value: 'musa', key: 'musa', locale: false, + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#moore-threads-musa', icon: }, { label: 'Iluvatar Corex', + description: '', value: 'corex', key: 'corex', locale: false, + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#iluvatar-corex', icon: }, { @@ -73,6 +85,7 @@ const supportedHardPlatforms = [ value: 'cambricon', key: 'cambricon', locale: false, + link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#cambricon-mlu', icon: }, { @@ -84,9 +97,50 @@ const supportedHardPlatforms = [ } ]; -const SupportedHardware = () => { +const Header = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + justify-content: center; + align-self: center; +`; + +const renderHeader = (item: any) => { return ( - +
+ {item.icon} + {item.label} +
+ ); +}; + +const dataList = supportedHardPlatforms.map((item) => { + return { + label: renderHeader(item), + value: item.value, + key: item.key, + locale: item.locale + }; +}); + +interface SupportedHardwareProps { + onSelect?: (provider: string, item: any) => void; + currentProvider?: string; +} + +const SupportedHardware: React.FC = ({ + onSelect, + currentProvider +}) => { + return ( + ); }; diff --git a/src/pages/cluster-management/credentials.tsx b/src/pages/cluster-management/credentials.tsx index 958b12e2..c32f1f47 100644 --- a/src/pages/cluster-management/credentials.tsx +++ b/src/pages/cluster-management/credentials.tsx @@ -1,3 +1,4 @@ +import { fromClusterCreationAtom } from '@/atoms/clusters'; import DeleteModal from '@/components/delete-modal'; import IconFont from '@/components/icon-font'; import { FilterBar } from '@/components/page-tools'; @@ -5,9 +6,10 @@ import { PageAction } from '@/config'; import type { PageActionType } from '@/config/types'; import useTableFetch from '@/hooks/use-table-fetch'; import { PageContainer } from '@ant-design/pro-components'; -import { useIntl } from '@umijs/max'; +import { useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { ConfigProvider, Empty, Table, message } from 'antd'; +import { useAtom } from 'jotai'; import { useState } from 'react'; import { createCredential, @@ -52,8 +54,9 @@ const Credentials: React.FC = () => { deleteAPI: deleteCredential, contentForDelete: 'menu.clusterManagement.credentials' }); - + const [isFromCluster] = useAtom(fromClusterCreationAtom); const intl = useIntl(); + const navigate = useNavigate(); const [openModalStatus, setOpenModalStatus] = useState<{ provider: ProviderType; open: boolean; @@ -97,6 +100,9 @@ const Credentials: React.FC = () => { }); } else { await createCredential({ data: params }); + // if (isFromCluster) { + // navigate(-1); + // } } fetchData(); setOpenModalStatus({ ...openModalStatus, open: false }); diff --git a/src/pages/cluster-management/hooks/use-pools-columns.tsx b/src/pages/cluster-management/hooks/use-pools-columns.tsx index 234275f8..c50580b9 100644 --- a/src/pages/cluster-management/hooks/use-pools-columns.tsx +++ b/src/pages/cluster-management/hooks/use-pools-columns.tsx @@ -1,13 +1,13 @@ import AutoTooltip from '@/components/auto-tooltip'; import DropdownButtons from '@/components/drop-down-buttons'; +import { SealColumnProps } from '@/components/seal-table/types'; import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { ColumnsType } from 'antd/es/table'; import type { SortOrder } from 'antd/es/table/interface'; import dayjs from 'dayjs'; import _ from 'lodash'; import { useMemo } from 'react'; -import { RenderInstanceOption } from '../components/pool-form'; +import { RenderOption } from '../components/pool-form'; import { NodePoolListItem as ListItem } from '../config/types'; const actionItems = [ @@ -30,7 +30,7 @@ const actionItems = [ const usePoolsColumns = ( handleSelect: (val: string, record: ListItem) => void, sortOrder?: SortOrder -): ColumnsType => { +): SealColumnProps[] => { const intl = useIntl(); return useMemo(() => { @@ -66,7 +66,7 @@ const usePoolsColumns = ( render: (text: string, record: ListItem) => ( { if (spec.vram) parts.push(`${spec.vram} VRAM`); if (spec.vcpus) parts.push(`${spec.vcpus} vCPUs`); - if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`); if (spec.ram) parts.push(`${spec.ram} RAM`); - if (spec.scratchDisk) { - parts.push(`${spec.scratchDisk} Scratch disk`); - } + // if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`); + // if (spec.scratchDisk) { + // parts.push(`${spec.scratchDisk} Scratch disk`); + // } return parts.join(' / '); }; +const formatLabel = (instanceSpec: any): string => { + return `${_.toUpper(instanceSpec.gpu_info?.model.replace(/_/g, ' '))}`; +}; + export const useProviderRegions = () => { const [regions, setRegions] = useAtom(regionListAtom); const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom); @@ -119,10 +123,12 @@ export const useProviderRegions = () => { ?.filter((sItem: any) => sItem.gpu_info && sItem.available) .map((item: any) => { const specInfo = parseSpec(item); + const label = formatLabel(item); + const description = `${label} ${item.gpu_info?.count}X`; return { - label: formatSpec(specInfo), + label: `${description} - ${formatSpec(specInfo)}`, value: item.slug, - description: item.description, + description: description, specInfo: specInfo, vendor: _.get(_.split(item.gpu_info?.model, '_'), 0), available: item.available, @@ -148,10 +154,7 @@ export const useProviderRegions = () => { name: item.name, description: item.description, vendor: _.camelCase(item.distribution), - specInfo: { - size: `${item.size_gigabytes} GiB`, - minDiskSize: `${item.min_disk_size} GiB` - }, + specInfo: {}, regions: item.regions || [] }; }); diff --git a/src/pages/cluster-management/step-forms/basic-form.tsx b/src/pages/cluster-management/step-forms/basic-form.tsx index 0071e691..a7b52860 100644 --- a/src/pages/cluster-management/step-forms/basic-form.tsx +++ b/src/pages/cluster-management/step-forms/basic-form.tsx @@ -41,7 +41,7 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => { return (
{intl.formatMessage({ id: 'clusters.create.configBasic' })} diff --git a/src/pages/llmodels/components/gpu-card.tsx b/src/pages/llmodels/components/gpu-card.tsx index a602e85d..5ccd87d3 100644 --- a/src/pages/llmodels/components/gpu-card.tsx +++ b/src/pages/llmodels/components/gpu-card.tsx @@ -8,6 +8,7 @@ import '../style/gpu-card.less'; const CardWrapper = styled.div` display: flex; + gap: 10px; flex-direction: column; align-items: flex-start; justify-content: center; @@ -18,7 +19,6 @@ const CardWrapper = styled.div` const Header = styled.div` width: 100%; - margin-bottom: 10px; `; const Description = styled.div` @@ -36,7 +36,7 @@ export const CardContainer: React.FC<{ return (
{header}
- {description} + {description && {description}}
); }; diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index 5a4b6d50..61886550 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -64,7 +64,9 @@ export const addWorkerGuide: Record = { --ipc=host \\ -v gpustack-data:/var/lib/gpustack \\ ${params.image} \\ - --server-url ${params.server} --registration-token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} \\ + --registration-token ${params.token} \\ + --worker-ip ${params.workerip}`; } }, npu: {