fix: add notification when editing cluster's k8s_options

This commit is contained in:
Yuxing Deng
2026-06-05 10:19:08 +08:00
committed by jialin
parent 3212882895
commit 0c16e2be7a
9 changed files with 148 additions and 7 deletions
@@ -1,13 +1,22 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { FormDrawer } from '@gpustack/core-ui';
import React, { useRef } from 'react';
import { ProviderType } from '../config';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import React, { useRef, useState } from 'react';
import { ProviderType, ProviderValueMap } from '../config';
import {
ClusterFormData as FormData,
ClusterListItem as ListItem
} from '../config/types';
import ClusterForm from './cluster-form';
const ModalFooterStyle = {
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
};
type AddModalProps = {
title: string;
action: PageActionType;
@@ -28,7 +37,12 @@ const AddCluster: React.FC<AddModalProps> = ({
onOk,
onCancel
}) => {
const intl = useIntl();
const form = useRef<any>(null);
// Whether the user has changed any k8s_options field. Lifted from ClusterForm
// so the "re-run registration" notice can sit in the drawer footer, above the
// Save/Cancel buttons (mirrors the model edit interaction).
const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false);
const handleSubmit = () => {
form.current?.submit();
@@ -53,6 +67,27 @@ const AddCluster: React.FC<AddModalProps> = ({
onCancel={handleCancel}
onSubmit={handleSubmit}
width={710}
footer={
<>
{action === PageAction.EDIT &&
provider === ProviderValueMap.Kubernetes &&
k8sOptionsChanged && (
<AlertBlockInfo
type="warning"
style={{ margin: '8px 24px 0' }}
icon={<ExclamationCircleFilled />}
message={intl.formatMessage({
id: 'clusters.edit.k8sOptions.changed.tip'
})}
></AlertBlockInfo>
)}
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={ModalFooterStyle}
></ModalFooter>
</>
}
>
<ClusterForm
ref={form}
@@ -61,6 +96,7 @@ const AddCluster: React.FC<AddModalProps> = ({
action={action}
currentData={currentData}
onFinish={handleOk}
onK8sOptionsChange={setK8sOptionsChanged}
/>
</FormDrawer>
);
@@ -25,7 +25,10 @@ import {
} from '../config/types';
import AdvanceConfig from '../step-forms/advance-config';
import CloudProvider from './cloud-provider-form';
import K8sAdvancedOptions, { GpuInstanceServiceSwitch } from './k8s-pod-spec';
import K8sAdvancedOptions, {
GpuInstanceServiceSwitch,
K8sOptionsChangeWatcher
} from './k8s-pod-spec';
type AddModalProps = {
action: PageActionType;
@@ -33,10 +36,23 @@ type AddModalProps = {
provider: ProviderType;
credentialList: Global.BaseOption<number>[];
onFinish: (values: FormData) => void;
// Reports whether the user has changed any k8s_options field, so the parent
// can show the "re-run registration" notice in the footer.
onK8sOptionsChange?: (changed: boolean) => void;
ref?: any;
};
const ClusterForm: React.FC<AddModalProps> = forwardRef(
({ action, provider, currentData, credentialList, onFinish }, ref) => {
(
{
action,
provider,
currentData,
credentialList,
onFinish,
onK8sOptionsChange
},
ref
) => {
const [form] = Form.useForm();
const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]);
@@ -291,6 +307,14 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
}
]}
></CollapsePanel>
{provider === ProviderValueMap.Kubernetes && onK8sOptionsChange && (
<K8sOptionsChangeWatcher
action={action}
currentData={currentData}
onChange={onK8sOptionsChange}
/>
)}
</Form>
</FormContext.Provider>
);
@@ -1,9 +1,12 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react';
import _ from 'lodash';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { ClusterListItem as ListItem } from '../config/types';
import ImageCredential from './image-credential';
import K8SVolumeMount from './k8s-volume-mount';
@@ -170,6 +173,55 @@ export const GpuInstancesStaticAddressForm: React.FC = () => {
);
};
// Strip UI-only / undefined-valued noise so two k8s_options snapshots compare
// on real content. `sourceType` is derived from `volumeSource` purely for the
// volume-mount UI (see cluster-form init), and the JSON round-trip drops
// undefined-valued keys so a missing key and `key: undefined` compare equal.
const cleanK8sOptions = (opts: any) => {
const cloned = _.cloneDeep(opts || {});
if (Array.isArray(cloned.volumeMounts)) {
cloned.volumeMounts = cloned.volumeMounts.map(
({ sourceType, ...rest }: any) => rest
);
}
return JSON.parse(JSON.stringify(cloned));
};
// Headless watcher: in EDIT mode it reports (via onChange) whether the user has
// changed any k8s_options field from the cluster's saved values. It renders
// nothing — the notice itself is shown in the form footer, above Save/Cancel
// (see cluster-create.tsx), mirroring the model edit interaction. Must be
// mounted inside the cluster <Form> so the watch reads the form store.
export const K8sOptionsChangeWatcher: React.FC<{
action: PageActionType;
currentData?: ListItem;
onChange: (changed: boolean) => void;
}> = ({ action, currentData, onChange }) => {
// `preserve: true` so the watch tracks the full store, including
// gpuInstanceOptions which is toggled via setFieldValue without a mounted
// Form.Item (mirrors GpuInstanceServiceSwitch).
const k8sOptions = Form.useWatch(['k8s_options'], { preserve: true });
const changed =
action === PageAction.EDIT &&
!_.isEqual(
cleanK8sOptions(currentData?.k8s_options),
cleanK8sOptions(k8sOptions)
);
useEffect(() => {
onChange(changed);
}, [changed, onChange]);
// Clear the footer notice when this form unmounts (e.g. switching steps or
// provider) so a stale warning never lingers over the buttons.
useEffect(() => {
return () => onChange(false);
}, [onChange]);
return null;
};
// Kubernetes-specific options that live inside the cluster's advanced section.
const K8sAdvancedOptions: React.FC<{
action: PageActionType;
@@ -1,7 +1,8 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useUserSettings from '@/hooks/use-user-settings';
import { Input as CInput, IconFont } from '@gpustack/core-ui';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { AlertBlockInfo, Input as CInput, IconFont } from '@gpustack/core-ui';
import { YamlEditor } from '@gpustack/core-ui/yaml-editor';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
@@ -120,6 +121,14 @@ const ClusterAdvanceConfig: React.FC<{
<Title>
{intl.formatMessage({ id: 'clusters.create.workerConfig' })}
</Title>
{action === PageAction.EDIT && (
<AlertBlockInfo
type="warning"
style={{ marginBottom: 8 }}
icon={<ExclamationCircleFilled />}
message={intl.formatMessage({ id: 'clusters.edit.workerConfig.tip' })}
></AlertBlockInfo>
)}
<YamlEditor
ref={editorRef}
isDarkTheme={isDarkTheme}