Files
gpustack-ui/src/pages/gpu-service/templates/forms/basic.tsx
T
gitlawrandjialin fbb707d014 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.
2026-06-03 10:14:50 +08:00

220 lines
6.1 KiB
TypeScript

import PluginExtraFields from '@/components/plugin-extra-fields';
import { validateLabelNameRegxFor63 } from '@/config';
import { PageActionType } from '@/config/types';
import {
Input as CInput,
InputNumber,
Select as SealSelect,
Textarea,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd';
import { useMemo } from 'react';
import { GPUsConfigs } from '../../../resources/config/gpu-driver';
import formStyles from '../../instances/styles/instances.module.less';
import {
ImagePullPolicyOptions,
normalizeCommand,
stringifyCommand
} from '../config';
import { FormData } from '../config/types';
import Env from './env';
import Ports from './ports';
export interface BasicResourceMax {
cpu?: number | null;
memory?: number | null;
localStorage?: number | null;
}
interface BasicProps {
page?: 'template' | 'instance';
action?: PageActionType;
disabled?: boolean;
onceMaxRequest?: BasicResourceMax;
}
const Basic: React.FC<BasicProps> = ({
page = 'template',
action,
onceMaxRequest,
disabled
}) => {
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const manufacturerOptions = useMemo(
() =>
Object.values(GPUsConfigs).map((item) => ({
label: item.label,
value: item.gpuVendor
})),
[]
);
const renderStorageLabel = (): React.ReactNode => {
if (page === 'instance' && onceMaxRequest?.localStorage != null) {
return intl.formatMessage(
{ id: 'gpuservice.instance.containerDisk.remaining' },
{ count: onceMaxRequest.localStorage }
);
}
return intl.formatMessage({ id: 'gpuservice.template.containerDisk' });
};
return (
<>
<div data-field="template"></div>
{page === 'template' && (
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
message: intl.formatMessage({
id: 'gpuservice.form.rule.name'
})
}
]}
>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Form.Item<FormData>
name="displayName"
rules={[
{
max: 63,
message: intl.formatMessage({
id: 'gpuservice.template.displayName.max'
})
}
]}
>
<CInput.Input
label={intl.formatMessage({
id: 'gpuservice.template.displayName'
})}
showCount
trim={false}
maxLength={63}
/>
</Form.Item>
<PluginExtraFields
name="CreateOrgScopeField"
context={{ action, allowGlobal: true }}
/>
<Form.Item<FormData>
name="manufacturer"
rules={[
{
required: true,
message: getRuleMessage('select', 'resources.table.vendor')
}
]}
>
<SealSelect
disabled={disabled}
label={intl.formatMessage({ id: 'resources.table.vendor' })}
required
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
/>
</Form.Item>
</>
)}
<Form.Item<FormData>
name={['spec', 'image']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.template.image')
}
]}
>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.template.image' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'imagePullPolicy']}>
<SealSelect
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.template.imagePullPolicy'
})}
options={ImagePullPolicyOptions.map((item) => ({
label: intl.formatMessage({ id: item.label }),
value: item.value
}))}
/>
</Form.Item>
<div className={formStyles.command}>
<Form.Item<FormData>
name={['spec', 'command']}
normalize={normalizeCommand}
getValueProps={(value) => ({ value: stringifyCommand(value) })}
>
<Textarea
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
placeholder={intl.formatMessage({
id: 'gpuservice.template.command.placeholder'
})}
trim={false}
alwaysFocus
scaleSize
/>
</Form.Item>
</div>
<Flex gap={12}>
{page === 'template' && (
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 'volumeMount']}>
<CInput.Input
label={intl.formatMessage({
id: 'gpuservice.template.mountPath'
})}
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}
disabled={disabled}
/>
</Form.Item>
</div>
)}
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'localStorage']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => ({
value: value ? String(value).replace(/Gi$/, '') : ''
})}
>
<InputNumber
label={renderStorageLabel()}
max={onceMaxRequest?.localStorage ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
</Flex>
<Ports disabled={disabled} />
<Env disabled={disabled} />
</>
);
};
export default Basic;