feat: template port name
This commit is contained in:
@@ -176,16 +176,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
|
||||
// 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<AddModalProps> = ({
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
|
||||
</ColTitle>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
@@ -290,17 +284,20 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.templates' })}
|
||||
</ColTitle>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.templates' })}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface FormData {
|
||||
ports: {
|
||||
port: number;
|
||||
protocol?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
env: {
|
||||
name: string;
|
||||
|
||||
@@ -68,7 +68,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
||||
<TemplateContent>
|
||||
<div className="name">
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{item.name}
|
||||
{item.displayName || item.name || '-'}
|
||||
</AutoTooltip>
|
||||
</div>
|
||||
<div className="info">
|
||||
|
||||
@@ -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<string, string> = {
|
||||
nvidia: 'green',
|
||||
amd: 'volcano',
|
||||
ascend: 'orange',
|
||||
hygon: 'magenta',
|
||||
moorthreads: 'cyan',
|
||||
iluvatar: 'purple',
|
||||
metax: 'geekblue',
|
||||
cambricon: 'gold',
|
||||
thead: 'red',
|
||||
cpu: 'blue'
|
||||
};
|
||||
|
||||
const manufacturerLabelMap: Record<string, string> = Object.values(
|
||||
GPUsConfigs
|
||||
).reduce(
|
||||
(acc, item) => {
|
||||
if (item.gpuVendor) acc[item.gpuVendor] = item.label;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 'CPU' } as Record<string, string>
|
||||
);
|
||||
|
||||
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<keyof typeof imageLogoMap>).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<TemplateCardProps> = ({ data, onSelect }) => {
|
||||
};
|
||||
|
||||
const renderLogo = () => {
|
||||
const imageLogo = matchImageLogo(data.spec?.image);
|
||||
if (imageLogo) {
|
||||
return <LogoImg src={imageLogo} height={22} />;
|
||||
}
|
||||
switch (data.manufacturer) {
|
||||
case manfacturerValueMap.NVIDIA:
|
||||
return <LogoImg src={nvidiaLogo} height={18} />;
|
||||
@@ -141,6 +207,19 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderManufacturerTag = () => {
|
||||
if (!data.manufacturer) return null;
|
||||
const label =
|
||||
manufacturerLabelMap[data.manufacturer] ??
|
||||
data.manufacturer.toUpperCase();
|
||||
const color = manufactureColorMap[data.manufacturer] ?? 'purple';
|
||||
return (
|
||||
<ThemeTag color={color} style={{ fontWeight: 400 }}>
|
||||
{label}
|
||||
</ThemeTag>
|
||||
);
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
return (
|
||||
<span onClick={handleonClickAction} className="operations">
|
||||
@@ -177,8 +256,9 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
||||
<Content>
|
||||
<CardName>
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{data.name || '-'}
|
||||
{data.displayName || data.name || '-'}
|
||||
</AutoTooltip>
|
||||
{renderManufacturerTag()}
|
||||
</CardName>
|
||||
<InfoItem>
|
||||
<span>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -76,6 +76,25 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="displayName"
|
||||
rules={[
|
||||
{
|
||||
max: 63,
|
||||
message: intl.formatMessage({
|
||||
id: 'gpuservice.template.displayName.max'
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInput.Input
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.template.displayName'
|
||||
})}
|
||||
showCount
|
||||
maxLength={63}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="manufacturer"
|
||||
rules={[
|
||||
|
||||
@@ -36,6 +36,7 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = 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<TemplateFormProps> = forwardRef(
|
||||
ports: [
|
||||
{
|
||||
protocol: 'TCP',
|
||||
port: 22
|
||||
port: 22,
|
||||
name: 'SSH'
|
||||
}
|
||||
],
|
||||
volumeMount: '/workspace',
|
||||
|
||||
@@ -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<number, string> = {
|
||||
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<PortNameInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState<string>(value ?? '');
|
||||
const isComposingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isComposingRef.current) {
|
||||
setLocalValue(value ?? '');
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<CInput.Input
|
||||
showCount
|
||||
maxLength={PORT_NAME_MAX}
|
||||
value={localValue}
|
||||
placeholder={placeholder}
|
||||
onCompositionStart={() => {
|
||||
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<PortItemProps> = ({ item, index, onChange }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ width: 120 }}>
|
||||
<SealSelect
|
||||
value={item.protocol}
|
||||
options={protocolOptions}
|
||||
onChange={(value) => {
|
||||
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%' }}
|
||||
></SealSelect>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ width: 120 }}>
|
||||
<CInputNumber
|
||||
min={1}
|
||||
max={65535}
|
||||
precision={0}
|
||||
value={item.port}
|
||||
onChange={(value) => {
|
||||
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%' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<PortNameInput
|
||||
value={item.name}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.template.ports.name'
|
||||
})}
|
||||
onChange={(next) => {
|
||||
onChange({
|
||||
...item,
|
||||
name: next
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user