From fbb707d0141df3011c43053297c1f1b141a743fb Mon Sep 17 00:00:00 2001 From: gitlawr Date: Tue, 2 Jun 2026 21:04:40 +0800 Subject: [PATCH] feat(gpu-service): scope create forms by the selected organization In the platform-admin "all organizations" view, add an organization picker (the CreateOrgScopeField slot) to the SSH public key, storage type, storage, GPU instance and instance-template create forms, placed below the name / display-name fields. Drop the hidden owner field and let the owner derive from the request context, matching the model-route form. For GPU instances, scope the instance-type list to clusters the chosen org owns (client-side, by cluster owner) so an instance can't be scheduled onto another org's cluster; when the org owns none, show "no instance type available" and clear the selection, cluster and CPU/memory fields. Instance templates gain a Global level (NULL owner) and an owner tag on the card. --- src/locales/en-US/gpuservice.ts | 1 + src/locales/ja-JP/gpuservice.ts | 2 + src/locales/ru-RU/gpuservice.ts | 1 + src/locales/tr-TR/gpuservice.ts | 1 + src/locales/zh-CN/gpuservice.ts | 1 + .../_components/owner-principal-id-field.tsx | 36 ----- .../instances/components/add-modal.tsx | 150 ++++++++++++++++-- .../gpu-service/instances/forms/basic.tsx | 4 +- .../gpu-service/instances/forms/index.tsx | 58 ++++++- .../instances/forms/instance-type.tsx | 76 +++++---- .../gpu-service/public-keys/forms/basic.tsx | 4 +- .../gpu-service/storage-types/forms/basic.tsx | 4 +- src/pages/gpu-service/storage/forms/basic.tsx | 4 +- .../templates/components/template-card.tsx | 5 + .../gpu-service/templates/config/types.ts | 5 + .../gpu-service/templates/forms/basic.tsx | 8 + .../gpu-service/templates/forms/index.tsx | 2 +- 17 files changed, 272 insertions(+), 90 deletions(-) delete mode 100644 src/pages/_components/owner-principal-id-field.tsx diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 2ab68e29..aa08d07c 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -97,6 +97,7 @@ export default { 'gpuservice.instance.templates': 'Instance Templates', 'gpuservice.instance.section.storage': 'Storage', 'gpuservice.instance.type.required': 'Please select an instance type', + 'gpuservice.instance.type.noAvailable': 'No instance type available', 'gpuservice.instance.gpuCount': 'GPU Count', 'gpuservice.instance.gpuCount.required': 'Please enter the GPU count', 'gpuservice.instance.gpuCount.max': diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index c40315dd..3e75e583 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -96,6 +96,8 @@ export default { 'gpuservice.instance.templates': 'インスタンステンプレート', 'gpuservice.instance.section.storage': 'ストレージボリューム', 'gpuservice.instance.type.required': 'インスタンスタイプを選択してください', + 'gpuservice.instance.type.noAvailable': + '利用可能なインスタンスタイプがありません', 'gpuservice.instance.gpuCount': 'GPU 数', 'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください', 'gpuservice.instance.gpuCount.max': diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index a794b65f..e64b0120 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -100,6 +100,7 @@ export default { 'gpuservice.instance.templates': 'Шаблоны экземпляров', 'gpuservice.instance.section.storage': 'Том хранилища', 'gpuservice.instance.type.required': 'Выберите тип экземпляра', + 'gpuservice.instance.type.noAvailable': 'Нет доступных типов экземпляров', 'gpuservice.instance.gpuCount': 'Количество GPU', 'gpuservice.instance.gpuCount.required': 'Введите количество GPU', 'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index b67191ec..520dfbe6 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -97,6 +97,7 @@ export default { 'gpuservice.instance.templates': 'Örnek Şablonları', 'gpuservice.instance.section.storage': 'Depolama Hacmi', 'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin', + 'gpuservice.instance.type.noAvailable': 'Kullanılabilir örnek türü yok', 'gpuservice.instance.gpuCount': 'GPU Sayısı', 'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin', 'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index b7543497..b55d9c38 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -92,6 +92,7 @@ export default { 'gpuservice.instance.templates': '实例模板', 'gpuservice.instance.section.storage': '存储卷', 'gpuservice.instance.type.required': '请选择实例类型', + 'gpuservice.instance.type.noAvailable': '无可用的实例类型', 'gpuservice.instance.gpuCount': 'GPU 数量', 'gpuservice.instance.gpuCount.required': '请输入 GPU 数量', 'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡', diff --git a/src/pages/_components/owner-principal-id-field.tsx b/src/pages/_components/owner-principal-id-field.tsx deleted file mode 100644 index 4b6d0562..00000000 --- a/src/pages/_components/owner-principal-id-field.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { currentOrganizationIdAtom } from '@/atoms/user'; -import { Input as CInput } from '@gpustack/core-ui'; -import { Form } from 'antd'; -import type { NamePath } from 'antd/es/form/interface'; -import { useAtomValue } from 'jotai'; -import { useEffect } from 'react'; - -interface OwnerPrincipalIdFieldProps { - name?: NamePath; -} - -// Pins `owner_principal_id` to the Org the caller is currently acting -// under. Cluster ownership is irrelevant here: cluster_access grants -// let one Org schedule on another Org's cluster, but the resource the -// caller creates still belongs to *their* Org, and the backend enforces -// `owner_principal_id == ctx.current_principal_id`. -const OwnerPrincipalIdField: React.FC = ({ - name = 'owner_principal_id' -}) => { - const currentOrgId = useAtomValue(currentOrganizationIdAtom); - const form = Form.useFormInstance(); - - useEffect(() => { - if (currentOrgId != null) { - form.setFieldValue(name, currentOrgId); - } - }, [currentOrgId, name, form]); - - return ( - - ); -}; - -export default OwnerPrincipalIdField; diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index e648f19a..dcc08b80 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -1,5 +1,6 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import Separator from '@/pages/llmodels/components/separator'; import { SearchOutlined } from '@ant-design/icons'; import { @@ -10,7 +11,7 @@ import { } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Empty, Input, Typography } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { ListItem as TemplateItem } from '../../templates/config/types'; import useQueryTemplates from '../../templates/services/use-query-templates'; import { FormData, InstanceTypeItem, ListItem } from '../config/types'; @@ -97,8 +98,58 @@ const AddModal: React.FC = ({ } = useQueryInstanceTypes(); const { detailData: templatesData, fetchData: fetchTemplates } = useQueryTemplates(); + const { clusterList, fetchClusterList } = useQueryClusterList(); + // Set by the create-scope picker (admin "All" view) via onScopeChange. + // undefined = no picker (org context) → no client-side scoping. + const [scopeOrgId, setScopeOrgId] = useState( + undefined + ); const templateList = templatesData?.items || []; + + // A GPU instance is scheduled on the chosen instance type's cluster, and + // its owner is that cluster's owner. So when a platform admin targets an + // org, restrict each instance type's candidates to clusters that org owns + // (dropping tiers/types left with none). Header-independent: filters the + // fetched list client-side, so it doesn't rely on the request scope. + const filterTypesByOwner = ( + types: InstanceTypeItem[], + clusters: Array<{ + id?: number; + value?: number; + owner_principal_id?: number; + }>, + orgId?: number | null + ): InstanceTypeItem[] => { + if (orgId == null) return types; + const owned = new Set( + (clusters || []) + .filter((c) => c.owner_principal_id === orgId) + .map((c) => c.id ?? c.value) + ); + return types + .map((it) => ({ + ...it, + status: { + ...it.status, + acceleratorTiers: (it.status?.acceleratorTiers ?? []) + .map((tier: any) => ({ + ...tier, + candidates: (tier.candidates ?? []).filter((c: any) => + owned.has(Number(c.cluster)) + ) + })) + .filter((tier: any) => (tier.candidates ?? []).length > 0) + } + })) + .filter((it) => (it.status?.acceleratorTiers ?? []).length > 0); + }; + + const ownedInstanceTypes = useMemo( + () => filterTypesByOwner(instanceTypeList, clusterList as any, scopeOrgId), + + [instanceTypeList, clusterList, scopeOrgId] + ); // const readonly = action === PageAction.VIEW; const readonly = false; const isRecreate = realAction === PageAction.CREATE; @@ -177,7 +228,13 @@ const AddModal: React.FC = ({ // initial for first const applyAutoSelection = ( instanceTypes: InstanceTypeItem[], - templates: TemplateItem[] + templates: TemplateItem[], + clusters?: Array<{ + id?: number; + value?: number; + owner_principal_id?: number; + }>, + orgId?: number | null ) => { // On edit / view, surface the persisted selection in the card list. if (!shouldAutoSelectResource) { @@ -195,9 +252,24 @@ const AddModal: React.FC = ({ return; } - const first = instanceTypes.find((item) => !item.disabled); + // Scope to clusters the chosen org owns (admin "All" view). + const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId); + const first = owned.find((item) => !item.disabled); - if (!first) return; + if (!first) { + // The chosen org has no clusters (hence no instance types). Clear any + // prior pick so a stale instance type / cross-org cluster isn't left + // on the form. + setInstanceTypeSelection({ + instanceType: undefined, + manufacturer: undefined + }); + setTemplateId(undefined); + form.current?.applyInstanceType?.(undefined); + form.current?.setFieldValue?.('clusterId', null); + form.current?.setFieldValue?.(['spec', 'type'], undefined); + return; + } // On create, auto-select the first instance type in the list @@ -209,6 +281,52 @@ const AddModal: React.FC = ({ applySelection(first, template); }; + // Fetch the (tenant-scoped) instance types + templates and auto-select. + // The query hook cancels any in-flight request on each new call, so when + // this runs twice in quick succession (drawer open, then the scope + // picker settling on its default) the latest scope's result wins. + const loadCreateResources = (orgId?: number | null) => { + const session = ++sessionRef.current; + Promise.all([ + fetchData({ page: -1 }), + fetchTemplates({ page: -1 }), + fetchClusterList({ page: -1 }) + ]).then(([instanceResItems, templatesRes, clusters]) => { + if (sessionRef.current !== session) return; + applyAutoSelection( + instanceResItems || [], + templatesRes?.items || [], + (Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [], + orgId + ); + }); + }; + + // Platform admin retargeted the create to another org (or Global). The + // instance-type / cluster offerings are tenant-scoped, so drop the + // current pick and reload for the new scope. The request interceptor + // already carries the new org header by the time this fires. + const handleScopeChange = (orgId?: number | null) => { + if (!open || action !== PageAction.CREATE) return; + setScopeOrgId(orgId); + setInstanceTypeSelection({ + instanceType: undefined, + manufacturer: undefined + }); + setTemplateId(undefined); + // Also clear the instance-type-derived form state (the selected type + // card + its limits, the cluster, and spec.type). The cluster decides + // where the instance is scheduled, so a stale pick from the previous + // scope must not survive — otherwise an instance owned by the newly + // chosen org could land on the old org's cluster. The reload's + // owner-scoped auto-selection re-fills them from the new org, or leaves + // them empty (blocking submit) when the chosen org has no clusters. + form.current?.applyInstanceType?.(undefined); + form.current?.setFieldValue?.('clusterId', null); + form.current?.setFieldValue?.(['spec', 'type'], undefined); + loadCreateResources(orgId); + }; + useEffect(() => { if (!open) { sessionRef.current += 1; @@ -219,25 +337,27 @@ const AddModal: React.FC = ({ setTemplateId(undefined); setInstanceKeyword(''); setTemplateKeyword(''); + setScopeOrgId(undefined); return; } if (action === PageAction.CREATE) { - const session = ++sessionRef.current; - Promise.all([fetchData({ page: -1 }), fetchTemplates({ page: -1 })]).then( - ([instanceResItems, templatesRes]) => { - if (sessionRef.current !== session) return; - applyAutoSelection(instanceResItems || [], templatesRes?.items || []); - } - ); + loadCreateResources(); } }, [open, shouldAutoSelectResource, action]); - // filter instance types - const filteredInstanceTypes = instanceTypeList.filter((item) => + // filter instance types (already scoped to the chosen org's clusters) + const filteredInstanceTypes = ownedInstanceTypes.filter((item) => matchKeyword([item.name], instanceKeyword) ); + // No instance types for the chosen org (e.g. it owns no clusters), and not + // mid-fetch — drives the "no available instance type" message in the form. + const noAvailableInstanceTypes = + action === PageAction.CREATE && + !instanceTypesLoading && + ownedInstanceTypes.length === 0; + // filter templates based on selection and keyword const filteredTemplates = templateList.filter((item) => { if ( @@ -445,8 +565,10 @@ const AddModal: React.FC = ({ currentData={data} disabled={readonly} onFinish={onFinish} + onScopeChange={handleScopeChange} open={open} - instanceTypeList={instanceTypeList} + instanceTypeList={ownedInstanceTypes} + noAvailableInstanceTypes={noAvailableInstanceTypes} /> diff --git a/src/pages/gpu-service/instances/forms/basic.tsx b/src/pages/gpu-service/instances/forms/basic.tsx index 01a83fe1..50c3b60f 100644 --- a/src/pages/gpu-service/instances/forms/basic.tsx +++ b/src/pages/gpu-service/instances/forms/basic.tsx @@ -1,9 +1,9 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { PageAction, validateLabelNameRegxFor63 } from '@/config'; import { PageActionType } from '@/config/types'; import { Input as CInput, useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; -import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field'; import { FormData } from '../config/types'; import formStyles from '../styles/instances.module.less'; @@ -18,7 +18,6 @@ const Basic = ({ const { getRuleMessage } = useAppUtils(); return ( <> - data-field="name" name="name" @@ -46,6 +45,7 @@ const Basic = ({ label={intl.formatMessage({ id: 'common.table.displayName' })} /> +
name="description" hidden> void; onFinish: (values: FormData) => Promise; } @@ -101,6 +110,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( disabled, open, instanceTypeList = [], + noAvailableInstanceTypes, + onScopeChange, onFinish } = props; const intl = useIntl(); @@ -110,6 +121,15 @@ const GPUServiceInstanceForm: React.FC = forwardRef( const formAction = realAction === PageAction.CREATE ? PageAction.CREATE : action; const sshEnabled = Form.useWatch('enable_ssh', form); + // `organization_id` is owned by the create-scope picker slot; it only + // exists/changes when a platform admin retargets the form. Watch it + // so the parent can re-scope offerings (see onScopeChange). + const scopeOrgId = Form.useWatch('organization_id', form); + const scopeInitRef = useRef(true); + // Keep the latest callback in a ref so the scope-change effect can call it + // without depending on its identity (parent may pass a new fn each render). + const onScopeChangeRef = useRef(onScopeChange); + onScopeChangeRef.current = onScopeChange; const { sshkeyOptions, fetchData: fetchSSHData } = useQuerySshkeys(); const [sshOverlayOpen, setSshOverlayOpen] = useState(false); const { getScrollElementScrollableHeight } = useWrapperContext(); @@ -133,6 +153,17 @@ const GPUServiceInstanceForm: React.FC = forwardRef( } }, [open, action]); + // Skip the first run (initial mount value); thereafter notify the + // parent whenever the chosen create scope changes so it can reload + // the tenant-scoped instance-type / template lists. + useEffect(() => { + if (scopeInitRef.current) { + scopeInitRef.current = false; + return; + } + onScopeChangeRef.current?.(scopeOrgId); + }, [scopeOrgId]); + const ports = Form.useWatch(['spec', 'ports'], form) || []; const hasSSHPort = useMemo( @@ -248,6 +279,23 @@ const GPUServiceInstanceForm: React.FC = forwardRef( if (!instanceType) { setSelectedInstanceType(undefined); setOnceMaxRequest({ cpu: null, memory: null, localStorage: null }); + // Also reset the instance-type-derived form fields (cluster, type and + // the CPU / memory / GPU-count resources) so the previous scope's + // numbers don't linger when the new org has no clusters. + const spec = form.getFieldValue('spec') || {}; + form.setFieldsValue({ + clusterId: null, + spec: { + ...spec, + type: undefined, + resources: { + ...spec.resources, + accelerator: undefined, + cpu: undefined, + ram: undefined + } + } + } as any); return; } const candidate = pickCandidateForAccelerator( @@ -402,7 +450,14 @@ const GPUServiceInstanceForm: React.FC = forwardRef( form.setFieldsValue(values as any); }, getFieldsValue: () => form.getFieldsValue(), - applyInstanceType: (instanceType: InstanceTypeItem) => { + applyInstanceType: (instanceType?: InstanceTypeItem) => { + if (!instanceType) { + // Clear the current instance-type selection (e.g. the create scope + // switched to an org with no clusters) so its card/limits don't + // linger. + resolveAndApply(undefined, 0); + return; + } const count = instanceType.spec?.acceleratable ? 1 : 0; resolveAndApply(instanceType, count); } @@ -501,6 +556,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( selectedInstanceType={selectedInstanceType} currentData={currentData as any} onceMaxRequest={onceMaxRequest} + noAvailableTypes={noAvailableInstanceTypes} onGPUCountChange={handleAcceleratorChange} /> ) diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 18ceb0c5..1fdc85b6 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -30,10 +30,12 @@ const SelectedCard = styled.div` interface InstanceTypePickerProps { selectedInstanceType?: InstanceTypeItemModel; + noAvailable?: boolean; } const InstanceTypePicker: React.FC = ({ - selectedInstanceType + selectedInstanceType, + noAvailable }) => { const intl = useIntl(); return ( @@ -42,7 +44,11 @@ const InstanceTypePicker: React.FC = ({ ) : ( - {intl.formatMessage({ id: 'gpuservice.instance.type.required' })} + {intl.formatMessage({ + id: noAvailable + ? 'gpuservice.instance.type.noAvailable' + : 'gpuservice.instance.type.required' + })} )} @@ -55,6 +61,10 @@ interface InstanceTypeFormItemProps { currentData?: ListItem; selectedInstanceType?: InstanceTypeItemModel; onceMaxRequest?: BasicResourceMax; + // True when the (org-scoped) instance-type list is empty — e.g. the chosen + // org owns no clusters. Surface a "no available" message instead of the + // "please select" placeholder + empty CPU / memory inputs. + noAvailableTypes?: boolean; onGPUCountChange?: (value: number) => void; } @@ -64,6 +74,7 @@ const InstanceTypeFormItem: React.FC = ({ currentData, selectedInstanceType, onceMaxRequest, + noAvailableTypes, onGPUCountChange }) => { const intl = useIntl(); @@ -152,12 +163,15 @@ const InstanceTypeFormItem: React.FC = ({ ]} > {action === PageAction.CREATE && ( - + )} {action === PageAction.EDIT && renderInstanceType()} - {isGPU && ( + {!noAvailableTypes && isGPU && ( name={['spec', 'resources', 'accelerator']} hidden={action === PageAction.EDIT} @@ -232,32 +246,34 @@ const InstanceTypeFormItem: React.FC = ({ /> )} - -
- - name={['spec', 'resources', 'ram']} - normalize={(value) => (value ? `${value}Gi` : null)} - getValueProps={(value) => ({ - value: _.toString(value).replace(/Gi$/, '') - })} - > - - -
-
- name={['spec', 'resources', 'cpu']}> - - -
-
+ {!noAvailableTypes && ( + +
+ + name={['spec', 'resources', 'ram']} + normalize={(value) => (value ? `${value}Gi` : null)} + getValueProps={(value) => ({ + value: _.toString(value).replace(/Gi$/, '') + })} + > + + +
+
+ name={['spec', 'resources', 'cpu']}> + + +
+
+ )}
); }; diff --git a/src/pages/gpu-service/public-keys/forms/basic.tsx b/src/pages/gpu-service/public-keys/forms/basic.tsx index 35b5a30e..0638479c 100644 --- a/src/pages/gpu-service/public-keys/forms/basic.tsx +++ b/src/pages/gpu-service/public-keys/forms/basic.tsx @@ -1,8 +1,8 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { PageAction, validateLabelNameRegxFor63 } from '@/config'; import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; -import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field'; import { FormData } from '../config/types'; const Basic = ({ action }: { action: string }) => { @@ -11,7 +11,6 @@ const Basic = ({ action }: { action: string }) => { return ( <> - name="name" rules={[ @@ -37,6 +36,7 @@ const Basic = ({ action }: { action: string }) => { label={intl.formatMessage({ id: 'common.table.displayName' })} /> + name={['spec', 'data']} rules={[ diff --git a/src/pages/gpu-service/storage-types/forms/basic.tsx b/src/pages/gpu-service/storage-types/forms/basic.tsx index d07ba052..7fcc1af2 100644 --- a/src/pages/gpu-service/storage-types/forms/basic.tsx +++ b/src/pages/gpu-service/storage-types/forms/basic.tsx @@ -1,3 +1,4 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { PageAction, validateLabelNameRegxFor63 } from '@/config'; import { Input as CInput, @@ -6,7 +7,6 @@ import { } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; -import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field'; import { StorageTypeKindOptions } from '../config'; import { FormData } from '../config/types'; @@ -16,7 +16,6 @@ const Basic = ({ action }: { action: string }) => { return ( <> - name="name" rules={[ @@ -42,6 +41,7 @@ const Basic = ({ action }: { action: string }) => { label={intl.formatMessage({ id: 'common.table.displayName' })} /> + name="type" rules={[ diff --git a/src/pages/gpu-service/storage/forms/basic.tsx b/src/pages/gpu-service/storage/forms/basic.tsx index fcf0ab2e..d6dd0dc2 100644 --- a/src/pages/gpu-service/storage/forms/basic.tsx +++ b/src/pages/gpu-service/storage/forms/basic.tsx @@ -1,3 +1,4 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { PageAction, validateLabelNameRegxFor63 } from '@/config'; import { Input as CInput, @@ -8,7 +9,6 @@ import { import { useIntl } from '@umijs/max'; import { Flex, Form } from 'antd'; import { useContext } from 'react'; -import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field'; import { FormContext } from '../config/form-context'; import { FormData } from '../config/types'; @@ -19,7 +19,6 @@ const Basic = ({ action, open }: { action: string; open: boolean }) => { return ( <> - name="name" rules={[ @@ -45,6 +44,7 @@ const Basic = ({ action, open }: { action: string; open: boolean }) => { label={intl.formatMessage({ id: 'common.table.displayName' })} /> +
diff --git a/src/pages/gpu-service/templates/components/template-card.tsx b/src/pages/gpu-service/templates/components/template-card.tsx index 9b89376f..5090359a 100644 --- a/src/pages/gpu-service/templates/components/template-card.tsx +++ b/src/pages/gpu-service/templates/components/template-card.tsx @@ -13,6 +13,7 @@ import theadLogoZH from '@/assets/logo/t-head-zh.png'; import tensorflowkLogo from '@/assets/logo/tensorflow.svg'; import ubuntuLogo from '@/assets/logo/ubuntu_logo.png'; import vllmLogo from '@/assets/logo/vllm.png'; +import PluginExtraFields from '@/components/plugin-extra-fields'; import { GPUsConfigs, manfacturerValueMap @@ -257,6 +258,10 @@ const TemplateCardItem: React.FC = ({ data, onSelect }) => { {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 528ba03d..9bf1d7df 100644 --- a/src/pages/gpu-service/templates/config/types.ts +++ b/src/pages/gpu-service/templates/config/types.ts @@ -35,6 +35,11 @@ export interface FormData { export interface ListItem extends FormData { id: number; status?: string; + // NULL = a shared, admin-managed preset visible to everyone; a + // non-NULL id scopes the template to a single owner. Surfaced via + // the `OwnerScopeTag` slot on the card so the owner is visible at a + // glance. Optional on the wire — absent in single-owner builds. + owner_principal_id?: number | null; created_at?: string; updated_at?: string; } diff --git a/src/pages/gpu-service/templates/forms/basic.tsx b/src/pages/gpu-service/templates/forms/basic.tsx index 9ebb5da5..d1c71ad6 100644 --- a/src/pages/gpu-service/templates/forms/basic.tsx +++ b/src/pages/gpu-service/templates/forms/basic.tsx @@ -1,4 +1,6 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { validateLabelNameRegxFor63 } from '@/config'; +import { PageActionType } from '@/config/types'; import { Input as CInput, InputNumber, @@ -28,12 +30,14 @@ export interface BasicResourceMax { interface BasicProps { page?: 'template' | 'instance'; + action?: PageActionType; disabled?: boolean; onceMaxRequest?: BasicResourceMax; } const Basic: React.FC = ({ page = 'template', + action, onceMaxRequest, disabled }) => { @@ -105,6 +109,10 @@ const Basic: React.FC = ({ maxLength={63} /> + name="manufacturer" rules={[ diff --git a/src/pages/gpu-service/templates/forms/index.tsx b/src/pages/gpu-service/templates/forms/index.tsx index ea29e31e..d53485ad 100644 --- a/src/pages/gpu-service/templates/forms/index.tsx +++ b/src/pages/gpu-service/templates/forms/index.tsx @@ -82,7 +82,7 @@ const GPUServiceTemplateForm: React.FC = forwardRef( } }} > - + ); }