From 4c7b023613c9cace3c414d7c34e975e8b75996ad Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Fri, 17 Apr 2026 12:00:49 +0200 Subject: [PATCH 1/3] fix(alerts): prevent ability to create alerts when service has never been deployed + fix API call early-return condition --- .../alert-rules-overview.tsx | 21 +++++++++--- .../alerting-creation-flow.tsx | 2 +- .../service-alerting/service-alerting.tsx | 33 +++++++++++++++---- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx b/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx index c5492aa18cf..f381580e75d 100644 --- a/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx +++ b/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx @@ -4,6 +4,7 @@ import { type PropsWithChildren, type ReactNode, useMemo, useState } from 'react import { useNavigate } from 'react-router-dom' import { match } from 'ts-pattern' import { type AnyService } from '@qovery/domains/services/data-access' +import { useDeploymentStatus } from '@qovery/domains/services/feature' import { APPLICATION_MONITORING_ALERTS_URL, APPLICATION_MONITORING_ALERT_EDIT_URL, @@ -101,6 +102,10 @@ export function AlertRulesOverview({ const { openModal, closeModal } = useModal() const { openModalConfirmation } = useModalConfirmation() const navigate = useNavigate() + const { data: deploymentStatus } = useDeploymentStatus({ + environmentId: service?.environment?.id, + serviceId: service?.id, + }) const { mutate: deleteAlertRule } = useDeleteAlertRule({ organizationId }) @@ -156,6 +161,10 @@ export function AlertRulesOverview({ [selectedAlertRuleIds.size, selectableAlertRules.length] ) + const canCreateAlerts = useMemo(() => { + return deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + }, [deploymentStatus]) + const toggleSelectAll = (checked: boolean) => { if (checked) { const allIds = new Set(selectableAlertRules.map((alertRule) => alertRule.id)) @@ -240,10 +249,14 @@ export function AlertRulesOverview({ )}

{onCreateKeyAlerts && ( - + +
+ +
+
)} ) : ( diff --git a/libs/domains/observability/feature/src/lib/alerting/alerting-creation-flow/alerting-creation-flow.tsx b/libs/domains/observability/feature/src/lib/alerting/alerting-creation-flow/alerting-creation-flow.tsx index 7ff45e92059..48330b0e0a9 100644 --- a/libs/domains/observability/feature/src/lib/alerting/alerting-creation-flow/alerting-creation-flow.tsx +++ b/libs/domains/observability/feature/src/lib/alerting/alerting-creation-flow/alerting-creation-flow.tsx @@ -164,7 +164,7 @@ export function AlertingCreationFlow({ service?.min_running_instances !== service?.max_running_instances if (!containerName) return - if (hasPublicPort && !ingressName) return + if (hasPublicPort && !(ingressName || httpRouteName)) return if (hasAutoscaling && !hpaName) return try { diff --git a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx index c31f1861137..f876deba24a 100644 --- a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx @@ -1,8 +1,8 @@ -import { type PropsWithChildren } from 'react' +import { type PropsWithChildren, useMemo } from 'react' import { useParams } from 'react-router-dom' import { type AnyService } from '@qovery/domains/services/data-access' -import { useService } from '@qovery/domains/services/feature' -import { Button, Chart, Heading, Icon, Section, useModal } from '@qovery/shared/ui' +import { useDeploymentStatus, useService } from '@qovery/domains/services/feature' +import { Button, Chart, Heading, Icon, Section, Tooltip, useModal } from '@qovery/shared/ui' import { AlertRulesOverview } from '../../alerting/alert-rules-overview/alert-rules-overview' import { CreateKeyAlertsModal } from '../../alerting/create-key-alerts-modal/create-key-alerts-modal' import { useEnvironment } from '../../hooks/use-environment/use-environment' @@ -15,6 +15,10 @@ interface ServiceAlertingContentProps extends PropsWithChildren { function ServiceAlertingContent({ organizationId, projectId, service, children }: ServiceAlertingContentProps) { const { openModal, closeModal } = useModal() + const { data: deploymentStatus } = useDeploymentStatus({ + environmentId: service?.environment?.id, + serviceId: service?.id, + }) const createKeyAlertsModal = () => { openModal({ @@ -32,15 +36,30 @@ function ServiceAlertingContent({ organizationId, projectId, service, children } }) } + const canCreateAlerts = useMemo(() => { + return deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + }, [deploymentStatus]) + return (
Alert rules - + +
+ +
+
From 860d8ab0cfbd432f8cfe1362e3c5dc5b94f5bd45 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Fri, 17 Apr 2026 13:19:36 +0200 Subject: [PATCH 2/3] Update unit tests --- .../lib/service/service-alerting/service-alerting.spec.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx index 2444abb1b95..77ce18e7b68 100644 --- a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx @@ -8,6 +8,7 @@ const mockUseAlertRulesGhosted = jest.fn() const mockUseDeleteAlertRule = jest.fn() const mockUseEnvironment = jest.fn() const mockUseService = jest.fn() +const mockUseDeploymentStatus = jest.fn() jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -32,6 +33,7 @@ jest.mock('../../hooks/use-environment/use-environment', () => ({ jest.mock('@qovery/domains/services/feature', () => ({ useService: (params: unknown) => mockUseService(params), + useDeploymentStatus: (params: unknown) => mockUseDeploymentStatus(params), })) describe('ServiceAlerting', () => { @@ -93,6 +95,10 @@ describe('ServiceAlerting', () => { mockUseDeleteAlertRule.mockReturnValue({ mutate: jest.fn(), }) + mockUseDeploymentStatus.mockReturnValue({ + data: { service_deployment_status: 'DEPLOYED' }, + isFetched: true, + }) }) it('should render loader when environment is not loaded', () => { From 0fd55338e5f2cf5e9e38f9cf5400bd15250faa71 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Fri, 17 Apr 2026 15:37:52 +0200 Subject: [PATCH 3/3] Post-review fixes --- .../alert-rules-overview.tsx | 5 ++++- .../service-alerting.spec.tsx | 22 ++++++++++++++++++- .../service-alerting/service-alerting.tsx | 5 ++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx b/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx index f381580e75d..c1491ee01c0 100644 --- a/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx +++ b/libs/domains/observability/feature/src/lib/alerting/alert-rules-overview/alert-rules-overview.tsx @@ -162,7 +162,10 @@ export function AlertRulesOverview({ ) const canCreateAlerts = useMemo(() => { - return deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + return ( + deploymentStatus?.service_deployment_status !== undefined && + deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + ) }, [deploymentStatus]) const toggleSelectAll = (checked: boolean) => { diff --git a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx index 77ce18e7b68..a4b331bcd48 100644 --- a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.spec.tsx @@ -1,5 +1,5 @@ import { AlertRuleState } from 'qovery-typescript-axios' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { renderWithProviders, screen, within } from '@qovery/shared/util-tests' import { ServiceAlerting } from './service-alerting' const mockUseParams = jest.fn() @@ -101,6 +101,26 @@ describe('ServiceAlerting', () => { }) }) + it('should display tooltip explaining alerts require deployment when service was never deployed', async () => { + mockUseDeploymentStatus.mockReturnValue({ + data: { service_deployment_status: 'NEVER_DEPLOYED' }, + isFetched: true, + }) + + const { userEvent } = renderWithProviders() + + const headerToolbar = screen.getByRole('heading', { name: 'Alert rules' }).parentElement + expect(headerToolbar).toBeInTheDocument() + const newAlertButton = within(headerToolbar as HTMLElement).getByRole('button', { name: /new alert/i }) + await userEvent.hover(newAlertButton.parentElement ?? newAlertButton) + + expect( + await screen.findByRole('tooltip', { + name: 'You need to deploy your service to create alerts', + }) + ).toBeInTheDocument() + }) + it('should render loader when environment is not loaded', () => { mockUseEnvironment.mockReturnValue({ data: undefined, diff --git a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx index f876deba24a..4fd3c6985c1 100644 --- a/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-alerting/service-alerting.tsx @@ -37,7 +37,10 @@ function ServiceAlertingContent({ organizationId, projectId, service, children } } const canCreateAlerts = useMemo(() => { - return deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + return ( + deploymentStatus?.service_deployment_status !== undefined && + deploymentStatus?.service_deployment_status !== 'NEVER_DEPLOYED' + ) }, [deploymentStatus]) return (