diff --git a/src/assets/logo/jupyter.png b/src/assets/logo/jupyter.png new file mode 100644 index 00000000..e3297c1f Binary files /dev/null and b/src/assets/logo/jupyter.png differ diff --git a/src/assets/logo/pytorch_black.png b/src/assets/logo/pytorch_black.png new file mode 100644 index 00000000..f729f096 Binary files /dev/null and b/src/assets/logo/pytorch_black.png differ diff --git a/src/assets/logo/tensorflow.svg b/src/assets/logo/tensorflow.svg new file mode 100644 index 00000000..c0778626 --- /dev/null +++ b/src/assets/logo/tensorflow.svg @@ -0,0 +1 @@ + diff --git a/src/assets/logo/ubuntu.png b/src/assets/logo/ubuntu.png new file mode 100644 index 00000000..22875fcb Binary files /dev/null and b/src/assets/logo/ubuntu.png differ diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 8f76a8b0..6e64d2dd 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -358,7 +358,7 @@ export default (props: any) => { config={{ apiBaseUrl: GPUSTACK_API_BASE_URL, theme: userSettings.theme, - iconUrl: '//at.alicdn.com/t/c/font_4613488_r6z6oew38db.js', + iconUrl: '//at.alicdn.com/t/c/font_4613488_vy4szhexci.js', isDarkTheme: userSettings.isDarkTheme, defaultColorPrimary: COLOR_PRIMARY }} diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index a15a7ff5..ec166546 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -15,10 +15,16 @@ export default { 'gpuservice.template.mountPath': 'Mount Path', 'gpuservice.template.containerDisk': 'Container Disk (GB)', 'gpuservice.template.memory': 'Memory (GB)', + 'gpuservice.template.displayName': 'Display Name', + 'gpuservice.template.displayName.max': + 'Display name cannot exceed 63 characters.', 'gpuservice.template.ports': 'Ports', 'gpuservice.template.ports.add': 'Add Port', 'gpuservice.template.ports.invalid': 'Please complete the port configuration.', + 'gpuservice.template.ports.name': 'Name', + 'gpuservice.template.ports.name.max': + 'Port name cannot exceed 16 characters.', 'gpuservice.template.env': 'Environment Variables', 'gpuservice.template.env.add': 'Add Environment Variable', 'gpuservice.template.env.invalid': @@ -53,8 +59,7 @@ export default { 'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.ram': 'RAM', - 'gpuservice.instance.search.type.placeholder': - 'Search by name, VRAM, memory or vCPU', + 'gpuservice.instance.search.type.placeholder': 'Search by name', 'gpuservice.instance.search.template.placeholder': 'Search by template name, image or mount path', 'gpuservice.instance.template.image': 'Image', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index ed59c68e..645372b9 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -15,9 +15,15 @@ export default { 'gpuservice.template.mountPath': 'マウントパス', 'gpuservice.template.containerDisk': 'コンテナディスク (GB)', 'gpuservice.template.memory': 'メモリ (GB)', + 'gpuservice.template.displayName': '表示名', + 'gpuservice.template.displayName.max': + '表示名は 63 文字以内で入力してください。', 'gpuservice.template.ports': 'ポート', 'gpuservice.template.ports.add': 'ポートを追加', 'gpuservice.template.ports.invalid': 'ポート設定を完成させてください。', + 'gpuservice.template.ports.name': '名前', + 'gpuservice.template.ports.name.max': + 'ポート名は 16 文字以内で入力してください。', 'gpuservice.template.env': '環境変数', 'gpuservice.template.env.add': '環境変数を追加', 'gpuservice.template.env.invalid': '環境変数を完成させてください。', @@ -51,8 +57,7 @@ export default { 'gpuservice.instance.sliced': '分割', 'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.ram': 'RAM', - 'gpuservice.instance.search.type.placeholder': - '名前、VRAM、メモリまたは vCPU で検索', + 'gpuservice.instance.search.type.placeholder': '名前で検索', 'gpuservice.instance.search.template.placeholder': 'テンプレート名、イメージまたはマウントパスで検索', 'gpuservice.instance.template.image': 'イメージ', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 06fb38c0..a5efd9c1 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -16,9 +16,15 @@ export default { 'gpuservice.template.mountPath': 'Путь монтирования', 'gpuservice.template.containerDisk': 'Диск контейнера (GB)', 'gpuservice.template.memory': 'Память (GB)', + 'gpuservice.template.displayName': 'Отображаемое имя', + 'gpuservice.template.displayName.max': + 'Отображаемое имя не должно превышать 63 символа.', 'gpuservice.template.ports': 'Порты', 'gpuservice.template.ports.add': 'Добавить порт', 'gpuservice.template.ports.invalid': 'Заполните настройки портов полностью.', + 'gpuservice.template.ports.name': 'Имя', + 'gpuservice.template.ports.name.max': + 'Имя порта не должно превышать 16 символов.', 'gpuservice.template.env': 'Переменные окружения', 'gpuservice.template.env.add': 'Добавить переменную окружения', 'gpuservice.template.env.invalid': @@ -53,8 +59,7 @@ export default { 'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.memory': 'Память', 'gpuservice.instance.ram': 'RAM', - 'gpuservice.instance.search.type.placeholder': - 'Поиск по имени, VRAM, памяти или vCPU', + 'gpuservice.instance.search.type.placeholder': 'Поиск по имени', 'gpuservice.instance.search.template.placeholder': 'Поиск по имени шаблона, образу или пути монтирования', 'gpuservice.instance.template.image': 'Образ', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index b049fde1..7c05331a 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -15,10 +15,16 @@ export default { 'gpuservice.template.mountPath': 'Bağlama Yolu', 'gpuservice.template.containerDisk': 'Konteyner Diski (GB)', 'gpuservice.template.memory': 'Bellek (GB)', + 'gpuservice.template.displayName': 'Görünen Ad', + 'gpuservice.template.displayName.max': + 'Görünen ad 63 karakterden uzun olamaz.', 'gpuservice.template.ports': 'Bağlantı Noktaları', 'gpuservice.template.ports.add': 'Bağlantı Noktası Ekle', 'gpuservice.template.ports.invalid': 'Bağlantı noktası yapılandırmasını eksiksiz doldurun.', + 'gpuservice.template.ports.name': 'Ad', + 'gpuservice.template.ports.name.max': + 'Bağlantı noktası adı 16 karakterden uzun olamaz.', 'gpuservice.template.env': 'Ortam Değişkenleri', 'gpuservice.template.env.add': 'Ortam Değişkeni Ekle', 'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.', @@ -52,8 +58,7 @@ export default { 'gpuservice.instance.sliced': 'Bölünmüş', 'gpuservice.instance.memory': 'Bellek', 'gpuservice.instance.ram': 'RAM', - 'gpuservice.instance.search.type.placeholder': - "Ada, VRAM, belleğe veya vCPU'ya göre ara", + 'gpuservice.instance.search.type.placeholder': 'Ada göre ara', 'gpuservice.instance.search.template.placeholder': 'Şablon adına, imaja veya bağlama yoluna göre ara', 'gpuservice.instance.template.image': 'İmaj', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index 1fbd2d6c..7cb77080 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -15,9 +15,13 @@ export default { 'gpuservice.template.mountPath': '挂载路径', 'gpuservice.template.containerDisk': '容器磁盘(GB)', 'gpuservice.template.memory': '内存(GB)', + 'gpuservice.template.displayName': '显示名称', + 'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符', 'gpuservice.template.ports': '端口', 'gpuservice.template.ports.add': '添加端口', 'gpuservice.template.ports.invalid': '请填写完整的端口配置', + 'gpuservice.template.ports.name': '名称', + 'gpuservice.template.ports.name.max': '端口名称不能超过 16 个字符', 'gpuservice.template.env': '环境变量', 'gpuservice.template.env.add': '添加环境变量', 'gpuservice.template.env.invalid': '请填写完整的环境变量', @@ -50,7 +54,7 @@ export default { 'gpuservice.instance.sliced': '切分', 'gpuservice.instance.memory': '显存', 'gpuservice.instance.ram': '内存', - 'gpuservice.instance.search.type.placeholder': '搜索名称、显存、内存或 vCPU', + 'gpuservice.instance.search.type.placeholder': '搜索名称', 'gpuservice.instance.search.template.placeholder': '搜索模板名称、镜像或挂载路径', 'gpuservice.instance.template.image': '镜像', diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 888f6d1a..f9c0de6a 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -176,16 +176,7 @@ const AddModal: React.FC = ({ // filter instance types const filteredInstanceTypes = instanceTypeList.filter((item) => - matchKeyword( - [ - item.metadata?.name, - item.spec?.memory, - item.status?.cpu?.capacity, - item.status?.ram?.capacity, - item.status?.accelerator?.remaining - ], - instanceKeyword - ) + matchKeyword([item.metadata?.name], instanceKeyword) ); // filter templates based on selection and keyword @@ -256,17 +247,20 @@ const AddModal: React.FC = ({
- - {intl.formatMessage({ id: 'gpuservice.instance.types' })} -
+ + {intl.formatMessage({ id: 'gpuservice.instance.types' })} + } @@ -290,17 +284,20 @@ const AddModal: React.FC = ({
- - {intl.formatMessage({ id: 'gpuservice.instance.templates' })} -
+ + {intl.formatMessage({ id: 'gpuservice.instance.templates' })} + } diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index 94ecd3c2..f6423362 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -15,6 +15,7 @@ export interface FormData { ports: { port: number; protocol?: string; + name?: string; }[]; env: { name: string; diff --git a/src/pages/gpu-service/instances/forms/template-selector.tsx b/src/pages/gpu-service/instances/forms/template-selector.tsx index 704cbcb8..72380c07 100644 --- a/src/pages/gpu-service/instances/forms/template-selector.tsx +++ b/src/pages/gpu-service/instances/forms/template-selector.tsx @@ -68,7 +68,7 @@ const TemplateSelector: React.FC = ({
- {item.name} + {item.displayName || item.name || '-'}
diff --git a/src/pages/gpu-service/templates/components/template-card.tsx b/src/pages/gpu-service/templates/components/template-card.tsx index 32da7994..96041476 100644 --- a/src/pages/gpu-service/templates/components/template-card.tsx +++ b/src/pages/gpu-service/templates/components/template-card.tsx @@ -2,17 +2,27 @@ import ascendLogo from '@/assets/logo/ascend.png'; import CambriconPNG from '@/assets/logo/cambricon.png'; import hyponPNG from '@/assets/logo/hygon.png'; import iluvatarWEBP from '@/assets/logo/Iluvatar.png'; +import jupyterLogo from '@/assets/logo/jupyter.png'; import metaxLogo from '@/assets/logo/metax.png'; import mooreLogo from '@/assets/logo/moore-logo.png'; import nvidiaLogo from '@/assets/logo/nvidia.png'; +import pytorchBlackLogo from '@/assets/logo/pytorch_black.png'; +import sgLangLogo from '@/assets/logo/sglang.png'; import theadLogoEN from '@/assets/logo/t-head-en.png'; import theadLogoZH from '@/assets/logo/t-head-zh.png'; -import { manfacturerValueMap } from '@/pages/resources/config/gpu-driver'; +import tensorflowkLogo from '@/assets/logo/tensorflow.svg'; +import ubuntuLogo from '@/assets/logo/ubuntu.png'; +import vllmLogo from '@/assets/logo/vllm.png'; +import { + GPUsConfigs, + manfacturerValueMap +} from '@/pages/resources/config/gpu-driver'; import { AutoTooltip, DropdownActions, IconFont, - TemplateCard + TemplateCard, + ThemeTag } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Button, Tag } from 'antd'; @@ -20,6 +30,56 @@ import styled from 'styled-components'; import { templateActions } from '../config'; import { ListItem } from '../config/types'; +const imageLogoMap = { + vllm: vllmLogo, + sglang: sgLangLogo, + cann: ascendLogo, + jupyter: jupyterLogo, + pytorch: pytorchBlackLogo, + tensorflow: tensorflowkLogo, + ubuntu: ubuntuLogo +} as const; + +export const manufactureColorMap: Record = { + nvidia: 'green', + amd: 'volcano', + ascend: 'orange', + hygon: 'magenta', + moorthreads: 'cyan', + iluvatar: 'purple', + metax: 'geekblue', + cambricon: 'gold', + thead: 'red', + cpu: 'blue' +}; + +const manufacturerLabelMap: Record = Object.values( + GPUsConfigs +).reduce( + (acc, item) => { + if (item.gpuVendor) acc[item.gpuVendor] = item.label; + return acc; + }, + { cpu: 'CPU' } as Record +); + +const matchImageLogo = (image: string | undefined): string | null => { + if (!image) return null; + const lower = image.toLowerCase(); + let matched: keyof typeof imageLogoMap | null = null; + let earliest = Infinity; + (Object.keys(imageLogoMap) as Array).forEach( + (key) => { + const idx = lower.indexOf(key); + if (idx !== -1 && idx < earliest) { + earliest = idx; + matched = key; + } + } + ); + return matched ? imageLogoMap[matched] : null; +}; + const StyledCard = styled(TemplateCard)` &:hover { .operations { @@ -38,6 +98,8 @@ const Header = styled.div` .title { display: flex; align-items: center; + gap: 8px; + min-width: 0; } `; @@ -73,7 +135,7 @@ const InfoItem = styled.div` color: var(--ant-color-text-quaternary); } .value { - color: var(--ant-color-text-secondary); + color: var(--ant-color-text-tertiary); } `; @@ -98,6 +160,10 @@ const TemplateCardItem: React.FC = ({ data, onSelect }) => { }; const renderLogo = () => { + const imageLogo = matchImageLogo(data.spec?.image); + if (imageLogo) { + return ; + } switch (data.manufacturer) { case manfacturerValueMap.NVIDIA: return ; @@ -141,6 +207,19 @@ const TemplateCardItem: React.FC = ({ data, onSelect }) => { } }; + const renderManufacturerTag = () => { + if (!data.manufacturer) return null; + const label = + manufacturerLabelMap[data.manufacturer] ?? + data.manufacturer.toUpperCase(); + const color = manufactureColorMap[data.manufacturer] ?? 'purple'; + return ( + + {label} + + ); + }; + const renderActions = () => { return ( @@ -177,8 +256,9 @@ const TemplateCardItem: React.FC = ({ data, onSelect }) => { - {data.name || '-'} + {data.displayName || data.name || '-'} + {renderManufacturerTag()} diff --git a/src/pages/gpu-service/templates/config/types.ts b/src/pages/gpu-service/templates/config/types.ts index db261d35..528ba03d 100644 --- a/src/pages/gpu-service/templates/config/types.ts +++ b/src/pages/gpu-service/templates/config/types.ts @@ -1,6 +1,7 @@ export interface PortItem { protocol: 'UDP' | 'TCP'; port: number; + name?: string; } export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never'; @@ -13,6 +14,7 @@ export interface EnvItem { export interface FormData { name: string; description: string; + displayName?: string; manufacturer: string; spec: { image: string; diff --git a/src/pages/gpu-service/templates/forms/basic.tsx b/src/pages/gpu-service/templates/forms/basic.tsx index e2cf4759..1174533b 100644 --- a/src/pages/gpu-service/templates/forms/basic.tsx +++ b/src/pages/gpu-service/templates/forms/basic.tsx @@ -76,6 +76,25 @@ const Basic: React.FC = ({ page = 'template', onceMaxRequest }) => { required /> + + name="displayName" + rules={[ + { + max: 63, + message: intl.formatMessage({ + id: 'gpuservice.template.displayName.max' + }) + } + ]} + > + + name="manufacturer" rules={[ diff --git a/src/pages/gpu-service/templates/forms/index.tsx b/src/pages/gpu-service/templates/forms/index.tsx index 09b2246a..e9eb359f 100644 --- a/src/pages/gpu-service/templates/forms/index.tsx +++ b/src/pages/gpu-service/templates/forms/index.tsx @@ -36,6 +36,7 @@ const GPUServiceTemplateForm: React.FC = forwardRef( const handleFinish = async (values: FormData) => { await onFinish({ ...values, + displayName: values.displayName?.trim() || values.name, spec: { ...values.spec, command: values.spec?.command?.filter(Boolean) ?? [] @@ -65,7 +66,8 @@ const GPUServiceTemplateForm: React.FC = forwardRef( ports: [ { protocol: 'TCP', - port: 22 + port: 22, + name: 'SSH' } ], volumeMount: '/workspace', diff --git a/src/pages/gpu-service/templates/forms/ports.tsx b/src/pages/gpu-service/templates/forms/ports.tsx index 31d9514a..458d1368 100644 --- a/src/pages/gpu-service/templates/forms/ports.tsx +++ b/src/pages/gpu-service/templates/forms/ports.tsx @@ -1,14 +1,18 @@ import { + Input as CInput, InputNumber as CInputNumber, MetadataList, Select as SealSelect } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; +import { useEffect, useRef, useState } from 'react'; import { FormData, PortItem as PortItemType } from '../config/types'; type PortProtocol = PortItemType['protocol']; +const PORT_NAME_MAX = 16; + const protocolOptions = [ { label: 'UDP', @@ -20,43 +24,154 @@ const protocolOptions = [ } ]; +const wellKnownTcpPortNames: Record = { + 22: 'SSH', + 80: 'HTTP', + 443: 'HTTPS' +}; + +const getAutoFilledName = ( + protocol: PortProtocol | undefined, + port: number | undefined, + previousProtocol: PortProtocol | undefined, + previousPort: number | undefined, + currentName: string | undefined +): string | undefined => { + if (protocol !== 'TCP' || !port) { + return currentName; + } + const suggested = wellKnownTcpPortNames[port]; + if (!suggested) { + return currentName; + } + const previousSuggested = + previousProtocol === 'TCP' && previousPort + ? wellKnownTcpPortNames[previousPort] + : undefined; + // Only overwrite when the name is empty or matches a previously suggested + // well-known name — never stomp on a user-entered value. + if (!currentName || currentName === previousSuggested) { + return suggested; + } + return currentName; +}; + interface PortItemProps { item: PortItemType; index: number; onChange: (item: PortItemType) => void; } +interface PortNameInputProps { + value: string | undefined; + onChange: (value: string) => void; + placeholder: string; +} + +const PortNameInput: React.FC = ({ + value, + onChange, + placeholder +}) => { + const [localValue, setLocalValue] = useState(value ?? ''); + const isComposingRef = useRef(false); + + useEffect(() => { + if (!isComposingRef.current) { + setLocalValue(value ?? ''); + } + }, [value]); + + return ( + { + isComposingRef.current = true; + }} + onCompositionEnd={(e) => { + isComposingRef.current = false; + const next = (e.target as HTMLInputElement).value; + setLocalValue(next); + onChange(next); + }} + onChange={(e) => { + const next = e.target.value; + setLocalValue(next); + if (!isComposingRef.current) { + onChange(next); + } + }} + style={{ width: '100%' }} + /> + ); +}; + const PortItem: React.FC = ({ item, index, onChange }) => { + const intl = useIntl(); return (
-
+
{ + const nextProtocol = value as PortProtocol; onChange({ ...item, - protocol: value as PortProtocol + protocol: nextProtocol, + name: getAutoFilledName( + nextProtocol, + item.port, + item.protocol, + item.port, + item.name + ) }); }} style={{ width: '100%' }} >
-
+
{ + const nextPort = + typeof value === 'number' ? value : (undefined as any); onChange({ ...item, - port: typeof value === 'number' ? value : (undefined as any) + port: nextPort, + name: getAutoFilledName( + item.protocol, + nextPort, + item.protocol, + item.port, + item.name + ) }); }} style={{ width: '100%' }} />
+
+ { + onChange({ + ...item, + name: next + }); + }} + /> +
); }; @@ -75,7 +190,8 @@ const Ports: React.FC = () => { ...ports, { protocol: 'TCP', - port: undefined as any + port: undefined as any, + name: '' } ]); }; @@ -113,6 +229,19 @@ const Ports: React.FC = () => { ) ); } + const hasInvalidName = value.some( + (item: PortItemType) => + item.name && item.name.length > PORT_NAME_MAX + ); + if (hasInvalidName) { + return Promise.reject( + new Error( + intl.formatMessage({ + id: 'gpuservice.template.ports.name.max' + }) + ) + ); + } return Promise.resolve(); } }