@@ -142,11 +142,12 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
{overview.services_overview.service_count > 0 && overview.deployment_status ? (
) : (
-
+
)}
>
)}
diff --git a/libs/domains/organizations/data-access/src/lib/domains-organizations-data-access.ts b/libs/domains/organizations/data-access/src/lib/domains-organizations-data-access.ts
index 71803c9da3d..8af6f5aee41 100644
--- a/libs/domains/organizations/data-access/src/lib/domains-organizations-data-access.ts
+++ b/libs/domains/organizations/data-access/src/lib/domains-organizations-data-access.ts
@@ -434,6 +434,23 @@ export const mutations = {
)
return response.data
},
+ async unlinkArgoCdDestinationClusterMapping({
+ organizationId,
+ agentClusterId,
+ argocdClusterUrl,
+ }: {
+ organizationId: string
+ agentClusterId: string
+ argocdClusterUrl: string
+ }) {
+ const response = await argoCdApi.deleteArgoCdDestinationClusterMapping(
+ organizationId,
+ agentClusterId,
+ argocdClusterUrl,
+ { headers: { 'Content-Type': 'application/json' } }
+ )
+ return response.data
+ },
async listTfVarsFilesFromGitRepo({
organizationId,
repository,
diff --git a/libs/domains/organizations/feature/src/index.ts b/libs/domains/organizations/feature/src/index.ts
index e8188ab8cc4..bacd8532db6 100644
--- a/libs/domains/organizations/feature/src/index.ts
+++ b/libs/domains/organizations/feature/src/index.ts
@@ -90,6 +90,7 @@ export * from './lib/hooks/use-container-registry-associated-services/use-contai
export * from './lib/hooks/use-organization-credentials/use-organization-credentials'
export * from './lib/hooks/use-organization-argocd-integrations/use-organization-argocd-integrations'
export * from './lib/hooks/use-save-argocd-destination-cluster-mapping/use-save-argocd-destination-cluster-mapping'
+export * from './lib/hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping'
export * from './lib/hooks/use-parse-terraform-variables-from-git-repo/use-parse-terraform-variables-from-git-repo'
export * from './lib/hooks/use-list-tfvars-files-from-git-repo/use-list-tfvars-files-from-git-repo'
export * from './lib/invoice-banner/invoice-banner'
diff --git a/libs/domains/organizations/feature/src/lib/hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping.ts b/libs/domains/organizations/feature/src/lib/hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping.ts
new file mode 100644
index 00000000000..1594a959efb
--- /dev/null
+++ b/libs/domains/organizations/feature/src/lib/hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping.ts
@@ -0,0 +1,23 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { mutations } from '@qovery/domains/organizations/data-access'
+import { queries } from '@qovery/state/util-queries'
+
+export function useUnlinkArgoCdDestinationClusterMapping() {
+ const queryClient = useQueryClient()
+
+ return useMutation(mutations.unlinkArgoCdDestinationClusterMapping, {
+ onSuccess(_, { organizationId }) {
+ queryClient.invalidateQueries({
+ queryKey: queries.organizations.argoCdDestinationClusterMappings({ organizationId }).queryKey,
+ })
+ },
+ meta: {
+ notifyOnSuccess: {
+ title: 'The ArgoCD cluster mapping has been unlinked',
+ },
+ notifyOnError: true,
+ },
+ })
+}
+
+export default useUnlinkArgoCdDestinationClusterMapping
diff --git a/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.spec.tsx
index 752899e11dd..b6d94d71515 100644
--- a/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.spec.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.spec.tsx
@@ -6,6 +6,7 @@ import { SettingsArgoCdIntegration } from './settings-argocd-integration'
const mockOpenModal = jest.fn()
const mockCloseModal = jest.fn()
+const mockUnlinkArgoCdDestinationClusterMapping = jest.fn()
jest.mock('@tanstack/react-router', () => ({
...jest.requireActual('@tanstack/react-router'),
@@ -15,6 +16,14 @@ jest.mock('@tanstack/react-router', () => ({
jest.mock('../hooks/use-organization-argocd-integrations/use-organization-argocd-integrations', () => ({
useOrganizationArgoCdIntegrations: jest.fn(),
}))
+jest.mock(
+ '../hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping',
+ () => ({
+ useUnlinkArgoCdDestinationClusterMapping: () => ({
+ mutateAsync: mockUnlinkArgoCdDestinationClusterMapping,
+ }),
+ })
+)
jest.mock('@qovery/shared/ui', () => ({
...jest.requireActual('@qovery/shared/ui'),
useModal: () => ({
@@ -33,6 +42,7 @@ describe('SettingsArgoCdIntegration', () => {
useParamsMock.mockReturnValue({ organizationId: 'org-1' } as never)
mockOpenModal.mockReset()
mockCloseModal.mockReset()
+ mockUnlinkArgoCdDestinationClusterMapping.mockReset()
})
it('should render an empty state when no integration is configured', () => {
@@ -47,7 +57,7 @@ describe('SettingsArgoCdIntegration', () => {
expect(screen.getAllByRole('button', { name: 'Add ArgoCD' })).toHaveLength(2)
})
- it('should render integration cards with linked and unlinked sections', () => {
+ it('should render integration cards with linked and unlinked sections', async () => {
useOrganizationArgoCdIntegrationsMock.mockReturnValue({
data: [
{
@@ -81,15 +91,65 @@ describe('SettingsArgoCdIntegration', () => {
isLoading: false,
} as ReturnType)
- renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
expect(screen.getByText('ArgoCD running on')).toBeInTheDocument()
expect(screen.getByText('Linked clusters (1)')).toBeInTheDocument()
expect(screen.getByText('Unlinked clusters (1)')).toBeInTheDocument()
expect(screen.getByText('AWS EKS Demo')).toBeInTheDocument()
+
+ await userEvent.click(screen.getByText('Unlinked clusters (1)'))
+
+ expect(
+ screen.getByText(
+ 'Unlinked clusters are clusters detected by ArgoCD that are not yet associated with a cluster in Qovery. Add the cluster to Qovery, then link it here to display the applications running on it.'
+ )
+ ).toBeInTheDocument()
+ expect(screen.getByText('external-prod')).toBeInTheDocument()
+ expect(screen.getByText('unmapped.example.com')).toBeInTheDocument()
expect(screen.getByText('Connected')).toBeInTheDocument()
})
+ it('should unlink a linked cluster mapping', async () => {
+ mockUnlinkArgoCdDestinationClusterMapping.mockResolvedValue({})
+ useOrganizationArgoCdIntegrationsMock.mockReturnValue({
+ data: [
+ {
+ agent_cluster_id: 'cluster-1',
+ agent_cluster_name: 'undeletable_cluster',
+ agent_cluster_cloud_provider: 'AWS',
+ credentials_id: 'integration-1',
+ argocd_url: 'https://argocd.example.com',
+ status: 'connected',
+ last_checked_at: '2026-04-28T12:20:00.000Z',
+ linked_clusters: [
+ {
+ argocd_cluster_url: 'https://kubernetes.default.svc',
+ argocd_cluster_name: 'kube-system',
+ qovery_cluster_id: 'cluster-1',
+ qovery_cluster_name: 'AWS EKS Demo',
+ qovery_cluster_cloud_provider: 'AWS',
+ qovery_cluster_type: 'MANAGED',
+ applications_count: 4,
+ },
+ ],
+ unlinked_clusters: [],
+ },
+ ],
+ isLoading: false,
+ } as ReturnType)
+
+ const { userEvent } = renderWithProviders( )
+
+ await userEvent.click(screen.getByTestId('unlink-linked-cluster-https://kubernetes.default.svc'))
+
+ expect(mockUnlinkArgoCdDestinationClusterMapping).toHaveBeenCalledWith({
+ organizationId: 'org-1',
+ agentClusterId: 'cluster-1',
+ argocdClusterUrl: 'https://kubernetes.default.svc',
+ })
+ })
+
it('should render an importing state when the integration has no cluster mapping yet', () => {
useOrganizationArgoCdIntegrationsMock.mockReturnValue({
data: [
diff --git a/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.tsx b/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.tsx
index 29b94a683b8..2c42272ec67 100644
--- a/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-argocd-integration/settings-argocd-integration.tsx
@@ -2,17 +2,19 @@ import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { useParams } from '@tanstack/react-router'
import {
type ArgoCdInstanceMappingResponse,
+ type ArgoCdLinkedClusterDetails,
type ArgoCdUnlinkedClusterDetails,
type KubernetesEnum,
} from 'qovery-typescript-axios'
import { type ReactNode, Suspense, useEffect, useMemo, useState } from 'react'
import { useDeleteArgoCdCredentials } from '@qovery/domains/clusters/feature'
import { SettingsHeading } from '@qovery/shared/console-shared'
-import { Badge, Button, EmptyState, Icon, Link, ModalConfirmation, Section, useModal } from '@qovery/shared/ui'
+import { Badge, Button, EmptyState, Icon, Link, ModalConfirmation, Section, Tooltip, useModal } from '@qovery/shared/ui'
import { timeAgo } from '@qovery/shared/util-dates'
import { useDocumentTitle } from '@qovery/shared/util-hooks'
import { useOrganizationArgoCdIntegrations } from '../hooks/use-organization-argocd-integrations/use-organization-argocd-integrations'
import { useSaveArgoCdDestinationClusterMapping } from '../hooks/use-save-argocd-destination-cluster-mapping/use-save-argocd-destination-cluster-mapping'
+import { useUnlinkArgoCdDestinationClusterMapping } from '../hooks/use-unlink-argocd-destination-cluster-mapping/use-unlink-argocd-destination-cluster-mapping'
import { ArgoCdIntegrationCardSkeleton } from './argocd-integration-skeleton'
import { ConnectArgoCdModal } from './connect-argocd-modal/connect-argocd-modal'
import { LinkClusterModal, type LinkClusterModalResponse } from './link-cluster-modal/link-cluster-modal'
@@ -92,9 +94,16 @@ interface ArgoCdIntegrationCardProps {
cluster: ArgoCdUnlinkedClusterDetails,
response: LinkClusterModalResponse
) => void
+ onUnlinkCluster: (integrationId: string, cluster: ArgoCdLinkedClusterDetails) => void
}
-function ArgoCdIntegrationCard({ integration, onEdit, onDelete, onLinkCluster }: ArgoCdIntegrationCardProps) {
+function ArgoCdIntegrationCard({
+ integration,
+ onEdit,
+ onDelete,
+ onLinkCluster,
+ onUnlinkCluster,
+}: ArgoCdIntegrationCardProps) {
const { organizationId = '' } = useParams({ strict: false })
const { openModal, closeModal } = useModal()
const [isLinkedSectionOpen, setIsLinkedSectionOpen] = useState(true)
@@ -215,6 +224,18 @@ function ArgoCdIntegrationCard({ integration, onEdit, onDelete, onLinkCluster }: