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 &&
}
+
+ ))}
+
+ >
+ )}