fix: alignment api
This commit is contained in:
@@ -27,3 +27,14 @@ export const initialPasswordAtom = atomWithStorage<string>(
|
||||
'initialPassword',
|
||||
''
|
||||
);
|
||||
|
||||
export const getCurrentOrganizationId = (): string => {
|
||||
try {
|
||||
const raw = localStorage.getItem('currentOrganizationId');
|
||||
if (raw) {
|
||||
const value = JSON.parse(raw);
|
||||
if (value != null) return String(value);
|
||||
}
|
||||
} catch {}
|
||||
return 'gpustack-organization-1';
|
||||
};
|
||||
|
||||
@@ -356,7 +356,7 @@ export default (props: any) => {
|
||||
config={{
|
||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||
theme: userSettings.theme,
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_nujs54seb6.js',
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_t4uxqkp138r.js',
|
||||
isDarkTheme: userSettings.isDarkTheme,
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
}}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'gpuservice.template.imagePullPolicy.never': 'Never',
|
||||
'gpuservice.template.command': 'Container Startup Command',
|
||||
'gpuservice.template.command.placeholder':
|
||||
'One argument per line, e.g.:\n/bin/bash\n-c\nyour command',
|
||||
'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Mount Path',
|
||||
'gpuservice.template.containerDisk': 'Container Disk (Gi)',
|
||||
'gpuservice.template.memory': 'Memory (Gi)',
|
||||
@@ -48,14 +48,16 @@ export default {
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'The current instance type supports at most {count} GPU(s)',
|
||||
'gpuservice.instance.stock': 'Stock',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'Memory',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.search.type.placeholder':
|
||||
'Search by name, VRAM, memory or vCPU',
|
||||
'gpuservice.instance.search.template.placeholder':
|
||||
'Search by template name, image or mount path',
|
||||
'gpuservice.instance.template.image': 'Image',
|
||||
'gpuservice.instance.template.mount': 'Mount',
|
||||
'gpuservice.instance.connect': 'Connect',
|
||||
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
||||
'gpuservice.storage': 'Storage',
|
||||
'gpuservice.storage.add': 'Add Storage',
|
||||
'gpuservice.storage.edit': 'Edit Storage',
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'gpuservice.template.imagePullPolicy.never': '取得しない',
|
||||
'gpuservice.template.command': 'コンテナ起動コマンド',
|
||||
'gpuservice.template.command.placeholder':
|
||||
'1行に1つの引数、例:\n/bin/bash\n-c\nyour command',
|
||||
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'マウントパス',
|
||||
'gpuservice.template.containerDisk': 'コンテナディスク (Gi)',
|
||||
'gpuservice.template.memory': 'メモリ (Gi)',
|
||||
@@ -46,14 +46,16 @@ export default {
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'現在のインスタンスタイプは最大 {count} 個の GPU をサポートします',
|
||||
'gpuservice.instance.stock': '在庫',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'メモリ',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.search.type.placeholder':
|
||||
'名前、VRAM、メモリまたは vCPU で検索',
|
||||
'gpuservice.instance.search.template.placeholder':
|
||||
'テンプレート名、イメージまたはマウントパスで検索',
|
||||
'gpuservice.instance.template.image': 'イメージ',
|
||||
'gpuservice.instance.template.mount': 'マウント',
|
||||
'gpuservice.instance.connect': '接続',
|
||||
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
||||
'gpuservice.storage': 'ストレージ',
|
||||
'gpuservice.storage.add': 'ストレージを追加',
|
||||
'gpuservice.storage.edit': 'ストレージを編集',
|
||||
|
||||
@@ -12,7 +12,7 @@ export default {
|
||||
'gpuservice.template.imagePullPolicy.never': 'Никогда не загружать',
|
||||
'gpuservice.template.command': 'Команда запуска контейнера',
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Один аргумент на строку, например:\n/bin/bash\n-c\nyour command',
|
||||
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||
'gpuservice.template.containerDisk': 'Диск контейнера (Gi)',
|
||||
'gpuservice.template.memory': 'Память (Gi)',
|
||||
@@ -48,14 +48,16 @@ export default {
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'Текущий тип экземпляра поддерживает максимум {count} GPU',
|
||||
'gpuservice.instance.stock': 'Остаток',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'Память',
|
||||
'gpuservice.instance.memory': 'Память',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.search.type.placeholder':
|
||||
'Поиск по имени, VRAM, памяти или vCPU',
|
||||
'gpuservice.instance.search.template.placeholder':
|
||||
'Поиск по имени шаблона, образу или пути монтирования',
|
||||
'gpuservice.instance.template.image': 'Образ',
|
||||
'gpuservice.instance.template.mount': 'Монтирование',
|
||||
'gpuservice.instance.connect': 'Подключение',
|
||||
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
||||
'gpuservice.storage': 'Хранилище',
|
||||
'gpuservice.storage.add': 'Добавить хранилище',
|
||||
'gpuservice.storage.edit': 'Редактировать хранилище',
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'gpuservice.template.imagePullPolicy.never': 'Asla çekme',
|
||||
'gpuservice.template.command': 'Konteyner Başlatma Komutu',
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Her satırda bir argüman, örn.:\n/bin/bash\n-c\nyour command',
|
||||
'Argümanları boşlukla ayırın; boşluk içeren argümanları tırnak içine alın, örn.: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||
'gpuservice.template.containerDisk': 'Konteyner Diski (Gi)',
|
||||
'gpuservice.template.memory': 'Bellek (Gi)',
|
||||
@@ -47,14 +47,16 @@ export default {
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'Mevcut örnek türü en fazla {count} GPU destekler',
|
||||
'gpuservice.instance.stock': 'Stok',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'Bellek',
|
||||
'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.template.placeholder':
|
||||
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
||||
'gpuservice.instance.template.image': 'İmaj',
|
||||
'gpuservice.instance.template.mount': 'Bağlama',
|
||||
'gpuservice.instance.connect': 'Bağlan',
|
||||
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
||||
'gpuservice.storage': 'Depolama',
|
||||
'gpuservice.storage.add': 'Depolama Ekle',
|
||||
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'gpuservice.template.imagePullPolicy.never': '从不拉取',
|
||||
'gpuservice.template.command': '容器启动命令',
|
||||
'gpuservice.template.command.placeholder':
|
||||
'每行一个参数,例如:\n/bin/bash\n-c\nyour command',
|
||||
'使用空格分隔参数;含空格的参数请用引号包裹,例如:/bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': '挂载路径',
|
||||
'gpuservice.template.containerDisk': '容器磁盘(Gi)',
|
||||
'gpuservice.template.memory': '内存(Gi)',
|
||||
@@ -52,6 +52,8 @@ export default {
|
||||
'搜索模板名称、镜像或挂载路径',
|
||||
'gpuservice.instance.template.image': '镜像',
|
||||
'gpuservice.instance.template.mount': '挂载',
|
||||
'gpuservice.instance.connect': '连接',
|
||||
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
||||
'gpuservice.storage': '存储',
|
||||
'gpuservice.storage.add': '添加存储',
|
||||
'gpuservice.storage.edit': '编辑存储',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { omitPathParams } from '../../utils';
|
||||
import { InstanceTypeItem, ListItem } from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_API = (params: {
|
||||
@@ -19,6 +20,9 @@ export async function queryGPUServiceInstances(
|
||||
},
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
@@ -26,7 +30,7 @@ export async function queryGPUServiceInstances(
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
@@ -40,6 +44,9 @@ export async function createGPUServiceInstance(
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<ListItem>(
|
||||
GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
@@ -62,6 +69,9 @@ export async function updateGPUServiceInstance(
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<ListItem>(
|
||||
`${GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
@@ -83,6 +93,9 @@ export async function deleteGPUServiceInstance(
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request(
|
||||
`${GPU_SERVICE_INSTANCES_API({
|
||||
namespace: params.namespace,
|
||||
@@ -101,11 +114,14 @@ export async function queryGPUServiceInstanceTypes(
|
||||
params: Global.K8sSearchParams & { clusterID?: number },
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<InstanceTypeItem>>(
|
||||
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
@@ -118,11 +134,14 @@ export async function queryGPUServiceInstanceTypeItems(
|
||||
},
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<InstanceTypeItem>(
|
||||
`${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`,
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import Separator from '@/pages/llmodels/components/separator';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
@@ -61,20 +62,32 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const form = useRef<any>(null);
|
||||
const autoSelectedRef = useRef(false);
|
||||
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
|
||||
const [templateId, setTemplateId] = useState<number>();
|
||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
|
||||
const { detailData, fetchData } = useQueryInstanceTypes();
|
||||
const {
|
||||
detailData,
|
||||
loading: instanceTypesLoading,
|
||||
fetchData
|
||||
} = useQueryInstanceTypes();
|
||||
const { detailData: templatesData, fetchData: fetchTemplates } =
|
||||
useQueryTemplates();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
autoSelectedRef.current = false;
|
||||
fetchData({});
|
||||
fetchTemplates({ page: 1, perPage: 100 });
|
||||
} else {
|
||||
setSelectedInstanceType(undefined);
|
||||
setSelectedManufacturer(undefined);
|
||||
setTemplateId(undefined);
|
||||
setInstanceKeyword('');
|
||||
setTemplateKeyword('');
|
||||
}
|
||||
}, [open, fetchData, fetchTemplates]);
|
||||
|
||||
@@ -119,6 +132,59 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
);
|
||||
}, [templateKeyword, manufacturerMatchedTemplates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (action !== PageAction.CREATE) return;
|
||||
if (autoSelectedRef.current) return;
|
||||
if (!instanceTypeList.length) return;
|
||||
if (!templatesData) return;
|
||||
|
||||
const firstInstanceType = instanceTypeList[0];
|
||||
const manufacturer = firstInstanceType.spec?.manufacturer;
|
||||
const name = firstInstanceType.metadata?.name;
|
||||
const matchedTemplate = manufacturer
|
||||
? templateList.find((t) => t.manufacturer === manufacturer)
|
||||
: templateList[0];
|
||||
|
||||
autoSelectedRef.current = true;
|
||||
setSelectedInstanceType(name);
|
||||
setSelectedManufacturer(manufacturer);
|
||||
if (matchedTemplate) {
|
||||
setTemplateId(matchedTemplate.id);
|
||||
}
|
||||
|
||||
const applyToForm = () => {
|
||||
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
||||
const updatedSpec = {
|
||||
...currentSpec,
|
||||
type: name,
|
||||
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
|
||||
};
|
||||
|
||||
if (matchedTemplate) {
|
||||
form.current?.setFieldsValue({
|
||||
manufacturer: matchedTemplate.manufacturer,
|
||||
spec: {
|
||||
...updatedSpec,
|
||||
...matchedTemplate.spec,
|
||||
resources: {
|
||||
...(updatedSpec.resources || {}),
|
||||
...(matchedTemplate.spec?.resources || {})
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
form.current?.setFieldsValue({ spec: updatedSpec });
|
||||
}
|
||||
};
|
||||
|
||||
if (form.current) {
|
||||
applyToForm();
|
||||
} else {
|
||||
queueMicrotask(applyToForm);
|
||||
}
|
||||
}, [open, action, instanceTypeList, templateList, templatesData]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.current?.submit();
|
||||
};
|
||||
@@ -229,15 +295,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{filteredInstanceTypes.length > 0 ? (
|
||||
<InstanceTypeList
|
||||
value={selectedInstanceType}
|
||||
dataList={filteredInstanceTypes}
|
||||
onChange={handleInstanceTypeChange}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
<InstanceTypeList
|
||||
value={selectedInstanceType}
|
||||
dataList={filteredInstanceTypes}
|
||||
loading={instanceTypesLoading}
|
||||
onChange={handleInstanceTypeChange}
|
||||
/>
|
||||
</PanelBody>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { AutoTooltip, IconFont, StatusTag, ThemeTag } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
convertKiToGi,
|
||||
InstanceTypePhaseLabelMap,
|
||||
InstanceTypePhaseStatus
|
||||
} from '../config';
|
||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--ant-color-text);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const Meta = styled.div`
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 13px;
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
.dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--ant-color-text-quaternary);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
`;
|
||||
|
||||
interface InstanceTypeItemProps {
|
||||
item: InstanceTypeItemModel;
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
item,
|
||||
showStatus = true
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const name = item.metadata?.name;
|
||||
const acceleratable = item.spec?.acceleratable;
|
||||
const product = item.spec?.product;
|
||||
const displayName = product || (!acceleratable ? 'CPU' : name);
|
||||
const manufacturer = item.spec?.manufacturer?.toUpperCase();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<Flex gap={8} align="center">
|
||||
<AutoTooltip ghost minWidth={20} maxWidth={180}>
|
||||
{displayName}
|
||||
</AutoTooltip>
|
||||
{acceleratable && manufacturer && (
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--ant-color-text-tertiary)',
|
||||
fontWeight: 400
|
||||
}}
|
||||
>
|
||||
<ThemeTag color="purple">{manufacturer}</ThemeTag>
|
||||
</span>
|
||||
)}
|
||||
</Flex>
|
||||
{showStatus && (
|
||||
<Flex gap={8} align="center">
|
||||
{item.status?.phase && (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status:
|
||||
InstanceTypePhaseStatus[item.status.phase] ?? 'inactive',
|
||||
text:
|
||||
InstanceTypePhaseLabelMap[item.status.phase] ??
|
||||
item.status.phase,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Title>
|
||||
<Meta>
|
||||
<span style={{ display: 'flex', height: 15 }}>
|
||||
{acceleratable && (
|
||||
<span className="meta-row">
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-gpu1" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||
</span>
|
||||
<span>{convertKiToGi(item.spec?.memory) ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
<span className="dot"></span>
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-cube" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}
|
||||
</span>
|
||||
<span>{item.status?.accelerator?.remaining ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="meta-row">
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-ram-02" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
</span>
|
||||
<span>{item.status?.ram?.capacity ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
<span className="dot"></span>
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-cpu" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>vCPU</span>
|
||||
<span>{item.status?.cpu?.capacity ?? '-'}</span>
|
||||
</Flex>
|
||||
</span>
|
||||
</span>
|
||||
</Meta>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstanceTypeItem;
|
||||
@@ -1,13 +1,11 @@
|
||||
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Tag } from 'antd';
|
||||
import FileSkeleton from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||
import { TemplateCard } from '@gpustack/core-ui';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
InstanceTypePhaseLabelMap,
|
||||
InstanceTypePhaseStatus,
|
||||
InstanceTypePhaseValueMap
|
||||
} from '../config';
|
||||
import { InstanceTypeItem } from '../config/types';
|
||||
import { InstanceTypePhaseValueMap } from '../config';
|
||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||
import InstanceTypeItem from './instance-type-item';
|
||||
|
||||
const TypeGrid = styled.div`
|
||||
display: flex;
|
||||
@@ -15,53 +13,42 @@ const TypeGrid = styled.div`
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const TypeName = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--ant-color-text);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const TypeMeta = styled.div`
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 13px;
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--ant-color-text-quaternary);
|
||||
}
|
||||
`;
|
||||
|
||||
interface InstanceTypeListProps {
|
||||
value?: string;
|
||||
onChange?: (item: InstanceTypeItem) => void;
|
||||
dataList?: InstanceTypeItem[];
|
||||
onChange?: (item: InstanceTypeItemModel) => void;
|
||||
dataList?: InstanceTypeItemModel[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const isAvailable = (item: InstanceTypeItem) =>
|
||||
const isAvailable = (item: InstanceTypeItemModel) =>
|
||||
item.status?.phase === InstanceTypePhaseValueMap.Active;
|
||||
|
||||
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
dataList = []
|
||||
dataList = [],
|
||||
loading
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const handleSelect = (item: InstanceTypeItem) => {
|
||||
const handleSelect = (item: InstanceTypeItemModel) => {
|
||||
if (!isAvailable(item)) return;
|
||||
onChange?.(item);
|
||||
};
|
||||
|
||||
if (!dataList.length) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Spin spinning size="middle">
|
||||
<TypeGrid style={{ minHeight: 200 }}>
|
||||
{_.times(6, (index: number) => (
|
||||
<FileSkeleton key={index} counts={3} itemHeight={106} />
|
||||
))}
|
||||
</TypeGrid>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypeGrid>
|
||||
{dataList.map((item) => {
|
||||
@@ -78,58 +65,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
disabled={disabled}
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
<TypeName>
|
||||
<Flex gap={8} align="center">
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{name}
|
||||
</AutoTooltip>
|
||||
</Flex>
|
||||
<Flex gap={8} align="center">
|
||||
{item.status?.phase && (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status:
|
||||
InstanceTypePhaseStatus[item.status.phase] ??
|
||||
'inactive',
|
||||
text:
|
||||
InstanceTypePhaseLabelMap[item.status.phase] ??
|
||||
item.status.phase,
|
||||
message: ''
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.spec?.acceleratable && (
|
||||
<Tag
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
color: 'var(--ant-color-text-tertiary)'
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
|
||||
{item.status?.accelerator?.remaining ?? '-'}
|
||||
</span>
|
||||
</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
</TypeName>
|
||||
<TypeMeta>
|
||||
<span style={{ display: 'flex', height: 15 }}>
|
||||
{item.spec?.acceleratable && (
|
||||
<span className="meta-row">
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
|
||||
{item.spec?.memory ?? '-'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="meta-row gap-16">
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
|
||||
{item.status?.ram?.capacity ?? '-'}
|
||||
</span>
|
||||
<span>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
|
||||
</span>
|
||||
</TypeMeta>
|
||||
<InstanceTypeItem item={item} />
|
||||
</TemplateCard>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const status: Record<string, StatusType> = {
|
||||
[InstanceStatusValueMap.InitializeFailed]: StatusMaps.error,
|
||||
[InstanceStatusValueMap.Initialized]: StatusMaps.transitioning,
|
||||
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
|
||||
[InstanceStatusValueMap.NotReady]: StatusMaps.warning,
|
||||
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
|
||||
[InstanceStatusValueMap.Ready]: StatusMaps.success
|
||||
};
|
||||
|
||||
@@ -90,13 +90,19 @@ export const StorageModeValueMap = {
|
||||
// Constant SSH public key resource name used when SSH is enabled
|
||||
export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default';
|
||||
|
||||
const KI_TO_GI = 1024 * 1024;
|
||||
const GI_DIVISOR: Record<string, number> = {
|
||||
Ki: 1024 * 1024,
|
||||
Mi: 1024,
|
||||
Gi: 1
|
||||
};
|
||||
|
||||
export const convertKiToGi = (value?: string): string | undefined => {
|
||||
if (!value) return value;
|
||||
const match = /^(-?\d+(?:\.\d+)?)Ki$/.exec(value);
|
||||
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/.exec(value);
|
||||
if (!match) return value;
|
||||
return `${_.round(Number(match[1]) / KI_TO_GI, 2)}Gi`;
|
||||
const [, num, unit] = match;
|
||||
if (unit === 'Ti') return `${_.round(Number(num), 2)}Ti`;
|
||||
return `${_.round(Number(num) / GI_DIVISOR[unit], 2)}Gi`;
|
||||
};
|
||||
|
||||
export const transformInstanceTypeResource = (
|
||||
|
||||
@@ -82,7 +82,12 @@ export interface ListItem extends Omit<FormData, 'metadata'> {
|
||||
};
|
||||
}
|
||||
|
||||
type Quality = `${number}Ki` | `${number}Gi`;
|
||||
type Quality =
|
||||
| `${number}`
|
||||
| `${number}Ki`
|
||||
| `${number}Mi`
|
||||
| `${number}Gi`
|
||||
| `${number}Ti`;
|
||||
|
||||
export interface InstanceTypeResource {
|
||||
capacity?: Quality;
|
||||
@@ -90,30 +95,42 @@ export interface InstanceTypeResource {
|
||||
remaining?: Quality;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
accelerator: InstanceTypeResource;
|
||||
cpu: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
phase: string;
|
||||
phaseMessage?: string;
|
||||
ram: InstanceTypeResource;
|
||||
}
|
||||
|
||||
export interface InstanceTypeMetadata {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
creationTimestamp?: string;
|
||||
generateName?: string;
|
||||
generation?: number;
|
||||
finalizers?: string[];
|
||||
}
|
||||
|
||||
export interface InstanceTypeSpec {
|
||||
acceleratable: boolean;
|
||||
group: string;
|
||||
computeCapability?: string;
|
||||
family?: string;
|
||||
manufacturer?: string;
|
||||
memory?: string;
|
||||
product?: string;
|
||||
sliced?: number;
|
||||
}
|
||||
|
||||
export interface InstanceTypeItem {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
id: number;
|
||||
spec: {
|
||||
acceleratable: boolean;
|
||||
computeCapability?: string;
|
||||
family?: string;
|
||||
group: string;
|
||||
manufacturer?: string;
|
||||
memory?: string;
|
||||
product?: string;
|
||||
sliced?: number;
|
||||
};
|
||||
status: {
|
||||
accelerator: InstanceTypeResource;
|
||||
cpu: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
phase: string;
|
||||
phaseMessage?: string;
|
||||
ram: InstanceTypeResource;
|
||||
};
|
||||
metadata: InstanceTypeMetadata;
|
||||
spec: InstanceTypeSpec;
|
||||
status: InstanceTypeStatus;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import useGetSshkey from '../../public-keys/services/use-get-sshkey';
|
||||
import { DefaultImagePullPolicy } from '../../templates/config';
|
||||
import TemplateBasicForm from '../../templates/forms/basic';
|
||||
import { DEFAULT_SSH_PUBLIC_KEY_NAME } from '../config';
|
||||
import { FormData, InstancePort, ListItem } from '../config/types';
|
||||
import Basic from './basic';
|
||||
import InstanceTypeFormItem from './instance-type';
|
||||
@@ -53,19 +52,19 @@ const TABKeysMap = {
|
||||
const requiredFields = {
|
||||
[TABKeysMap.BASIC]: {
|
||||
sort: 1,
|
||||
fields: ['name']
|
||||
fields: ['metadata.name']
|
||||
},
|
||||
[TABKeysMap.INSTANCE_TYPE]: {
|
||||
sort: 2,
|
||||
fields: ['type']
|
||||
fields: ['spec.type', 'spec.resources.accelerator']
|
||||
},
|
||||
[TABKeysMap.TEMPLATE]: {
|
||||
sort: 3,
|
||||
fields: ['image', 'ports']
|
||||
fields: ['spec.image', 'spec.ports', 'spec.env']
|
||||
},
|
||||
[TABKeysMap.STORAGE]: {
|
||||
sort: 4,
|
||||
fields: ['volume']
|
||||
fields: ['spec.volume.persistent.name', 'spec.volume.ephemeral.capacity']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,7 +142,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
const ports = (Form.useWatch(['spec', 'ports'], form) ||
|
||||
[]) as InstancePort[];
|
||||
const hasSshPort = useMemo(
|
||||
() => ports.some((p) => Number(p?.port) === SSH_PORT),
|
||||
() =>
|
||||
ports.some((p) => Number(p?.port) === SSH_PORT && p.protocol === 'TCP'),
|
||||
[ports]
|
||||
);
|
||||
|
||||
@@ -151,12 +151,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
scrollTabsRef.current?.handleTargetChange(key);
|
||||
};
|
||||
|
||||
const { handleOnFinishFailed } = useFinishFailed({
|
||||
const { handleOnFinishFailed: rawHandleOnFinishFailed } = useFinishFailed({
|
||||
requiredFields,
|
||||
onTargetChange,
|
||||
updateActiveKey
|
||||
});
|
||||
|
||||
// useFinishFailed only matches errorFields[].name[0] against `fields`,
|
||||
// but our form fields are nested (e.g. ['spec','type']), so collapse each
|
||||
// error path into a single dotted string before delegating.
|
||||
const handleOnFinishFailed = (errorInfo: any) => {
|
||||
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
|
||||
...field,
|
||||
name: [
|
||||
Array.isArray(field.name) ? field.name.join('.') : String(field.name)
|
||||
]
|
||||
}));
|
||||
rawHandleOnFinishFailed({ ...errorInfo, errorFields });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
@@ -180,43 +193,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}
|
||||
}, [action, currentData, form, open, namespace]);
|
||||
|
||||
const buildPayload = (values: InstanceFormValues): FormData => {
|
||||
const acceleratorVal = values.spec?.resources?.accelerator;
|
||||
const acceleratorStr =
|
||||
acceleratorVal !== undefined &&
|
||||
acceleratorVal !== null &&
|
||||
(acceleratorVal as unknown) !== ''
|
||||
? String(acceleratorVal)
|
||||
: undefined;
|
||||
|
||||
const spec: FormData['spec'] = {
|
||||
...values.spec,
|
||||
resources: {
|
||||
...values.spec?.resources,
|
||||
accelerator: acceleratorStr
|
||||
}
|
||||
} as FormData['spec'];
|
||||
|
||||
// SSH public key — include only when checkbox is checked AND port 22 is in ports
|
||||
const portsList = (values.spec?.ports || []) as InstancePort[];
|
||||
const has22 = portsList.some((p) => Number(p?.port) === SSH_PORT);
|
||||
if (values.enable_ssh && has22) {
|
||||
spec.sshPublicKey = { name: DEFAULT_SSH_PUBLIC_KEY_NAME };
|
||||
} else {
|
||||
delete (spec as any).sshPublicKey;
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
name: values.metadata?.name,
|
||||
namespace: values.metadata?.namespace || namespace
|
||||
},
|
||||
spec
|
||||
} as FormData;
|
||||
};
|
||||
|
||||
const handleFinish = async (values: InstanceFormValues) => {
|
||||
const payload = buildPayload(values);
|
||||
await onFinish(values);
|
||||
};
|
||||
|
||||
@@ -265,9 +242,16 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
resources: {
|
||||
accelerator: '1'
|
||||
},
|
||||
volume: { persistent: { name: '' } },
|
||||
sshPublicKey: {
|
||||
name: 'gpustack-organization-ssh-public-key'
|
||||
},
|
||||
volume: {
|
||||
ephemeral: {
|
||||
capacity: '20Gi'
|
||||
},
|
||||
persistent: {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
enable_ssh: false
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
|
||||
import { InputNumber as CInputNumber } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Form, Tag } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import InstanceTypeItem from '../components/instance-type-item';
|
||||
import { InstanceTypePhaseValueMap } from '../config';
|
||||
import { FormData, InstanceTypeItem } from '../config/types';
|
||||
import {
|
||||
FormData,
|
||||
InstanceTypeItem as InstanceTypeItemModel
|
||||
} from '../config/types';
|
||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||
|
||||
const FieldBlock = styled.div`
|
||||
@@ -12,87 +16,31 @@ const FieldBlock = styled.div`
|
||||
`;
|
||||
|
||||
const SelectedCard = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) max-content;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
background: var(--ant-color-bg-container);
|
||||
`;
|
||||
|
||||
const SummaryTitle = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ant-color-text);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const SummaryMeta = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
interface InstanceTypePickerProps {
|
||||
value?: string;
|
||||
dataList: InstanceTypeItem[];
|
||||
dataList: InstanceTypeItemModel[];
|
||||
}
|
||||
|
||||
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
||||
value,
|
||||
dataList
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const selected = useMemo(() => {
|
||||
return dataList.find((item) => (item.metadata?.name || '') === value);
|
||||
}, [value, dataList]);
|
||||
|
||||
if (!selected) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectedCard>
|
||||
<div>
|
||||
<SummaryTitle>
|
||||
<Flex gap={16} align="center">
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{selected?.metadata?.name || '-'}
|
||||
</AutoTooltip>
|
||||
{selected && selected.spec?.acceleratable && (
|
||||
<Tag
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
color: 'var(--ant-color-text-tertiary)'
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
|
||||
{selected?.status?.accelerator?.remaining ?? '-'}
|
||||
</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
</SummaryTitle>
|
||||
<SummaryMeta>
|
||||
{selected?.spec?.acceleratable && (
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
|
||||
{selected?.spec?.memory ?? '-'}
|
||||
</span>
|
||||
)}
|
||||
<Flex gap={16}>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
|
||||
{selected?.status?.ram?.capacity ?? '-'}
|
||||
</span>
|
||||
<span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
|
||||
</Flex>
|
||||
</SummaryMeta>
|
||||
</div>
|
||||
</SelectedCard>
|
||||
</>
|
||||
<SelectedCard>
|
||||
<InstanceTypeItem item={selected} showStatus={false} />
|
||||
</SelectedCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,8 +62,9 @@ const InstanceTypeFormItem = () => {
|
||||
}, [dataList, typeName]);
|
||||
|
||||
const maxGpuCount = useMemo(() => {
|
||||
const remaining = selectedInstanceType?.status?.accelerator?.remaining;
|
||||
const num = remaining ? Number(remaining) : NaN;
|
||||
const onceMaxRequest =
|
||||
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
||||
const num = onceMaxRequest ? Number(onceMaxRequest) : NaN;
|
||||
return Number.isFinite(num) && num > 0 ? num : 1;
|
||||
}, [selectedInstanceType]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex, Form, Radio } from 'antd';
|
||||
@@ -8,7 +8,7 @@ import { FormData as StorageFormData } from '../../storage/config/types';
|
||||
import useCreateStorage from '../../storage/services/use-create-storage';
|
||||
import useQueryStorage from '../../storage/services/use-query-storage';
|
||||
import { StorageModeValueMap } from '../config';
|
||||
import { FormData, InstanceVolume } from '../config/types';
|
||||
import { FormData } from '../config/types';
|
||||
import StorageOverlay from './storage-overlay';
|
||||
|
||||
const FieldBlock = styled.div`
|
||||
@@ -19,33 +19,22 @@ const DEFAULT_TEMP_CAPACITY_GB = 50;
|
||||
|
||||
const StorageVolume = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { detailData: storageData, fetchData: fetchStorage } =
|
||||
useQueryStorage();
|
||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||
const [storageMode, setStorageMode] = useState<string>(
|
||||
StorageModeValueMap.Existing
|
||||
StorageModeValueMap.Temporary
|
||||
);
|
||||
|
||||
const form = Form.useFormInstance<FormData>();
|
||||
const volume = Form.useWatch(['spec', 'volume'], form) as
|
||||
| InstanceVolume
|
||||
| undefined;
|
||||
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
|
||||
useEffect(() => {
|
||||
fetchStorage({});
|
||||
}, []);
|
||||
|
||||
// const storageMode = useMemo(() => {
|
||||
// if (volume?.ephemeral && !volume?.persistent) {
|
||||
// return StorageModeValueMap.Temporary;
|
||||
// }
|
||||
// return StorageModeValueMap.Existing;
|
||||
// }, [volume?.ephemeral, volume?.persistent]);
|
||||
|
||||
const storageOptions = useMemo(
|
||||
() =>
|
||||
(storageData?.items || []).map((item) => ({
|
||||
@@ -94,15 +83,15 @@ const StorageVolume = () => {
|
||||
value={storageMode}
|
||||
onChange={(e) => handleModeChange(e.target.value)}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
|
||||
value: StorageModeValueMap.Temporary
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistent'
|
||||
}),
|
||||
value: StorageModeValueMap.Existing
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
|
||||
value: StorageModeValueMap.Temporary
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,50 @@
|
||||
import { tableSorter } from '@/config/settings';
|
||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
||||
import { ExportOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AutoTooltip,
|
||||
CopyButton,
|
||||
DropdownButtons,
|
||||
StatusTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
type ConnectEntry =
|
||||
| { type: 'ssh'; key: string; command: string }
|
||||
| { type: 'http'; key: string; port: number; url: string };
|
||||
|
||||
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||
const ports = record.status?.ports || [];
|
||||
const hasSshKey = !!record.spec?.sshPublicKey?.name;
|
||||
|
||||
if (!ip) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ports
|
||||
.filter((p) => p.nodePort)
|
||||
.map<ConnectEntry>((p) => {
|
||||
const isSsh = hasSshKey && p.protocol === 'TCP' && p.port === 22;
|
||||
return isSsh
|
||||
? {
|
||||
type: 'ssh',
|
||||
key: `ssh-${p.nodePort}`,
|
||||
command: `ssh root@${ip} -p ${p.nodePort}`
|
||||
}
|
||||
: {
|
||||
type: 'http',
|
||||
key: `http-${p.nodePort}`,
|
||||
port: p.port,
|
||||
url: `http://${ip}:${p.nodePort}`
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
interface ColumnsHookProps {
|
||||
handleSelect: (val: string, record: ListItem) => void;
|
||||
sortOrder: string[];
|
||||
@@ -23,7 +61,7 @@ const useInstancesColumns = ({
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: ['metadata', 'name'],
|
||||
key: 'name',
|
||||
sorter: tableSorter(1),
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
@@ -33,39 +71,101 @@ const useInstancesColumns = ({
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.connect' }),
|
||||
key: 'connect',
|
||||
render: (_text, record: ListItem) => {
|
||||
const entries = getConnectEntries(record);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={6} vertical>
|
||||
{entries.map((entry) =>
|
||||
entry.type === 'ssh' ? (
|
||||
<Flex key={entry.key} align="center" style={{ height: 22 }}>
|
||||
<span
|
||||
className="text-tertiary"
|
||||
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||
>
|
||||
SSH
|
||||
</span>
|
||||
<CopyButton
|
||||
text={entry.command}
|
||||
type="link"
|
||||
size="small"
|
||||
tips={intl.formatMessage({
|
||||
id: 'gpuservice.instance.connect.copySshCommand'
|
||||
})}
|
||||
style={{
|
||||
padding: '0 6px',
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'var(--ant-color-fill-tertiary)',
|
||||
fontSize: 12
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex key={entry.key} align="center" style={{ height: 22 }}>
|
||||
<span
|
||||
className="text-tertiary"
|
||||
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||
>
|
||||
HTTP
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
padding: '0 6px',
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: 'var(--ant-color-fill-tertiary)'
|
||||
}}
|
||||
>
|
||||
{entry.port}
|
||||
<ExportOutlined style={{ fontSize: 10, marginLeft: 4 }} />
|
||||
</Button>
|
||||
</Flex>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||
dataIndex: ['status', 'phase'],
|
||||
key: 'status',
|
||||
sorter: tableSorter(2),
|
||||
sorter: false,
|
||||
render: (value: string, record: ListItem) => {
|
||||
return (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: InstanceStatusLabelMap[value] || value
|
||||
text: InstanceStatusLabelMap[value] || value,
|
||||
message: record?.status?.phaseMessage || ''
|
||||
}}
|
||||
></StatusTag>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.template.image' }),
|
||||
dataIndex: ['spec', 'image'],
|
||||
key: 'image',
|
||||
sorter: tableSorter(3),
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (value: string) => (
|
||||
<AutoTooltip ghost>{value || '-'}</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: ['metadata', 'creationTimestamp'],
|
||||
key: 'creationTimestamp',
|
||||
sorter: tableSorter(5),
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
@@ -33,8 +33,7 @@ import useUpdateInstance from './services/use-update-instance';
|
||||
|
||||
const GPUService: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
@@ -43,12 +42,13 @@ const GPUService: React.FC = () => {
|
||||
[namespace, clusterID]
|
||||
);
|
||||
|
||||
const fetchInstances = useCallback(
|
||||
const fetchInstances = useMemoizedFn(
|
||||
async (
|
||||
params: any,
|
||||
options?: any
|
||||
): Promise<Global.PageResponse<ListItem>> => {
|
||||
if (!clusterID) {
|
||||
const effectiveClusterID = params.cluster_id ?? clusterID;
|
||||
if (!effectiveClusterID) {
|
||||
return {
|
||||
items: [],
|
||||
pagination: {
|
||||
@@ -60,7 +60,7 @@ const GPUService: React.FC = () => {
|
||||
} as Global.PageResponse<ListItem>;
|
||||
}
|
||||
const res = await queryGPUServiceInstances(
|
||||
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
|
||||
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||
options
|
||||
);
|
||||
const total = res.items?.length ?? 0;
|
||||
@@ -74,8 +74,7 @@ const GPUService: React.FC = () => {
|
||||
perPage
|
||||
}
|
||||
};
|
||||
},
|
||||
[namespace, clusterID]
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -96,7 +95,8 @@ const GPUService: React.FC = () => {
|
||||
key: PaginationKey.Instances,
|
||||
fetchAPI: fetchInstances,
|
||||
deleteAPI: deleteInstance,
|
||||
watch: true,
|
||||
watch: false,
|
||||
polling: true,
|
||||
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
|
||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
|
||||
});
|
||||
@@ -134,18 +134,24 @@ const GPUService: React.FC = () => {
|
||||
const k8sClusters = clusters.filter(
|
||||
(item: any) => item.provider === ProviderValueMap.Kubernetes
|
||||
);
|
||||
if (k8sClusters.length > 0) {
|
||||
const firstCluster = k8sClusters[0];
|
||||
if (k8sClusters.length === 0) {
|
||||
return;
|
||||
}
|
||||
const storedCluster = k8sClusters.find(
|
||||
(item: any) => item.id === currentCluster?.id
|
||||
);
|
||||
const targetCluster = storedCluster ?? k8sClusters[0];
|
||||
if (!storedCluster) {
|
||||
setCurrentCluster({
|
||||
...firstCluster,
|
||||
label: firstCluster.name,
|
||||
value: firstCluster.id
|
||||
});
|
||||
handleQueryChange({
|
||||
cluster_id: firstCluster.id,
|
||||
page: 1
|
||||
...targetCluster,
|
||||
label: targetCluster.name,
|
||||
value: targetCluster.id
|
||||
});
|
||||
}
|
||||
handleQueryChange({
|
||||
cluster_id: targetCluster.id,
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelClusterRequest();
|
||||
@@ -202,7 +208,11 @@ const GPUService: React.FC = () => {
|
||||
if (val === 'edit') {
|
||||
handleEditInstance(row);
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({ ...row, name: row.metadata?.name });
|
||||
handleDelete({
|
||||
...row,
|
||||
name: row.metadata?.name,
|
||||
id: row.metadata?.name as any
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,7 +235,7 @@ const GPUService: React.FC = () => {
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-cloud-outlined" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
filters={_.pick(queryParams, ['search', 'manufacturer'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.instance.nofound'
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -12,8 +12,7 @@ interface CreateInstanceParams {
|
||||
}
|
||||
|
||||
export default function useCreateInstance() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -13,8 +13,7 @@ interface UpdateInstanceParams {
|
||||
}
|
||||
|
||||
export default function useUpdateInstance() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { INPUT_WIDTH } from '@/constants';
|
||||
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Form } from 'antd';
|
||||
import { Button, Form, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import PageBox from '../../_components/page-box';
|
||||
import useGetSshkey from './services/use-get-sshkey';
|
||||
@@ -36,6 +35,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
||||
await updateSshkey({
|
||||
data: values
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
||||
return (
|
||||
<PageBox>
|
||||
<Form
|
||||
style={{ width: INPUT_WIDTH.large }}
|
||||
style={{ width: 700 }}
|
||||
name="gpuServicePublicKeyForm"
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
@@ -76,7 +76,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
||||
trim={false}
|
||||
alwaysFocus
|
||||
autoSize={{ minRows: 8, maxRows: 16 }}
|
||||
style={{ width: INPUT_WIDTH.large, minHeight: 186 }}
|
||||
style={{ width: 700, minHeight: 186 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { omitPathParams } from '../../utils';
|
||||
import { ListItem, StorageClassItem } from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_STORAGE_API = (params: {
|
||||
@@ -17,6 +18,9 @@ export async function queryGPUServiceStorage(
|
||||
},
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
@@ -24,7 +28,7 @@ export async function queryGPUServiceStorage(
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
@@ -38,6 +42,9 @@ export async function createGPUServiceStorage(
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<ListItem>(
|
||||
GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
@@ -60,6 +67,9 @@ export async function updateGPUServiceStorage(
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<ListItem>(
|
||||
`${GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
@@ -78,6 +88,9 @@ export async function deleteGPUServiceStorage(params: {
|
||||
clusterID?: number;
|
||||
id: number;
|
||||
}) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request(
|
||||
`${GPU_SERVICE_STORAGE_API({
|
||||
namespace: params.namespace,
|
||||
@@ -95,11 +108,14 @@ export async function queryStorageClass(
|
||||
},
|
||||
options?: any
|
||||
) {
|
||||
if (!params.clusterID) {
|
||||
return;
|
||||
}
|
||||
return request<Global.K8sPageResponse<StorageClassItem>>(
|
||||
STORAGE_CLASS_API({ clusterID: params.clusterID }),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
params: omitPathParams(params),
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { StatusMaps } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
import { icons } from '@gpustack/core-ui';
|
||||
|
||||
export const StoragePhaseValueMap = {
|
||||
Available: 'Available',
|
||||
Unavailable: 'Unavailable',
|
||||
Pending: 'Pending'
|
||||
};
|
||||
|
||||
export const StoragePhaseLabelMap: Record<string, string> = {
|
||||
[StoragePhaseValueMap.Available]: 'Available',
|
||||
[StoragePhaseValueMap.Unavailable]: 'Unavailable',
|
||||
[StoragePhaseValueMap.Pending]: 'Pending'
|
||||
};
|
||||
|
||||
export const status: Record<string, StatusType> = {
|
||||
[StoragePhaseValueMap.Available]: StatusMaps.success,
|
||||
[StoragePhaseValueMap.Unavailable]: StatusMaps.error,
|
||||
[StoragePhaseValueMap.Pending]: StatusMaps.transitioning
|
||||
};
|
||||
|
||||
export const StorageTypeValueMap = {
|
||||
Local: 'local',
|
||||
Shared: 'shared',
|
||||
|
||||
@@ -32,12 +32,23 @@ export interface Metadata {
|
||||
resourceVersion: string;
|
||||
creationTimestamp: string;
|
||||
annotations?: Record<string, string>;
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedField[];
|
||||
}
|
||||
|
||||
export interface ListItem extends Omit<FormData, 'metadata'> {
|
||||
id: number;
|
||||
export interface ListItem {
|
||||
metadata: Metadata;
|
||||
spec: {
|
||||
type: string;
|
||||
capacity: string;
|
||||
accessMode: AccessModeType;
|
||||
};
|
||||
status: {
|
||||
phase: 'Available' | 'Unavailable' | 'Pending';
|
||||
volume: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface StorageClassItem {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { tableSorter } from '@/config/settings';
|
||||
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
|
||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import { rowActionList, StorageTypeLabelMap } from '../config';
|
||||
import {
|
||||
rowActionList,
|
||||
status,
|
||||
StoragePhaseLabelMap,
|
||||
StorageTypeLabelMap
|
||||
} from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
@@ -23,7 +27,7 @@ const useStorageColumns = ({
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: ['metadata', 'name'],
|
||||
key: 'name',
|
||||
sorter: tableSorter(1),
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
@@ -37,7 +41,7 @@ const useStorageColumns = ({
|
||||
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||
dataIndex: ['spec', 'type'],
|
||||
key: 'type',
|
||||
sorter: tableSorter(2),
|
||||
sorter: false,
|
||||
render: (value: string) =>
|
||||
StorageTypeLabelMap[value]
|
||||
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
|
||||
@@ -47,21 +51,28 @@ const useStorageColumns = ({
|
||||
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
||||
dataIndex: ['spec', 'capacity'],
|
||||
key: 'capacity',
|
||||
sorter: tableSorter(3),
|
||||
sorter: false,
|
||||
render: (value: string) => value ?? '-'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.storage.accessMode' }),
|
||||
dataIndex: ['spec', 'accessMode'],
|
||||
key: 'accessMode',
|
||||
sorter: tableSorter(4),
|
||||
render: (value: string) => value || '-'
|
||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||
dataIndex: ['status', 'phase'],
|
||||
key: 'status',
|
||||
sorter: false,
|
||||
render: (value: string) => (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: StoragePhaseLabelMap[value] || value
|
||||
}}
|
||||
></StatusTag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: ['metadata', 'creationTimestamp'],
|
||||
key: 'creationTimestamp',
|
||||
sorter: tableSorter(5),
|
||||
sorter: false,
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
@@ -34,8 +34,7 @@ import useUpdateStorage from './services/use-update-storage';
|
||||
|
||||
const GPUServiceStorage: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
@@ -49,7 +48,8 @@ const GPUServiceStorage: React.FC = () => {
|
||||
params: any,
|
||||
options?: any
|
||||
): Promise<Global.PageResponse<ListItem>> => {
|
||||
if (!clusterID) {
|
||||
const effectiveClusterID = params.cluster_id ?? clusterID;
|
||||
if (!effectiveClusterID) {
|
||||
return {
|
||||
items: [],
|
||||
pagination: {
|
||||
@@ -61,7 +61,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
} as Global.PageResponse<ListItem>;
|
||||
}
|
||||
const res = await queryGPUServiceStorage(
|
||||
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
|
||||
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||
options
|
||||
);
|
||||
const total = res.items?.length ?? 0;
|
||||
@@ -98,6 +98,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
fetchAPI: fetchStorage,
|
||||
deleteAPI: deleteStorage,
|
||||
watch: false,
|
||||
polling: true,
|
||||
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
|
||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
||||
});
|
||||
@@ -184,7 +185,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
try {
|
||||
if (openAddModalStatus.action === PageAction.EDIT) {
|
||||
await updateStorage({
|
||||
id: openAddModalStatus.currentData!.id,
|
||||
id: openAddModalStatus.currentData!.metadata?.name as any,
|
||||
data
|
||||
});
|
||||
} else {
|
||||
@@ -203,7 +204,11 @@ const GPUServiceStorage: React.FC = () => {
|
||||
if (val === 'edit') {
|
||||
handleEditStorage(row);
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({ ...row, name: row.metadata?.name });
|
||||
handleDelete({
|
||||
...row,
|
||||
name: row.metadata?.name,
|
||||
id: row.metadata?.name as any
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,7 +231,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-storage-outlined" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
filters={_.pick(queryParams, ['search'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.gpuservice.storage.nofound'
|
||||
})}
|
||||
@@ -296,7 +301,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
}}
|
||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||
showSorterTooltip={false}
|
||||
rowKey="id"
|
||||
rowKey={(record) => record.metadata?.name as any}
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
size: 'middle',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -12,8 +12,7 @@ interface CreateStorageParams {
|
||||
}
|
||||
|
||||
export default function useCreateStorage() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -7,8 +7,7 @@ import { queryGPUServiceStorage } from '../apis';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
export default function useQueryStorage() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -13,8 +13,7 @@ interface UpdateStorageParams {
|
||||
}
|
||||
|
||||
export default function useUpdateStorage() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
|
||||
@@ -35,6 +35,49 @@ export const ImagePullPolicyOptions: {
|
||||
|
||||
export const DefaultImagePullPolicy: ImagePullPolicy = 'IfNotPresent';
|
||||
|
||||
export const normalizeCommand = (value: string): string[] => {
|
||||
if (!value) return [];
|
||||
const tokens: string[] = [];
|
||||
let current = '';
|
||||
let inQuote: '"' | "'" | null = null;
|
||||
let quoteStart = -1;
|
||||
for (const ch of value) {
|
||||
if (inQuote) {
|
||||
if (ch === inQuote) {
|
||||
inQuote = null;
|
||||
quoteStart = -1;
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
} else if (ch === '"' || ch === "'") {
|
||||
inQuote = ch;
|
||||
quoteStart = current.length;
|
||||
} else if (/\s/.test(ch)) {
|
||||
tokens.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (inQuote && quoteStart >= 0) {
|
||||
current =
|
||||
current.slice(0, quoteStart) + inQuote + current.slice(quoteStart);
|
||||
}
|
||||
tokens.push(current);
|
||||
return tokens;
|
||||
};
|
||||
|
||||
export const stringifyCommand = (tokens?: string[]): string => {
|
||||
if (!tokens?.length) return '';
|
||||
return tokens
|
||||
.map((token) => {
|
||||
if (!/\s/.test(token)) return token;
|
||||
if (token.startsWith('"') || token.startsWith("'")) return token;
|
||||
return `"${token}"`;
|
||||
})
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export const templateActions = [
|
||||
{
|
||||
label: 'common.button.edit',
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { GPUsConfigs } from '../../../resources/config/gpu-driver';
|
||||
import { ImagePullPolicyOptions } from '../config';
|
||||
import {
|
||||
ImagePullPolicyOptions,
|
||||
normalizeCommand,
|
||||
stringifyCommand
|
||||
} from '../config';
|
||||
import { FormData } from '../config/types';
|
||||
import Env from './env';
|
||||
import Ports from './ports';
|
||||
@@ -96,8 +99,8 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'command']}
|
||||
normalize={(value: string) => _.split(value, '\n').map(_.trim)}
|
||||
getValueProps={(value) => ({ value: _.join(value, '\n') })}
|
||||
normalize={normalizeCommand}
|
||||
getValueProps={(value) => ({ value: stringifyCommand(value) })}
|
||||
>
|
||||
<Textarea
|
||||
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
|
||||
|
||||
@@ -61,13 +61,20 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
|
||||
initialValues={{
|
||||
spec: {
|
||||
imagePullPolicy: DefaultImagePullPolicy,
|
||||
command: [],
|
||||
command: ['tail', '-f', '/dev/null'],
|
||||
ports: [
|
||||
{
|
||||
protocol: 'TCP',
|
||||
port: 22
|
||||
}
|
||||
],
|
||||
volumeMount: '/workspace',
|
||||
resources: {
|
||||
cpu: '1',
|
||||
ram: '2Gi',
|
||||
localStorage: '15Gi',
|
||||
accelerator: ''
|
||||
},
|
||||
env: []
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export const omitPathParams = <T extends Record<string, any>>(
|
||||
params: T
|
||||
): Omit<T, 'namespace' | 'clusterID'> => {
|
||||
return _.omit(params, [
|
||||
'namespace',
|
||||
'clusterID',
|
||||
'page',
|
||||
'perPage',
|
||||
'cluster_id'
|
||||
]) as Omit<T, 'namespace' | 'clusterID'>;
|
||||
};
|
||||
Reference in New Issue
Block a user