Files
gpustack-ui/src/pages/gpu-service/storage/forms/index.tsx
T
gitlawrandjialin 644aba3fc2 fix: scope storage type picker to the target organization
The storage create form fetched persistent-volume types once with no
tenant scope, so in the platform-admin "All" view the dropdown listed
every org's types and never re-scoped when the create-scope picker
retargeted the form — letting an org with no cluster-access grant pick
another org's types. The inline "add storage" sub-drawer in the instance
create form had the same gap plus a redundant org picker that could
retarget the storage away from the instance's org.

Both surfaces now pin the storage-type list to the chosen org (via the
X-Organization-Id header); the sub-drawer inherits the instance's scope
and hides its own picker. Page/table listings stay unscoped so every
row's type label still renders.
2026-07-08 16:31:53 +08:00

125 lines
4.1 KiB
TypeScript

import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useMemoizedFn } from 'ahooks';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface StorageFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
// Hide the create-scope org picker when the host already fixes the
// tenant scope (e.g. the instance create form). Defaults to shown.
showOrgScope?: boolean;
// Fired when the create-scope picker (platform admin "All" view)
// retargets the form to another org. Only emitted on genuine changes,
// never on the initial mount, and never in builds where the picker
// isn't mounted (the watched field stays undefined). Lets the parent
// reload the org-scoped storage-type options.
onScopeChange?: (orgId: number | null | undefined) => void;
onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
}
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
(props, ref) => {
const {
action,
currentData,
open,
showOrgScope = true,
onScopeChange,
onFinish,
onFinishFailed
} = props;
const [form] = Form.useForm<FormData>();
// `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 initial/default scope can be propagated once the picker resolves it.
const scopeOrgId = Form.useWatch('organization_id', form);
const scopeInitRef = useRef(true);
// Stable wrapper so the effect / change handler always call the latest
// callback without re-subscribing on the parent's fn identity.
const orgScope = useMemoizedFn((orgId?: number | null) => {
onScopeChange?.(orgId);
});
// Genuine retarget by the create-scope picker: drop the stale type pick
// (it may name a type the newly chosen org can't reference) and reload the
// org-scoped list. Wired to the picker's own onChange, so it fires only on
// a user selection — never on the picker's programmatic default.
const handleOrgScopeChange = useMemoizedFn((orgId?: number | null) => {
scopeInitRef.current = false;
form.setFieldValue(['spec', 'type'], undefined);
orgScope(orgId ?? null);
});
useEffect(() => {
if (!open) {
// Re-arm the initial-scope propagation for the next open.
scopeInitRef.current = true;
return;
}
// Propagate the initial/default scope once the picker resolves it so the
// parent loads the org-scoped type list, leaving any pre-filled type
// intact. User-driven retargets go through handleOrgScopeChange instead.
if (scopeInitRef.current && scopeOrgId != null) {
scopeInitRef.current = false;
orgScope(scopeOrgId);
}
}, [open, scopeOrgId, orgScope]);
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name,
displayName: currentData.displayName,
description: currentData.description,
spec: {
capacity: currentData.spec?.capacity,
type: currentData.spec?.type
}
});
}
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServiceStorageForm"
form={form}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
preserve={false}
initialValues={{}}
>
<Basic
action={action}
open={open}
showOrgScope={showOrgScope}
onOrgScopeChange={handleOrgScopeChange}
/>
</Form>
);
}
);
export default GPUServiceStorageForm;