Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { ForwardedRef, ReactNode } from 'react'
import { applicationFactoryMock, environmentFactoryMock } from '@qovery/shared/factories'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { EnvironmentActionToolbar } from './environment-action-toolbar'

const mockEnvironment = environmentFactoryMock(1)[0]
const mockServices = applicationFactoryMock(3)
let mockServices: unknown[] = applicationFactoryMock(3)
const mockDeployEnvironment = jest.fn()
const mockDeleteEnvironment = jest.fn()
const mockNavigate = jest.fn()
Expand Down Expand Up @@ -36,7 +36,18 @@ jest.mock('@tanstack/react-router', () => ({
useNavigate: () => mockNavigate,
useLocation: () => ({ pathname: '/', search: '' }),
useRouter: () => ({ buildLocation: () => ({ href: '/' }) }),
Link: ({ children, ...props }: { children?: ReactNode; [key: string]: unknown }) => <a {...props}>{children}</a>,
Link: jest
.requireActual('react')
.forwardRef(
(
{ children, ...props }: { children?: ReactNode; [key: string]: unknown },
ref: ForwardedRef<HTMLAnchorElement>
) => (
<a {...props} ref={ref}>
{children}
</a>
)
),
}))

jest.mock('@qovery/shared/ui', () => ({
Expand Down Expand Up @@ -95,6 +106,7 @@ describe('EnvironmentActionToolbar', () => {
state: 'DEPLOYED',
deployment_status: 'OUT_OF_DATE',
}
mockServices = applicationFactoryMock(3)
mockVariables = [
{
key: 'QOVERY_KUBERNETES_NAMESPACE_NAME',
Expand Down Expand Up @@ -137,6 +149,54 @@ describe('EnvironmentActionToolbar', () => {
expect(mockOpenModalConfirmation).not.toHaveBeenCalled()
})

it('should display an ArgoCD warning on redeploy when the environment contains ArgoCD services', async () => {
mockServices = [
applicationFactoryMock(1)[0],
{
id: 'argocd-service-1',
name: 'ArgoCD service',
service_type: 'ARGOCD_APP',
serviceType: 'ARGOCD_APP',
},
]

const { userEvent } = renderWithProviders(<EnvironmentActionToolbar environment={mockEnvironment} />)

await userEvent.click(screen.getByLabelText(/manage deployment/i))
await userEvent.hover(screen.getByLabelText(/argocd deployment information/i))

expect(await screen.findAllByText('Environment has changed and needs to be applied')).toHaveLength(2)
expect(
await screen.findAllByText('Redeploy will only target Qovery created services and not ArgoCD imported ones.')
).toHaveLength(2)
})

it('should display an ArgoCD warning on deploy when the environment contains ArgoCD services', async () => {
mockDeploymentStatus = {
state: 'READY',
deployment_status: 'OUT_OF_DATE',
}
mockServices = [
applicationFactoryMock(1)[0],
{
id: 'argocd-service-1',
name: 'ArgoCD service',
service_type: 'ARGOCD_APP',
serviceType: 'ARGOCD_APP',
},
]

const { userEvent } = renderWithProviders(<EnvironmentActionToolbar environment={mockEnvironment} />)

await userEvent.click(screen.getByLabelText(/manage deployment/i))
await userEvent.hover(screen.getByLabelText(/argocd deployment information/i))

expect(await screen.findAllByText('Environment has changed and needs to be applied')).toHaveLength(2)
expect(
await screen.findAllByText('Redeploy will only target Qovery created services and not ArgoCD imported ones.')
).toHaveLength(2)
})

it('should keep a confirmation modal for stop', async () => {
const { userEvent } = renderWithProviders(<EnvironmentActionToolbar environment={mockEnvironment} />)

Expand Down Expand Up @@ -207,4 +267,23 @@ describe('EnvironmentActionToolbar', () => {
expect(screen.getByText('Namespace ID')).toBeInTheDocument()
expect(screen.getByText('z1234567-env-name')).toBeInTheDocument()
})

it('should disable ArgoCD environment deletion', async () => {
const { userEvent } = renderWithProviders(
<EnvironmentActionToolbar environment={mockEnvironment} isArgoCdEnvironment />
)

await userEvent.click(screen.getByLabelText(/other actions/i))

const deleteEnvironmentItem = screen.getByRole('menuitem', { name: /delete environment/i })
expect(deleteEnvironmentItem).toHaveAttribute('aria-disabled', 'true')

await userEvent.hover(screen.getByText('Delete environment'))

expect(
await screen.findByRole('tooltip', {
name: 'ArgoCD environment can only be deleted by revoking the integration',
})
).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'qovery-typescript-axios'
import { useState } from 'react'
import { match } from 'ts-pattern'
import { isArgoCd } from '@qovery/domains/services/data-access'
// eslint-disable-next-line @nx/enforce-module-boundaries
import { useServices } from '@qovery/domains/services/feature'
import { useVariables } from '@qovery/domains/variables/feature'
Expand Down Expand Up @@ -76,6 +77,24 @@ export function MenuManageDeployment({
// XXX: Required to display a warning for managed Database
// https://qovery.atlassian.net/jira/software/projects/FRT/boards/23?selectedIssue=FRT-1416
const { data: services = [] } = useServices({ environmentId: environment.id })
const hasArgoCdServices = services.some(isArgoCd)

const argoCdDeploymentTooltipContent = (
<span className="flex flex-col gap-1">
{displayYellowColor ? <span>Environment has changed and needs to be applied</span> : null}
<span>Redeploy will only target Qovery created services and not ArgoCD imported ones.</span>
</span>
)

const tooltipDeployWithArgoCdServices = hasArgoCdServices ? (
<Tooltip side="bottom" content={argoCdDeploymentTooltipContent}>
<span aria-label="ArgoCD deployment information" className="flex">
<Icon iconName={displayYellowColor ? 'circle-exclamation' : 'circle-info'} iconStyle="regular" />
</span>
</Tooltip>
) : (
tooltipEnvironmentNeedUpdate
)

const mutationDeploy = () =>
deployEnvironment({
Expand Down Expand Up @@ -182,7 +201,7 @@ export function MenuManageDeployment({
>
<div className="flex w-full items-center justify-between">
Deploy
{tooltipEnvironmentNeedUpdate}
{tooltipDeployWithArgoCdServices}
</div>
</DropdownMenu.Item>
)}
Expand All @@ -194,7 +213,7 @@ export function MenuManageDeployment({
>
<div className="flex w-full items-center justify-between">
Redeploy
{tooltipEnvironmentNeedUpdate}
{tooltipDeployWithArgoCdServices}
</div>
</DropdownMenu.Item>
)}
Expand Down Expand Up @@ -244,10 +263,12 @@ export function MenuManageDeployment({
export function MenuOtherActions({
state,
environment,
isArgoCdEnvironment = false,
variant = 'default',
}: {
state: StateEnum
environment: Environment
isArgoCdEnvironment?: boolean
variant?: ActionToolbarVariant
}) {
const [isActionsOpen, setIsActionsOpen] = useState(false)
Expand Down Expand Up @@ -386,9 +407,17 @@ export function MenuOtherActions({
{isDeleteAvailable(state) && (
<>
<DropdownMenu.Separator />
<DropdownMenu.Item color="red" icon={<Icon iconName="trash" />} onSelect={mutationDeleteEnvironment}>
Delete environment
</DropdownMenu.Item>
{isArgoCdEnvironment ? (
<DropdownMenu.Item color="red" icon={<Icon iconName="trash" />} disabled>
<Tooltip content="ArgoCD environment can only be deleted by revoking the integration">
<span>Delete environment</span>
</Tooltip>
</DropdownMenu.Item>
) : (
<DropdownMenu.Item color="red" icon={<Icon iconName="trash" />} onSelect={mutationDeleteEnvironment}>
Delete environment
</DropdownMenu.Item>
)}
</>
)}
</DropdownMenu.Content>
Expand All @@ -398,10 +427,15 @@ export function MenuOtherActions({

export interface EnvironmentActionToolbarProps {
environment: Environment
isArgoCdEnvironment?: boolean
variant?: ActionToolbarVariant
}

export function EnvironmentActionToolbar({ environment, variant = 'default' }: EnvironmentActionToolbarProps) {
export function EnvironmentActionToolbar({
environment,
isArgoCdEnvironment = false,
variant = 'default',
}: EnvironmentActionToolbarProps) {
const { data: countServices, isFetched: isFetchedServices } = useServiceCount({ environmentId: environment.id })

const { data: deploymentStatus } = useDeploymentStatus({ environmentId: environment.id })
Expand Down Expand Up @@ -434,7 +468,11 @@ export function EnvironmentActionToolbar({ environment, variant = 'default' }: E
<Icon iconName="timeline" />
</Link>
</Tooltip>
<MenuOtherActions environment={environment} state={deploymentStatus.state} />
<MenuOtherActions
environment={environment}
state={deploymentStatus.state}
isArgoCdEnvironment={isArgoCdEnvironment}
/>
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ const overview: EnvironmentOverviewResponse = {

describe('EnvironmentSection', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.clearAllMocks()
})

afterEach(() => {
jest.clearAllTimers()
jest.useRealTimers()
})

it('should navigate to the environment when clicking the row', async () => {
const { userEvent } = renderWithProviders(
<EnvironmentSection type={EnvironmentModeEnum.DEVELOPMENT} items={[overview]} />
Expand Down Expand Up @@ -120,5 +126,7 @@ describe('EnvironmentSection', () => {
)

expect(screen.getByText('ArgoCD')).toBeInTheDocument()
const manageDeploymentButton = screen.getByRole('button', { name: /manage deployment/i })
expect(manageDeploymentButton).toBeEnabled()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ const { Table } = TablePrimitives
const gridLayoutClassName =
'grid w-full grid-cols-[minmax(280px,2fr)_minmax(220px,1.4fr)_minmax(240px,1.2fr)_minmax(140px,1fr)_96px]'

function DisabledManageDeploymentButton() {
function DisabledManageDeploymentButton({ tooltip }: { tooltip: string }) {
return (
<Tooltip content="Add at least one service to deploy this environment">
<Tooltip content={tooltip}>
<div>
<Button aria-label="Manage Deployment" color="neutral" variant="outline" size="sm" iconOnly disabled>
<div className="flex h-full w-full items-center justify-center gap-1.5">
Expand Down Expand Up @@ -142,11 +142,12 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
{overview.services_overview.service_count > 0 && overview.deployment_status ? (
<MenuManageDeployment environment={environment} deploymentStatus={overview.deployment_status} />
) : (
<DisabledManageDeploymentButton />
<DisabledManageDeploymentButton tooltip="Add at least one service to deploy this environment" />
)}
<MenuOtherActions
environment={environment}
state={overview.deployment_status?.last_deployment_state ?? StateEnum.READY}
isArgoCdEnvironment={isArgoCdEnvironment}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions libs/domains/organizations/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading