fix: add notification when editing cluster's k8s_options
This commit is contained in:
@@ -110,6 +110,10 @@ export default {
|
|||||||
'{count} new workers have been added to the cluster.',
|
'{count} new workers have been added to the cluster.',
|
||||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
||||||
|
'clusters.edit.workerConfig.tip':
|
||||||
|
'Changes to the worker configuration take effect only after restarting the affected workers.',
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export default {
|
|||||||
'{count} new workers have been added to the cluster.',
|
'{count} new workers have been added to the cluster.',
|
||||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||||
|
'clusters.edit.workerConfig.tip':
|
||||||
|
'ワーカー設定の変更は、対象のワーカーを再起動した後に有効になります。',
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export default {
|
|||||||
'{count} новых воркеров были добавлены в кластер.',
|
'{count} новых воркеров были добавлены в кластер.',
|
||||||
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
||||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||||
|
'clusters.edit.workerConfig.tip':
|
||||||
|
'Изменения конфигурации воркера вступают в силу только после перезапуска соответствующих воркеров.',
|
||||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Укажите имя для контейнера воркера.',
|
'Укажите имя для контейнера воркера.',
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export default {
|
|||||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||||
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
||||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
||||||
|
'clusters.edit.workerConfig.tip':
|
||||||
|
'İşçi düğüm yapılandırmasındaki değişiklikler yalnızca ilgili işçi düğümleri yeniden başlatıldıktan sonra etkili olur.',
|
||||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'İşçi düğüm konteyneri için bir ad belirtin.',
|
'İşçi düğüm konteyneri için bir ad belirtin.',
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ export default {
|
|||||||
'已将 {count} 个新节点添加到集群中。',
|
'已将 {count} 个新节点添加到集群中。',
|
||||||
'clusters.create.serverUrl': 'GPUStack Server 节点地址',
|
'clusters.create.serverUrl': 'GPUStack Server 节点地址',
|
||||||
'clusters.create.workerConfig': '节点配置',
|
'clusters.create.workerConfig': '节点配置',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。',
|
||||||
|
'clusters.edit.workerConfig.tip':
|
||||||
|
'修改节点配置后,需要重启对应节点才会生效。',
|
||||||
'clusters.addworker.containerName': '节点容器名称',
|
'clusters.addworker.containerName': '节点容器名称',
|
||||||
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
||||||
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { FormDrawer } from '@gpustack/core-ui';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import React, { useRef } from 'react';
|
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { ProviderType } from '../config';
|
import { useIntl } from '@umijs/max';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { ProviderType, ProviderValueMap } from '../config';
|
||||||
import {
|
import {
|
||||||
ClusterFormData as FormData,
|
ClusterFormData as FormData,
|
||||||
ClusterListItem as ListItem
|
ClusterListItem as ListItem
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import ClusterForm from './cluster-form';
|
import ClusterForm from './cluster-form';
|
||||||
|
|
||||||
|
const ModalFooterStyle = {
|
||||||
|
padding: '16px 24px 8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
};
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
@@ -28,7 +37,12 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
onOk,
|
onOk,
|
||||||
onCancel
|
onCancel
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const form = useRef<any>(null);
|
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 = () => {
|
const handleSubmit = () => {
|
||||||
form.current?.submit();
|
form.current?.submit();
|
||||||
@@ -53,6 +67,27 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
width={710}
|
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
|
<ClusterForm
|
||||||
ref={form}
|
ref={form}
|
||||||
@@ -61,6 +96,7 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
action={action}
|
action={action}
|
||||||
currentData={currentData}
|
currentData={currentData}
|
||||||
onFinish={handleOk}
|
onFinish={handleOk}
|
||||||
|
onK8sOptionsChange={setK8sOptionsChanged}
|
||||||
/>
|
/>
|
||||||
</FormDrawer>
|
</FormDrawer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import {
|
|||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import AdvanceConfig from '../step-forms/advance-config';
|
import AdvanceConfig from '../step-forms/advance-config';
|
||||||
import CloudProvider from './cloud-provider-form';
|
import CloudProvider from './cloud-provider-form';
|
||||||
import K8sAdvancedOptions, { GpuInstanceServiceSwitch } from './k8s-pod-spec';
|
import K8sAdvancedOptions, {
|
||||||
|
GpuInstanceServiceSwitch,
|
||||||
|
K8sOptionsChangeWatcher
|
||||||
|
} from './k8s-pod-spec';
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
@@ -33,10 +36,23 @@ type AddModalProps = {
|
|||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
credentialList: Global.BaseOption<number>[];
|
credentialList: Global.BaseOption<number>[];
|
||||||
onFinish: (values: FormData) => void;
|
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;
|
ref?: any;
|
||||||
};
|
};
|
||||||
const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
||||||
({ action, provider, currentData, credentialList, onFinish }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
action,
|
||||||
|
provider,
|
||||||
|
currentData,
|
||||||
|
credentialList,
|
||||||
|
onFinish,
|
||||||
|
onK8sOptionsChange
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
||||||
@@ -291,6 +307,14 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
></CollapsePanel>
|
></CollapsePanel>
|
||||||
|
|
||||||
|
{provider === ProviderValueMap.Kubernetes && onK8sOptionsChange && (
|
||||||
|
<K8sOptionsChangeWatcher
|
||||||
|
action={action}
|
||||||
|
currentData={currentData}
|
||||||
|
onChange={onK8sOptionsChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui';
|
import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import React from 'react';
|
import _ from 'lodash';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { ClusterListItem as ListItem } from '../config/types';
|
||||||
import ImageCredential from './image-credential';
|
import ImageCredential from './image-credential';
|
||||||
import K8SVolumeMount from './k8s-volume-mount';
|
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.
|
// Kubernetes-specific options that live inside the cluster's advanced section.
|
||||||
const K8sAdvancedOptions: React.FC<{
|
const K8sAdvancedOptions: React.FC<{
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useUserSettings from '@/hooks/use-user-settings';
|
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 { YamlEditor } from '@gpustack/core-ui/yaml-editor';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Form } from 'antd';
|
import { Button, Form } from 'antd';
|
||||||
@@ -120,6 +121,14 @@ const ClusterAdvanceConfig: React.FC<{
|
|||||||
<Title>
|
<Title>
|
||||||
{intl.formatMessage({ id: 'clusters.create.workerConfig' })}
|
{intl.formatMessage({ id: 'clusters.create.workerConfig' })}
|
||||||
</Title>
|
</Title>
|
||||||
|
{action === PageAction.EDIT && (
|
||||||
|
<AlertBlockInfo
|
||||||
|
type="warning"
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
icon={<ExclamationCircleFilled />}
|
||||||
|
message={intl.formatMessage({ id: 'clusters.edit.workerConfig.tip' })}
|
||||||
|
></AlertBlockInfo>
|
||||||
|
)}
|
||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
isDarkTheme={isDarkTheme}
|
isDarkTheme={isDarkTheme}
|
||||||
|
|||||||
Reference in New Issue
Block a user