From eeaba12dd52cc2ab38b46f03be52df8642bcb0b3 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 26 Mar 2026 09:28:25 +0100 Subject: [PATCH 01/10] fix(deployment-logs): rendering history by service id not all from the environment --- .../domains/service-logs/feature/src/index.ts | 1 - .../breadcrumb-deployment-history.spec.tsx | 97 ------------- .../breadcrumb-deployment-history.tsx | 130 ------------------ .../deployment-logs-content.tsx | 9 +- .../use-service-deployment-history.ts | 31 +++++ .../list-deployment-logs.spec.tsx | 46 ++++--- .../list-deployment-logs.tsx | 19 +-- .../src/lib/ui/breadcrumb/breadcrumb.tsx | 24 +--- libs/shared/util-js/src/lib/trim-id.spec.ts | 4 +- libs/shared/util-js/src/lib/trim-id.ts | 2 +- 10 files changed, 75 insertions(+), 288 deletions(-) delete mode 100644 libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.spec.tsx delete mode 100644 libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.tsx create mode 100644 libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts diff --git a/libs/domains/service-logs/feature/src/index.ts b/libs/domains/service-logs/feature/src/index.ts index ddeca8380a0..1105be812e2 100644 --- a/libs/domains/service-logs/feature/src/index.ts +++ b/libs/domains/service-logs/feature/src/index.ts @@ -1,7 +1,6 @@ export * from './lib/list-service-logs/list-service-logs' export * from './lib/list-deployment-logs/list-deployment-logs' export * from './lib/service-stage-ids-context/service-stage-ids-context' -export * from './lib/breadcrumb-deployment-history/breadcrumb-deployment-history' export * from './lib/breadcrumb-deployment-logs/breadcrumb-deployment-logs' export * from './lib/sidebar-pod-statuses/sidebar-pod-statuses' export * from './lib/deployment-logs/deployment-logs' diff --git a/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.spec.tsx b/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.spec.tsx deleted file mode 100644 index a68eff6df0d..00000000000 --- a/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.spec.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useService, useDeploymentHistory as useServiceDeploymentHistory } from '@qovery/domains/services/feature' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import { useDeploymentHistory } from '../hooks/use-deployment-history/use-deployment-history' -import { BreadcrumbDeploymentHistory } from './breadcrumb-deployment-history' - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ - organizationId: 'org-123', - projectId: 'proj-456', - environmentId: 'env-789', - }), -})) - -jest.mock('@qovery/domains/services/feature') -jest.mock('../hooks/use-deployment-history/use-deployment-history') - -const mockDeploymentHistory = [ - { - identifier: { execution_id: 'version-1' }, - auditing_data: { created_at: '2023-01-01T00:00:00Z' }, - status: 'RUNNING', - stages: [ - { - name: 'Build', - status: 'SUCCESS', - duration: '5m', - services: [ - { - identifier: { - service_id: 'service-1', - }, - }, - ], - }, - ], - }, - { - identifier: { execution_id: 'version-2' }, - auditing_data: { created_at: '2023-01-01T00:00:00Z' }, - status: 'RUNNING', - stages: [ - { - name: 'Build', - status: 'SUCCESS', - duration: '5m', - services: [ - { - identifier: { - service_id: 'service-1', - }, - }, - { - identifier: { - service_id: 'service-2', - }, - }, - ], - }, - ], - }, -] - -describe('BreadcrumbDeploymentHistory', () => { - beforeEach(() => { - useDeploymentHistory.mockReturnValue({ - data: mockDeploymentHistory, - isFetched: true, - }) - useServiceDeploymentHistory.mockReturnValue({ - data: mockDeploymentHistory, - isFetched: true, - }) - useService.mockReturnValue({ - data: { - service_type: 'APPLICATION', - }, - isFetched: true, - }) - }) - - it('renders correctly with deployment history', async () => { - const { userEvent } = renderWithProviders(, { - container: document.body, - }) - - expect(screen.getByText('Deployment History')).toBeInTheDocument() - expect(screen.getByText('Latest')).toBeInTheDocument() - - const dropdownButton = screen.getByRole('button') - - await userEvent.click(dropdownButton) - - expect(screen.getByText('versi...n-1')).toBeInTheDocument() - expect(screen.getByText('versi...n-2')).toBeInTheDocument() - }) -}) diff --git a/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.tsx b/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.tsx deleted file mode 100644 index 6db66e08232..00000000000 --- a/libs/domains/service-logs/feature/src/lib/breadcrumb-deployment-history/breadcrumb-deployment-history.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import clsx from 'clsx' -import { Link, useParams } from 'react-router-dom' -import { match } from 'ts-pattern' -import { useService, useDeploymentHistory as useServiceDeploymentHistory } from '@qovery/domains/services/feature' -import { - DEPLOYMENT_LOGS_VERSION_URL, - ENVIRONMENT_LOGS_URL, - ENVIRONMENT_PRE_CHECK_LOGS_URL, - ENVIRONMENT_STAGES_URL, -} from '@qovery/shared/routes' -import { Button, DropdownMenu, Icon, StatusChip, Tooltip } from '@qovery/shared/ui' -import { dateFullFormat } from '@qovery/shared/util-dates' -import { trimId } from '@qovery/shared/util-js' -import { useDeploymentHistory } from '../hooks/use-deployment-history/use-deployment-history' - -export interface BreadcrumbDeploymentHistoryProps { - type: 'DEPLOYMENT' | 'STAGES' | 'PRE_CHECK' - serviceId?: string - versionId?: string -} - -export function BreadcrumbDeploymentHistory({ type, serviceId, versionId }: BreadcrumbDeploymentHistoryProps) { - const { organizationId = '', projectId = '', environmentId = '' } = useParams() - const { data: service } = useService({ - serviceId: serviceId ?? '', - suspense: true, - }) - const { data: serviceDeploymentHistory } = useServiceDeploymentHistory({ - serviceId: serviceId ?? '', - serviceType: service?.service_type ?? 'APPLICATION', - suspense: true, - }) - - const { data: allDeploymentHistory = [], isFetched: isFetchedDeloymentHistory } = useDeploymentHistory({ - environmentId, - suspense: true, - }) - const filteredDeploymentHistory = serviceId - ? allDeploymentHistory.filter((history) => - history.stages.some((stage) => stage.services.some((service) => service.identifier.service_id === serviceId)) - ) - : allDeploymentHistory - - const deploymentHistory = serviceDeploymentHistory || filteredDeploymentHistory || [] - - if (!isFetchedDeloymentHistory || deploymentHistory.length === 0) return null - - return ( -
-
- Deployment History -
- - - {!versionId || versionId === deploymentHistory[0]?.identifier.execution_id ? ( - - - Latest - - - - ) : deploymentHistory.find((h) => h.identifier.execution_id === versionId)?.auditing_data.created_at ? ( - - {dateFullFormat( - deploymentHistory.find((h) => h.identifier.execution_id === versionId)?.auditing_data.created_at ?? - 0 - )} - - ) : ( - Not available - )} - - - - - - Deployment History - {deploymentHistory.map((history) => ( - - - ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + - DEPLOYMENT_LOGS_VERSION_URL(serviceId, history.identifier.execution_id) - ) - .with( - 'STAGES', - () => - ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + - ENVIRONMENT_STAGES_URL(history.identifier.execution_id) - ) - .with( - 'PRE_CHECK', - () => - ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + - ENVIRONMENT_PRE_CHECK_LOGS_URL(history.identifier.execution_id ?? '') - ) - .exhaustive()} - > - - {trimId(history.identifier.execution_id ?? '')} - - - {dateFullFormat(history.auditing_data.created_at)} - - - - - ))} - - -
-
-
- ) -} - -export default BreadcrumbDeploymentHistory diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx index a3bb467001f..ae22ca0db63 100644 --- a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx +++ b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx @@ -118,16 +118,11 @@ export function DeploymentLogsContent({ suspense: true, }) - useDocumentTitle(`Deployment logs - ${service?.name ?? 'Loading...'}`) + useDocumentTitle(`Deployment logs - ${service?.name ?? 'Loading…'}`) const serviceStatus = getServiceStatusesById(deploymentStages, serviceId) as Status - if (!serviceStatus && isFetchedService) - return ( -
-
-
- ) + if (!serviceStatus && isFetchedService) return
const stageFromServiceId = getStageFromServiceId(deploymentStages ?? [], serviceId) diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts new file mode 100644 index 00000000000..07fdb901096 --- /dev/null +++ b/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react' +import { useDeploymentHistory } from '../use-deployment-history/use-deployment-history' + +export interface UseServiceDeploymentHistoryProps { + environmentId: string + serviceId: string + suspense?: boolean +} + +export function useServiceDeploymentHistory({ + environmentId, + serviceId, + suspense = false, +}: UseServiceDeploymentHistoryProps) { + const query = useDeploymentHistory({ environmentId, suspense }) + + const data = useMemo( + () => + (query.data ?? []).filter((history) => + history.stages?.some((stage) => stage.services?.some((service) => service.identifier.service_id === serviceId)) + ), + [query.data, serviceId] + ) + + return { + ...query, + data, + } +} + +export default useServiceDeploymentHistory diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.spec.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.spec.tsx index 56f8a32720e..aa7eb7b06a4 100644 --- a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.spec.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.spec.tsx @@ -4,18 +4,20 @@ import { useDeploymentStatus, useLinks, useService } from '@qovery/domains/servi import { environmentFactoryMock } from '@qovery/shared/factories' import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' import { useDeploymentLogs } from '../hooks/use-deployment-logs/use-deployment-logs' +import { useServiceDeploymentHistory } from '../hooks/use-service-deployment-history/use-service-deployment-history' import { ListDeploymentLogs } from './list-deployment-logs' window.HTMLElement.prototype.scroll = jest.fn() jest.mock('../hooks/use-deployment-logs/use-deployment-logs') +jest.mock('../hooks/use-service-deployment-history/use-service-deployment-history') jest.mock('@qovery/domains/services/feature') jest.mock('@tanstack/react-router', () => ({ ...jest.requireActual('@tanstack/react-router'), useSearch: () => ({}), useNavigate: () => jest.fn(), - useParams: () => ({ organizationId: '1' }), + useParams: () => ({ organizationId: '1', projectId: '2', serviceId: 'service-1', executionId: '4' }), useLocation: () => ({ pathname: '/', search: '', hash: '' }), useRouter: () => ({ buildLocation: () => ({ href: '/' }), @@ -33,23 +35,6 @@ jest.mock('../hooks/use-generate-build-usage-report/use-generate-build-usage-rep isLoading: false, }), })) - -jest.mock('../hooks/use-deployment-history/use-deployment-history', () => ({ - ...jest.requireActual('../hooks/use-deployment-history/use-deployment-history'), - useDeploymentHistory: () => ({ - data: [ - { - identifier: { - execution_id: '4', - }, - auditing_data: { - created_at: '2024-09-18T07:03:29.819774Z', - }, - }, - ], - }), -})) - describe('ListDeploymentLogs', () => { const mockEnvironment = environmentFactoryMock(1)[0] @@ -113,6 +98,31 @@ describe('ListDeploymentLogs', () => { ] beforeEach(() => { + useServiceDeploymentHistory.mockReturnValue({ + data: [ + { + identifier: { + execution_id: '4', + }, + auditing_data: { + created_at: '2024-09-18T07:03:29.819774Z', + }, + status: 'SUCCESS', + stages: [ + { + services: [ + { + identifier: { + service_id: 'service-1', + }, + }, + ], + }, + ], + }, + ] as DeploymentHistoryEnvironmentV2[], + }) + useDeploymentLogs.mockReturnValue({ data: mockLogs, pauseLogs: false, diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx index 5152466dc33..ae715daaf80 100644 --- a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx @@ -27,9 +27,9 @@ import { dateYearMonthDayHourMinuteSecond } from '@qovery/shared/util-dates' import { trimId } from '@qovery/shared/util-js' import { DeploymentLogsPlaceholder } from '../deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder' import HeaderLogs from '../header-logs/header-logs' -import { useDeploymentHistory } from '../hooks/use-deployment-history/use-deployment-history' import { type EnvironmentLogIds, useDeploymentLogs } from '../hooks/use-deployment-logs/use-deployment-logs' import { useGenerateBuildUsageReport } from '../hooks/use-generate-build-usage-report/use-generate-build-usage-report' +import { useServiceDeploymentHistory } from '../hooks/use-service-deployment-history/use-service-deployment-history' import { ProgressIndicator } from '../progress-indicator/progress-indicator' import { ServiceStageIdsContext } from '../service-stage-ids-context/service-stage-ids-context' import { ShowNewLogsButton } from '../show-new-logs-button/show-new-logs-button' @@ -163,8 +163,9 @@ export function ListDeploymentLogs({ const { data: service } = useService({ environmentId: environment.id, serviceId, suspense: true }) const { data: deploymentStatus } = useDeploymentStatus({ environmentId: environment.id, serviceId, suspense: true }) - const { data: environmentDeploymentHistory = [] } = useDeploymentHistory({ + const { data: deploymentHistory = [] } = useServiceDeploymentHistory({ environmentId: environment.id, + serviceId, suspense: true, }) const { @@ -289,11 +290,10 @@ export function ListDeploymentLogs({ [columnFilters] ) - const isLastVersion = environmentDeploymentHistory?.[0]?.identifier.execution_id === executionId || !executionId + const isLastVersion = deploymentHistory?.[0]?.identifier.execution_id === executionId || !executionId const currentDeployment = executionId - ? environmentDeploymentHistory.find((d) => d.identifier.execution_id === executionId) - : environmentDeploymentHistory[0] - + ? deploymentHistory.find((d) => d.identifier.execution_id === executionId) + : deploymentHistory[0] const isDeploymentProgressing = isLastVersion ? match(deploymentStatus?.state) .with( @@ -326,7 +326,7 @@ export function ListDeploymentLogs({ const isError = serviceStatus?.state?.includes('ERROR') function HeaderLogsComponent() { - const currentDeploymentHistory = environmentDeploymentHistory.find((d) => d.identifier.execution_id === executionId) + const currentDeploymentHistory = deploymentHistory.find((d) => d.identifier.execution_id === executionId) return (
@@ -351,7 +352,7 @@ export function ListDeploymentLogs({ - {environmentDeploymentHistory.map((deployment) => ( + {deploymentHistory.map((deployment) => (
diff --git a/libs/pages/layout/src/lib/ui/breadcrumb/breadcrumb.tsx b/libs/pages/layout/src/lib/ui/breadcrumb/breadcrumb.tsx index 33899df3da8..1c5d913a872 100644 --- a/libs/pages/layout/src/lib/ui/breadcrumb/breadcrumb.tsx +++ b/libs/pages/layout/src/lib/ui/breadcrumb/breadcrumb.tsx @@ -4,7 +4,7 @@ import { memo, useCallback, useEffect, useState } from 'react' import { useLocation, useMatch, useNavigate, useParams } from 'react-router-dom' import { match } from 'ts-pattern' import { EnvironmentMode, useDeploymentStages } from '@qovery/domains/environments/feature' -import { BreadcrumbDeploymentHistory, BreadcrumbDeploymentLogs } from '@qovery/domains/service-logs/feature' +import { BreadcrumbDeploymentLogs } from '@qovery/domains/service-logs/feature' import { ServiceStateChip, useServices } from '@qovery/domains/services/feature' import { IconEnum } from '@qovery/shared/enums' import { @@ -403,30 +403,8 @@ export function Breadcrumb(props: BreadcrumbProps) { statusStages={statusStages} /> -
/
- )} - {(matchEnvironmentStage || matchEnvironmentStageVersion || matchEnvironmentPreCheckVersion) && statusStages && ( - <> -
/
- - - )} {matchServiceLogs && services && ( <>
/
diff --git a/libs/shared/util-js/src/lib/trim-id.spec.ts b/libs/shared/util-js/src/lib/trim-id.spec.ts index 169b9ff42ef..32dc317cb3f 100644 --- a/libs/shared/util-js/src/lib/trim-id.spec.ts +++ b/libs/shared/util-js/src/lib/trim-id.spec.ts @@ -2,7 +2,7 @@ import { trimId } from './trim-id' describe('trimId', () => { it('should truncate in middle with default values', () => { - expect(trimId('foobarbazquxquux')).toEqual('fooba...uux') + expect(trimId('foobarbazquxquux')).toEqual('fooba…uux') }) it('should truncate at start with default values', () => { expect(trimId('foobarbazquxquux', 'start')).toEqual('fooba') @@ -11,7 +11,7 @@ describe('trimId', () => { expect(trimId('foobarbazquxquux', 'end')).toEqual('uux') }) it('should truncate in middle with custom values', () => { - expect(trimId('foobarbazquxquux', 'both', { startOffset: 1, endOffset: 1 })).toEqual('f...x') + expect(trimId('foobarbazquxquux', 'both', { startOffset: 1, endOffset: 1 })).toEqual('f…x') }) it('should truncate at start with custom values', () => { expect(trimId('foobarbazquxquux', 'start', { startOffset: 2 })).toEqual('fo') diff --git a/libs/shared/util-js/src/lib/trim-id.ts b/libs/shared/util-js/src/lib/trim-id.ts index dbd36d7fd26..765f492fb01 100644 --- a/libs/shared/util-js/src/lib/trim-id.ts +++ b/libs/shared/util-js/src/lib/trim-id.ts @@ -11,6 +11,6 @@ export const trimId = ( return match(type) .with('start', () => start) .with('end', () => end) - .with('both', () => `${start}...${end}`) + .with('both', () => `${start}…${end}`) .exhaustive() } From ad41d564b7119001b461b2e54bc4b47f6136b4de Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 26 Mar 2026 13:31:44 +0100 Subject: [PATCH 02/10] fix(deployment-logs): update deployment logs placeholder and improve icon rendering --- .../deployment-logs-placeholder.spec.tsx | 16 +++- .../deployment-logs-placeholder.tsx | 93 ++++++++++++------- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx index 58919794cef..e9ac7d181f8 100644 --- a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx +++ b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx @@ -1,3 +1,4 @@ +import { type ReactNode } from 'react' import { applicationFactoryMock, environmentFactoryMock } from '@qovery/shared/factories' import { renderWithProviders, screen } from '@qovery/shared/util-tests' import { DeploymentLogsPlaceholder, type DeploymentLogsPlaceholderProps } from './deployment-logs-placeholder' @@ -9,14 +10,20 @@ jest.mock('@qovery/domains/services/feature', () => ({ useService: () => ({ data: mockApplication }), })) -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), useParams: () => ({ organizationId: 'org-123', projectId: 'proj-123', environmentId: 'env-123', serviceId: 'serv-123', + executionId: 'exec-1', }), + Link: ({ children, ...props }: { children?: ReactNode; [key: string]: unknown }) => ( + + {children} + + ), })) describe('DeploymentLogsPlaceholder', () => { @@ -114,9 +121,8 @@ describe('DeploymentLogsPlaceholder', () => { /> ) - expect( - screen.getByText('This service was deployed more than 30 days ago and thus no deployment logs are available.') - ).toBeInTheDocument() + expect(screen.getByText("Deployment logs are no longer available due to the deployment's age.")).toBeInTheDocument() + expect(screen.getByText('No logs to display.')).toBeInTheDocument() }) it('should render queued state', () => { diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.tsx index 810961db23a..1fdf0a051ba 100644 --- a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.tsx +++ b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder.tsx @@ -1,3 +1,4 @@ +import { useParams } from '@tanstack/react-router' import { type DeploymentHistoryEnvironmentV2, type Environment, @@ -6,7 +7,6 @@ import { ServiceDeploymentStatusEnum, type Status, } from 'qovery-typescript-axios' -import { useParams } from 'react-router-dom' import { useService } from '@qovery/domains/services/feature' import { type DeploymentService } from '@qovery/shared/interfaces' import { Icon, Link, LoaderDots, StatusChip } from '@qovery/shared/ui' @@ -17,25 +17,44 @@ function ErrorIcon() { return ( - + - + - + - + + + + + ) +} + +function HistoryUnavailableIcon() { + return ( + + + + + + + + @@ -54,7 +73,7 @@ export function LoaderPlaceholder({

{title}

- {description} + {description}
) @@ -67,20 +86,20 @@ function DeploymentHistoryPlaceholder({ serviceName: string deploymentsByServiceId: DeploymentService[] }) { - const { organizationId = '', projectId = '', environmentId = '', versionId = '' } = useParams() + const { organizationId = '', projectId = '', environmentId = '', executionId = '' } = useParams({ strict: false }) return (
-

+

{serviceName} service was not deployed within this deployment execution.

-

+

Below the list of executions where this service was deployed.

-
-
+
+
Last deployment logs
@@ -88,8 +107,8 @@ function DeploymentHistoryPlaceholder({ deploymentsByServiceId.map((deploymentHistory: DeploymentService) => (
- {trimId(deploymentHistory.execution_id || '')} + {trimId(deploymentHistory.execution_id || '')} - + {dateFullFormat(deploymentHistory.auditing_data.created_at)}
)) ) : ( -

No history deployment available for this service.

+

No history deployment available for this service.

)}
-
-

+

+

Only the last 20 deployments of the environment over the last 30 days are available.

@@ -141,7 +160,7 @@ export function DeploymentLogsPlaceholder({ environmentDeploymentHistory, preCheckStage, }: DeploymentLogsPlaceholderProps) { - const { serviceId = '', versionId = '' } = useParams() + const { serviceId = '', executionId = '' } = useParams({ strict: false }) const { state: serviceState, @@ -177,15 +196,18 @@ export function DeploymentLogsPlaceholder({ // Show no logs available state if (hideLogs && service && deploymentsByServiceId.length === 0 && outOfDateOrUpToDate) { + if (serviceDeploymentStatus === ServiceDeploymentStatusEnum.NEVER_DEPLOYED) { + return

No logs on this execution for {service.name}.

+ } + return ( - <> -

No logs on this execution for {service.name}.

- {serviceDeploymentStatus !== ServiceDeploymentStatusEnum.NEVER_DEPLOYED && ( -

- This service was deployed more than 30 days ago and thus no deployment logs are available. -

- )} - +
+ +
+

Deployment logs are no longer available due to the deployment's age.

+ No logs to display. +
+
) } @@ -204,15 +226,18 @@ export function DeploymentLogsPlaceholder({
- + - + @@ -240,7 +265,7 @@ export function DeploymentLogsPlaceholder({ organizationId: environment.organization.id, projectId: environment.project.id, environmentId: environment.id, - deploymentId: versionId, + deploymentId: executionId, }} > Open precheck @@ -270,7 +295,7 @@ export function DeploymentLogsPlaceholder({ organizationId: environment.organization.id, projectId: environment.project.id, environmentId: environment.id, - deploymentId: versionId, + deploymentId: executionId, }} > Open pipeline From 3e6056369db09f3ae2d72c500a5cb64d07431f65 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 26 Mar 2026 14:26:05 +0100 Subject: [PATCH 03/10] fix(deployment-logs): add loading state with placeholder during log retrieval --- .../$serviceId/deployments/logs/$executionId.tsx | 14 +++++++++++++- libs/domains/service-logs/feature/src/index.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx index c4406c25f7b..9a7110276ba 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx @@ -1,5 +1,7 @@ import { createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' import { DeploymentLogs } from '@qovery/domains/service-logs/feature' +import { LoaderPlaceholder } from '@qovery/domains/service-logs/feature' export const Route = createFileRoute( '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId' @@ -8,5 +10,15 @@ export const Route = createFileRoute( }) function RouteComponent() { - return + return ( + + +
+ } + > + + + ) } diff --git a/libs/domains/service-logs/feature/src/index.ts b/libs/domains/service-logs/feature/src/index.ts index 1105be812e2..740e3ec74f3 100644 --- a/libs/domains/service-logs/feature/src/index.ts +++ b/libs/domains/service-logs/feature/src/index.ts @@ -5,3 +5,4 @@ export * from './lib/breadcrumb-deployment-logs/breadcrumb-deployment-logs' export * from './lib/sidebar-pod-statuses/sidebar-pod-statuses' export * from './lib/deployment-logs/deployment-logs' export * from './lib/deployment-logs/deployment-logs-content/deployment-logs-content' +export * from './lib/deployment-logs/deployment-logs-placeholder/deployment-logs-placeholder' From 570b82d6db45fd5c9c825808b492a279a60ff5b6 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 26 Mar 2026 16:03:38 +0100 Subject: [PATCH 04/10] fix(deployment-logs): adjust loading states and improve deployment history handling --- .../deployments/logs/$executionId.tsx | 2 +- .../deployment-logs-content.tsx | 1 + .../lib/deployment-logs/deployment-logs.tsx | 8 +- .../src/lib/header-logs/header-logs.tsx | 9 +- .../use-deployment-logs.ts | 16 +- .../use-service-deployment-history.ts | 22 +- .../list-deployment-logs.tsx | 468 ++++++++++-------- 7 files changed, 291 insertions(+), 235 deletions(-) diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx index 9a7110276ba..c819976df8a 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/deployments/logs/$executionId.tsx @@ -13,7 +13,7 @@ function RouteComponent() { return ( +
} diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx index ae22ca0db63..0f2589a9e98 100644 --- a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx +++ b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs-content/deployment-logs-content.tsx @@ -181,6 +181,7 @@ export function DeploymentLogsContent({ environmentStatus={environmentStatus} stage={stageFromServiceId} preCheckStage={preCheckStage} + hasNewDeploymentBanner={showBannerNew} /> ) diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs.tsx index 35839a4f52a..dd71467b841 100644 --- a/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/deployment-logs/deployment-logs.tsx @@ -21,8 +21,8 @@ const WebSocketListenerMemo = memo(MetricsWebSocketListener) function Loader() { return ( -
-
+
+
@@ -118,11 +118,11 @@ export function DeploymentLogs() { }) useEffect(() => { - // Reset local state when URL parameters change + // Reset page-level deployment state only when the overall scope changes. setDeploymentStages(undefined) setEnvironmentStatus(undefined) setPreCheckStage(undefined) - }, [organizationId, projectId, environmentId, executionId]) + }, [organizationId, projectId, environmentId]) return (
diff --git a/libs/domains/service-logs/feature/src/lib/header-logs/header-logs.tsx b/libs/domains/service-logs/feature/src/lib/header-logs/header-logs.tsx index 20a8020587b..a6684140419 100644 --- a/libs/domains/service-logs/feature/src/lib/header-logs/header-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/header-logs/header-logs.tsx @@ -1,11 +1,5 @@ import { useParams, useRouter, useSearch } from '@tanstack/react-router' -import { - DatabaseModeEnum, - type DeploymentHistoryEnvironmentV2, - type Environment, - type EnvironmentStatus, - type Status, -} from 'qovery-typescript-axios' +import { DatabaseModeEnum, type Environment, type EnvironmentStatus, type Status } from 'qovery-typescript-axios' import { type PropsWithChildren, useMemo } from 'react' import { match } from 'ts-pattern' import { @@ -26,7 +20,6 @@ export interface HeaderLogsProps extends PropsWithChildren { environment: Environment serviceStatus: Status environmentStatus?: EnvironmentStatus - deploymentHistory?: DeploymentHistoryEnvironmentV2 } export function HeaderLogs({ diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-deployment-logs/use-deployment-logs.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-deployment-logs/use-deployment-logs.ts index abcdc5548e4..344c3e885d1 100644 --- a/libs/domains/service-logs/feature/src/lib/hooks/use-deployment-logs/use-deployment-logs.ts +++ b/libs/domains/service-logs/feature/src/lib/hooks/use-deployment-logs/use-deployment-logs.ts @@ -79,6 +79,9 @@ export function useDeploymentLogs({ onMessage: messageHandler, }) + // If there are no logs, set the flush delay to 0, otherwise use the debounce time + const flushDelay = logs.length === 0 ? 0 : debounceTime + useEffect(() => { if (messageChunks.length === 0 || pauseLogs) return @@ -87,19 +90,20 @@ export function useDeploymentLogs({ setMessageChunks((prevChunks) => prevChunks.slice(1)) setLogs((prevLogs) => { const combinedLogs = [...prevLogs, ...messageChunks[0]] + + if (!hash && combinedLogs.length > 1000) { + setDebounceTime(100) + } + return [...new Map(combinedLogs.map((item) => [item['timestamp'], item])).values()] }) - - if (!hash && logs.length > 1000) { - setDebounceTime(100) - } } - }, debounceTime) + }, flushDelay) return () => { clearTimeout(timerId) } - }, [messageChunks, pauseLogs, hash]) + }, [messageChunks, pauseLogs, hash, flushDelay]) // Filter deployment logs by serviceId and stageId // Display entries when the name is "delete" or stageId is empty or equal with current stageId diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts index 07fdb901096..0b7a2324ea9 100644 --- a/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts +++ b/libs/domains/service-logs/feature/src/lib/hooks/use-service-deployment-history/use-service-deployment-history.ts @@ -1,5 +1,5 @@ -import { useMemo } from 'react' -import { useDeploymentHistory } from '../use-deployment-history/use-deployment-history' +import { useQuery } from '@tanstack/react-query' +import { queries } from '@qovery/state/util-queries' export interface UseServiceDeploymentHistoryProps { environmentId: string @@ -12,20 +12,14 @@ export function useServiceDeploymentHistory({ serviceId, suspense = false, }: UseServiceDeploymentHistoryProps) { - const query = useDeploymentHistory({ environmentId, suspense }) - - const data = useMemo( - () => - (query.data ?? []).filter((history) => + return useQuery({ + ...queries.environments.deploymentHistoryV2({ environmentId }), + suspense, + select: (data) => + data?.filter((history) => history.stages?.some((stage) => stage.services?.some((service) => service.identifier.service_id === serviceId)) ), - [query.data, serviceId] - ) - - return { - ...query, - data, - } + }) } export default useServiceDeploymentHistory diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx index ae715daaf80..7ba82fb3afb 100644 --- a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx @@ -11,6 +11,7 @@ import clsx from 'clsx' import download from 'downloadjs' import posthog from 'posthog-js' import { + type DeploymentHistoryEnvironmentV2, type Environment, type EnvironmentStatus, type EnvironmentStatusesWithStagesPreCheckStage, @@ -141,15 +142,112 @@ export interface ListDeploymentLogsProps { stage?: Stage environmentStatus?: EnvironmentStatus preCheckStage?: EnvironmentStatusesWithStagesPreCheckStage + hasNewDeploymentBanner?: boolean } -export function ListDeploymentLogs({ +interface DeploymentLogsHeaderProps { + environment: Environment + serviceStatus: Status + environmentStatus?: EnvironmentStatus + deploymentHistory: DeploymentHistoryEnvironmentV2[] +} + +const DeploymentLogsHeader = memo(function DeploymentLogsHeader({ + environment, + serviceStatus, + environmentStatus, + deploymentHistory, +}: DeploymentLogsHeaderProps) { + const { organizationId = '', projectId = '', serviceId = '', executionId = '' } = useParams({ strict: false }) + const [open, setOpen] = useState(false) + + const currentDeploymentHistory = useMemo( + () => deploymentHistory.find((deployment) => deployment.identifier.execution_id === executionId), + [deploymentHistory, executionId] + ) + + const selectedDeploymentHistory = executionId ? currentDeploymentHistory : deploymentHistory[0] + const selectedDeploymentDate = selectedDeploymentHistory?.auditing_data.created_at + const isLastVersion = deploymentHistory[0]?.identifier.execution_id === executionId || !executionId + + return ( + +
+ + + + + + {deploymentHistory.map((deployment) => ( + + + + {trimId(deployment.identifier.execution_id ?? '')} + + + {dateYearMonthDayHourMinuteSecond(new Date(deployment.auditing_data.created_at))} + + + + + ))} + + +
+
+ ) +}) + +interface DeploymentLogsBodyProps { + environment: Environment + serviceStatus: Status + stage?: Stage + environmentStatus?: EnvironmentStatus + preCheckStage?: EnvironmentStatusesWithStagesPreCheckStage + deploymentHistory: DeploymentHistoryEnvironmentV2[] + hasNewDeploymentBanner: boolean +} + +function DeploymentLogsBody({ environment, environmentStatus, serviceStatus, stage, preCheckStage, -}: ListDeploymentLogsProps) { + deploymentHistory, + hasNewDeploymentBanner, +}: DeploymentLogsBodyProps) { const { hash } = useLocation() const { organizationId = '', projectId = '', serviceId = '', executionId = '' } = useParams({ strict: false }) const refScrollSection = useRef(null) @@ -163,11 +261,6 @@ export function ListDeploymentLogs({ const { data: service } = useService({ environmentId: environment.id, serviceId, suspense: true }) const { data: deploymentStatus } = useDeploymentStatus({ environmentId: environment.id, serviceId, suspense: true }) - const { data: deploymentHistory = [] } = useServiceDeploymentHistory({ - environmentId: environment.id, - serviceId, - suspense: true, - }) const { data: logs = [], pauseLogs, @@ -198,7 +291,7 @@ export function ListDeploymentLogs({ if (hash && section) { setPauseLogs(true) setShowPreviousLogs(true) - const row = document.getElementById(hash.substring(1)) // remove the '#' from the hash + const row = document.getElementById(hash.substring(1)) if (row) { // scroll the section to the row's position setTimeout(() => { @@ -268,14 +361,13 @@ export function ListDeploymentLogs({ const currentFilter = prevFilters.find((filter) => filter.id === 'details.stage.step') if (currentFilter?.value === type) { return defaultColumnsFilters - } else { - return [ - { - id: 'details.stage.step', - value: type, - }, - ] } + return [ + { + id: 'details.stage.step', + value: type, + }, + ] }) setTimeout(() => { const section = refScrollSection.current @@ -286,9 +378,11 @@ export function ListDeploymentLogs({ ) const isFilterActive = useMemo( - () => (type: FilterType) => columnFilters.some((f) => f.value === type), + () => (type: FilterType) => columnFilters.some((filter) => filter.value === type), [columnFilters] ) + const emptyStateHeightClass = hasNewDeploymentBanner ? 'h-[calc(100vh-156px)]' : 'h-[calc(100vh-116px)]' + const logsViewportHeightClass = hasNewDeploymentBanner ? 'h-[calc(100vh-249px)]' : 'h-[calc(100vh-209px)]' const isLastVersion = deploymentHistory?.[0]?.identifier.execution_id === executionId || !executionId const currentDeployment = executionId @@ -325,86 +419,20 @@ export function ListDeploymentLogs({ }, [logs, isDeploymentProgressing]) const isError = serviceStatus?.state?.includes('ERROR') - function HeaderLogsComponent() { - const currentDeploymentHistory = deploymentHistory.find((d) => d.identifier.execution_id === executionId) - - return ( - -
- - - - - - {deploymentHistory.map((deployment) => ( - - - - {trimId(deployment.identifier.execution_id ?? '')} - - - {dateYearMonthDayHourMinuteSecond(new Date(deployment.auditing_data.created_at))} - - - - - ))} - - -
-
- ) - } if (!logs || logs.length === 0 || !serviceStatus.is_part_last_deployment) { return ( -
-
- -
-
- -
+
+
+
+
@@ -412,124 +440,160 @@ export function ListDeploymentLogs({ } return ( -
-
- -
- -
- {(isError || isCrashLoopDetected) && ( - - )} + <> +
+ +
+ {(isError || isCrashLoopDetected) && ( - {match(service) - .with({ serviceType: 'CONTAINER' }, () => false) - .with({ serviceType: 'DATABASE', mode: 'CONTAINER' }, () => false) - .with({ serviceType: 'JOB', source: P.when(isJobContainerSource) }, () => false) - .with({ serviceType: 'HELM', values_override: P.when(isHelmRepositorySource) }, () => false) - .otherwise(() => true) && - currentDeployment?.identifier.execution_id && ( - - - - - - )} -
-
-
{ - if ( - !pauseLogs && - refScrollSection.current && - refScrollSection.current.clientHeight !== refScrollSection.current.scrollHeight && - event.deltaY < 0 - ) { - setPauseLogs(true) - setNewMessagesAvailable(false) - } - }} - > - {logs.length >= 500 && ( - )} - false) + .with({ serviceType: 'DATABASE', mode: 'CONTAINER' }, () => false) + .with({ serviceType: 'JOB', source: P.when(isJobContainerSource) }, () => false) + .with({ serviceType: 'HELM', values_override: P.when(isHelmRepositorySource) }, () => false) + .otherwise(() => true) && + currentDeployment?.identifier.execution_id && ( + + + + + + )} +
- {isLastVersion && ( - +
{ + if ( + !pauseLogs && + refScrollSection.current && + refScrollSection.current.clientHeight !== refScrollSection.current.scrollHeight && + event.deltaY < 0 + ) { + setPauseLogs(true) + setNewMessagesAvailable(false) + } + }} + > + {logs.length >= 500 && ( + )} + + + {table.getRowModel().rows.map((row) => ( + + ))} + + + {isDeploymentProgressing && ( + + )} +
+ {isLastVersion && ( + + )} + + ) +} + +export function ListDeploymentLogs({ + environment, + environmentStatus, + serviceStatus, + stage, + preCheckStage, + hasNewDeploymentBanner = false, +}: ListDeploymentLogsProps) { + const { executionId = '', serviceId = '' } = useParams({ strict: false }) + const { data: deploymentHistory = [] } = useServiceDeploymentHistory({ + environmentId: environment.id, + serviceId, + suspense: true, + }) + + return ( +
+
+ +
) From 3c6aa6afba9b4b6dfd1b30317ef2b7ce19f083d1 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 26 Mar 2026 16:52:46 +0100 Subject: [PATCH 05/10] fix(deployment-logs): change download button size from medium to small for better UI consistency --- .../src/lib/list-deployment-logs/list-deployment-logs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx index 7ba82fb3afb..ce3d4abed99 100644 --- a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx @@ -506,7 +506,7 @@ function DeploymentLogsBody({