diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_de_DE.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_de_DE.json index 8f297912b289..097d85ab5193 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_de_DE.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_de_DE.json @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Erzwingt, dass jeder neue Pod jedes Mal die benötigten Images herunterlädt.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "Die Verwendung des Plug-Ins für den NodeRestriction-Zugangscontroller schränkt die Node- und POD-Objekte ein, die ein Kubelet ändern kann. Wenn sie durch diesen Zugangscontroller eingeschränkt sind, dürfen Kubelets nur ihr eigenes API-Node-Objekt und nur die API-Pod-Objekte ändern, die an ihren Node gebunden sind.", "kube_service_cluster_admission_plugins_error": "Beim Zurücksetzen Ihres Clusters ist ein Fehler aufgetreten: {{ message }}", - "kube_service_cluster_admission_plugins_success": "Die Änderung der Plugins wurde registriert." + "kube_service_cluster_admission_plugins_success": "Die Änderung der Plugins wurde registriert.", + "kube_service_cluster_etcd_quota": "Verwendung des ETCD-Quotas", + "kube_service_cluster_etcd_quota_info": "Das ETCD-Quota stellt den der ETCD-Datenbank Ihres verwalteten Clusters zugewiesenen Speicherplatz dar.", + "kube_service_etcd_quota_error": "Dieser Cluster verwendet mehr als 80% des zugewiesenen ETCD-Speicherplatzes.", + "kube_service_etcd_quota_error_link": "Wie verwalte ich meine ETCD-Ressourcen?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_en_GB.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_en_GB.json index 27f94fd9e169..f53f2ad72232 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_en_GB.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_en_GB.json @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Forces each new pod to download the required images each time.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "Using the NodeRestriction admission controller plug-in limits the Node and Pod objects that a Kubelet can modify. When throttled by this admission controller, Kubelets are only allowed to modify their own API Node object and only those API Pod objects that are bound to their node.", "kube_service_cluster_admission_plugins_error": "An error has occurred resetting your cluster: {{ message }}", - "kube_service_cluster_admission_plugins_success": "The Admission Plugins modification has been processed." + "kube_service_cluster_admission_plugins_success": "The Admission Plugins modification has been processed.", + "kube_service_cluster_etcd_quota": "Use of ETCD quota", + "kube_service_cluster_etcd_quota_info": "The ETCD quota represents the space allocated to the ETCD database in your managed cluster.", + "kube_service_etcd_quota_error": "This cluster uses more than 80% of the allocated ETCD space.", + "kube_service_etcd_quota_error_link": "How do I manage my ETCD resources?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_es_ES.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_es_ES.json index beed0020e7e5..01cdf02d6c49 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_es_ES.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_es_ES.json @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Fuerza cada nuevo pod a descargar las imágenes necesarias cada vez.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "El uso del plugin del controlador de admisión NodeRestriction limita los objetos Node y Pod que un kubelet puede modificar. Cuando están limitados por este controlador de admisión, los kubelets solo están autorizados a modificar su propio objeto API Node y únicamente los objetos API Pod asociados a sus nodos.", "kube_service_cluster_admission_plugins_error": "Se ha producido un error al restaurar el cluster: {{ message }}.", - "kube_service_cluster_admission_plugins_success": "La modificación de los Admission Plugins se ha registrado." + "kube_service_cluster_admission_plugins_success": "La modificación de los Admission Plugins se ha registrado.", + "kube_service_cluster_etcd_quota": "Uso de la cuota ETCD", + "kube_service_cluster_etcd_quota_info": "La cuota ETCD representa el espacio asignado a la base de datos ETCD del clúster administrado.", + "kube_service_etcd_quota_error": "Este clúster usa más del 80% del espacio ETCD asignado.", + "kube_service_etcd_quota_error_link": "¿Cómo gestionar mis recursos ETCD?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_CA.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_CA.json index ffd30e2b62b9..8cd1be70bc63 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_CA.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_fr_CA.json @@ -93,6 +93,9 @@ "kube_service_cluster_network_vrack_default_gateway": "Passerelle par défaut du vRACK (DHCP)", "kube_service_cluster_network_vrack_customer_gateway": "Passerelle : {{ vRackGatewayIp }}", "kubernetes_add_private_network": "Configurer un réseau", - - "kube_service_network_edit": "Modifier les paramètres du réseau" + "kube_service_network_edit": "Modifier les paramètres du réseau", + "kube_service_cluster_etcd_quota": "Utilisation du quota ETCD", + "kube_service_cluster_etcd_quota_info": "Le quota ETCD représente l'espace alloué à la base de donnée ETCD de votre cluster managé.", + "kube_service_etcd_quota_error": "Ce cluster utilise plus de 80% de l'espace ETCD attribué.", + "kube_service_etcd_quota_error_link": "Comment gérer mes ressources ETCD ?" } 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 ffd30e2b62b9..8cd1be70bc63 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 @@ -93,6 +93,9 @@ "kube_service_cluster_network_vrack_default_gateway": "Passerelle par défaut du vRACK (DHCP)", "kube_service_cluster_network_vrack_customer_gateway": "Passerelle : {{ vRackGatewayIp }}", "kubernetes_add_private_network": "Configurer un réseau", - - "kube_service_network_edit": "Modifier les paramètres du réseau" + "kube_service_network_edit": "Modifier les paramètres du réseau", + "kube_service_cluster_etcd_quota": "Utilisation du quota ETCD", + "kube_service_cluster_etcd_quota_info": "Le quota ETCD représente l'espace alloué à la base de donnée ETCD de votre cluster managé.", + "kube_service_etcd_quota_error": "Ce cluster utilise plus de 80% de l'espace ETCD attribué.", + "kube_service_etcd_quota_error_link": "Comment gérer mes ressources ETCD ?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_it_IT.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_it_IT.json index f8a7045f6447..150d630dc09e 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_it_IT.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_it_IT.json @@ -80,7 +80,7 @@ "kube_service_cluster_network_subnet": "Sottorete", "kube_service_cluster_network_lb_subnet": "Sottorete Load Balancer", "kube_service_cluster_network_error": "Si è verificato un errore durante il caricamento delle informazioni di rete: {{ error }}", - "kube_service_network_edit": "Modificare i parametri della rete", + "kube_service_network_edit": "Modificare le impostazioni di rete", "kubernetes_add_private_network": "Configurare una rete", "kube_service_cluster_admission_plugins": "Admission Plugins", "kube_service_cluster_admission_plugins_mutation": "Attivare/disattivare i plugin", @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Forza ogni nuovo pod a scaricare le immagini necessarie ogni volta.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "L'utilizzo del plugin del controller di ammissione NodeRestriction limita gli oggetti Node e Pod che un kubelet può modificare. Quando sono limitati da questo controller di ammissione, i kubelet sono autorizzati a modificare solo il proprio oggetto API Node ed esclusivamente gli oggetti API Pod associati al loro nodo.", "kube_service_cluster_admission_plugins_error": "Si è verificato un errore durante il ripristino del cluster: {{ message }}", - "kube_service_cluster_admission_plugins_success": "La modifica degli Admission Plugins è stata presa in carico." + "kube_service_cluster_admission_plugins_success": "La modifica degli Admission Plugins è stata presa in carico.", + "kube_service_cluster_etcd_quota": "Utilizzo della quota ETCD", + "kube_service_cluster_etcd_quota_info": "La quota ETCD rappresenta lo spazio assegnato al database ETCD del cluster gestito.", + "kube_service_etcd_quota_error": "Questo cluster utilizza oltre l'80% dello spazio ETCD assegnato.", + "kube_service_etcd_quota_error_link": "Come si gestiscono le risorse ETCD?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pl_PL.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pl_PL.json index 729caf190ea4..a0c6f9b5356d 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pl_PL.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pl_PL.json @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Wprowadź regułę, zgodnie z którą każdy nowy pod zawsze pobiera wymagane obrazy.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "Użycie wtyczki kontrolera dostępu NodeRestriction ogranicza obiekty Node i Pod, które kubelet może modyfikować. Gdy obiekty te są ograniczone przez kontroler dostępu, kublety mogą modyfikować tylko własny obiekt API Node i tylko obiekty API Pod powiązane z ich węzłem.", "kube_service_cluster_admission_plugins_error": "Wystąpił błąd podczas resetu klastra: {{ message }}.", - "kube_service_cluster_admission_plugins_success": "Dyspozycja modyfikacji Admission Plugins została przyjęta." + "kube_service_cluster_admission_plugins_success": "Dyspozycja modyfikacji Admission Plugins została przyjęta.", + "kube_service_cluster_etcd_quota": "Wykorzystanie limitu ETCD", + "kube_service_cluster_etcd_quota_info": "Rozmiar ETCD to przestrzeń przydzielona do bazy danych ETCD zarządzanego klastra.", + "kube_service_etcd_quota_error": "Ten klaster zużywa ponad 80% przydzielonego miejsca ETCD.", + "kube_service_etcd_quota_error_link": "Jak zarządzać zasobami ETCD?" } diff --git a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pt_PT.json b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pt_PT.json index 4dc3cbe4774b..db82dc2b933c 100644 --- a/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pt_PT.json +++ b/packages/manager/apps/pci-kubernetes/public/translations/service/Messages_pt_PT.json @@ -80,7 +80,7 @@ "kube_service_cluster_network_subnet": "Sub-rede", "kube_service_cluster_network_lb_subnet": "Sub-rede Load Balancer", "kube_service_cluster_network_error": "Ocorreu um erro ao carregar as informações de rede: {{ error }}", - "kube_service_network_edit": "Modificar os parâmetros da rede", + "kube_service_network_edit": "Modificar as definições da rede", "kubernetes_add_private_network": "Configurar uma rede", "kube_service_cluster_admission_plugins": "Admissão Plugins", "kube_service_cluster_admission_plugins_mutation": "Ativar/desativar os plugins", @@ -93,5 +93,9 @@ "kube_service_cluster_admission_plugins_always_pull_image_explanation": "Força cada novo pod a carregar as imagens necessárias a cada vez.", "kube_service_cluster_admission_plugins_node_restriction_explanation": "A utilização do plug-in do controlador de admissão NodeRestriction limita os objetos Node e Pod que um kubelet pode modificar. Quando restritas por este controlador de admissão, os kubelets só podem modificar o seu próprio objeto API Node e apenas os objetos API Pod que estão ligados ao nó.", "kube_service_cluster_admission_plugins_error": "Ocorreu um erro ao reiniciar o seu cluster: {{ message }}", - "kube_service_cluster_admission_plugins_success": "A modificação das Admission Plugins foi tomada em conta." + "kube_service_cluster_admission_plugins_success": "A modificação das Admission Plugins foi tomada em conta.", + "kube_service_cluster_etcd_quota": "Utilização da quota ETCD", + "kube_service_cluster_etcd_quota_info": "A quota ETCD representa o espaço atribuído à base de dados ETCD do seu cluster gerido.", + "kube_service_etcd_quota_error": "Este cluster utiliza mais de 80% do espaço ETCD atribuído.", + "kube_service_etcd_quota_error_link": "Como gerir os meus recursos ETCD?" } diff --git a/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts b/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts index e30e8aa76c4e..9ed0233880cc 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts @@ -325,3 +325,10 @@ export async function editNetwork( } return Promise.all(todo); } + +export const getKubeEtcdUsage = async (projectId: string, kubeId: string) => { + const { data } = await v6.get( + `/cloud/project/${projectId}/kube/${kubeId}/metrics/etcdUsage`, + ); + return data; +}; diff --git a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.spec.ts b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.spec.ts index e405eb610ab3..e5be30ee1104 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.spec.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.spec.ts @@ -3,6 +3,7 @@ import { describe, it, vi } from 'vitest'; import * as ApiKubernetesModule from '@/api/data/kubernetes'; import { useAllKube, + useGetClusterEtcdUsage, useKubernetesCluster, useKubes, useRenameKubernetesCluster, @@ -176,3 +177,36 @@ describe('useUpdateKubePolicy', () => { expect(mockError).not.toHaveBeenCalled(); }); }); + +describe('useGetClusterEtcdUsage', () => { + afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + it('fetches etcd usage successfully', async () => { + const mockData = { usage: 500, quota: 1024 }; + vi.spyOn(ApiKubernetesModule, 'getKubeEtcdUsage').mockResolvedValueOnce( + mockData, + ); + + const { result } = renderHook( + () => useGetClusterEtcdUsage('project-valid', 'kube1'), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toEqual(mockData); + }); + + it('handles error when fetching etcd usage', async () => { + vi.spyOn(ApiKubernetesModule, 'getKubeEtcdUsage').mockRejectedValueOnce( + new Error('Network Error'), + ); + + const { result } = renderHook( + () => useGetClusterEtcdUsage('project-error', 'kube1'), + { wrapper }, + ); + await waitFor(() => expect(result.current.isError).toBe(true)); + }); +}); diff --git a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts index 83d27a967db5..a41a91a0a81f 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts @@ -33,6 +33,7 @@ import { updateKubernetesCluster, updateKubeVersion, updateOidcProvider, + getKubeEtcdUsage, } from '../data/kubernetes'; import { getPrivateNetworkName } from '../data/network'; import { useAllPrivateNetworks } from './useNetwork'; @@ -627,3 +628,14 @@ export const useCreateKubernetesCluster = ({ ...mutation, }; }; + +export const useGetClusterEtcdUsage = (projectId, kubeId) => { + const queryKey = ['project', projectId, 'kube', kubeId, 'etcd', 'usage']; + return useQuery({ + queryKey, + queryFn: async () => { + const data = await getKubeEtcdUsage(projectId, kubeId); + return data; + }, + }); +}; diff --git a/packages/manager/apps/pci-kubernetes/src/components/input/PopoverTrigger.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/input/PopoverTrigger.component.tsx new file mode 100644 index 000000000000..05d09da2cde1 --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/input/PopoverTrigger.component.tsx @@ -0,0 +1,30 @@ +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_COLOR_INTENT, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { OsdsText, OsdsIcon } from '@ovhcloud/ods-components/react'; + +const PopoverTrigger = ({ title }: { title: string }) => ( + + event.stopPropagation()} + > + {title} + + + +); + +export default PopoverTrigger; diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.spec.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.spec.tsx new file mode 100644 index 000000000000..68c2b5bbde36 --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.spec.tsx @@ -0,0 +1,95 @@ +import { render, waitFor } from '@testing-library/react'; +import * as manager from '@ovh-ux/manager-react-components'; +import { vi } from 'vitest'; +import { UseQueryResult } from '@tanstack/react-query'; +import ClusterEtcd from './ClusterETCD.component'; +import { wrapper } from '@/wrapperRenders'; +import * as useKubernetesModule from '@/api/hooks/useKubernetes'; +import { getColorByPercentage } from '@/helpers'; + +describe('ClusterEtcd', () => { + it('renders progress bar and usage text correctly', async () => { + const mockUsage = 500; + const mockQuota = 1000; + const mockPercentage = (mockUsage / mockQuota) * 100; + + vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ + data: { usage: mockUsage, quota: mockQuota }, + isPending: false, + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); + + const { getByText, container } = render(, { wrapper }); + + await waitFor(() => { + const progressBar = container.querySelector('osds-progress-bar'); + const progressBarColor = getColorByPercentage(mockPercentage); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar?.getAttribute('color')).toBe(progressBarColor); + expect(progressBar?.getAttribute('value')).toBe( + mockPercentage.toString(), + ); + expect( + getByText('500 unit_size_B / 1000 unit_size_B'), + ).toBeInTheDocument(); + }); + }); + + it('applies correct styles based on progress percentage', async () => { + const mockUsage = 700; + const mockQuota = 1000; + const mockPercentage = (mockUsage / mockQuota) * 100; + + vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ + data: { usage: mockUsage, quota: mockQuota }, + isPending: false, + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); + + const { container } = render(, { wrapper }); + + await waitFor(() => { + const progressBar = container.querySelector('osds-progress-bar'); + const progressBarStyle = getColorByPercentage(mockPercentage); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveProperty('color', progressBarStyle); + }); + }); + + it('renders correct usage and quota information', async () => { + const mockUsage = 300; + const mockQuota = 600; + + vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ + data: { usage: mockUsage, quota: mockQuota }, + isPending: false, + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); + + const { getByText } = render(, { wrapper }); + + await waitFor(() => { + expect( + getByText('300 unit_size_B / 600 unit_size_B'), + ).toBeInTheDocument(); + }); + }); + it('should call addError only once when percentage is over 80%', async () => { + const mockUsage = 550; + const mockQuota = 600; + const addWarning = vi.fn(); + vi.spyOn(manager, 'useNotifications').mockReturnValue({ + addWarning, + }); + + // Mock useGetClusterEtcdUsage to return usage leading to percentage > 80% + vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ + data: { usage: mockUsage, quota: mockQuota }, + isPending: false, + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); + + // Render the component + render(); + await waitFor(() => expect(addWarning).toHaveBeenCalledTimes(1)); + // Check that addWarning is called only once + }); +}); diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx new file mode 100644 index 000000000000..ffc1696be55f --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -0,0 +1,119 @@ +import { useEffect, useMemo } from 'react'; +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_COLOR_INTENT, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; + +import { useTranslation } from 'react-i18next'; +import { useBytes } from '@ovh-ux/manager-pci-common'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { + OsdsIcon, + OsdsLink, + OsdsProgressBar, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { useParams } from 'react-router-dom'; +import { useGetClusterEtcdUsage } from '@/api/hooks/useKubernetes'; +import { getColorByPercentage, QUOTA_ERROR_URL } from '@/helpers'; + +const getProgressBarStyle = (color: string) => ` + progress[value] { + --progress: calc(var(--w) * (attr(value) / 100)); + --color: ${color}; + --background: lightgrey; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + background: var(--background); + } + progress[value]::-webkit-progress-bar { + background: var(--background); + } + progress[value]::-webkit-progress-value { + background: var(--color); + } + progress[value]::-moz-progress-bar { + background: var(--color); +} +`; + +function ClusterEtcd() { + const { projectId, kubeId } = useParams(); + const { formatBytes } = useBytes(); + + const { data: { usage: used, quota: total } = {} } = useGetClusterEtcdUsage( + projectId, + kubeId, + ); + const percentage = useMemo(() => (used / total) * 100, [used, total]); + const { t } = useTranslation(['service']); + const { addWarning } = useNotifications(); + + useEffect(() => { + if (percentage >= 80) { + addWarning( + <> + + {t('kube_service_etcd_quota_error')} + + +
+ + {' '} + {t('kube_service_etcd_quota_error_link')} + + + + , + true, + ); + } + }, [percentage]); + + useEffect(() => { + const progressBarElement = document.querySelector('osds-progress-bar'); + const { shadowRoot } = progressBarElement; + const style = document.createElement('style'); + style.textContent = getProgressBarStyle(getColorByPercentage(percentage)); + shadowRoot.appendChild(style); + }, [percentage]); + + return ( +
+ + + {formatBytes(used, 0, 1024)} / {formatBytes(total, 0, 1024)} + +
+ ); +} +export default ClusterEtcd; diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.spec.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.spec.tsx index 301a77944bf0..5f281b88c30a 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.spec.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterInformation.component.spec.tsx @@ -1,13 +1,19 @@ -import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import * as manager from '@ovh-ux/manager-react-components'; +import { describe, it, expect, vi } from 'vitest'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; import ClusterInformation from '@/components/service/ClusterInformation.component'; +import { wrapper } from '@/wrapperRenders'; import { TKube } from '@/types'; +import * as ApiKube from '@/api/data/kubernetes'; const renderClusterInformation = (kubeDetail) => - render(); + render(, { wrapper }); describe('ClusterInformation', () => { + const mockData = { usage: 500, quota: 1024 }; + vi.spyOn(ApiKube, 'getKubeEtcdUsage').mockResolvedValueOnce(mockData); + const kubeDetail = { id: '1', name: 'Cluster1', @@ -35,25 +41,32 @@ describe('ClusterInformation', () => { ], } as TKube; - it('renders cluster information correctly', () => { - renderClusterInformation(kubeDetail); + it('renders cluster information correctly', async () => { + const { debug } = renderClusterInformation(kubeDetail); - expect( - screen.getByText(/kube_service_cluster_information/i), - ).toBeInTheDocument(); - expect(screen.getByText('Cluster1')).toBeInTheDocument(); - expect( - screen.getByText('kube_service_cluster_status_READY'), - ).toBeInTheDocument(); - expect(screen.getByText('1.18')).toBeInTheDocument(); + await waitFor(() => { + debug(); + expect( + screen.getByText(/kube_service_cluster_information/i), + ).toBeInTheDocument(); + expect(screen.getByText('kube_list_id')).toBeInTheDocument(); - expect( - screen.getByTestId('admission-plugin-chip AlwaysPullImages'), - ).toHaveProperty('color', ODS_THEME_COLOR_INTENT.success); - expect( - screen.getByTestId('admission-plugin-chip NodeRestriction'), - ).toHaveProperty('color', ODS_THEME_COLOR_INTENT.warning); - expect(screen.getByText('Region1')).toBeInTheDocument(); + expect(screen.getByText('Cluster1')).toBeInTheDocument(); + expect( + screen.getByText('kube_service_cluster_status_READY'), + ).toBeInTheDocument(); + expect(screen.getByText('1.18')).toBeInTheDocument(); + expect( + screen.getByTestId('admission-plugin-chip AlwaysPullImages'), + ).toHaveProperty('color', ODS_THEME_COLOR_INTENT.success); + expect( + screen.getByTestId('admission-plugin-chip NodeRestriction'), + ).toHaveProperty('color', ODS_THEME_COLOR_INTENT.warning); + expect(screen.getByText('Region1')).toBeInTheDocument(); + expect( + screen.getByText('kube_service_cluster_etcd_quota'), + ).toBeInTheDocument(); + }); }); it('renders cluster ID with clipboard component', () => { 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 d3b9bf081931..de39f8859043 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 @@ -1,4 +1,6 @@ import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { useEffect } from 'react'; + import { ODS_TEXT_LEVEL, ODS_TEXT_SIZE, @@ -13,12 +15,15 @@ import { useTranslation } from 'react-i18next'; import { Clipboard, TileBlock as TileLine, + useNotifications, } from '@ovh-ux/manager-react-components'; import { TKube } from '@/types'; import ClusterStatus from './ClusterStatus.component'; - +import ClusterETCD from './ClusterETCD.component'; +import TileLineLegacy from './TileLine.component'; import AdmissionPlugins from './AdmissionPlugins.component'; import { isProcessing } from './ClusterManagement.component'; +import ClusterTile from './ClusterTile.component'; export type ClusterInformationProps = { kubeDetail: TKube; @@ -29,6 +34,9 @@ export default function ClusterInformation({ }: Readonly) { const { t } = useTranslation('service'); const { t: tDetail } = useTranslation('listing'); + const { clearNotifications } = useNotifications(); + + useEffect(() => clearNotifications, []); return ( - @@ -65,6 +72,7 @@ export default function ClusterInformation({ + + } value={} /> { + it('renders the cluster quota text', () => { + render(); + + // Check if the translated quota text is rendered + expect( + screen.getByText('kube_service_cluster_etcd_quota'), + ).toBeInTheDocument(); + }); + + it('renders the help icon with correct color and size', () => { + const { container } = render(); + const icon = container.querySelector('osds-icon'); // Assuming the icon has a role of 'img' + expect(icon).toHaveAttribute('color', ODS_THEME_COLOR_INTENT.primary); + fireEvent.click(icon); + expect( + screen.getByText('kube_service_cluster_etcd_quota_info'), + ).toBeVisible(); + }); +}); diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx new file mode 100644 index 000000000000..f64d1df83450 --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from 'react-i18next'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; + +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_LEVEL, +} from '@ovhcloud/ods-components'; +import { + OsdsIcon, + OsdsLink, + OsdsPopover, + OsdsPopoverContent, + OsdsText, +} from '@ovhcloud/ods-components/react'; + +import PopoverTrigger from '../input/PopoverTrigger.component'; +import { QUOTA_ERROR_URL } from '@/helpers'; + +const ClusterTile = () => { + const { t } = useTranslation(['service', 'logs']); + return ( + + + + + <> + {t('kube_service_cluster_etcd_quota_info')} + + {t('logs:see_more_label')} + + + + + + + + ); +}; + +export default ClusterTile; diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/TileLine.component.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/TileLine.component.tsx index a3dc658a802d..c33d1b8c1332 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/TileLine.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/TileLine.component.tsx @@ -3,7 +3,7 @@ import { ODS_TEXT_LEVEL, ODS_TEXT_SIZE } from '@ovhcloud/ods-components'; import { OsdsDivider, OsdsText } from '@ovhcloud/ods-components/react'; type TileLineProps = { - title: string; + title: string | JSX.Element; value: JSX.Element; }; export default function TileLine({ title, value }: Readonly) { diff --git a/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts b/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts index 5337791a9979..5b58a4eb1b84 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts @@ -5,6 +5,7 @@ import { getFormatedKubeVersion, isIPValid, paginateResults, + getColorByPercentage, } from '@/helpers/index'; describe('helper', () => { @@ -66,3 +67,23 @@ describe('helper', () => { expect(result).toBe(false); }); }); + +describe('getColorByPercentage', () => { + it('should return primary color for percentage <= 69', () => { + expect(getColorByPercentage(50)).toBe('var(--ods-color-primary-500)'); + expect(getColorByPercentage(69)).toBe('var(--ods-color-primary-500)'); + }); + + it('should return warning color for percentage between 70 and 79', () => { + expect(getColorByPercentage(75)).toBe('var(--ods-color-warning-500)'); + }); + + it('should return error color for percentage between 80 and 100', () => { + expect(getColorByPercentage(85)).toBe('var(--ods-color-error-500)'); + expect(getColorByPercentage(100)).toBe('var(--ods-color-error-500)'); + }); + + it('should return last color in thresholds if percentage exceeds 100', () => { + expect(getColorByPercentage(120)).toBe('var(--ods-color-error-500)'); + }); +}); diff --git a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts index 91baea6d9f5e..31d92d7ca7ac 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts @@ -1,6 +1,8 @@ import { PaginationState } from '@ovh-ux/manager-react-components'; export const REFETCH_INTERVAL_DURATION = 15_000; +export const QUOTA_ERROR_URL = + 'https://docs.ovh.com/gb/en/kubernetes/etcd-quota-error/'; export const compareFunction = (key: keyof T) => (a: T, b: T) => { const aValue = a[key] || ''; @@ -74,3 +76,41 @@ export const isIPValid = (ip: string) => { return false; } }; + +type ColorThreshold = { + threshold: number; // The upper limit for the threshold + color: string; // The color associated with this threshold +}; +/** + * Returns the appropriate color based on the provided percentage. + * + * @param percentage - The percentage value (0 to 100). + * @returns - The color corresponding to the percentage. + */ +export function getColorByPercentage(percentage: number): string { + const colorThresholds: ColorThreshold[] = [ + { + threshold: 70, + color: 'var(--ods-color-primary-500)', + }, // Color for less than or equal to 33.333% + { + threshold: 80, + color: 'var(--ods-color-warning-500)', + }, // Color for between 33.333% and 66% + { threshold: 100, color: 'var(--ods-color-error-500)' }, // Color for greater than 80% + ].sort((a, b) => a.threshold - b.threshold); + // Sort thresholds to ensure they are in ascending order + colorThresholds.sort((a, b) => a.threshold - b.threshold); + // Loop through thresholds to find the appropriate color + for (let i = 0; i < colorThresholds.length; i += 1) { + const { threshold, color } = colorThresholds[i]; + if (percentage === 100) { + return 'var(--ods-color-error-500)'; + } + if (percentage < threshold) { + return color; + } + } + // If percentage exceeds all thresholds, return the last color + return colorThresholds[colorThresholds.length - 1].color; +} diff --git a/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx b/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx index cc007cd0accd..bd67653dd3e6 100644 --- a/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx +++ b/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx @@ -16,7 +16,13 @@ export const shellContext = { }, }; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); export const wrapper = ({ children }) => (