style: gpu instance service

This commit is contained in:
jialin
2026-06-01 21:06:56 +08:00
committed by jialin
parent 65ea65e9ae
commit e314f25a77
8 changed files with 49 additions and 134 deletions
@@ -4,10 +4,15 @@ import {
PlusOutlined,
QuestionCircleOutlined
} from '@ant-design/icons';
import { Input as CInput, LabelSelector, useAppUtils } from '@gpustack/core-ui';
import {
Input as CInput,
LabelSelector,
SwitchCard,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Form, Switch, Tooltip } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { Button, Form, Tooltip } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import { GpuInstanceOptions } from '../config/types';
import K8SVolumeMount from './k8s-volume-mount';
@@ -180,6 +185,7 @@ const NamespaceForm: React.FC = () => {
<Form.Item
name={['k8s_options', 'namespace']}
style={{ marginBottom: 0 }}
normalize={(value) => value || null}
>
<CInput.Input
label={intl.formatMessage({ id: 'clusters.namespace.title' })}
@@ -201,6 +207,7 @@ const OperatorImageForm: React.FC = () => {
<Form.Item
name={['k8s_options', 'operatorImage']}
style={{ marginBottom: 0 }}
normalize={(value) => value || null}
>
<CInput.Input
label={intl.formatMessage({ id: 'clusters.operatorImage.title' })}
@@ -211,92 +218,40 @@ const OperatorImageForm: React.FC = () => {
);
};
// GPU-instance support. The backend treats the mere presence of
// `gpuInstanceOptions` as the enable flag, so the switch toggles the whole
// object in/out of the form rather than setting a boolean field; the static
// address (optional even when enabled) is nested underneath.
//
// We drive the toggle from local state (not Form.useWatch) because the
// gpuInstanceOptions path has no registered Form.Item of its own — useWatch
// doesn't reliably re-render on setFieldValue for such paths, which left the
// switch unresponsive. Local state owns the visible state and we mirror it
// into the form via setFieldValue so submit still collects it.
const GpuInstanceOptionsForm: React.FC<{
initialValue?: GpuInstanceOptions;
}> = ({ initialValue }) => {
const intl = useIntl();
const form = Form.useFormInstance();
const [enabled, setEnabled] = useState<boolean>(!!initialValue);
const [address, setAddress] = useState<string>(
initialValue?.gpuInstancesAccessStaticAddress || ''
);
const initializedRef = useRef<boolean>(!!initialValue);
const writeForm = (en: boolean, addr: string) => {
form.setFieldValue(
['k8s_options', 'gpuInstanceOptions'],
en ? { gpuInstancesAccessStaticAddress: addr } : undefined
);
};
// Mirror a seeded initial value into the form on mount so submit collects it.
useEffect(() => {
if (initialValue) {
writeForm(true, initialValue.gpuInstancesAccessStaticAddress || '');
}
}, []);
// Adopt currentData arriving after mount (async edit load), once. After the
// user has interacted (`initializedRef`), local state owns the section.
useEffect(() => {
if (initializedRef.current) return;
if (initialValue) {
setEnabled(true);
setAddress(initialValue.gpuInstancesAccessStaticAddress || '');
writeForm(true, initialValue.gpuInstancesAccessStaticAddress || '');
initializedRef.current = true;
}
}, [initialValue]);
const handleToggle = (checked: boolean) => {
initializedRef.current = true;
setEnabled(checked);
if (!checked) {
setAddress('');
}
writeForm(checked, checked ? address : '');
};
const handleAddressChange = (e: any) => {
const next = typeof e === 'string' ? e : (e?.target?.value ?? '');
setAddress(next);
writeForm(true, next);
};
return (
<SectionWrap>
<Title>
<div className="flex-center gap-8">
<span className="flex-center gap-4">
<span>
{intl.formatMessage({ id: 'clusters.gpuInstances.title' })}
</span>
<Tooltip
title={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })}
>
<QuestionCircleOutlined
style={{ color: 'var(--ant-color-text-secondary)' }}
/>
</Tooltip>
</span>
<Switch checked={enabled} onChange={handleToggle} />
</div>
</Title>
{enabled && (
<SwitchCard
styles={{
wrapper: {
borderRadius: 'var(--ant-border-radius-lg)',
paddingInline: 14,
marginBottom: 24
}
}}
value={enabled}
onChange={handleToggle}
label={intl.formatMessage({ id: 'clusters.gpuInstances.title' })}
description={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })}
>
<Form.Item
name={[
'k8s_options',
'gpuInstanceOptions',
'gpuInstancesAccessStaticAddress'
]}
normalize={(value) => value || null}
noStyle
>
<CInput.Input
isInFormItems={false}
value={address}
onChange={handleAddressChange}
label={intl.formatMessage({
id: 'clusters.gpuInstances.staticAddress'
})}
@@ -304,8 +259,8 @@ const GpuInstanceOptionsForm: React.FC<{
id: 'clusters.gpuInstances.staticAddress.tip'
})}
></CInput.Input>
)}
</SectionWrap>
</Form.Item>
</SwitchCard>
);
};
@@ -315,12 +270,12 @@ const K8sPodSpec: React.FC<{
}> = ({ action, initialGpuInstanceOptions }) => {
return (
<>
<GpuInstanceOptionsForm initialValue={initialGpuInstanceOptions} />
<NamespaceForm />
<K8SVolumeMount action={action}></K8SVolumeMount>
<ImageCredentialsForm />
<NodeSelectorForm />
<OperatorImageForm />
<GpuInstanceOptionsForm initialValue={initialGpuInstanceOptions} />
</>
);
};