From 2bcf5d3c89e2617eb2f80fd2b5f219c262dc5c0a Mon Sep 17 00:00:00 2001 From: CDS Translator Agent Date: Fri, 27 Sep 2024 11:01:04 +0000 Subject: [PATCH 01/14] fix(i18n): add missing translations [CDS 3343] Signed-off-by: CDS Translator Agent From a3cd471cae3d444e21749f1ec7674226780ee9af Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Mon, 18 Nov 2024 10:38:10 +0100 Subject: [PATCH 02/14] feat(pci.kubernetes): init etcd component ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../manager/apps/pci-kubernetes/package.json | 1 + .../pci-kubernetes/src/api/data/kubernetes.ts | 9 +++ .../src/api/hooks/useKubernetes.ts | 17 +++++ .../service/ClusterETCD.component.tsx | 21 +++++ .../service/ClusterInformation.component.tsx | 8 +- .../apps/pci-kubernetes/src/helpers/index.ts | 76 +++++++++++++++++++ .../pci-kubernetes/src/schema/kubernetes.ts | 13 ++++ 7 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx create mode 100644 packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts diff --git a/packages/manager/apps/pci-kubernetes/package.json b/packages/manager/apps/pci-kubernetes/package.json index c1f0252bbcf7..8aaa835f0283 100644 --- a/packages/manager/apps/pci-kubernetes/package.json +++ b/packages/manager/apps/pci-kubernetes/package.json @@ -43,6 +43,7 @@ "react-i18next": "^14.1.2", "react-router-dom": "^6.3.0", "react-use": "^17.5.0", + "zod": "^3.22.4", "zustand": "^4.5.5" }, "devDependencies": { 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..f8077c31e980 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts @@ -1,5 +1,7 @@ import { fetchIcebergV6, v6 } from '@ovh-ux/manager-core-api'; import { TKube, TNetworkConfiguration } from '@/types'; +import { validateSchema } from '@/helpers'; +import { EtcdUsageSchema, TKubeEtcdUsage } from '@/schema/kubernetes'; export const getKubernetesCluster = async ( projectId: string, @@ -325,3 +327,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 validateSchema({ schema: EtcdUsageSchema, data }); +}; 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..00f01886268a 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,19 @@ export const useCreateKubernetesCluster = ({ ...mutation, }; }; + +export const useGetClusterEtcdUsage = (projectId, kubeId) => { + const queryKey = ['project', projectId, 'kube', kubeId, 'etcd', 'usage']; + const query = useQuery({ + queryKey, + queryFn: async () => { + const data = await getKubeEtcdUsage(projectId, kubeId); + return data; + }, + enabled: Boolean(projectId), + suspense: true, + }); + return { + ...query, + }; +}; 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..44a18467bdba --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -0,0 +1,21 @@ +import { OsdsProgressBar } from '@ovhcloud/ods-components/react'; +import { useParams } from 'react-router-dom'; +import { useGetClusterEtcdUsage } from '@/api/hooks/useKubernetes'; +import { formatBytes } from '@/helpers'; +// TODO rajouter le seuil +function ClusterEtcd() { + const { projectId, kubeId } = useParams(); + const { + data: { usage: used, quota: total }, + } = useGetClusterEtcdUsage(projectId, kubeId); + const percentage = (used / total) * 100; + return ( +
+ +
+ {formatBytes(used)} / {formatBytes(total)} +
+
+ ); +} +export default ClusterEtcd; 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..99ec8acc02c0 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 @@ -16,6 +16,7 @@ import { } from '@ovh-ux/manager-react-components'; import { TKube } from '@/types'; import ClusterStatus from './ClusterStatus.component'; +import ClusterETCD from './ClusterETCD.component'; import AdmissionPlugins from './AdmissionPlugins.component'; import { isProcessing } from './ClusterManagement.component'; @@ -46,11 +47,9 @@ export default function ClusterInformation({ {t('kube_service_cluster_information')} - - - @@ -91,10 +89,12 @@ export default function ClusterInformation({ {kubeDetail.region} - + + + ); diff --git a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts index 91baea6d9f5e..4a497396fa8e 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts @@ -1,4 +1,5 @@ import { PaginationState } from '@ovh-ux/manager-react-components'; +import { z } from 'zod'; export const REFETCH_INTERVAL_DURATION = 15_000; @@ -74,3 +75,78 @@ export const isIPValid = (ip: string) => { return false; } }; +/** + * Validates data against a given Zod schema. + * + * @template T - Type of the data to validate. + * @param params - Parameters for the function. + * @param params.schema - The Zod schema used for validation. + * @param params.data - The data to validate. + * @param params.onInvalid - Optional function called in case of validation error. + * @returns - The validated data or null if validation fails. + */ +export function validateSchema({ + schema, + data, + onInvalid, +}: { + schema: z.Schema; + data: T; + onInvalid?: (error: z.ZodError) => void; +}) { + try { + const validatedData = schema.parse(data); + return validatedData as T; + } catch (error) { + if (onInvalid && error instanceof z.ZodError) { + onInvalid(error); + } + } + return null; +} +/** + * Converts a number of bytes into a human-readable format (e.g., Ko, Mo, Go, To). + * + * @param bytes - The number of bytes to convert. + * @returns - The formatted string representing the size in appropriate units. + */ +export function formatBytes(bytes: number): string { + const units = ['o', 'KiB', 'MiB', 'GiB', 'TiB']; + const convertionRate = 1024; + let unitIndex = 0; + let size = bytes; // Use a separate variable to hold the size value + while (size >= convertionRate && unitIndex < units.length - 1) { + size /= convertionRate; + unitIndex += 1; + } + return `${Math.round(size)} ${units[unitIndex]}`; +} +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). + * @param thresholds - The color thresholds to use for determining color. + * @returns - The color corresponding to the percentage. + */ +export function getColorByPercentage(percentage: number): string { + const colorThresholds: ColorThreshold[] = [ + { threshold: 33.333, color: 'blue' }, // Color for less than or equal to 33.333% + { threshold: 66, color: 'yellow' }, // Color for between 33.333% and 66% + { threshold: 100, color: 'red' }, // Color for greater than 66% + ].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 <= threshold) { + return color; + } + } + // If percentage exceeds all thresholds, return the last color + return thresholds[thresholds.length - 1].color; +} diff --git a/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts b/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts new file mode 100644 index 000000000000..d46d5087af12 --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const EtcdUsageSchema = z.object({ + quota: z + .number() + .int() + .nonnegative(), + usage: z + .number() + .int() + .nonnegative(), +}); +export type TKubeEtcdUsage = z.infer; From 440cc2d00f2925c4259aa2b856f673b7efa83ddc Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Fri, 15 Nov 2024 16:03:10 +0100 Subject: [PATCH 03/14] feat(pci.kubernetes): add cluster ETCD component and tests ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../manager/apps/pci-kubernetes/package.json | 1 + .../translations/service/Messages_fr_FR.json | 6 +- .../src/api/hooks/useKubernetes.spec.ts | 35 +++++++ .../src/api/hooks/useKubernetes.ts | 8 +- .../service/ClusterETCD.component.spec.tsx | 95 +++++++++++++++++++ .../service/ClusterETCD.component.tsx | 77 +++++++++++++-- .../ClusterInformation.component.spec.tsx | 67 +++++++++---- .../service/ClusterInformation.component.tsx | 16 +++- .../service/ClusterTile.component.spec.tsx | 27 ++++++ .../service/ClusterTile.component.tsx | 55 +++++++++++ .../pci-kubernetes/src/helpers/index.spec.ts | 80 +++++++++++++++- .../apps/pci-kubernetes/src/helpers/index.ts | 17 +++- .../pci-kubernetes/src/wrapperRenders.tsx | 25 +++-- 13 files changed, 459 insertions(+), 50 deletions(-) create mode 100644 packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.spec.tsx create mode 100644 packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.spec.tsx create mode 100644 packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx diff --git a/packages/manager/apps/pci-kubernetes/package.json b/packages/manager/apps/pci-kubernetes/package.json index 8aaa835f0283..19f1e009fbcf 100644 --- a/packages/manager/apps/pci-kubernetes/package.json +++ b/packages/manager/apps/pci-kubernetes/package.json @@ -39,6 +39,7 @@ "i18next-http-backend": "^2.5.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.1.2", "react-hook-form": "^7.52.1", "react-i18next": "^14.1.2", "react-router-dom": "^6.3.0", 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..41e45a09aedd 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,8 @@ "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é. Veuillez consulter notre guide pour en savoir plus sur le quota ETCD.  {{link}}", + "kube_service_etcd_quota_error": "Ce cluster utilise plus de 80% de l'espace ETCD attribué. Consultez cette page pour éviter tout impact sur votre service: {{link}}" } 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..fb053d638303 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,37 @@ 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 }, + ); + + expect(result.current).toBe(null); + }); +}); 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 00f01886268a..4d8584df12d0 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts @@ -4,6 +4,7 @@ import { UndefinedInitialDataOptions, useMutation, useQuery, + useSuspenseQuery, } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -631,16 +632,11 @@ export const useCreateKubernetesCluster = ({ export const useGetClusterEtcdUsage = (projectId, kubeId) => { const queryKey = ['project', projectId, 'kube', kubeId, 'etcd', 'usage']; - const query = useQuery({ + return useSuspenseQuery({ queryKey, queryFn: async () => { const data = await getKubeEtcdUsage(projectId, kubeId); return data; }, - enabled: Boolean(projectId), - suspense: true, }); - return { - ...query, - }; }; 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..1f23d802872b --- /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 { UseSuspenseQueryResult } from '@tanstack/react-query'; +import ClusterEtcd from './ClusterETCD.component'; +import { wrapper } from '@/wrapperRenders'; +import * as useKubernetesModule from '@/api/hooks/useKubernetes'; +import { formatBytes, 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 UseSuspenseQueryResult<{ 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(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`), + ).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 UseSuspenseQueryResult<{ 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 UseSuspenseQueryResult<{ usage: number; quota: number }>); + + const { getByText } = render(, { wrapper }); + + await waitFor(() => { + expect( + getByText(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`), + ).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 UseSuspenseQueryResult<{ 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 index 44a18467bdba..3486a4112147 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -1,20 +1,83 @@ -import { OsdsProgressBar } from '@ovhcloud/ods-components/react'; +import { useEffect, useMemo } from 'react'; +import { + ODS_TEXT_COLOR_INTENT, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { Trans, useTranslation } from 'react-i18next'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { OsdsProgressBar, OsdsText } from '@ovhcloud/ods-components/react'; import { useParams } from 'react-router-dom'; import { useGetClusterEtcdUsage } from '@/api/hooks/useKubernetes'; -import { formatBytes } from '@/helpers'; -// TODO rajouter le seuil +import { formatBytes, getColorByPercentage, QUOTA_ERROR_URL } from '@/helpers'; + +const getProgressBarStyle = (color: string) => ` + progress[value] { + --progress: calc(var(--w) * (attr(value) / 100)); /* Largeur de la progression en fonction du pourcentage */ + --color: ${color}; + --background: lightgrey; /* Couleur de fond */ + -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 { data: { usage: used, quota: total }, } = useGetClusterEtcdUsage(projectId, kubeId); - const percentage = (used / total) * 100; + const percentage = useMemo(() => (used / total) * 100, []); + const { t } = useTranslation(['service']); + const { addWarning } = useNotifications(); + + useEffect(() => { + if (percentage > 80) { + addWarning( + }}> + {t('kube_service_etcd_quota_error', { + link: QUOTA_ERROR_URL, + })} + , + 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); + }, []); + return (
- -
+ + {formatBytes(used)} / {formatBytes(total)} -
+
); } 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..e066359b2906 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,46 @@ describe('ClusterInformation', () => { ], } as TKube; - it('renders cluster information correctly', () => { - renderClusterInformation(kubeDetail); + it.skip('calls clearNotifications on unmount', async () => { + const { unmount } = renderClusterInformation(kubeDetail); + const mockClearNotifications = vi.fn(); + + vi.spyOn(manager, 'useNotifications').mockReturnValue({ + clearNotifications: mockClearNotifications, + }); + expect(mockClearNotifications).not.toHaveBeenCalled(); + act(() => unmount()); + // not working issue + // https://github.com/testing-library/react-hooks-testing-library/issues/847 + expect(mockClearNotifications).toHaveBeenCalledTimes(1); + }); + + 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 99ec8acc02c0..12ad2e705bfd 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,6 +15,7 @@ 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'; @@ -20,6 +23,7 @@ import ClusterETCD from './ClusterETCD.component'; import AdmissionPlugins from './AdmissionPlugins.component'; import { isProcessing } from './ClusterManagement.component'; +import ClusterTile from './ClusterTile.component'; export type ClusterInformationProps = { kubeDetail: TKube; @@ -30,6 +34,9 @@ export default function ClusterInformation({ }: Readonly) { const { t } = useTranslation('service'); const { t: tDetail } = useTranslation('listing'); + const { clearNotifications } = useNotifications(); + + useEffect(() => clearNotifications, []); return ( + + + + - + { + // hacky, need to use a help icon and a tooltip inside the string label + } + ) as unknown) as string}> diff --git a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.spec.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.spec.tsx new file mode 100644 index 000000000000..ab7265f86e3a --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.spec.tsx @@ -0,0 +1,27 @@ +import { describe, it, vi, expect } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { useTranslation } from 'react-i18next'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import ClusterTile from '@/components/service/ClusterTile.component'; +import { QUOTA_ERROR_URL } from '@/helpers'; + +describe('ClusterTile', () => { + 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..7984078e4589 --- /dev/null +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx @@ -0,0 +1,55 @@ +import { useTranslation, Trans } from 'react-i18next'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; + +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { + OsdsIcon, + OsdsPopover, + OsdsPopoverContent, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { QUOTA_ERROR_URL } from '@/helpers'; + +const ClusterTile = () => { + const { t } = useTranslation(['service']); + return ( + + + event.stopPropagation()} + > + {t('kube_service_cluster_etcd_quota')} + + + + + + }}> + {t('kube_service_cluster_etcd_quota_info', { + link: QUOTA_ERROR_URL, + })} + + + + + ); +}; + +export default ClusterTile; 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..a2e92de7be8f 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts @@ -1,10 +1,14 @@ -import { describe, it } from 'vitest'; +import { describe, it, vi } from 'vitest'; +import { z } from 'zod'; import { compareFunction, formatIP, getFormatedKubeVersion, isIPValid, paginateResults, + validateSchema, + formatBytes, + getColorByPercentage, } from '@/helpers/index'; describe('helper', () => { @@ -66,3 +70,77 @@ describe('helper', () => { expect(result).toBe(false); }); }); + +describe('validateSchema', () => { + const schema = z.object({ + name: z.string(), + age: z + .number() + .int() + .positive(), + }); + + it('should return validated data if data is valid', () => { + const data = { name: 'John', age: 30 }; + const result = validateSchema({ schema, data }); + expect(result).toEqual(data); + }); + + it('should return null and call onInvalid if data is invalid', () => { + const data = { name: 'John', age: -5 }; + const onInvalidMock = vi.fn(); + const result = validateSchema({ schema, data, onInvalid: onInvalidMock }); + + expect(result).toBeNull(); + expect(onInvalidMock).toHaveBeenCalledTimes(1); + expect(onInvalidMock.mock.calls[0][0]).toBeInstanceOf(z.ZodError); + }); + + it('should return null without calling onInvalid if data is invalid and onInvalid is not provided', () => { + const data = { name: 'John', age: -5 }; + const result = validateSchema({ schema, data }); + expect(result).toBeNull(); + }); +}); + +describe('formatBytes', () => { + it('should format bytes correctly for values under 1024', () => { + expect(formatBytes(512)).toBe('512 o'); + }); + + it('should format bytes correctly in KiB', () => { + expect(formatBytes(2048)).toBe('2 KiB'); + }); + + it('should format bytes correctly in MiB', () => { + expect(formatBytes(1048576)).toBe('1 MiB'); + }); + + it('should format bytes correctly in GiB', () => { + expect(formatBytes(1073741824)).toBe('1 GiB'); + }); + + it('should format bytes correctly in TiB', () => { + expect(formatBytes(1099511627776)).toBe('1 TiB'); + }); +}); + +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 4a497396fa8e..c088c8c24ea5 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts @@ -2,6 +2,8 @@ import { PaginationState } from '@ovh-ux/manager-react-components'; import { z } from 'zod'; 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] || ''; @@ -129,14 +131,19 @@ type ColorThreshold = { * Returns the appropriate color based on the provided percentage. * * @param percentage - The percentage value (0 to 100). - * @param thresholds - The color thresholds to use for determining color. * @returns - The color corresponding to the percentage. */ export function getColorByPercentage(percentage: number): string { const colorThresholds: ColorThreshold[] = [ - { threshold: 33.333, color: 'blue' }, // Color for less than or equal to 33.333% - { threshold: 66, color: 'yellow' }, // Color for between 33.333% and 66% - { threshold: 100, color: 'red' }, // Color for greater than 66% + { + threshold: 69, + color: 'var(--ods-color-primary-500)', + }, // Color for less than or equal to 33.333% + { + threshold: 79, + 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); @@ -148,5 +155,5 @@ export function getColorByPercentage(percentage: number): string { } } // If percentage exceeds all thresholds, return the last color - return thresholds[thresholds.length - 1].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..199c6abd7cb6 100644 --- a/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx +++ b/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx @@ -4,6 +4,7 @@ import { ShellContextType, } from '@ovh-ux/manager-react-shell-client'; import { vi } from 'vitest'; +import { ErrorBoundary } from 'react-error-boundary'; export const shellContext = { environment: { @@ -16,14 +17,22 @@ export const shellContext = { }, }; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); export const wrapper = ({ children }) => ( - - - {children} - - + Error From Test}> + + + {children} + + + ); From 6352186d1aa2d08c2d841805931e36076e42e02d Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Mon, 18 Nov 2024 14:19:13 +0100 Subject: [PATCH 04/14] feat(pci.kubernetes): add tile and sonar fix ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../input/PopoverTrigger.component.tsx | 30 +++++++++++++++++++ .../service/ClusterETCD.component.tsx | 2 +- .../ClusterInformation.component.spec.tsx | 14 --------- .../service/ClusterInformation.component.tsx | 6 ++-- .../service/ClusterTile.component.tsx | 29 +++--------------- .../components/service/TileLine.component.tsx | 2 +- 6 files changed, 38 insertions(+), 45 deletions(-) create mode 100644 packages/manager/apps/pci-kubernetes/src/components/input/PopoverTrigger.component.tsx 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.tsx b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx index 3486a4112147..491333ea272a 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -45,7 +45,7 @@ function ClusterEtcd() { useEffect(() => { if (percentage > 80) { addWarning( - }}> + }}> {t('kube_service_etcd_quota_error', { link: QUOTA_ERROR_URL, })} 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 e066359b2906..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 @@ -41,20 +41,6 @@ describe('ClusterInformation', () => { ], } as TKube; - it.skip('calls clearNotifications on unmount', async () => { - const { unmount } = renderClusterInformation(kubeDetail); - const mockClearNotifications = vi.fn(); - - vi.spyOn(manager, 'useNotifications').mockReturnValue({ - clearNotifications: mockClearNotifications, - }); - expect(mockClearNotifications).not.toHaveBeenCalled(); - act(() => unmount()); - // not working issue - // https://github.com/testing-library/react-hooks-testing-library/issues/847 - expect(mockClearNotifications).toHaveBeenCalledTimes(1); - }); - it('renders cluster information correctly', async () => { const { debug } = renderClusterInformation(kubeDetail); 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 12ad2e705bfd..d2cc33aa1df0 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 @@ -20,7 +20,7 @@ import { 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'; @@ -106,9 +106,7 @@ export default function ClusterInformation({ { // hacky, need to use a help icon and a tooltip inside the string label } - ) as unknown) as string}> - - + } value={} /> ); 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 index 7984078e4589..f43b02fc781f 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx @@ -1,47 +1,26 @@ import { useTranslation, Trans } from 'react-i18next'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; import { - ODS_ICON_NAME, - ODS_ICON_SIZE, - ODS_TEXT_LEVEL, - ODS_TEXT_SIZE, -} from '@ovhcloud/ods-components'; -import { - OsdsIcon, 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']); return ( - - event.stopPropagation()} - > - {t('kube_service_cluster_etcd_quota')} - - - + - }}> + }}> {t('kube_service_cluster_etcd_quota_info', { link: QUOTA_ERROR_URL, })} 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..0ad4e0d17ee5 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: JSX.Element; value: JSX.Element; }; export default function TileLine({ title, value }: Readonly) { From 687ad73dcd8d012ec5283b5200a0715bfa5b2ea8 Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Mon, 18 Nov 2024 15:28:22 +0100 Subject: [PATCH 05/14] feat(pci.kubernetes): fix build ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../src/components/service/TileLine.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ad4e0d17ee5..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: JSX.Element; + title: string | JSX.Element; value: JSX.Element; }; export default function TileLine({ title, value }: Readonly) { From 311362061fc33a41c4bdda7aad9b3b722504e00c Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Tue, 19 Nov 2024 15:44:54 +0100 Subject: [PATCH 06/14] feat(pci.kubernetes): pr review 1 ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../manager/apps/pci-kubernetes/package.json | 1 - .../translations/service/Messages_fr_FR.json | 5 +- .../pci-kubernetes/src/api/data/kubernetes.ts | 5 +- .../service/ClusterETCD.component.tsx | 60 +++++++++++++++---- .../service/ClusterInformation.component.tsx | 5 +- .../service/ClusterTile.component.tsx | 39 +++++++++--- .../pci-kubernetes/src/helpers/index.spec.ts | 59 +----------------- .../apps/pci-kubernetes/src/helpers/index.ts | 48 +-------------- .../pci-kubernetes/src/schema/kubernetes.ts | 13 ---- 9 files changed, 87 insertions(+), 148 deletions(-) delete mode 100644 packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts diff --git a/packages/manager/apps/pci-kubernetes/package.json b/packages/manager/apps/pci-kubernetes/package.json index 19f1e009fbcf..eeef86026920 100644 --- a/packages/manager/apps/pci-kubernetes/package.json +++ b/packages/manager/apps/pci-kubernetes/package.json @@ -44,7 +44,6 @@ "react-i18next": "^14.1.2", "react-router-dom": "^6.3.0", "react-use": "^17.5.0", - "zod": "^3.22.4", "zustand": "^4.5.5" }, "devDependencies": { 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 41e45a09aedd..3814b26c80a1 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 @@ -95,6 +95,7 @@ "kubernetes_add_private_network": "Configurer un 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é. Veuillez consulter notre guide pour en savoir plus sur le quota ETCD.  {{link}}", - "kube_service_etcd_quota_error": "Ce cluster utilise plus de 80% de l'espace ETCD attribué. Consultez cette page pour éviter tout impact sur votre service: {{link}}" + "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/src/api/data/kubernetes.ts b/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts index f8077c31e980..1eee197d0433 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts @@ -1,7 +1,6 @@ import { fetchIcebergV6, v6 } from '@ovh-ux/manager-core-api'; import { TKube, TNetworkConfiguration } from '@/types'; -import { validateSchema } from '@/helpers'; -import { EtcdUsageSchema, TKubeEtcdUsage } from '@/schema/kubernetes'; +import { TKubeEtcdUsage } from '@/schema/kubernetes'; export const getKubernetesCluster = async ( projectId: string, @@ -332,5 +331,5 @@ export const getKubeEtcdUsage = async (projectId: string, kubeId: string) => { const { data } = await v6.get( `/cloud/project/${projectId}/kube/${kubeId}/metrics/etcdUsage`, ); - return validateSchema({ schema: EtcdUsageSchema, data }); + return data; }; 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 index 491333ea272a..0d858201875b 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -1,21 +1,32 @@ 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 { Trans, useTranslation } from 'react-i18next'; +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 { OsdsProgressBar, OsdsText } from '@ovhcloud/ods-components/react'; +import { + OsdsIcon, + OsdsLink, + OsdsProgressBar, + OsdsText, +} from '@ovhcloud/ods-components/react'; import { useParams } from 'react-router-dom'; import { useGetClusterEtcdUsage } from '@/api/hooks/useKubernetes'; -import { formatBytes, getColorByPercentage, QUOTA_ERROR_URL } from '@/helpers'; +import { getColorByPercentage, QUOTA_ERROR_URL } from '@/helpers'; const getProgressBarStyle = (color: string) => ` progress[value] { - --progress: calc(var(--w) * (attr(value) / 100)); /* Largeur de la progression en fonction du pourcentage */ + --progress: calc(var(--w) * (attr(value) / 100)); --color: ${color}; - --background: lightgrey; /* Couleur de fond */ + --background: lightgrey; -webkit-appearance: none; -moz-appearance: none; appearance: none; @@ -30,11 +41,13 @@ const getProgressBarStyle = (color: string) => ` } 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); @@ -45,11 +58,34 @@ function ClusterEtcd() { useEffect(() => { if (percentage > 80) { addWarning( - }}> - {t('kube_service_etcd_quota_error', { - link: QUOTA_ERROR_URL, - })} - , + <> + {t('kube_service_etcd_quota_error')} +
+ + {' '} + + {t('kube_service_etcd_quota_error_link')} + + + + + , true, ); } @@ -76,7 +112,7 @@ function ClusterEtcd() { color={ODS_TEXT_COLOR_INTENT.text} className="mt-4 float-right" > - {formatBytes(used)} / {formatBytes(total)} + {formatBytes(used, 0, 1024)} / {formatBytes(total, 0, 1024)}
); 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 d2cc33aa1df0..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 @@ -83,6 +83,7 @@ export default function ClusterInformation({ {kubeDetail.version} + } value={} /> - { - // hacky, need to use a help icon and a tooltip inside the string label - } - } value={} /> ); 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 index f43b02fc781f..f64d1df83450 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterTile.component.tsx @@ -1,17 +1,25 @@ -import { useTranslation, Trans } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; -import { ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; 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']); + const { t } = useTranslation(['service', 'logs']); return ( @@ -20,11 +28,26 @@ const ClusterTile = () => { color={ODS_THEME_COLOR_INTENT.text} level={ODS_TEXT_LEVEL.body} > - }}> - {t('kube_service_cluster_etcd_quota_info', { - link: QUOTA_ERROR_URL, - })} - + <> + {t('kube_service_cluster_etcd_quota_info')} + + {t('logs:see_more_label')} + + + +
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 a2e92de7be8f..5b58a4eb1b84 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts @@ -1,13 +1,10 @@ -import { describe, it, vi } from 'vitest'; -import { z } from 'zod'; +import { describe, it } from 'vitest'; import { compareFunction, formatIP, getFormatedKubeVersion, isIPValid, paginateResults, - validateSchema, - formatBytes, getColorByPercentage, } from '@/helpers/index'; @@ -71,60 +68,6 @@ describe('helper', () => { }); }); -describe('validateSchema', () => { - const schema = z.object({ - name: z.string(), - age: z - .number() - .int() - .positive(), - }); - - it('should return validated data if data is valid', () => { - const data = { name: 'John', age: 30 }; - const result = validateSchema({ schema, data }); - expect(result).toEqual(data); - }); - - it('should return null and call onInvalid if data is invalid', () => { - const data = { name: 'John', age: -5 }; - const onInvalidMock = vi.fn(); - const result = validateSchema({ schema, data, onInvalid: onInvalidMock }); - - expect(result).toBeNull(); - expect(onInvalidMock).toHaveBeenCalledTimes(1); - expect(onInvalidMock.mock.calls[0][0]).toBeInstanceOf(z.ZodError); - }); - - it('should return null without calling onInvalid if data is invalid and onInvalid is not provided', () => { - const data = { name: 'John', age: -5 }; - const result = validateSchema({ schema, data }); - expect(result).toBeNull(); - }); -}); - -describe('formatBytes', () => { - it('should format bytes correctly for values under 1024', () => { - expect(formatBytes(512)).toBe('512 o'); - }); - - it('should format bytes correctly in KiB', () => { - expect(formatBytes(2048)).toBe('2 KiB'); - }); - - it('should format bytes correctly in MiB', () => { - expect(formatBytes(1048576)).toBe('1 MiB'); - }); - - it('should format bytes correctly in GiB', () => { - expect(formatBytes(1073741824)).toBe('1 GiB'); - }); - - it('should format bytes correctly in TiB', () => { - expect(formatBytes(1099511627776)).toBe('1 TiB'); - }); -}); - describe('getColorByPercentage', () => { it('should return primary color for percentage <= 69', () => { expect(getColorByPercentage(50)).toBe('var(--ods-color-primary-500)'); diff --git a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts index c088c8c24ea5..dd57465c2cdf 100644 --- a/packages/manager/apps/pci-kubernetes/src/helpers/index.ts +++ b/packages/manager/apps/pci-kubernetes/src/helpers/index.ts @@ -1,5 +1,4 @@ import { PaginationState } from '@ovh-ux/manager-react-components'; -import { z } from 'zod'; export const REFETCH_INTERVAL_DURATION = 15_000; export const QUOTA_ERROR_URL = @@ -77,52 +76,7 @@ export const isIPValid = (ip: string) => { return false; } }; -/** - * Validates data against a given Zod schema. - * - * @template T - Type of the data to validate. - * @param params - Parameters for the function. - * @param params.schema - The Zod schema used for validation. - * @param params.data - The data to validate. - * @param params.onInvalid - Optional function called in case of validation error. - * @returns - The validated data or null if validation fails. - */ -export function validateSchema({ - schema, - data, - onInvalid, -}: { - schema: z.Schema; - data: T; - onInvalid?: (error: z.ZodError) => void; -}) { - try { - const validatedData = schema.parse(data); - return validatedData as T; - } catch (error) { - if (onInvalid && error instanceof z.ZodError) { - onInvalid(error); - } - } - return null; -} -/** - * Converts a number of bytes into a human-readable format (e.g., Ko, Mo, Go, To). - * - * @param bytes - The number of bytes to convert. - * @returns - The formatted string representing the size in appropriate units. - */ -export function formatBytes(bytes: number): string { - const units = ['o', 'KiB', 'MiB', 'GiB', 'TiB']; - const convertionRate = 1024; - let unitIndex = 0; - let size = bytes; // Use a separate variable to hold the size value - while (size >= convertionRate && unitIndex < units.length - 1) { - size /= convertionRate; - unitIndex += 1; - } - return `${Math.round(size)} ${units[unitIndex]}`; -} + type ColorThreshold = { threshold: number; // The upper limit for the threshold color: string; // The color associated with this threshold diff --git a/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts b/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts deleted file mode 100644 index d46d5087af12..000000000000 --- a/packages/manager/apps/pci-kubernetes/src/schema/kubernetes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -export const EtcdUsageSchema = z.object({ - quota: z - .number() - .int() - .nonnegative(), - usage: z - .number() - .int() - .nonnegative(), -}); -export type TKubeEtcdUsage = z.infer; From 34276e6f2e87cdc71ede6b9fab8dd386bb50a08c Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Tue, 19 Nov 2024 17:03:37 +0100 Subject: [PATCH 07/14] feat(pci.kubernetes): use query instead of suspense ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../src/api/hooks/useKubernetes.spec.ts | 3 +-- .../src/api/hooks/useKubernetes.ts | 2 +- .../service/ClusterETCD.component.spec.tsx | 16 ++++++++-------- .../service/ClusterETCD.component.tsx | 9 +++++---- .../apps/pci-kubernetes/src/wrapperRenders.tsx | 17 +++++++---------- 5 files changed, 22 insertions(+), 25 deletions(-) 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 fb053d638303..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 @@ -207,7 +207,6 @@ describe('useGetClusterEtcdUsage', () => { () => useGetClusterEtcdUsage('project-error', 'kube1'), { wrapper }, ); - - expect(result.current).toBe(null); + 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 4d8584df12d0..62bb813107ed 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts @@ -632,7 +632,7 @@ export const useCreateKubernetesCluster = ({ export const useGetClusterEtcdUsage = (projectId, kubeId) => { const queryKey = ['project', projectId, 'kube', kubeId, 'etcd', 'usage']; - return useSuspenseQuery({ + return useQuery({ queryKey, queryFn: async () => { const data = await getKubeEtcdUsage(projectId, kubeId); 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 index 1f23d802872b..68c2b5bbde36 100644 --- 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 @@ -1,11 +1,11 @@ import { render, waitFor } from '@testing-library/react'; import * as manager from '@ovh-ux/manager-react-components'; import { vi } from 'vitest'; -import { UseSuspenseQueryResult } from '@tanstack/react-query'; +import { UseQueryResult } from '@tanstack/react-query'; import ClusterEtcd from './ClusterETCD.component'; import { wrapper } from '@/wrapperRenders'; import * as useKubernetesModule from '@/api/hooks/useKubernetes'; -import { formatBytes, getColorByPercentage } from '@/helpers'; +import { getColorByPercentage } from '@/helpers'; describe('ClusterEtcd', () => { it('renders progress bar and usage text correctly', async () => { @@ -16,7 +16,7 @@ describe('ClusterEtcd', () => { vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ data: { usage: mockUsage, quota: mockQuota }, isPending: false, - } as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>); + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); const { getByText, container } = render(, { wrapper }); @@ -30,7 +30,7 @@ describe('ClusterEtcd', () => { mockPercentage.toString(), ); expect( - getByText(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`), + getByText('500 unit_size_B / 1000 unit_size_B'), ).toBeInTheDocument(); }); }); @@ -43,7 +43,7 @@ describe('ClusterEtcd', () => { vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ data: { usage: mockUsage, quota: mockQuota }, isPending: false, - } as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>); + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); const { container } = render(, { wrapper }); @@ -63,13 +63,13 @@ describe('ClusterEtcd', () => { vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ data: { usage: mockUsage, quota: mockQuota }, isPending: false, - } as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>); + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); const { getByText } = render(, { wrapper }); await waitFor(() => { expect( - getByText(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`), + getByText('300 unit_size_B / 600 unit_size_B'), ).toBeInTheDocument(); }); }); @@ -85,7 +85,7 @@ describe('ClusterEtcd', () => { vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({ data: { usage: mockUsage, quota: mockQuota }, isPending: false, - } as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>); + } as unknown) as UseQueryResult<{ usage: number; quota: number }>); // Render the component render(); 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 index 0d858201875b..6935a64b557e 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -48,10 +48,11 @@ function ClusterEtcd() { const { projectId, kubeId } = useParams(); const { formatBytes } = useBytes(); - const { - data: { usage: used, quota: total }, - } = useGetClusterEtcdUsage(projectId, kubeId); - const percentage = useMemo(() => (used / total) * 100, []); + const { data: { usage: used, quota: total } = {} } = useGetClusterEtcdUsage( + projectId, + kubeId, + ); + const percentage = useMemo(() => (used / total) * 100, [used, total]); const { t } = useTranslation(['service']); const { addWarning } = useNotifications(); diff --git a/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx b/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx index 199c6abd7cb6..bd67653dd3e6 100644 --- a/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx +++ b/packages/manager/apps/pci-kubernetes/src/wrapperRenders.tsx @@ -4,7 +4,6 @@ import { ShellContextType, } from '@ovh-ux/manager-react-shell-client'; import { vi } from 'vitest'; -import { ErrorBoundary } from 'react-error-boundary'; export const shellContext = { environment: { @@ -26,13 +25,11 @@ const queryClient = new QueryClient({ }); export const wrapper = ({ children }) => ( - Error From Test}> - - - {children} - - - + + + {children} + + ); From 43b3ed21122e6ebb18d044e88d7ee4a12e7fb95e Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Tue, 19 Nov 2024 17:05:30 +0100 Subject: [PATCH 08/14] feat(pci.kubernetes): delete react boundary ref: TAPC-23 Signed-off-by: Pierre-Philippe --- packages/manager/apps/pci-kubernetes/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/manager/apps/pci-kubernetes/package.json b/packages/manager/apps/pci-kubernetes/package.json index eeef86026920..c1f0252bbcf7 100644 --- a/packages/manager/apps/pci-kubernetes/package.json +++ b/packages/manager/apps/pci-kubernetes/package.json @@ -39,7 +39,6 @@ "i18next-http-backend": "^2.5.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-error-boundary": "^4.1.2", "react-hook-form": "^7.52.1", "react-i18next": "^14.1.2", "react-router-dom": "^6.3.0", From 172ef16346473582102bdcacea18b43dd75e47ac Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Tue, 19 Nov 2024 17:09:11 +0100 Subject: [PATCH 09/14] feat(pci.kubernetes): delete useless dependancy ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts | 1 - 1 file changed, 1 deletion(-) 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 62bb813107ed..a41a91a0a81f 100644 --- a/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts +++ b/packages/manager/apps/pci-kubernetes/src/api/hooks/useKubernetes.ts @@ -4,7 +4,6 @@ import { UndefinedInitialDataOptions, useMutation, useQuery, - useSuspenseQuery, } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; From 352964cf6c7db7f20e23515594ee894ae6107cc2 Mon Sep 17 00:00:00 2001 From: Pierre-Philippe Date: Wed, 20 Nov 2024 14:35:22 +0100 Subject: [PATCH 10/14] feat(pci.kubernetes): fix product review ref: TAPC-23 Signed-off-by: Pierre-Philippe --- .../translations/service/Messages_fr_FR.json | 2 +- .../service/ClusterETCD.component.tsx | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) 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 3814b26c80a1..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 @@ -97,5 +97,5 @@ "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" + "kube_service_etcd_quota_error_link": "Comment gérer mes ressources ETCD ?" } 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 index 6935a64b557e..ffc1696be55f 100644 --- a/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx +++ b/packages/manager/apps/pci-kubernetes/src/components/service/ClusterETCD.component.tsx @@ -57,24 +57,22 @@ function ClusterEtcd() { const { addWarning } = useNotifications(); useEffect(() => { - if (percentage > 80) { + if (percentage >= 80) { addWarning( <> - {t('kube_service_etcd_quota_error')} + + {t('kube_service_etcd_quota_error')} + +
{' '} - - {t('kube_service_etcd_quota_error_link')} - + {t('kube_service_etcd_quota_error_link')}