diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index bc15f8a0..e8992404 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -110,6 +110,10 @@ export default { '{count} new workers have been added to the cluster.', 'clusters.create.serverUrl': 'GPUStack Server URL', '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.tips': 'Specify a name for the worker container.', diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index 7aa75394..7c340033 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -110,6 +110,10 @@ export default { '{count} new workers have been added to the cluster.', 'clusters.create.serverUrl': 'GPUStack Server URL', 'clusters.create.workerConfig': 'Worker Configuration', + 'clusters.edit.k8sOptions.changed.tip': + 'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。', + 'clusters.edit.workerConfig.tip': + 'ワーカー設定の変更は、対象のワーカーを再起動した後に有効になります。', 'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName.tips': 'Specify a name for the worker container.', diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index 020a7482..58c42862 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -110,6 +110,10 @@ export default { '{count} новых воркеров были добавлены в кластер.', 'clusters.create.serverUrl': 'URL сервера GPUStack', 'clusters.create.workerConfig': 'Конфигурация воркера', + 'clusters.edit.k8sOptions.changed.tip': + 'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.', + 'clusters.edit.workerConfig.tip': + 'Изменения конфигурации воркера вступают в силу только после перезапуска соответствующих воркеров.', 'clusters.addworker.containerName': 'Имя контейнера воркера', 'clusters.addworker.containerName.tips': 'Укажите имя для контейнера воркера.', diff --git a/src/locales/tr-TR/clusters.ts b/src/locales/tr-TR/clusters.ts index ec650c3f..fa5b6784 100644 --- a/src/locales/tr-TR/clusters.ts +++ b/src/locales/tr-TR/clusters.ts @@ -110,6 +110,10 @@ export default { '{count} yeni işçi düğüm kümeye eklendi.', 'clusters.create.serverUrl': "GPUStack Sunucu URL'si", '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.tips': 'İşçi düğüm konteyneri için bir ad belirtin.', diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 5bc5d7c4..ab736c6d 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -108,6 +108,10 @@ export default { '已将 {count} 个新节点添加到集群中。', 'clusters.create.serverUrl': 'GPUStack Server 节点地址', 'clusters.create.workerConfig': '节点配置', + 'clusters.edit.k8sOptions.changed.tip': + '您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。', + 'clusters.edit.workerConfig.tip': + '修改节点配置后,需要重启对应节点才会生效。', 'clusters.addworker.containerName': '节点容器名称', 'clusters.addworker.containerName.tips': '为节点容器指定一个名称。', 'clusters.addworker.dataVolume': 'GPUStack 数据卷', diff --git a/src/pages/cluster-management/components/add-cluster.tsx b/src/pages/cluster-management/components/add-cluster.tsx index 6d0b8877..151cb3f7 100644 --- a/src/pages/cluster-management/components/add-cluster.tsx +++ b/src/pages/cluster-management/components/add-cluster.tsx @@ -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 = ({ onOk, onCancel }) => { + const intl = useIntl(); const form = useRef(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(false); const handleSubmit = () => { form.current?.submit(); @@ -53,6 +67,27 @@ const AddCluster: React.FC = ({ onCancel={handleCancel} onSubmit={handleSubmit} width={710} + footer={ + <> + {action === PageAction.EDIT && + provider === ProviderValueMap.Kubernetes && + k8sOptionsChanged && ( + } + message={intl.formatMessage({ + id: 'clusters.edit.k8sOptions.changed.tip' + })} + > + )} + + + } > = ({ action={action} currentData={currentData} onFinish={handleOk} + onK8sOptionsChange={setK8sOptionsChanged} /> ); diff --git a/src/pages/cluster-management/components/cluster-form.tsx b/src/pages/cluster-management/components/cluster-form.tsx index a1339278..424299a1 100644 --- a/src/pages/cluster-management/components/cluster-form.tsx +++ b/src/pages/cluster-management/components/cluster-form.tsx @@ -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[]; 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 = 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([]); @@ -291,6 +307,14 @@ const ClusterForm: React.FC = forwardRef( } ]} > + + {provider === ProviderValueMap.Kubernetes && onK8sOptionsChange && ( + + )} ); diff --git a/src/pages/cluster-management/components/k8s-pod-spec.tsx b/src/pages/cluster-management/components/k8s-pod-spec.tsx index b8bcd899..c2c127ab 100644 --- a/src/pages/cluster-management/components/k8s-pod-spec.tsx +++ b/src/pages/cluster-management/components/k8s-pod-spec.tsx @@ -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
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; diff --git a/src/pages/cluster-management/step-forms/advance-config.tsx b/src/pages/cluster-management/step-forms/advance-config.tsx index 9681dc71..a14ceb12 100644 --- a/src/pages/cluster-management/step-forms/advance-config.tsx +++ b/src/pages/cluster-management/step-forms/advance-config.tsx @@ -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<{ {intl.formatMessage({ id: 'clusters.create.workerConfig' })} + {action === PageAction.EDIT && ( + } + message={intl.formatMessage({ id: 'clusters.edit.workerConfig.tip' })} + > + )}