From 37dfbce3e33da41ae9a3ac31b5cc5e76dda15dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Mon, 16 Feb 2026 10:51:39 +0100 Subject: [PATCH 1/5] feat(cloud-credentials): implement cloud credentials management page and link tests --- .../$organizationId/settings/cloud-credentials.tsx | 3 ++- libs/domains/organizations/feature/src/index.ts | 1 + .../page-organization-credentials-feature.spec.tsx | 2 +- .../settings-cloud-credentials.tsx} | 12 +++++------- 4 files changed, 9 insertions(+), 9 deletions(-) rename libs/{pages/settings/src/lib/feature/page-organization-credentials-feature => domains/organizations/feature/src/lib/settings-cloud-credentials}/page-organization-credentials-feature.spec.tsx (95%) rename libs/{pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.tsx => domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx} (97%) diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/cloud-credentials.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/cloud-credentials.tsx index 6b3ca40bdac..f177678a8f0 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/cloud-credentials.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/cloud-credentials.tsx @@ -1,9 +1,10 @@ import { createFileRoute } from '@tanstack/react-router' +import { SettingsCloudCredentials } from '@qovery/domains/organizations/feature' export const Route = createFileRoute('/_authenticated/organization/$organizationId/settings/cloud-credentials')({ component: RouteComponent, }) function RouteComponent() { - return
Hello "/_authenticated/organization/$organizationId/settings/cloud-credentials"!
+ return } diff --git a/libs/domains/organizations/feature/src/index.ts b/libs/domains/organizations/feature/src/index.ts index 303e9ec5ef8..786dcf4b0a7 100644 --- a/libs/domains/organizations/feature/src/index.ts +++ b/libs/domains/organizations/feature/src/index.ts @@ -90,3 +90,4 @@ export * from './lib/invoice-banner/invoice-banner' export * from './lib/free-trial-banner/free-trial-banner' export * from './lib/settings-general/settings-general' export * from './lib/settings-labels-annotations/settings-labels-annotations' +export * from './lib/settings-cloud-credentials/settings-cloud-credentials' diff --git a/libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx similarity index 95% rename from libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx index 200fac30f33..e962b21c83c 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx @@ -1,6 +1,6 @@ import { type OrganizationCrendentialsResponseListResultsInner } from 'qovery-typescript-axios' import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import { PageOrganizationCredentialsFeature } from './page-organization-credentials-feature' +import { PageOrganizationCredentialsFeature } from '../../../../../../pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature' let mockCredentials: OrganizationCrendentialsResponseListResultsInner[] = [] jest.mock('@qovery/domains/organizations/feature', () => { diff --git a/libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx similarity index 97% rename from libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.tsx rename to libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx index 6518ace7e9e..77d52936793 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx @@ -1,16 +1,16 @@ import { useQueryClient } from '@tanstack/react-query' +import { useParams } from '@tanstack/react-router' import { CloudProviderEnum, type ClusterCredentials, type CredentialCluster } from 'qovery-typescript-axios' import { Suspense, useMemo, useState } from 'react' -import { useParams } from 'react-router-dom' import { match } from 'ts-pattern' import { useDeleteCloudProviderCredential } from '@qovery/domains/cloud-providers/feature' import { ClusterAvatar, ClusterCredentialsModal, CredentialsListClustersModal } from '@qovery/domains/clusters/feature' -import { useOrganizationCredentials } from '@qovery/domains/organizations/feature' import { NeedHelp } from '@qovery/shared/assistant/feature' import { BlockContent, Heading, Section, Skeleton } from '@qovery/shared/ui' import { Button, DropdownMenu, Icon, Indicator, useModal, useModalConfirmation } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' import { queries } from '@qovery/state/util-queries' +import { useOrganizationCredentials } from '../hooks/use-organization-credentials/use-organization-credentials' const convertToCloudProviderEnum = (cloudProvider: ClusterCredentials['object_type']): CloudProviderEnum => { return match(cloudProvider) @@ -140,7 +140,7 @@ const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: Crede const PageOrganizationCredentials = () => { const { openModal, closeModal } = useModal() - const { organizationId = '' } = useParams() + const { organizationId = '' } = useParams({ strict: false }) const { openModalConfirmation } = useModalConfirmation() const { mutate: deleteCloudProviderCredential } = useDeleteCloudProviderCredential() @@ -284,9 +284,9 @@ const Loader = () => ( ) -export function PageOrganizationCredentialsFeature() { +export function SettingsCloudCredentials() { useDocumentTitle('Cloud Crendentials - Organization settings') - const { organizationId = '' } = useParams() + const { organizationId = '' } = useParams({ strict: false }) const { openModal, closeModal } = useModal() const queryClient = useQueryClient() const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false) @@ -376,5 +376,3 @@ export function PageOrganizationCredentialsFeature() { ) } - -export default PageOrganizationCredentialsFeature From c7cd7119a2d1cb6393d04370c1f099cd0bc80c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Mon, 16 Feb 2026 12:18:11 +0100 Subject: [PATCH 2/5] refactor(cluster-credentials): enhance UI consistency and remove useless delete functionality in cred' modal --- .../cluster-credentials-modal.tsx | 58 ++----- .../credentials-list-clusters-modal.tsx | 14 +- ...-annotation-items-list-modal.spec.tsx.snap | 6 +- ...-organization-credentials-feature.spec.tsx | 134 --------------- .../settings-cloud-credentials.spec.tsx | 102 ++++++++++++ .../settings-cloud-credentials.tsx | 152 +++++++++--------- .../settings-general.spec.tsx | 8 +- .../settings-labels-annotations.spec.tsx | 6 +- .../settings-labels-annotations.tsx | 4 +- .../dropdown-menu/dropdown-menu.tsx | 27 ++++ 10 files changed, 240 insertions(+), 271 deletions(-) delete mode 100644 libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx create mode 100644 libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx diff --git a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx index 2d8761120da..5409c889484 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx +++ b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx @@ -125,7 +125,7 @@ function CalloutEdit({ - + The credential change won't be applied to the mirroring registry of this cluster. Make sure to update the credentials properly in this cluster's mirroring registry section. {clusterId && ( @@ -293,33 +293,6 @@ export function ClusterCredentialsModal({ } }) - const onDelete = async () => { - openModalConfirmation({ - title: 'Delete credential', - description: ( -

- To confirm the deletion of {credential?.name}, please type "delete" -

- ), - name: credential?.name, - confirmationMethod: 'action', - action: async () => { - if (credential?.id) { - try { - await deleteCloudProviderCredential({ - organizationId, - cloudProvider: cloudProviderLocal, - credentialId: credential.id, - }) - onClose() - } catch (error) { - console.error(error) - } - } - }, - }) - } - const watchType = methods.watch('type') const watchAzureApplicationId = methods.watch('azure_application_id') const watchAzureSubscriptionId = methods.watch('azure_subscription_id') @@ -368,7 +341,6 @@ export function ClusterCredentialsModal({ } onSubmit={onSubmit} onClose={onClose} - onDelete={onDelete} loading={isLoadingCreate || isLoadingEdit} isEdit={isEdit} submitLabel={submitLabel} @@ -399,7 +371,7 @@ export function ClusterCredentialsModal({ {watchType === 'STATIC' && ( <> {cloudProviderLocal === 'AWS' && ( -
+

1. Create a user for Qovery

Follow the instructions available on this page

-
+

1. Connect to your GCP Console and create/open a project

@@ -421,11 +393,11 @@ export function ClusterCredentialsModal({ https://console.cloud.google.com/
-
+

2. Open the embedded Google shell and run the following command

-
+
$ curl https://setup.qovery.com/create_credentials_gcp.sh | \ bash -s -- $GOOGLE_CLOUD_PROJECT @@ -440,7 +412,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" )} {cloudProviderLocal === 'SCW' && ( -
+

1. Generate Access key Id/Secret Access Key

Follow the instructions available on this page

-
+

1. Connect to your AWS Console

Make sure you are connected to the right AWS account

https://aws.amazon.com/fr/console/
-
+

2. Create a role for Qovery and grant assume role permissions

@@ -476,7 +448,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" Cloudformation stack
-
+

3. Insert here the role ARN

) : ( -
+

{cloudProviderLocal === 'GCP' ? '3. Download the key.json generated and drag and drop it here' @@ -562,7 +534,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" /> {isEditDirty && ( <> -
+
Confirm your secret key )} @@ -609,7 +581,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" /> {isEditDirty && ( <> -
+
Confirm your secret key )} @@ -759,7 +731,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" return ( <> -
+

2. Connect to your Azure Console and go to shell console

@@ -770,7 +742,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" https://portal.azure.com/
-
+

3. Open the embedded Azure shell and run the following command

@@ -782,7 +754,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account" Please note that this script can take up to 30 seconds to complete.

-
+
$ {snippet} diff --git a/libs/domains/clusters/feature/src/lib/credentials-list-clusters-modal/credentials-list-clusters-modal.tsx b/libs/domains/clusters/feature/src/lib/credentials-list-clusters-modal/credentials-list-clusters-modal.tsx index 9ce32d52478..cbb546167da 100644 --- a/libs/domains/clusters/feature/src/lib/credentials-list-clusters-modal/credentials-list-clusters-modal.tsx +++ b/libs/domains/clusters/feature/src/lib/credentials-list-clusters-modal/credentials-list-clusters-modal.tsx @@ -1,8 +1,7 @@ import * as Dialog from '@radix-ui/react-dialog' import { type ClusterCredentials, type CredentialCluster } from 'qovery-typescript-axios' -import { Link } from 'react-router-dom' import { CLUSTER_SETTINGS_CREDENTIALS_URL, CLUSTER_SETTINGS_URL, CLUSTER_URL } from '@qovery/shared/routes' -import { Heading, Section } from '@qovery/shared/ui' +import { Heading, Link, Section } from '@qovery/shared/ui' import { pluralize } from '@qovery/shared/util-js' import { ClusterAvatar } from '../cluster-avatar/cluster-avatar' @@ -21,20 +20,23 @@ export function CredentialsListClustersModal({ return (
- + Attached {pluralize(clusters.length, 'cluster', 'clusters')} ({clusters.length}) - Credential: {credential.name} -
+ + Credential: {credential.name} + +
{clusters.map((cluster) => ( -
+
{cluster.name}
diff --git a/libs/domains/organizations/feature/src/lib/label-annotation-items-list-modal/__snapshots__/label-annotation-items-list-modal.spec.tsx.snap b/libs/domains/organizations/feature/src/lib/label-annotation-items-list-modal/__snapshots__/label-annotation-items-list-modal.spec.tsx.snap index fdd88b55c57..90055a64e92 100644 --- a/libs/domains/organizations/feature/src/lib/label-annotation-items-list-modal/__snapshots__/label-annotation-items-list-modal.spec.tsx.snap +++ b/libs/domains/organizations/feature/src/lib/label-annotation-items-list-modal/__snapshots__/label-annotation-items-list-modal.spec.tsx.snap @@ -94,7 +94,8 @@ exports[`LabelAnnotationItemsListModal should match snapshots with annotation 1` Development @@ -249,7 +250,8 @@ exports[`LabelAnnotationItemsListModal should match snapshots with label 1`] = ` Development diff --git a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx deleted file mode 100644 index e962b21c83c..00000000000 --- a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/page-organization-credentials-feature.spec.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { type OrganizationCrendentialsResponseListResultsInner } from 'qovery-typescript-axios' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import { PageOrganizationCredentialsFeature } from '../../../../../../pages/settings/src/lib/feature/page-organization-credentials-feature/page-organization-credentials-feature' - -let mockCredentials: OrganizationCrendentialsResponseListResultsInner[] = [] -jest.mock('@qovery/domains/organizations/feature', () => { - return { - ...jest.requireActual('@qovery/domains/organizations/feature'), - useOrganizationCredentials: () => ({ - data: mockCredentials, - isLoading: false, - }), - } -}) - -describe('PageOrganizationCredentialsFeature', () => { - it('should render', () => { - const { baseElement } = renderWithProviders() - expect(baseElement).toBeTruthy() - }) - - describe('when there are no credentials', () => { - it('should render an empty state', () => { - renderWithProviders() - screen.getByText('All credentials related to your clusters will appear here after creation.') - }) - }) - - describe('when there are credentials', () => { - it('should render the credentials', () => { - mockCredentials = [ - { - credential: { - id: '1', - name: 'Credential 1', - object_type: 'AWS', - access_key_id: '123', - }, - clusters: [], - }, - { - credential: { - id: '2', - name: 'Credential 2', - object_type: 'GCP', - }, - clusters: [ - { - id: '1', - name: 'Cluster 1', - }, - ], - }, - ] - renderWithProviders() - - screen.getByText('Credential 1') - screen.getByText('Credential 2') - - const row = screen.getByText('Credential 1').parentElement?.parentElement?.parentElement - const buttons = row?.querySelectorAll('button') - const deleteButton = buttons?.[1] - - expect(deleteButton).toBeInTheDocument() - expect(deleteButton).toBeEnabled() - }) - - it('delete button should be displayed only when no clusters are attached', () => { - mockCredentials = [ - { - credential: { - id: '1', - name: 'Credential 1', - object_type: 'AWS', - access_key_id: '123', - }, - clusters: [ - { - id: '1', - name: 'Cluster 1', - }, - ], - }, - { - credential: { - id: '2', - name: 'Credential 2', - object_type: 'GCP', - }, - clusters: [], - }, - ] - renderWithProviders() - - const row = screen.getByText('Credential 1').parentElement?.parentElement?.parentElement - const deleteButton = row?.querySelector('button[data-testid="delete-credential"]') - - expect(deleteButton).toBeNull() - }) - - it('view button should be displayed only if clusters are attached', () => { - mockCredentials = [ - { - credential: { - id: '1', - name: 'Credential 1', - object_type: 'AWS', - access_key_id: '123', - }, - clusters: [ - { - id: '1', - name: 'Cluster 1', - }, - ], - }, - { - credential: { - id: '2', - name: 'Credential 2', - object_type: 'GCP', - }, - clusters: [], - }, - ] - renderWithProviders() - - const row = screen.getByText('Credential 1').parentElement?.parentElement?.parentElement - const viewButton = row?.querySelector('button[data-testid="view-credential"]') // View button is the first button in the row - - expect(viewButton).toBeInTheDocument() - }) - }) -}) diff --git a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx new file mode 100644 index 00000000000..e5d6ca52c98 --- /dev/null +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx @@ -0,0 +1,102 @@ +import { type OrganizationCrendentialsResponseListResultsInner } from 'qovery-typescript-axios' +import { useDeleteCloudProviderCredential } from '@qovery/domains/cloud-providers/feature' +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { useOrganizationCredentials } from '../hooks/use-organization-credentials/use-organization-credentials' +import { SettingsCloudCredentials } from './settings-cloud-credentials' + +jest.mock('../hooks/use-organization-credentials/use-organization-credentials') +jest.mock('@qovery/domains/cloud-providers/feature', () => ({ + ...jest.requireActual('@qovery/domains/cloud-providers/feature'), + useDeleteCloudProviderCredential: jest.fn(), +})) +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), + useParams: () => ({ organizationId: 'org-1' }), +})) + +describe('SettingsCloudCredentials', () => { + const useOrganizationCredentialsMock = useOrganizationCredentials as jest.MockedFunction< + typeof useOrganizationCredentials + > + const useDeleteCloudProviderCredentialMock = useDeleteCloudProviderCredential as jest.MockedFunction< + typeof useDeleteCloudProviderCredential + > + + let mockCredentials: OrganizationCrendentialsResponseListResultsInner[] = [] + + beforeEach(() => { + mockCredentials = [] + useOrganizationCredentialsMock.mockReturnValue({ + data: mockCredentials, + } as ReturnType) + useDeleteCloudProviderCredentialMock.mockReturnValue({ + mutate: jest.fn(), + } as ReturnType) + }) + + it('should render', () => { + const { baseElement } = renderWithProviders() + expect(baseElement).toBeTruthy() + }) + + it('should render an empty state when there are no credentials', () => { + renderWithProviders() + + screen.getByText('All credentials related to your clusters will appear here after creation.') + }) + + it('should render used and unused credentials with actions', () => { + mockCredentials = [ + { + credential: { + id: '1', + name: 'Credential 1', + object_type: 'AWS', + access_key_id: '123', + }, + clusters: [ + { + id: '1', + name: 'Cluster 1', + }, + ], + }, + { + credential: { + id: '2', + name: 'Credential 2', + object_type: 'GCP', + }, + clusters: [], + }, + ] + useOrganizationCredentialsMock.mockReturnValue({ + data: mockCredentials, + } as ReturnType) + + renderWithProviders() + + screen.getByText('Configured credentials') + screen.getByText('Unused credentials') + screen.getByText('Credential 1') + screen.getByText('Credential 2') + + expect(screen.getAllByTestId('view-credential')).toHaveLength(1) + expect(screen.getAllByTestId('delete-credential')).toHaveLength(1) + expect(screen.getByTestId('delete-credential')).toBeEnabled() + }) + + it('should show the cloud provider options in the create menu', async () => { + const { userEvent } = renderWithProviders() + + await userEvent.click(screen.getByText('New credential')) + + expect(await screen.findByText('AWS')).toBeInTheDocument() + expect(screen.getByText('GCP')).toBeInTheDocument() + expect(screen.getByText('Azure')).toBeInTheDocument() + expect(screen.getByText('Scaleway')).toBeInTheDocument() + + const awsItem = screen.getByText('AWS').closest('[role="menuitem"]') + expect(awsItem).toHaveClass('text-neutral') + }) +}) diff --git a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx index 77d52936793..c8d204ab164 100644 --- a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx @@ -5,8 +5,8 @@ import { Suspense, useMemo, useState } from 'react' import { match } from 'ts-pattern' import { useDeleteCloudProviderCredential } from '@qovery/domains/cloud-providers/feature' import { ClusterAvatar, ClusterCredentialsModal, CredentialsListClustersModal } from '@qovery/domains/clusters/feature' -import { NeedHelp } from '@qovery/shared/assistant/feature' -import { BlockContent, Heading, Section, Skeleton } from '@qovery/shared/ui' +import { SettingsHeading } from '@qovery/shared/console-shared' +import { BlockContent, Section, Skeleton } from '@qovery/shared/ui' import { Button, DropdownMenu, Icon, Indicator, useModal, useModalConfirmation } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' import { queries } from '@qovery/state/util-queries' @@ -34,7 +34,7 @@ type CredentialRowProps = { const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: CredentialRowProps) => { return (
@@ -44,42 +44,42 @@ const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: Crede className="-ml-1.5" />
- {credential.name} + {credential.name} {'role_arn' in credential && ( - Role ARN: - {credential.role_arn} + Role ARN: + {credential.role_arn} )} {'access_key_id' in credential && ( - Public Access Key: - {credential.access_key_id} + Public Access Key: + {credential.access_key_id} )} {'scaleway_access_key' in credential && ( - Access Key: - {credential.scaleway_access_key} + Access Key: + {credential.scaleway_access_key} )} {'scaleway_project_id' in credential && ( - Project ID: - {credential.scaleway_project_id} + Project ID: + {credential.scaleway_project_id} )} {'azure_tenant_id' in credential && ( - Tenant ID: - {credential.azure_tenant_id} + Tenant ID: + {credential.azure_tenant_id} )} {'azure_subscription_id' in credential && ( - Subscription ID: - {credential.azure_subscription_id} + Subscription ID: + {credential.azure_subscription_id} )}
@@ -88,18 +88,19 @@ const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: Crede {clusters.length > 0 && ( + {clusters.length} } > - - - {cloudProviderOptions.map((option) => ( - } - onClick={() => onSelectProvider(option.value)} - > - {option.label} - - ))} - - -
+
+
+ - }> - - + + + + + + {cloudProviderOptions.map((option) => ( + } + onClick={() => onSelectProvider(option.value)} + > + {option.label} + + ))} + +
+
) diff --git a/libs/domains/organizations/feature/src/lib/settings-general/settings-general.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-general/settings-general.spec.tsx index ea72f6fc409..cd1be8066e1 100644 --- a/libs/domains/organizations/feature/src/lib/settings-general/settings-general.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-general/settings-general.spec.tsx @@ -7,8 +7,8 @@ import { useEditOrganization } from '../hooks/use-edit-organization/use-edit-org import { useOrganization } from '../hooks/use-organization/use-organization' import { PageOrganizationGeneral, - PageOrganizationGeneralFeature, type PageOrganizationGeneralProps, + SettingsGeneral, handleSubmit, } from './settings-general' @@ -22,7 +22,7 @@ jest.mock('@tanstack/react-router', () => ({ useParams: () => ({ organizationId: '0' }), })) -describe('PageOrganizationGeneralFeature', () => { +describe('SettingsGeneral', () => { const useOrganizationMock = useOrganization as jest.MockedFunction const useEditOrganizationMock = useEditOrganization as jest.MockedFunction @@ -37,13 +37,13 @@ describe('PageOrganizationGeneralFeature', () => { }) it('should render successfully', async () => { - const { baseElement } = renderWithProviders() + const { baseElement } = renderWithProviders() await screen.findByTestId('input-name') expect(baseElement).toBeTruthy() }) it('should dispatch editOrganization if form is submitted', async () => { - const { userEvent } = renderWithProviders() + const { userEvent } = renderWithProviders() const input = await screen.findByTestId('input-name') await userEvent.clear(input) diff --git a/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.spec.tsx index 51281db8fbc..b7dca574e30 100644 --- a/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.spec.tsx @@ -1,7 +1,7 @@ import { renderWithProviders, screen } from '@qovery/shared/util-tests' import { useAnnotationsGroups } from '../hooks/use-annotations-groups/use-annotations-groups' import { useLabelsGroups } from '../hooks/use-labels-groups/use-labels-groups' -import { PageOrganizationLabelsAnnotationsFeature } from './settings-labels-annotations' +import { SettingsLabelsAnnotations } from './settings-labels-annotations' jest.mock('../hooks/use-annotations-groups/use-annotations-groups') jest.mock('../hooks/use-labels-groups/use-labels-groups') @@ -46,7 +46,7 @@ jest.mock('@tanstack/react-router', () => ({ useParams: () => ({ organizationId: '1' }), })) -describe('PageOrganizationLabelsAnnotationsFeature', () => { +describe('SettingsLabelsAnnotations', () => { beforeEach(() => { useAnnotationsGroupsMock.mockReturnValue({ data: mockAnnotationsGroup, @@ -59,7 +59,7 @@ describe('PageOrganizationLabelsAnnotationsFeature', () => { }) it('should render labels and annotations groups', () => { - renderWithProviders() + renderWithProviders() expect(screen.getByText('Labels & annotations')).toBeInTheDocument() expect(screen.getByText('Add new')).toBeInTheDocument() diff --git a/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.tsx b/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.tsx index c17786e8e60..0e5b924528b 100644 --- a/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-labels-annotations/settings-labels-annotations.tsx @@ -34,7 +34,7 @@ export function SettingsLabelsAnnotations() { const { openModalConfirmation } = useModalConfirmation() return ( -
+
diff --git a/libs/shared/ui/src/lib/components/dropdown-menu/dropdown-menu.tsx b/libs/shared/ui/src/lib/components/dropdown-menu/dropdown-menu.tsx index 1f292c4dd94..e7ea341d0f1 100644 --- a/libs/shared/ui/src/lib/components/dropdown-menu/dropdown-menu.tsx +++ b/libs/shared/ui/src/lib/components/dropdown-menu/dropdown-menu.tsx @@ -26,6 +26,12 @@ const dropdownMenuItemVariants = cva( 'hover:bg-surface-brand-component', 'hover:text-brand', ], + neutral: [ + 'data-[highlighted]:bg-surface-neutral-component', + 'data-[highlighted]:text-neutral', + 'hover:bg-surface-neutral-component', + 'hover:text-neutral', + ], red: [ 'data-[highlighted]:bg-surface-negative-component', 'data-[highlighted]:text-negative', @@ -55,6 +61,16 @@ const dropdownMenuItemVariants = cva( disabled: false, className: ['text-neutral'], }, + { + color: 'neutral', + disabled: true, + className: ['text-neutral-disabled'], + }, + { + color: 'neutral', + disabled: false, + className: ['text-neutral'], + }, { color: 'red', disabled: true, @@ -82,6 +98,7 @@ const dropdownMenuItemIconVariants = cva(['text-sm', 'mr-2'], { variants: { color: { brand: [], + neutral: [], red: [], yellow: [], }, @@ -101,6 +118,16 @@ const dropdownMenuItemIconVariants = cva(['text-sm', 'mr-2'], { disabled: false, className: ['text-brand-9'], }, + { + color: 'neutral', + disabled: true, + className: ['text-neutral-disabled'], + }, + { + color: 'neutral', + disabled: false, + className: ['text-neutral'], + }, { color: 'red', disabled: true, From ef05deec6e663363a14e9e9665932d054d5f1f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Mon, 16 Feb 2026 14:03:55 +0100 Subject: [PATCH 3/5] refactor(cluster-credentials-modal): remove delete functionality and clean up tests --- .../cluster-credentials-modal.spec.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.spec.tsx b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.spec.tsx index 5766797513f..931667a387a 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.spec.tsx +++ b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.spec.tsx @@ -2,7 +2,6 @@ import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form import { CloudProviderEnum } from 'qovery-typescript-axios' import * as useCreateCloudProviderCredentialHook from '@qovery/domains/cloud-providers/feature' import * as useEditCloudProviderCredentialHook from '@qovery/domains/cloud-providers/feature' -import * as useDeleteCloudProviderCredentialHook from '@qovery/domains/cloud-providers/feature' import { getByText, renderWithProviders, screen } from '@qovery/shared/util-tests' import * as useClusterCloudProviderInfoHook from '../hooks/use-cluster-cloud-provider-info/use-cluster-cloud-provider-info' import ClusterCredentialsModal, { type ClusterCredentialsModalProps, handleSubmit } from './cluster-credentials-modal' @@ -10,7 +9,6 @@ import ClusterCredentialsModal, { type ClusterCredentialsModalProps, handleSubmi jest.mock('@qovery/domains/cloud-providers/feature', () => ({ useCreateCloudProviderCredential: jest.fn(), useEditCloudProviderCredential: jest.fn(), - useDeleteCloudProviderCredential: jest.fn(), })) jest.mock('../hooks/use-cluster-cloud-provider-info/use-cluster-cloud-provider-info', () => ({ @@ -21,7 +19,6 @@ let props: ClusterCredentialsModalProps const mockCreateCredential = jest.fn() const mockEditCredential = jest.fn() -const mockDeleteCredential = jest.fn() describe('ClusterCredentialsModal', () => { beforeEach(() => { @@ -42,10 +39,6 @@ describe('ClusterCredentialsModal', () => { isLoading: false, }) - jest.spyOn(useDeleteCloudProviderCredentialHook, 'useDeleteCloudProviderCredential').mockReturnValue({ - mutateAsync: mockDeleteCredential, - }) - jest.spyOn(useClusterCloudProviderInfoHook, 'useClusterCloudProviderInfo').mockReturnValue({ data: { cloud_provider: 'AWS' }, isLoading: false, @@ -132,27 +125,5 @@ describe('ClusterCredentialsModal', () => { expect(mockEditCredential).toHaveBeenCalled() }) - - it('should handle delete confirmation', async () => { - const { userEvent } = renderWithProviders(wrapWithReactHookForm()) - - const deleteButton = screen.getByTestId('delete-button') - await userEvent.click(deleteButton) - - const confirmationInput = screen.getByTestId('input-value') - await userEvent.type(confirmationInput, 'delete') - - const modalTitle = screen.getByText('Delete credential') - expect(modalTitle).toBeInTheDocument() - - const confirmationModal = modalTitle.parentElement - - if (confirmationModal) { - const confirmButton = getByText(confirmationModal, 'Confirm') - await userEvent.click(confirmButton) - } - - expect(mockDeleteCredential).toHaveBeenCalled() - }) }) }) From e2d7e792e023a6f7c2bf7a30b78b54c6d3018399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Mon, 16 Feb 2026 14:18:05 +0100 Subject: [PATCH 4/5] fix(credentials): fix failing test --- .../lib/cluster-credentials-modal/cluster-credentials-modal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx index 5409c889484..f0ee6265d58 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx +++ b/libs/domains/clusters/feature/src/lib/cluster-credentials-modal/cluster-credentials-modal.tsx @@ -5,7 +5,6 @@ import { Controller, type FieldValues, FormProvider, useForm } from 'react-hook- import { P, match } from 'ts-pattern' import { useCreateCloudProviderCredential, - useDeleteCloudProviderCredential, useEditCloudProviderCredential, } from '@qovery/domains/cloud-providers/feature' import { CLUSTER_SETTINGS_IMAGE_REGISTRY_URL, CLUSTER_SETTINGS_URL, CLUSTER_URL } from '@qovery/shared/routes' @@ -162,7 +161,6 @@ export function ClusterCredentialsModal({ const { mutateAsync: createCloudProviderCredential, isLoading: isLoadingCreate } = useCreateCloudProviderCredential() const { mutateAsync: editCloudProviderCredential, isLoading: isLoadingEdit } = useEditCloudProviderCredential() - const { mutateAsync: deleteCloudProviderCredential } = useDeleteCloudProviderCredential() const [fileDetails, setFileDetails] = useState<{ name: string; size: number }>() const { getRootProps, getInputProps, isDragActive } = useDropzone({ From 429a1bce429061a8542bf81b41abe6ace6a22a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Wed, 18 Feb 2026 12:09:13 +0100 Subject: [PATCH 5/5] feat(cloud-credentials): fix review comments and add skeleton --- .../settings-cloud-credentials.spec.tsx | 6 ++ .../settings-cloud-credentials.tsx | 75 ++++++++++--------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx index e5d6ca52c98..f2c5a9016a6 100644 --- a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.spec.tsx @@ -39,6 +39,12 @@ describe('SettingsCloudCredentials', () => { expect(baseElement).toBeTruthy() }) + it('should render the content wrapper within the max-width container', () => { + const { container } = renderWithProviders() + + expect(container.querySelector('.max-w-content-with-navigation-left')).toBeInTheDocument() + }) + it('should render an empty state when there are no credentials', () => { renderWithProviders() diff --git a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx index c8d204ab164..7ddb29b2f8e 100644 --- a/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-cloud-credentials/settings-cloud-credentials.tsx @@ -101,7 +101,6 @@ const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: Crede type="button" data-testid="view-credential" iconOnly - className="h-9 w-9 justify-center p-0" > @@ -116,7 +115,6 @@ const CredentialRow = ({ credential, clusters, onEdit, onOpen, onDelete }: Crede type="button" iconOnly data-testid="edit-credential" - className="h-9 w-9 justify-center p-0" > @@ -239,50 +237,55 @@ const PageOrganizationCredentials = () => { }, [credentialRows]) return ( - }> -
- {credentialRows.length === 0 && ( - -
- -

- All credentials related to your clusters will appear here after creation. -

-
-
- )} + <> + {credentialRows.length === 0 && ( + +
+ +

+ All credentials related to your clusters will appear here after creation. +

+
+
+ )} - {usedCredentials.length > 0 && ( - - {usedCredentials.map((cred) => ( - - ))} - - )} + {usedCredentials.length > 0 && ( + + {usedCredentials.map((cred) => ( + + ))} + + )} - {unusedCredentials.length > 0 && ( - - {unusedCredentials.map((cred) => ( - - ))} - - )} -
-
+ {unusedCredentials.length > 0 && ( + + {unusedCredentials.map((cred) => ( + + ))} + + )} + ) } const Loader = () => ( - + {[0, 1, 2, 3].map((_, i) => (
- +
+ +
+ + +
+
+
))} @@ -369,7 +372,11 @@ export function SettingsCloudCredentials() {
- +
+ }> + + +
)