diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_FR.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_FR.json index 4c1929aaf8f9..49ef790f5655 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_FR.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_FR.json @@ -31,7 +31,10 @@ "kube_service_cluster_admission_plugins_desactivated": "Désactivé", "kube_service_cluster_admission_plugins_to_activate": "Activer", "kube_service_cluster_admission_plugins_info_restrictions": "Pour des raisons de sécurité, il n'est pas possible de désactiver le plugin Node Restriction.", + "kube_service_cluster_admission_plugins_info_restrictions_redeploy_after_change_admission": "Attention: veuillez noter que toute action sur  les Admisions Plugins implique un redéploiement de l'API Server de votre cluster.", "kube_service_cluster_admission_plugins_to_desactivate": "Désactiver", + "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Force chaque nouveau pod à télécharger les images requises à chaque fois.", + "kube_service_cluster_admission_plugins_node_restriction_explanation": "L'utilisation du plug-in du contrôleur d'admission NodeRestriction limite les objets Node et Pod qu'un kubelet peut modifier. Lorsqu'elles sont limitées par ce contrôleur d'admission, les kubelets ne sont autorisés à modifier que leur propre objet API Node et uniquement les objets API Pod qui sont liés à leur nœud.", "kube_service_cluster_admission_plugins_error": "Une erreur est survenue lors de la réinitialisation de votre cluster : {{ message }}", "kube_service_cluster_version": "Version mineure de Kubernetes", "kube_service_cluster_update_available": "Une nouvelle mise à jour touchant à la sécurité (patch version) est disponible", diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.spec.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.spec.tsx index a1c26e085f84..b2decf4261e0 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.spec.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.spec.tsx @@ -1,14 +1,26 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; import AdmissionPlugins from './AdmissionPlugins.component'; +const navigate = vi.fn(); +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useNavigate: () => navigate, +})); + describe('AdmissionPlugins', () => { it('renders plugins correctly', () => { const enabled = ['NodeRestriction']; const disabled = ['AlwaysPullImages']; - render(); + render( + , + ); // Check plugin labels expect(screen.getByText('Plugin Node Restriction')).toBeInTheDocument(); @@ -37,7 +49,9 @@ describe('AdmissionPlugins', () => { }); it('renders the mutation link', () => { - render(); + render( + , + ); const mutationLink = screen.getByText( 'kube_service_cluster_admission_plugins_mutation', @@ -45,12 +59,27 @@ describe('AdmissionPlugins', () => { expect(mutationLink).toBeInTheDocument(); }); - it('renders the mutation link', () => { - render(); + it('disables the mutation link when isProcessing is true', () => { + render(); const mutationLink = screen.getByText( 'kube_service_cluster_admission_plugins_mutation', ); - expect(mutationLink).toBeInTheDocument(); + expect(mutationLink).toBeDisabled(); + mutationLink.click(); + expect(navigate).not.toHaveBeenCalledWith('./admission-plugin'); + }); + + it('navigates to the admission-plugin page when the mutation link is clicked', () => { + render( + , + ); + + const mutationLink = screen.getByText( + 'kube_service_cluster_admission_plugins_mutation', + ); + mutationLink.click(); + + expect(navigate).toHaveBeenCalledWith('./admission-plugin'); }); }); diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.tsx index 45e02820510e..a4844b60c80c 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/AdmissionPlugins.component.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; import { OsdsChip, OsdsText, OsdsLink } from '@ovhcloud/ods-components/react'; import { useTranslation } from 'react-i18next'; -import { TKube } from '@/types'; +import { TAdmissionPlugin } from '@/types'; import usePluginState from '@/hooks/usePluginState'; export const plugins = [ @@ -16,23 +16,29 @@ export const plugins = [ label: 'Plugin Node Restriction', value: 'node', disabled: true, + tip: 'kube_service_cluster_admission_plugins_node_restriction_explanation', }, { name: 'AlwaysPullImages', label: 'Plugin Always Pull Images', value: 'pull', + tip: 'kube_service_cluster_admission_plugins_always_pull_image_explanation', }, ]; +export type AdmissionPluginsProps = TAdmissionPlugin & { + isProcessing: boolean; +}; + const AdmissionPlugins = ({ + isProcessing, disabled, enabled, -}: TKube['customization']['apiServer']['admissionPlugins']) => { +}: AdmissionPluginsProps) => { const { t } = useTranslation(['service']); - const pluginsState = usePluginState(enabled, disabled); const navigate = useNavigate(); - + const pluginsState = usePluginState(enabled, disabled); return (
{plugins.map((plugin) => ( @@ -64,7 +70,13 @@ const AdmissionPlugins = ({
))} navigate('./admission-plugin')} + // FIXME ODSDS 18 + disabled={isProcessing || undefined} + onClick={() => { + if (!isProcessing) { + navigate('./admission-plugin'); + } + }} color={ODS_THEME_COLOR_INTENT.primary} className="flex font-bold" > diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.tsx index e25b06bcd82c..bb39fd3d8d02 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.tsx @@ -15,6 +15,7 @@ import { TKube } from '@/types'; import ClusterStatus from './ClusterStatus.component'; import TileLine from './TileLine.component'; import AdmissionPlugins from './AdmissionPlugins.component'; +import { isProcessing } from './ClusterManagement.component'; export type ClusterInformationProps = { kubeDetail: TKube; @@ -70,6 +71,7 @@ export default function ClusterInformation({ title={t('kube_service_cluster_admission_plugins')} value={ } diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterManagement.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterManagement.component.tsx index 62585446f8d7..5769b1eb3091 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterManagement.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterManagement.component.tsx @@ -22,14 +22,15 @@ export type ClusterManagementProps = { kubeDetail: TKube; }; +export const isProcessing = (status: string) => + PROCESSING_STATUS.includes(status); + export default function ClusterManagement({ kubeDetail, }: Readonly) { const { t } = useTranslation('service'); const { t: tDetail } = useTranslation('listing'); - const isProcessing = (status: string) => PROCESSING_STATUS.includes(status); - const hrefRenameCluster = useHref('./name'); const hrefResetClusterConfig = useHref('./reset-kubeconfig'); const hrefResetCluster = useHref('./reset'); diff --git a/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugin.page.spec.tsx b/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugin.page.spec.tsx new file mode 100644 index 000000000000..95f8809a62fd --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugin.page.spec.tsx @@ -0,0 +1,103 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import AdmissionPluginsModal from './AdmissionPlugins.page'; +import * as useKubernetesModule from '@/api/hooks/useKubernetes'; +import { wrapper } from '@/wrapperRenders'; + +const navigate = vi.fn(); +const plugState = vi.fn(); +const updateAdmissionPlugin = vi.fn(); + +vi.mock('@/api/hooks/useKubernetes', () => ({ + useKubernetesCluster: vi.fn(), +})); + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useNavigate: () => navigate, + useParams: () => ({}), +})); + +vi.mock('../hooks/usePluginState', () => plugState); +vi.mock('@/api/hooks/useAdmissionPlugin/useAdmissionPlugin', () => ({ + useUpdateAdmissionPlugin: () => ({ + updateAdmissionPlugins: updateAdmissionPlugin, + }), +})); + +describe('AdmissionPluginsModal', () => { + beforeEach(() => { + vi.clearAllMocks(); + plugState.mockReturnValue(() => 'enabled'); + updateAdmissionPlugin.mockReturnValue({ + updateAdmissionPlugins: vi.fn(), + isPending: false, + }); + }); + + it('renders the modal with a spinner when loading', async () => { + (useKubernetesModule.useKubernetesCluster as Mock).mockReturnValue({ + data: null, + isPending: true, + }); + + render(, { wrapper }); + expect(screen.getByTestId('wrapper')).toBeInTheDocument(); + }); + + it('renders the modal with plugins when data is available', async () => { + (useKubernetesModule.useKubernetesCluster as Mock).mockReturnValue({ + data: { + customization: { + apiServer: { + admissionPlugins: { enabled: ['NodeRestrictions'], disabled: [] }, + }, + }, + }, + isPending: false, + }); + + const { container } = render(, { wrapper }); + const modal = container.querySelector('osds-modal'); + expect(modal).toBeInTheDocument(); + expect(modal).toHaveProperty( + 'headline', + 'kube_service_cluster_admission_plugins_mutation', + ); + }); + + it('handles plugin switch change', () => { + (useKubernetesModule.useKubernetesCluster as Mock).mockReturnValue({ + data: { + customization: { + apiServer: { + admissionPlugins: { enabled: ['AlwaysPullImages'], disabled: [] }, + }, + }, + }, + isPending: false, + }); + + render(, { wrapper }); + const switchElement = screen.getAllByText( + 'kube_service_cluster_admission_plugins_to_activate', + ); + fireEvent.change(switchElement[0], { detail: { current: 'enabled' } }); + }); + + it('handles cancel button click', () => { + render(, { wrapper }); + const cancelButton = screen.getByText( + 'common:common_stepper_cancel_button_label', + ); + fireEvent.click(cancelButton); + expect(navigate).toHaveBeenCalledWith('..'); + }); + + it('handles save button click', () => { + render(, { wrapper }); + const saveButton = screen.getByText('common:common_save_button_label'); + fireEvent.click(saveButton); + expect(updateAdmissionPlugin).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugins.page.tsx b/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugins.page.tsx index 06c6613bf851..35287d14e5e4 100644 --- a/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugins.page.tsx +++ b/packages/manager/apps/pci-kubernetes/src/pages/admission-plugin/AdmissionPlugins.page.tsx @@ -3,16 +3,17 @@ import { OsdsSwitchItem, OsdsButton, OsdsModal, - OsdsSpinner, OsdsTooltip, OsdsDivider, OsdsText, OsdsMessage, OsdsTooltipContent, + OsdsSpinner, } from '@ovhcloud/ods-components/react'; import { useCallback, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; + import { ODS_BUTTON_VARIANT, ODS_MESSAGE_TYPE, @@ -21,11 +22,13 @@ import { ODS_TEXT_COLOR_INTENT, ODS_TEXT_LEVEL, ODS_TEXT_SIZE, + OdsSwitchChangedEventDetail, + OsdsSwitchCustomEvent, } from '@ovhcloud/ods-components'; import { useTranslation, Translation } from 'react-i18next'; import { useNotifications } from '@ovh-ux/manager-react-components'; import { plugins } from '../../components/service/AdmissionPlugins.component'; -import { useKubeDetail } from '@/api/hooks/useKubernetes'; +import { useKubernetesCluster } from '@/api/hooks/useKubernetes'; import usePluginState from '@/hooks/usePluginState'; import { useUpdateAdmissionPlugin } from '@/api/hooks/useAdmissionPlugin/useAdmissionPlugin'; @@ -36,37 +39,48 @@ const AdmissionPluginsModal = () => { const navigate = useNavigate(); const onClose = () => navigate('..'); const { projectId, kubeId } = useParams(); - const { data: kubeDetail, isPending } = useKubeDetail(projectId, kubeId); + const { data: kubeDetail, isPending } = useKubernetesCluster( + projectId, + kubeId, + ); const { t } = useTranslation(['service']); const { - customization: { apiServer: { admissionPlugins } = {} } = {}, - } = kubeDetail; + customization: { + apiServer: { admissionPlugins }, + }, + } = kubeDetail ?? { + customization: { + apiServer: { admissionPlugins: { enabled: [], disabled: [] } }, + }, + }; const [pluginData, setPluginData] = useState(admissionPlugins); const pluginState = usePluginState(pluginData.enabled, pluginData.disabled); - useResponsiveModal('450px'); const { addError, addSuccess } = useNotifications(); - const handleChange = useCallback((e, name) => { - const value = e.detail.current; + const handleChange = useCallback( + (e: OsdsSwitchCustomEvent, name: string) => { + const value = e.detail.current; - setPluginData( - (prevPluginData) => - Object.fromEntries( - Object.entries(prevPluginData).map(([state, array]) => { - if (!array.includes(name) && state === value) { - return [state, [...array, name]]; - } - if (array.includes(name) && state !== value) { - return [state, array.filter((plugin) => plugin !== name)]; - } - return [state, array]; - }), - ) as TAdmissionPlugin, - ); - }, []); + setPluginData( + (prevPluginData) => + Object.fromEntries( + Object.entries(prevPluginData).map(([state, array]) => { + if (!array.includes(name) && state === value) { + return [state, [...array, name]]; + } + if (array.includes(name) && state !== value) { + return [state, array.filter((plugin) => plugin !== name)]; + } + return [state, array]; + }), + ) as TAdmissionPlugin, + ); + }, + [], + ); const { updateAdmissionPlugins, @@ -85,6 +99,7 @@ const AdmissionPluginsModal = () => { , true, ); + onClose(); }, onSuccess: () => { addSuccess( @@ -111,77 +126,89 @@ const AdmissionPluginsModal = () => { return ( { onClose(); }} > - {(isPending || isMutationUpdating) && ( - - )} + {isPending || isMutationUpdating ? ( +
+ +
+ ) : ( + <> + + {t('kube_service_cluster_admission_plugins_info_restrictions')} +
- - {t('kube_service_cluster_admission_plugins_info_restrictions')} - -
- {!isPending && - plugins.map((plugin, i) => ( -
-
- - - {plugin.name} - - - test - - - { - if (!plugin.disabled) { - handleChange(e, plugin.name); - } - }} - disabled={plugin.disabled} - > - - {t('kube_service_cluster_admission_plugins_to_activate')} - - {!plugin.disabled && ( - +
+ {!isPending && + plugins.map((plugin, i) => ( +
+
+ + + {plugin.name} + + + {t(plugin.tip)} + + + { + if (!plugin.disabled) { + handleChange(e, plugin.name); + } + }} + disabled={plugin.disabled} > - {t( - 'kube_service_cluster_admission_plugins_to_desactivate', + + {t( + 'kube_service_cluster_admission_plugins_to_activate', + )} + + {!plugin.disabled && ( + + {t( + 'kube_service_cluster_admission_plugins_to_desactivate', + )} + )} - - )} - -
- {i !== plugins.length - 1 && } -
- ))} -
+
+
+ {i !== plugins.length - 1 && } +
+ ))} +
+ + )}