diff --git a/libs/domains/service-logs/feature/src/index.ts b/libs/domains/service-logs/feature/src/index.ts
index 9d500187475..4932f0aba81 100644
--- a/libs/domains/service-logs/feature/src/index.ts
+++ b/libs/domains/service-logs/feature/src/index.ts
@@ -1,2 +1,4 @@
export * from './lib/list-service-logs/list-service-logs'
export * from './lib/update-time-context/update-time-context'
+export * from './lib/list-deployment-logs/list-deployment-logs'
+export * from './lib/service-stage-ids-context/service-stage-ids-context'
diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx
new file mode 100644
index 00000000000..fdbed37547b
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.spec.tsx
@@ -0,0 +1,100 @@
+import { applicationFactoryMock } from '@qovery/shared/factories'
+import { renderWithProviders, screen } from '@qovery/shared/util-tests'
+import { DeploymentLogsPlaceholder, type DeploymentLogsPlaceholderProps } from './deployment-logs-placeholder'
+
+const mockApplication = applicationFactoryMock(1)[0]
+
+jest.mock('@qovery/domains/services/feature', () => ({
+ useService: () => ({ data: mockApplication }),
+}))
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ organizationId: 'org-123',
+ projectId: 'proj-123',
+ environmentId: 'env-123',
+ serviceId: 'serv-123',
+ }),
+}))
+
+describe('DeploymentLogsPlaceholder', () => {
+ const props: DeploymentLogsPlaceholderProps = {
+ serviceStatus: undefined,
+ itemsLength: 0,
+ deploymentHistoryEnvironment: [],
+ }
+
+ it('should render deployment history', () => {
+ renderWithProviders(
+
+ )
+
+ expect(screen.getByText('Last deployment logs')).toBeInTheDocument()
+ expect(screen.getByText('d941d...-10')).toBeInTheDocument()
+ })
+
+ it('should render "No history deployment available"', () => {
+ props.itemsLength = 1
+ renderWithProviders()
+
+ expect(screen.getByText('No history deployment available for this service.')).toBeInTheDocument()
+ })
+
+ it('should render spinner', () => {
+ renderWithProviders(
+
+ )
+
+ expect(screen.getByTestId('spinner')).toBeInTheDocument()
+ })
+
+ it('should render no logs placeholder', () => {
+ renderWithProviders(
+
+ )
+
+ expect(
+ screen.getByText('This service was deployed more than 30 days ago and thus no deployment logs are available.')
+ ).toBeInTheDocument()
+ })
+})
diff --git a/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.tsx b/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.tsx
new file mode 100644
index 00000000000..6dce6e27173
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/deployment-logs-placeholder/deployment-logs-placeholder.tsx
@@ -0,0 +1,136 @@
+import { type DeploymentHistoryEnvironment, ServiceDeploymentStatusEnum, type Status } from 'qovery-typescript-axios'
+import { Link, useParams } from 'react-router-dom'
+import { match } from 'ts-pattern'
+import { useService } from '@qovery/domains/services/feature'
+import { type DeploymentService } from '@qovery/shared/interfaces'
+import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL } from '@qovery/shared/routes'
+import { StatusChip } from '@qovery/shared/ui'
+import { dateFullFormat } from '@qovery/shared/util-dates'
+import { mergeDeploymentServices, trimId } from '@qovery/shared/util-js'
+import { LoaderPlaceholder } from '../service-logs-placeholder/service-logs-placeholder'
+
+function DeploymentHistoryPlaceholder({
+ serviceName,
+ deploymentsByServiceId,
+}: {
+ serviceName: string
+ deploymentsByServiceId: DeploymentService[]
+}) {
+ const { organizationId = '', projectId = '', environmentId = '', versionId = '' } = useParams()
+
+ return (
+
+
+
+ {serviceName} service was not deployed within this deployment
+ execution.
+
+
+ Below the list of executions where this service was deployed.
+
+
+
+
+ Last deployment logs
+
+
+ {deploymentsByServiceId.length > 0 ? (
+ deploymentsByServiceId.map((deploymentHistory: DeploymentService) => (
+
+
+
+
+ {trimId(deploymentHistory.execution_id || '')}
+
+ {dateFullFormat(deploymentHistory.created_at)}
+
+
+ ))
+ ) : (
+
No history deployment available for this service.
+ )}
+
+
+
+ Only the last 20 deployments of the environment over the last 30 days are available.
+
+
+
+
+ )
+}
+
+export interface DeploymentLogsPlaceholderProps {
+ serviceStatus?: Status
+ itemsLength?: number
+ deploymentHistoryEnvironment?: DeploymentHistoryEnvironment[]
+}
+
+export function DeploymentLogsPlaceholder({
+ serviceStatus,
+ itemsLength,
+ deploymentHistoryEnvironment,
+}: DeploymentLogsPlaceholderProps) {
+ const { environmentId = '', serviceId = '' } = useParams()
+
+ const { service_deployment_status: serviceDeploymentStatus, is_part_last_deployment: isPartLastDeployment } =
+ serviceStatus || {}
+
+ const { data: service } = useService({ environmentId, serviceId })
+ const hideLogs = !isPartLastDeployment
+
+ const outOfDateOrUpToDate =
+ serviceDeploymentStatus === ServiceDeploymentStatusEnum.NEVER_DEPLOYED ||
+ serviceDeploymentStatus === ServiceDeploymentStatusEnum.UP_TO_DATE
+
+ const displaySpinner = match({ itemsLength, hideLogs, serviceDeploymentStatus, service })
+ .with(
+ {
+ itemsLength: 0,
+ hideLogs: false,
+ serviceDeploymentStatus: undefined,
+ service: undefined,
+ },
+ () => true
+ )
+ .otherwise(() => false)
+
+ const deploymentsByServiceId = mergeDeploymentServices(deploymentHistoryEnvironment).filter(
+ (deploymentHistory) => deploymentHistory.id === serviceId
+ )
+
+ if (displaySpinner) {
+ return
+ }
+
+ if (hideLogs && service) {
+ if (deploymentsByServiceId.length === 0 && outOfDateOrUpToDate) {
+ 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.
+
+ )}
+
+ )
+ }
+
+ return
+ }
+
+ return
+}
+
+export default DeploymentLogsPlaceholder
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
new file mode 100644
index 00000000000..6ea8921d1dd
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/hooks/use-deployment-logs/use-deployment-logs.ts
@@ -0,0 +1,126 @@
+import { type QueryClient } from '@tanstack/react-query'
+import { type EnvironmentLogs } from 'qovery-typescript-axios'
+import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
+import { useEnvironment } from '@qovery/domains/environments/feature'
+import { QOVERY_WS } from '@qovery/shared/util-node-env'
+import { useReactQueryWsSubscription } from '@qovery/state/util-queries'
+import { ServiceStageIdsContext } from '../../service-stage-ids-context/service-stage-ids-context'
+
+export interface UseDeploymentLogsProps {
+ organizationId?: string
+ projectId?: string
+ environmentId?: string
+ serviceId?: string
+ versionId?: string
+}
+
+const CHUNK_SIZE = 500
+
+// This hook simplifies the process of fetching and managing deployment logs data
+export function useDeploymentLogs({
+ organizationId,
+ projectId,
+ environmentId,
+ serviceId,
+ versionId,
+}: UseDeploymentLogsProps) {
+ const { data: environment } = useEnvironment({ environmentId })
+
+ // States for controlling log actions, showing new, previous or paused logs
+ const [newMessagesAvailable, setNewMessagesAvailable] = useState(false)
+ const [showPreviousLogs, setShowPreviousLogs] = useState(false)
+ const [pauseLogs, setPauseLogs] = useState(false)
+ const [debounceTime, setDebounceTime] = useState(1000)
+
+ const [logs, setLogs] = useState([])
+ const [messageChunks, setMessageChunks] = useState([])
+
+ const { stageId } = useContext(ServiceStageIdsContext)
+ const now = useMemo(() => Date.now(), [])
+
+ const messageHandler = useCallback(
+ (_: QueryClient, message: EnvironmentLogs[]) => {
+ setNewMessagesAvailable(true)
+ setMessageChunks((prevChunks) => {
+ const lastChunk = prevChunks[prevChunks.length - 1] || []
+ if (lastChunk.length < CHUNK_SIZE) {
+ return [...prevChunks.slice(0, -1), [...lastChunk, ...message]]
+ } else {
+ return [...prevChunks, [...message]]
+ }
+ })
+ },
+ [setMessageChunks]
+ )
+
+ useReactQueryWsSubscription({
+ url: QOVERY_WS + '/deployment/logs',
+ urlSearchParams: {
+ organization: organizationId,
+ cluster: environment?.cluster_id,
+ project: projectId,
+ environment: environmentId,
+ version: versionId,
+ },
+ enabled:
+ Boolean(organizationId) && Boolean(environment?.cluster_id) && Boolean(projectId) && Boolean(environmentId),
+ onMessage: messageHandler,
+ })
+
+ useEffect(() => {
+ if (messageChunks.length === 0 || pauseLogs) return
+
+ const timerId = setTimeout(() => {
+ if (!pauseLogs) {
+ setMessageChunks((prevChunks) => prevChunks.slice(1))
+ setLogs((prevLogs) => {
+ const combinedLogs = [...prevLogs, ...messageChunks[0]]
+ return [...new Map(combinedLogs.map((item) => [item['timestamp'], item])).values()]
+ })
+
+ if (logs.length > 1000) {
+ setDebounceTime(100)
+ }
+ }
+ }, debounceTime)
+
+ return () => {
+ clearTimeout(timerId)
+ }
+ }, [messageChunks, pauseLogs])
+
+ // Filter deployment logs by serviceId and stageId
+ // Display entries when the name is "delete" or stageId is empty or equal with current stageId
+ // Filter by the same transmitter ID and "Environment" or "TaskManager" type
+ const logsByServiceId = useMemo(
+ () =>
+ logs
+ .filter((currentData: EnvironmentLogs) => {
+ const { stage, transmitter } = currentData.details
+ const isDeleteStage = stage?.name === 'delete'
+ const isEmptyOrEqualStageId = !stage?.id || stage?.id === stageId
+ const isMatchingTransmitter =
+ transmitter?.type === 'Environment' || transmitter?.type === 'TaskManager' || transmitter?.id === serviceId
+
+ // Include the entry if any of the following conditions are true:
+ // 1. The stage name is "delete".
+ // 2. stageId is empty or equal with current stageId.
+ // 3. The transmitter matches serviceId and has a type of "Environment" or "TaskManager".
+ return (isDeleteStage || isEmptyOrEqualStageId) && isMatchingTransmitter
+ })
+ .filter((log, index, array) => (showPreviousLogs || index >= array.length - 500 ? true : +log.timestamp > now)),
+ [logs, stageId, serviceId, now, showPreviousLogs]
+ )
+
+ return {
+ data: logsByServiceId,
+ setNewMessagesAvailable,
+ newMessagesAvailable,
+ pauseLogs,
+ setPauseLogs,
+ showPreviousLogs,
+ setShowPreviousLogs,
+ }
+}
+
+export default useDeploymentLogs
diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-service-logs/use-service-logs.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-service-logs/use-service-logs.ts
index e4d1accc60f..08fd6cfc66b 100644
--- a/libs/domains/service-logs/feature/src/lib/hooks/use-service-logs/use-service-logs.ts
+++ b/libs/domains/service-logs/feature/src/lib/hooks/use-service-logs/use-service-logs.ts
@@ -57,7 +57,7 @@ export function useServiceLogs({
return () => {
clearTimeout(handler)
}
- }, [serviceMessagesMap.current.size, DEBOUNCE_TIME])
+ }, [serviceMessagesMap.current.size])
const now = useMemo(() => Date.now(), [])
const infraMessageHandler = useCallback(
diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.spec.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.spec.tsx
new file mode 100644
index 00000000000..ca7dd09bb66
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.spec.tsx
@@ -0,0 +1,74 @@
+import { StateEnum, type Status } from 'qovery-typescript-axios'
+import { renderWithProviders, screen } from '@qovery/shared/util-tests'
+import { FiltersStageStep, type FiltersStageStepProps } from './filters-stage-step'
+
+const mockToggleColumnFilter = jest.fn()
+const mockIsFilterActive = jest.fn()
+
+const defaultProps: FiltersStageStepProps = {
+ serviceStatus: {
+ state: StateEnum.BUILDING,
+ steps: {
+ details: [
+ { step_name: 'BUILD', status: 'SUCCESS', duration_sec: 60 },
+ { step_name: 'DEPLOYMENT', status: 'SUCCESS', duration_sec: 120 },
+ ],
+ },
+ } as Status,
+ toggleColumnFilter: mockToggleColumnFilter,
+ isFilterActive: mockIsFilterActive,
+}
+
+describe('FiltersStageStep', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('renders BUILD and DEPLOY buttons', () => {
+ renderWithProviders()
+ expect(screen.getByText('Build')).toBeInTheDocument()
+ expect(screen.getByText('Deploy')).toBeInTheDocument()
+ })
+
+ it('calls toggleColumnFilter with correct type when buttons are clicked', async () => {
+ const { userEvent } = renderWithProviders()
+ await userEvent.click(screen.getByText('Build'))
+ expect(mockToggleColumnFilter).toHaveBeenCalledWith('BUILD')
+ await userEvent.click(screen.getByText('Deploy'))
+ expect(mockToggleColumnFilter).toHaveBeenCalledWith('DEPLOY')
+ })
+
+ it('displays correct duration for build and deploy steps', () => {
+ renderWithProviders()
+ expect(screen.getByText('1m : 0s')).toBeInTheDocument() // BUILD duration
+ expect(screen.getByText('2m : 0s')).toBeInTheDocument() // DEPLOY duration
+ })
+
+ it('applies correct classes based on status and isFilterActive', () => {
+ mockIsFilterActive.mockImplementation((type) => type === 'BUILD')
+ renderWithProviders()
+
+ const buildButton = screen.getByText('Build').closest('button')
+ const deployButton = screen.getByText('Deploy').closest('button')
+
+ expect(buildButton).toHaveClass('border-brand-500', 'bg-neutral-500')
+ expect(deployButton).not.toHaveClass('border-neutral-300', 'bg-neutral-500')
+ })
+
+ it('handles different states correctly', () => {
+ const props = {
+ ...defaultProps,
+ serviceStatus: {
+ ...defaultProps.serviceStatus,
+ state: StateEnum.BUILDING,
+ },
+ }
+ renderWithProviders()
+
+ const buildButton = screen.getByText('Build').closest('button')
+ const deployButton = screen.getByText('Deploy').closest('button')
+
+ expect(buildButton).toHaveClass('border-neutral-500', 'bg-neutral-650')
+ expect(deployButton).not.toHaveClass('border-brand-500', 'bg-neutral-500')
+ })
+})
diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.tsx
new file mode 100644
index 00000000000..02038b814e4
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/filters-stage-step/filters-stage-step.tsx
@@ -0,0 +1,144 @@
+import clsx from 'clsx'
+import { type ServiceStepMetric, type StateEnum, type Status } from 'qovery-typescript-axios'
+import { type ServiceType } from 'qovery-ws-typescript-axios'
+import { useEffect, useState } from 'react'
+import { match } from 'ts-pattern'
+import { StatusChip } from '@qovery/shared/ui'
+import { twMerge, upperCaseFirstLetter } from '@qovery/shared/util-js'
+import { type FilterType } from '../list-deployment-logs'
+
+type StepMetricType = { build: ServiceStepMetric[]; deploy: ServiceStepMetric[] }
+
+interface StageStepProps {
+ type: Extract
+ state: StateEnum
+ steps: ServiceStepMetric[]
+ toggleColumnFilter: (type: FilterType) => void
+ isFilterActive: (type: FilterType) => boolean
+}
+
+function StageStep({ type, state, steps, toggleColumnFilter, isFilterActive }: StageStepProps) {
+ const totalDurationSec = steps.reduce((acc, step) => acc + (step.duration_sec || 0), 0)
+
+ const buildStep = steps.find((s) => s.step_name === 'BUILD')
+ const deployStep = steps.find((s) => s.step_name === 'DEPLOYMENT')
+
+ const status = match({ type, state, buildStep, deployStep })
+ .with({ type: 'BUILD' }, () => {
+ if (state === 'BUILDING') return 'BUILDING'
+ return buildStep?.status
+ })
+ .with({ type: 'DEPLOY' }, () => {
+ if (state === 'BUILDING') return 'READY'
+ if (state === 'DEPLOYING') return 'DEPLOYING'
+ return deployStep?.status
+ })
+ .exhaustive()
+
+ const [isFirstLoad, setIsFirstLoad] = useState(true)
+ useEffect(() => {
+ if (isFirstLoad) {
+ if (status === 'ERROR') {
+ toggleColumnFilter(type)
+ }
+ setIsFirstLoad(false)
+ } else if (status === 'ERROR') {
+ // Only toggle if status is 'ERROR'
+ toggleColumnFilter(type)
+ }
+ // On the first load, if status is 'ERROR', the column filter is toggled
+ // For all subsequent renders, the column filter is toggled only if the status is 'ERROR'
+ }, [status, toggleColumnFilter, isFirstLoad])
+
+ const isBuildingOrDeploying =
+ (type === 'BUILD' && status === 'BUILDING') || (type === 'DEPLOY' && status === 'DEPLOYING')
+
+ const buttonClasses = clsx(
+ 'flex items-center gap-1.5 rounded-lg border border-neutral-500 bg-neutral-650 px-2.5 py-1 text-sm font-medium text-neutral-300 transition hover:border-neutral-300 hover:bg-neutral-500',
+ {
+ 'text-white hover:border-green-500': status === 'SUCCESS',
+ 'text-white hover:border-red-500': status === 'ERROR',
+ 'text-white hover:border-brand-500': isBuildingOrDeploying,
+ 'border-brand-500 bg-neutral-500': isBuildingOrDeploying && isFilterActive(type),
+ 'border-green-500': status === 'SUCCESS' && isFilterActive(type),
+ 'border-red-500 bg-neutral-500': status === 'ERROR' && isFilterActive(type),
+ }
+ )
+
+ return (
+
+ )
+}
+
+export interface FiltersStageStepProps {
+ serviceStatus: Status
+ toggleColumnFilter: (type: FilterType) => void
+ isFilterActive: (type: FilterType) => boolean
+ serviceType?: ServiceType
+}
+
+export function FiltersStageStep({
+ serviceStatus: { steps, state },
+ toggleColumnFilter,
+ isFilterActive,
+ serviceType,
+}: FiltersStageStepProps) {
+ if (!steps?.details) return
+
+ const categorizedSteps = steps.details.reduce(
+ (acc, step) => {
+ if (!step.step_name) return acc
+
+ match(step.step_name)
+ .with('BUILD', 'BUILD_QUEUEING', 'GIT_CLONE', 'REGISTRY_CREATE_REPOSITORY', () => acc.build.push(step))
+ .with('DEPLOYMENT', 'DEPLOYMENT_QUEUEING', 'ROUTER_DEPLOYMENT', 'MIRROR_IMAGE', () => acc.deploy.push(step))
+ .exhaustive()
+
+ return acc
+ },
+ { build: [], deploy: [] } as StepMetricType
+ )
+
+ return (
+
+ {serviceType !== 'CONTAINER' && (
+ <>
+
+
+ >
+ )}
+
+
+ )
+}
+
+export default FiltersStageStep
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
new file mode 100644
index 00000000000..764d91718a0
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.spec.tsx
@@ -0,0 +1,166 @@
+import { type Status } from 'qovery-typescript-axios'
+import { useDeploymentStatus } from '@qovery/domains/services/feature'
+import { useServiceType } from '@qovery/domains/services/feature'
+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 { ListDeploymentLogs } from './list-deployment-logs'
+
+window.HTMLElement.prototype.scroll = jest.fn()
+
+jest.mock('../hooks/use-deployment-logs/use-deployment-logs')
+jest.mock('@qovery/domains/services/feature')
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ organizationId: '0',
+ projectId: '1',
+ environmentId: '2',
+ serviceId: '3',
+ versionId: '4',
+ }),
+}))
+
+describe('ListDeploymentLogs', () => {
+ const mockEnvironment = environmentFactoryMock(1)[0]
+
+ const mockDeploymentHistoryEnvironment = [{ id: '4', created_at: '2023-01-01T00:00:00Z' }]
+
+ const mockServiceStatus: Status = {
+ id: '111',
+ state: 'DELETE_ERROR',
+ service_deployment_status: 'UP_TO_DATE',
+ last_deployment_date: '2024-09-18T07:03:29.819774Z',
+ is_part_last_deployment: true,
+ steps: {
+ total_duration_sec: 69,
+ total_computing_duration_sec: 64,
+ details: [
+ {
+ step_name: 'BUILD_QUEUEING',
+ status: 'SUCCESS',
+ duration_sec: 0,
+ },
+ {
+ step_name: 'REGISTRY_CREATE_REPOSITORY',
+ status: 'SUCCESS',
+ duration_sec: 10,
+ },
+ {
+ step_name: 'GIT_CLONE',
+ status: 'SUCCESS',
+ duration_sec: 1,
+ },
+ {
+ step_name: 'BUILD',
+ status: 'SUCCESS',
+ duration_sec: 24,
+ },
+ {
+ step_name: 'DEPLOYMENT_QUEUEING',
+ status: 'SUCCESS',
+ duration_sec: 0,
+ },
+ {
+ step_name: 'DEPLOYMENT',
+ status: 'ERROR',
+ duration_sec: 29,
+ },
+ ],
+ },
+ }
+
+ const mockLogs = [
+ {
+ id: '1',
+ timestamp: '2023-01-01T00:00:00Z',
+ message: { safe_message: 'Log 1' },
+ details: { stage: { step: 'BUILD' } },
+ },
+ {
+ id: '2',
+ timestamp: '2023-01-01T00:01:00Z',
+ message: { safe_message: 'Log 2' },
+ details: { stage: { step: 'DEPLOY' } },
+ },
+ ]
+
+ beforeEach(() => {
+ useDeploymentLogs.mockReturnValue({
+ data: mockLogs,
+ pauseLogs: false,
+ setPauseLogs: jest.fn(),
+ newMessagesAvailable: false,
+ setNewMessagesAvailable: jest.fn(),
+ showPreviousLogs: false,
+ setShowPreviousLogs: jest.fn(),
+ })
+
+ useDeploymentStatus.mockReturnValue({
+ data: { state: 'RUNNING' },
+ })
+
+ useServiceType.mockReturnValue({
+ data: { serviceType: 'APPLICATION' },
+ })
+ })
+
+ it('should render successfully', () => {
+ const { baseElement } = renderWithProviders(
+
+ )
+ expect(baseElement).toBeTruthy()
+ })
+
+ it('should display logs', () => {
+ renderWithProviders(
+
+ )
+
+ expect(screen.getByText('Log 1')).toBeInTheDocument()
+ expect(screen.getByText('Log 2')).toBeInTheDocument()
+ })
+
+ it('should filter logs by stage step', async () => {
+ const { userEvent } = renderWithProviders(
+
+ )
+
+ const buildButton = screen.getByRole('button', { name: /build/i })
+ await userEvent.click(buildButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Log 1')).toBeInTheDocument()
+ expect(screen.queryByText('Log 2')).not.toBeInTheDocument()
+ })
+ })
+
+ it('should show progress indicator when deployment is progressing', () => {
+ useDeploymentStatus.mockReturnValue({
+ data: { state: 'BUILDING' },
+ })
+
+ renderWithProviders(
+
+ )
+
+ expect(screen.getByText('Streaming service logs')).toBeInTheDocument()
+ })
+})
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
new file mode 100644
index 00000000000..b01dd2d62ec
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/list-deployment-logs.tsx
@@ -0,0 +1,249 @@
+import {
+ type ColumnFiltersState,
+ type FilterFn,
+ createColumnHelper,
+ getCoreRowModel,
+ getFilteredRowModel,
+ useReactTable,
+} from '@tanstack/react-table'
+import download from 'downloadjs'
+import {
+ type DeploymentHistoryEnvironment,
+ type Environment,
+ type EnvironmentLogs,
+ type Status,
+} from 'qovery-typescript-axios'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useParams } from 'react-router-dom'
+import { match } from 'ts-pattern'
+import { useDeploymentStatus, useServiceType } from '@qovery/domains/services/feature'
+import { Button, Icon, TablePrimitives } from '@qovery/shared/ui'
+import { DeploymentLogsPlaceholder } from '../deployment-logs-placeholder/deployment-logs-placeholder'
+import { useDeploymentLogs } from '../hooks/use-deployment-logs/use-deployment-logs'
+import { ProgressIndicator } from '../progress-indicator/progress-indicator'
+import { ShowNewLogsButton } from '../show-new-logs-button/show-new-logs-button'
+import { ShowPreviousLogsButton } from '../show-previous-logs-button/show-previous-logs-button'
+import { FiltersStageStep } from './filters-stage-step/filters-stage-step'
+import { RowDeployment } from './row-deployment/row-deployment'
+
+const { Table } = TablePrimitives
+
+const MemoizedRowDeployment = memo(RowDeployment)
+
+export type FilterType = 'ALL' | 'DEPLOY' | 'BUILD'
+
+export interface ListDeploymentLogsProps {
+ environment: Environment
+ deploymentHistoryEnvironment: DeploymentHistoryEnvironment[]
+ serviceStatus: Status
+}
+
+export function ListDeploymentLogs({
+ environment,
+ deploymentHistoryEnvironment,
+ serviceStatus,
+}: ListDeploymentLogsProps) {
+ const { organizationId, projectId, serviceId, versionId } = useParams()
+ const refScrollSection = useRef(null)
+
+ const { data: serviceType } = useServiceType({ environmentId: environment.id, serviceId })
+ const { data: deploymentStatus } = useDeploymentStatus({ environmentId: environment.id, serviceId })
+ const {
+ data: logs = [],
+ pauseLogs,
+ setPauseLogs,
+ newMessagesAvailable,
+ setNewMessagesAvailable,
+ showPreviousLogs,
+ setShowPreviousLogs,
+ } = useDeploymentLogs({
+ organizationId,
+ projectId,
+ environmentId: environment.id,
+ serviceId,
+ versionId,
+ })
+
+ // `useEffect` used to scroll to the bottom of the logs when new logs are added or when the pauseLogs state changes
+ useEffect(() => {
+ const section = refScrollSection.current
+ if (!section) return
+
+ !pauseLogs && section.scroll(0, section.scrollHeight)
+ }, [logs, pauseLogs])
+
+ const columnHelper = createColumnHelper()
+
+ const customFilter: FilterFn = (row, columnId, filterValue) => {
+ if (filterValue === 'ALL') return true
+
+ const rowValue = row.getValue(columnId)
+ if (typeof rowValue === 'string') {
+ return rowValue.toLowerCase().includes(filterValue.toLowerCase())
+ }
+ return false
+ }
+
+ const columns = useMemo(
+ () => [
+ columnHelper.accessor('timestamp', {
+ filterFn: customFilter,
+ }),
+ columnHelper.accessor('message', {
+ filterFn: customFilter,
+ }),
+ columnHelper.accessor('details.stage.step', {
+ id: 'details.stage.step',
+ filterFn: customFilter,
+ }),
+ ],
+ [columnHelper]
+ )
+
+ const defaultColumnsFilters = useMemo(
+ () => [
+ {
+ id: 'details.stage.step',
+ value: 'ALL',
+ },
+ ],
+ []
+ )
+
+ const [columnFilters, setColumnFilters] = useState(defaultColumnsFilters)
+
+ const table = useReactTable({
+ data: logs,
+ state: { columnFilters },
+ onColumnFiltersChange: setColumnFilters,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ enableFilters: true,
+ getFilteredRowModel: getFilteredRowModel(),
+ })
+
+ const toggleColumnFilter = useCallback(
+ (type: FilterType) => {
+ setColumnFilters((prevFilters) => {
+ const currentFilter = prevFilters.find((filter) => filter.id === 'details.stage.step')
+ if (currentFilter?.value === type) {
+ return defaultColumnsFilters
+ } else {
+ return [
+ {
+ id: 'details.stage.step',
+ value: type,
+ },
+ ]
+ }
+ })
+ setTimeout(() => {
+ const section = refScrollSection.current
+ section?.scroll(0, section?.scrollHeight)
+ }, 10)
+ },
+ [defaultColumnsFilters]
+ )
+
+ const isFilterActive = useMemo(
+ () => (type: FilterType) => columnFilters.some((f) => f.value === type),
+ [columnFilters]
+ )
+
+ const isLastVersion = deploymentHistoryEnvironment?.[0]?.id === versionId || !versionId
+ const isDeploymentProgressing = isLastVersion
+ ? match(deploymentStatus?.state)
+ .with(
+ 'BUILDING',
+ 'DEPLOYING',
+ 'CANCELING',
+ 'DELETING',
+ 'RESTARTING',
+ 'STOPPING',
+ 'QUEUED',
+ 'DELETE_QUEUED',
+ 'RESTART_QUEUED',
+ 'STOP_QUEUED',
+ 'DEPLOYMENT_QUEUED',
+ () => true
+ )
+ .otherwise(() => false)
+ : false
+
+ if (!logs || logs.length === 0 || !serviceStatus.is_part_last_deployment) {
+ return (
+
+ )
+ }
+ return (
+
+
+
+
+
+
+
{
+ 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 && (
+
+ )}
+
+
+
+
+ )
+}
+
+export default ListDeploymentLogs
diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.spec.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.spec.tsx
new file mode 100644
index 00000000000..514cd1ac17f
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.spec.tsx
@@ -0,0 +1,31 @@
+import { renderWithProviders, screen } from '@qovery/shared/util-tests'
+import { RowDeployment } from './row-deployment'
+
+describe('RowDeployment', () => {
+ const mockProps = {
+ index: 0,
+ original: {
+ timestamp: '2023-04-01T12:00:00Z',
+ message: {
+ safe_message: 'Test log message',
+ },
+ details: {
+ stage: {
+ step: 'Building',
+ },
+ },
+ },
+ }
+
+ it('should render successfully', () => {
+ const { baseElement } = renderWithProviders()
+ expect(baseElement).toBeTruthy()
+ })
+
+ it('renders basic row content', () => {
+ renderWithProviders()
+
+ expect(screen.getByText('1')).toBeInTheDocument()
+ expect(screen.getByText('Test log message')).toBeInTheDocument()
+ })
+})
diff --git a/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.tsx b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.tsx
new file mode 100644
index 00000000000..621d071a1a1
--- /dev/null
+++ b/libs/domains/service-logs/feature/src/lib/list-deployment-logs/row-deployment/row-deployment.tsx
@@ -0,0 +1,52 @@
+import { type Row } from '@tanstack/react-table'
+import clsx from 'clsx'
+import { type EnvironmentLogs } from 'qovery-typescript-axios'
+import { useContext } from 'react'
+import { LogsType } from '@qovery/shared/enums'
+import { Ansi, TablePrimitives } from '@qovery/shared/ui'
+import { dateFullFormat, dateUTCString } from '@qovery/shared/util-dates'
+import { UpdateTimeContext } from '../../update-time-context/update-time-context'
+
+const { Table } = TablePrimitives
+
+export interface RowDeploymentProps extends Row {}
+
+export function RowDeployment({ index, original }: RowDeploymentProps) {
+ const { utc } = useContext(UpdateTimeContext)
+
+ const step = original.details?.stage?.step
+ const type = original.type
+
+ const success = step === 'Deployed'
+ const error = type === LogsType.ERROR || step === 'DeployedError'
+
+ return (
+
+
+ {index + 1}
+
+
+
+ {dateFullFormat(original.timestamp, utc ? 'UTC' : undefined, 'dd MMM, HH:mm:ss.SS')}
+
+
+
+
+ {type === LogsType.ERROR ? (
+ {original.error?.user_log_message}
+ ) : (
+ {original.message?.safe_message}
+ )}
+
+
+
+ )
+}
+
+export default RowDeployment
diff --git a/libs/domains/service-logs/feature/src/lib/list-service-logs/list-service-logs.tsx b/libs/domains/service-logs/feature/src/lib/list-service-logs/list-service-logs.tsx
index 4a83b7697cd..61bbd591007 100644
--- a/libs/domains/service-logs/feature/src/lib/list-service-logs/list-service-logs.tsx
+++ b/libs/domains/service-logs/feature/src/lib/list-service-logs/list-service-logs.tsx
@@ -306,10 +306,7 @@ export function ListServiceLogs({ clusterId }: ListServiceLogsProps) {
setShowPreviousLogs={setShowPreviousLogs}
setPauseLogs={setPauseLogs}
/>
-
+
{table.getRowModel().rows.map((row) => {
if (row.original.type === 'INFRA') {
diff --git a/libs/domains/service-logs/feature/src/lib/progress-indicator/progress-indicator.tsx b/libs/domains/service-logs/feature/src/lib/progress-indicator/progress-indicator.tsx
index b563244a07e..c63908d2907 100644
--- a/libs/domains/service-logs/feature/src/lib/progress-indicator/progress-indicator.tsx
+++ b/libs/domains/service-logs/feature/src/lib/progress-indicator/progress-indicator.tsx
@@ -1,20 +1,26 @@
+import { twMerge } from '@qovery/shared/util-js'
+
export interface ProgressIndicatorProps {
message: string
pauseLogs: boolean
+ className?: string
}
-export function ProgressIndicator({ pauseLogs, message }: ProgressIndicatorProps) {
+export function ProgressIndicator({ pauseLogs, message, className }: ProgressIndicatorProps) {
return (
{pauseLogs ? 'Streaming paused' : message}
{!pauseLogs && (
<>
-
-
-
+
+
+
>
)}
diff --git a/libs/pages/logs/environment/src/lib/feature/service-stage-ids-context/service-stage-ids-context.spec.tsx b/libs/domains/service-logs/feature/src/lib/service-stage-ids-context/service-stage-ids-context.spec.tsx
similarity index 100%
rename from libs/pages/logs/environment/src/lib/feature/service-stage-ids-context/service-stage-ids-context.spec.tsx
rename to libs/domains/service-logs/feature/src/lib/service-stage-ids-context/service-stage-ids-context.spec.tsx
diff --git a/libs/pages/logs/environment/src/lib/feature/service-stage-ids-context/service-stage-ids-context.tsx b/libs/domains/service-logs/feature/src/lib/service-stage-ids-context/service-stage-ids-context.tsx
similarity index 100%
rename from libs/pages/logs/environment/src/lib/feature/service-stage-ids-context/service-stage-ids-context.tsx
rename to libs/domains/service-logs/feature/src/lib/service-stage-ids-context/service-stage-ids-context.tsx
diff --git a/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.spec.tsx b/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.spec.tsx
index 9f4f7135c80..da88139bc3e 100644
--- a/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.spec.tsx
+++ b/libs/pages/logs/environment/src/lib/feature/deployment-logs-feature/deployment-logs-feature.spec.tsx
@@ -1,11 +1,11 @@
-import { render } from '__tests__/utils/setup-jest'
import {
type DeploymentStageWithServicesStatuses,
ServiceDeploymentStatusEnum,
StateEnum,
} from 'qovery-typescript-axios'
import { Route, Routes } from 'react-router-dom'
-import { LogsType } from '@qovery/shared/enums'
+import { environmentFactoryMock } from '@qovery/shared/factories'
+import { renderWithProviders } from '@qovery/shared/util-tests'
import DeploymentLogsFeature, {
type DeploymentLogsFeatureProps,
getServiceStatusesById,
@@ -44,25 +44,12 @@ const services: DeploymentStageWithServicesStatuses[] = [
describe('DeploymentLogsFeature', () => {
const props: DeploymentLogsFeatureProps = {
- logs: [
- {
- type: LogsType.INFO,
- timestamp: new Date().toString(),
- details: {
- stage: {
- step: 'Deployed',
- },
- },
- message: {
- safe_message: 'Log 1',
- },
- },
- ],
+ environment: environmentFactoryMock(1)[0],
statusStages: services,
}
it('should render successfully', () => {
- const { baseElement } = render(
+ const { baseElement } = renderWithProviders(
Date.now(), [])
- const [showPreviousLogs, setShowPreviousLogs] = useState(false)
- const [newMessagesAvailable, setNewMessagesAvailable] = useState(false)
- const [logs, setLogs] = useState([])
- const [loadingStatusDeploymentLogs, setLoadingStatusDeploymentLogs] = useState('not loaded')
- const [messageChunks, setMessageChunks] = useState([])
- const [debounceTime, setDebounceTime] = useState(1000)
- const [pauseStatusLogs, setPauseStatusLogs] = useState(false)
+ const { organizationId = '', projectId = '', serviceId = '' } = useParams()
- useDocumentTitle(
- `Deployment logs ${loadingStatusDeploymentLogs === 'loaded' ? `- ${service?.name}` : '- Loading...'}`
- )
+ const { data: service, isFetched: isFetchedService } = useService({ environmentId: environment.id, serviceId })
+ const { data: deploymentHistoryEnvironment = [] } = useDeploymentHistory({ environmentId: environment.id })
- const chunkSize = 500
+ useDocumentTitle(`Deployment logs - ${service?.name ?? 'Loading...'}`)
- const messageHandler = useCallback(
- (_: QueryClient, message: EnvironmentLogs[]) => {
- setNewMessagesAvailable(true)
- setLoadingStatusDeploymentLogs('loaded')
- setMessageChunks((prevChunks) => {
- const lastChunk = prevChunks[prevChunks.length - 1] || []
- if (lastChunk.length < chunkSize) {
- return [...prevChunks.slice(0, -1), [...lastChunk, ...message]]
- } else {
- return [...prevChunks, [...message]]
- }
- })
- },
- [setLoadingStatusDeploymentLogs, setMessageChunks]
- )
+ const serviceStatus = getServiceStatusesById(statusStages, serviceId) as Status
- useReactQueryWsSubscription({
- url: QOVERY_WS + '/deployment/logs',
- urlSearchParams: {
- organization: organizationId,
- cluster: environment?.cluster_id,
- project: projectId,
- environment: environmentId,
- version: versionId,
- },
- enabled:
- Boolean(organizationId) && Boolean(environment?.cluster_id) && Boolean(projectId) && Boolean(environmentId),
- onMessage: messageHandler,
- })
-
- useEffect(() => {
- if (messageChunks.length === 0 || pauseStatusLogs) return
-
- const timerId = setTimeout(() => {
- if (!pauseStatusLogs) {
- setMessageChunks((prevChunks) => prevChunks.slice(1))
- setLogs((prevLogs) => {
- const combinedLogs = [...prevLogs, ...messageChunks[0]]
- return [...new Map(combinedLogs.map((item) => [item['timestamp'], item])).values()]
- })
-
- if (logs.length > 1000) {
- setDebounceTime(100)
- }
- }
- }, debounceTime)
-
- return () => {
- clearTimeout(timerId)
- }
- }, [messageChunks, pauseStatusLogs])
-
- const serviceStatus = getServiceStatusesById(statusStages, serviceId)
- const hideDeploymentLogsBoolean = !(serviceStatus as Status)?.is_part_last_deployment
-
- // Filter deployment logs by serviceId and stageId
- // Display entries when the name is "delete" or stageId is empty or equal with current stageId
- // Filter by the same transmitter ID and "Environment" or "TaskManager" type
- const logsByServiceId = useMemo(
- () =>
- logs
- .filter((currentData: EnvironmentLogs) => {
- const { stage, transmitter } = currentData.details
- const isDeleteStage = stage?.name === 'delete'
- const isEmptyOrEqualStageId = !stage?.id || stage?.id === stageId
- const isMatchingTransmitter =
- transmitter?.type === 'Environment' || transmitter?.type === 'TaskManager' || transmitter?.id === serviceId
-
- // Include the entry if any of the following conditions are true:
- // 1. The stage name is "delete".
- // 2. stageId is empty or equal with current stageId.
- // 3. The transmitter matches serviceId and has a type of "Environment" or "TaskManager".
- return (isDeleteStage || isEmptyOrEqualStageId) && isMatchingTransmitter
- })
- .filter((log, index, array) => (showPreviousLogs || index >= array.length - 500 ? true : +log.timestamp > now)),
- [logs, stageId, serviceId, now, showPreviousLogs]
- )
-
- const errors = logsByServiceId
- .map((log: EnvironmentLogs, index: number) => ({
- index: index,
- errors: log.error,
- }))
- .filter((log) => log.errors)
-
- const isLastVersion = (deploymentHistory && deploymentHistory[0]?.id === versionId) || !versionId
- const isDeploymentProgressing: boolean = isLastVersion
- ? match(deploymentStatus?.state)
- .with(
- 'BUILDING',
- 'DEPLOYING',
- 'CANCELING',
- 'DELETING',
- 'RESTARTING',
- 'STOPPING',
- 'QUEUED',
- 'DELETE_QUEUED',
- 'RESTART_QUEUED',
- 'STOP_QUEUED',
- 'DEPLOYMENT_QUEUED',
- () => true
- )
- .otherwise(() => false)
- : false
+ if (!serviceStatus && isFetchedService)
+ return (
+
+ )
return (
- serviceStatus && (
-
+
+
+ db.mode === 'CONTAINER')
+ .otherwise(() => true)}
+ />
+
+
- )
+
)
}
diff --git a/libs/pages/logs/environment/src/lib/feature/pod-logs-feature/pod-logs-feature.tsx b/libs/pages/logs/environment/src/lib/feature/pod-logs-feature/pod-logs-feature.tsx
index dc97875e040..38cbdd23db0 100644
--- a/libs/pages/logs/environment/src/lib/feature/pod-logs-feature/pod-logs-feature.tsx
+++ b/libs/pages/logs/environment/src/lib/feature/pod-logs-feature/pod-logs-feature.tsx
@@ -10,7 +10,7 @@ export interface PodLogsFeatureProps {
clusterId: string
}
-function LinkLogs({ title, url, statusChip = true }: { title: string; url: string; statusChip?: boolean }) {
+export function LinkLogs({ title, url, statusChip = true }: { title: string; url: string; statusChip?: boolean }) {
const { environmentId = '', serviceId = '' } = useParams()
const location = useLocation()
diff --git a/libs/pages/logs/environment/src/lib/page-environment-logs.tsx b/libs/pages/logs/environment/src/lib/page-environment-logs.tsx
index 4311a83be3c..b34e36cb17c 100644
--- a/libs/pages/logs/environment/src/lib/page-environment-logs.tsx
+++ b/libs/pages/logs/environment/src/lib/page-environment-logs.tsx
@@ -3,6 +3,7 @@ import { type DeploymentStageWithServicesStatuses, type EnvironmentStatus } from
import { useCallback, useState } from 'react'
import { Route, Routes, matchPath, useLocation, useParams } from 'react-router-dom'
import { useEnvironment } from '@qovery/domains/environments/feature'
+import { ServiceStageIdsProvider } from '@qovery/domains/service-logs/feature'
import { useServices } from '@qovery/domains/services/feature'
import {
DEPLOYMENT_LOGS_URL,
@@ -16,7 +17,6 @@ import { QOVERY_WS } from '@qovery/shared/util-node-env'
import { useReactQueryWsSubscription } from '@qovery/state/util-queries'
import DeploymentLogsFeature from './feature/deployment-logs-feature/deployment-logs-feature'
import PodLogsFeature from './feature/pod-logs-feature/pod-logs-feature'
-import { ServiceStageIdsProvider } from './feature/service-stage-ids-context/service-stage-ids-context'
import Sidebar from './ui/sidebar/sidebar'
export function PageEnvironmentLogs() {
diff --git a/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.spec.tsx b/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.spec.tsx
deleted file mode 100644
index 3b1b5e0ffec..00000000000
--- a/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.spec.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { ClusterLogsStepEnum, ServiceDeploymentStatusEnum, StateEnum } from 'qovery-typescript-axios'
-import { applicationFactoryMock, deploymentLogFactoryMock } from '@qovery/shared/factories'
-import { dateFullFormat } from '@qovery/shared/util-dates'
-import { trimId } from '@qovery/shared/util-js'
-import { renderWithProviders, screen } from '@qovery/shared/util-tests'
-import DeploymentLogs, { type DeploymentLogsProps } from './deployment-logs'
-
-const mockLogs = deploymentLogFactoryMock(1)
-const mockApplication = applicationFactoryMock(1)[0]
-
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useParams: () => ({ organizationId: '1', projectId: '2', environmentId: '3', serviceId: '4', versionId: '5' }),
-}))
-
-describe('DeploymentLogs', () => {
- const props: DeploymentLogsProps = {
- loadingStatus: 'loaded',
- logs: mockLogs,
- errors: [
- {
- index: 0,
- error: {},
- timeAgo: '20',
- step: ClusterLogsStepEnum.DELETE_ERROR,
- },
- ],
- hideDeploymentLogs: false,
- serviceStatus: {
- id: '',
- state: StateEnum.READY,
- service_deployment_status: ServiceDeploymentStatusEnum.NEVER_DEPLOYED,
- },
- setPauseStatusLogs: jest.fn(),
- pauseStatusLogs: false,
- service: mockApplication,
- }
-
- beforeEach(() => {
- window.HTMLElement.prototype.scroll = jest.fn()
- })
-
- it('should render successfully', () => {
- const { baseElement } = renderWithProviders()
- expect(baseElement).toBeTruthy()
-
- const message = mockLogs[0].message?.safe_message || ''
- screen.getByText(message)
- })
-
- it('should render a placeholder with a deployment history', () => {
- props.hideDeploymentLogs = true
- props.loadingStatus = 'loaded'
- props.serviceStatus = {
- id: '',
- state: StateEnum.DEPLOYED,
- service_deployment_status: ServiceDeploymentStatusEnum.OUT_OF_DATE,
- }
-
- const name = mockApplication.name
-
- props.dataDeploymentHistory = [
- {
- id: 'deployment-id',
- created_at: new Date().toString(),
- applications: [
- {
- id: '4',
- created_at: new Date().toString(),
- name: 'my-app',
- },
- ],
- },
- ]
-
- renderWithProviders()
-
- screen.getByText(
- (_, element) => element?.textContent === `${name} service was not deployed within this deployment execution.`
- )
- screen.getByText(trimId(props.dataDeploymentHistory[0].id))
- screen.getByText(dateFullFormat(props.dataDeploymentHistory[0].created_at))
- })
-})
diff --git a/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.tsx b/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.tsx
deleted file mode 100644
index d616b83e015..00000000000
--- a/libs/pages/logs/environment/src/lib/ui/deployment-logs/deployment-logs.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import {
- type DeploymentHistoryEnvironment,
- type EnvironmentLogs,
- type ServiceDeploymentStatusEnum,
- type Status,
-} from 'qovery-typescript-axios'
-import { type Dispatch, type SetStateAction, useMemo } from 'react'
-import { Link, useParams } from 'react-router-dom'
-import { type AnyService } from '@qovery/domains/services/data-access'
-import { type ErrorLogsProps, LayoutLogs } from '@qovery/shared/console-shared'
-import { type DeploymentService, type LoadingStatus } from '@qovery/shared/interfaces'
-import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL } from '@qovery/shared/routes'
-import { Icon, StatusChip } from '@qovery/shared/ui'
-import { dateFullFormat } from '@qovery/shared/util-dates'
-import { mergeDeploymentServices, trimId } from '@qovery/shared/util-js'
-import RowDeployment from '../row-deployment/row-deployment'
-
-export interface DeploymentLogsProps {
- loadingStatus: LoadingStatus
- logs: EnvironmentLogs[]
- pauseStatusLogs: boolean
- setPauseStatusLogs: (pause: boolean) => void
- errors: ErrorLogsProps[]
- hideDeploymentLogs?: boolean
- serviceDeploymentStatus?: ServiceDeploymentStatusEnum
- dataDeploymentHistory?: DeploymentHistoryEnvironment[]
- service?: AnyService
- serviceStatus: Status
- isDeploymentProgressing?: boolean
- setShowPreviousLogs?: (showPreviousLogs: boolean) => void
- showPreviousLogs?: boolean
- newMessagesAvailable: boolean
- setNewMessagesAvailable: Dispatch>
-}
-
-export function DeploymentLogs({
- logs,
- errors,
- hideDeploymentLogs,
- pauseStatusLogs,
- setPauseStatusLogs,
- loadingStatus,
- dataDeploymentHistory,
- service,
- serviceStatus: { service_deployment_status: serviceDeploymentStatus, state: serviceState },
- isDeploymentProgressing,
- setShowPreviousLogs,
- showPreviousLogs,
- newMessagesAvailable,
- setNewMessagesAvailable,
-}: DeploymentLogsProps) {
- const { organizationId = '', projectId = '', environmentId = '', serviceId = '', versionId = '' } = useParams()
-
- const memoRow = useMemo(
- () =>
- logs
- // Hide Environment deployment error if selected service is the one in error
- .filter(({ details: { transmitter, stage } }) =>
- transmitter?.type === 'Environment' && stage?.step === 'DeployedError'
- ? serviceState !== 'DEPLOYMENT_ERROR'
- : true
- )
- .map((log: EnvironmentLogs, index: number) => ),
- [logs, serviceDeploymentStatus]
- )
-
- const deploymentsByServiceId = mergeDeploymentServices(dataDeploymentHistory).filter(
- (deploymentHistory) => deploymentHistory.id === serviceId
- )
-
- const placeholderDeploymentHistory = deploymentsByServiceId.length !== 0 && (
-
-
-
- {service?.name} service was not deployed within this deployment
- execution.
-
-
- Below the list of executions where this service was deployed.
-
-
-
-
- Last deployment logs
-
-
- {deploymentsByServiceId?.map((deploymentHistory: DeploymentService) => (
-
-
-
-
- {trimId(deploymentHistory.execution_id || '')}
-
- {dateFullFormat(deploymentHistory.created_at)}
-
-
- ))}
-
-
-
- Only the last 20 deployments of the environment over the last 30 days are available.
-
-
-
-
- )
-
- return (
-
- {logs.length >= 500 && showPreviousLogs === false && (
-
- )}
- {memoRow}
-
- )
-}
-
-export default DeploymentLogs
diff --git a/libs/pages/logs/environment/src/lib/ui/row-deployment/__snapshots__/row-deployment.spec.tsx.snap b/libs/pages/logs/environment/src/lib/ui/row-deployment/__snapshots__/row-deployment.spec.tsx.snap
deleted file mode 100644
index 06d667eb6a6..00000000000
--- a/libs/pages/logs/environment/src/lib/ui/row-deployment/__snapshots__/row-deployment.spec.tsx.snap
+++ /dev/null
@@ -1,159 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`RowDeployment should have cell error message 1`] = `
-
-
-
-
-
-
-
-
- error message
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`RowDeployment should have cell message with ANSI colors and links 1`] = `
-
-`;
-
-exports[`RowDeployment should have cell success message 1`] = `
-
-
-
-
-
-
-
-
- message
-
-
-
-
-
-
-
-
-
-`;
diff --git a/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.spec.tsx b/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.spec.tsx
deleted file mode 100644
index 4bdd685f6bb..00000000000
--- a/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.spec.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { LogsType } from '@qovery/shared/enums'
-import { deploymentLogFactoryMock } from '@qovery/shared/factories'
-import { renderWithProviders, screen } from '@qovery/shared/util-tests'
-import RowDeployment, { type RowDeploymentProps } from './row-deployment'
-
-jest.mock('date-fns-tz', () => ({
- format: jest.fn(() => '20 Sept, 19:44:44:44'),
- utcToZonedTime: jest.fn(),
-}))
-
-describe('RowDeployment', () => {
- const props: RowDeploymentProps = {
- data: deploymentLogFactoryMock(1)[0],
- index: 1,
- }
-
- it('should render successfully', () => {
- const { baseElement } = renderWithProviders()
- expect(baseElement).toBeTruthy()
- })
-
- it('should have success index color', () => {
- props.data = {
- type: LogsType.INFO,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'Deployed',
- },
- },
- }
-
- renderWithProviders()
-
- const index = screen.getByTestId('index')
-
- expect(index).toHaveClass('text-green-500 bg-neutral-550 group-hover:bg-neutral-650')
- })
-
- it('should have error index color', () => {
- props.data = {
- type: LogsType.ERROR,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'DeployedError',
- },
- },
- }
-
- renderWithProviders()
-
- const index = screen.getByTestId('index')
-
- expect(index).toHaveClass('text-red-500 bg-neutral-550 group-hover:bg-neutral-650')
- })
-
- it('should have error cell date color', () => {
- props.data = {
- type: LogsType.ERROR,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'DeployedError',
- },
- },
- }
-
- renderWithProviders()
-
- const cellDate = screen.getByTestId('cell-date')
-
- expect(cellDate).toHaveClass('py-1 pl-2 pr-3 font-code shrink-0 w-[158px] text-red-500')
- })
-
- it('should have success cell date color', () => {
- props.data = {
- type: LogsType.INFO,
- timestamp: new Date().toString(),
- details: {
- stage: {
- step: 'Deployed',
- },
- },
- }
-
- renderWithProviders()
-
- const cellDate = screen.getByTestId('cell-date')
-
- expect(cellDate).toHaveClass('py-1 pl-2 pr-3 font-code shrink-0 w-[158px] text-green-500')
- })
-
- it('should have cell success message', () => {
- props.data = {
- type: LogsType.INFO,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'Deployed',
- },
- transmitter: {
- name: 'message',
- },
- },
- message: {
- safe_message: 'message',
- },
- }
-
- const { container } = renderWithProviders()
- expect(container).toMatchSnapshot()
- })
-
- it('should have cell error message', () => {
- props.data = {
- type: LogsType.ERROR,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'DeployedError',
- },
- },
- error: {
- user_log_message: 'error message',
- },
- }
-
- const { container } = renderWithProviders()
- expect(container).toMatchSnapshot()
- })
-
- it('should have cell message with ANSI colors and links', () => {
- props.data = {
- type: LogsType.INFO,
- timestamp: '2024-02-15T13:51:01.685342358Z',
- details: {
- stage: {
- step: 'Deployed',
- },
- transmitter: {
- name: 'message',
- },
- },
- message: {
- safe_message: '\x1b[F\x1b[31;1mmy message https://qovery.com\x1b[m\x1b[E',
- },
- }
-
- const { container } = renderWithProviders()
- expect(container).toMatchSnapshot()
- })
-})
diff --git a/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.tsx b/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.tsx
deleted file mode 100644
index c6073e33460..00000000000
--- a/libs/pages/logs/environment/src/lib/ui/row-deployment/row-deployment.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { type EnvironmentLogs } from 'qovery-typescript-axios'
-import { useContext } from 'react'
-import { UpdateTimeContext } from '@qovery/domains/service-logs/feature'
-import { LogsType } from '@qovery/shared/enums'
-import { Ansi, CopyToClipboardButtonIcon } from '@qovery/shared/ui'
-import { dateFullFormat, dateUTCString } from '@qovery/shared/util-dates'
-
-export interface RowDeploymentProps {
- data: EnvironmentLogs
- index: number
-}
-
-export function RowDeployment(props: RowDeploymentProps) {
- const { index, data } = props
-
- const { utc } = useContext(UpdateTimeContext)
-
- const type = (data as EnvironmentLogs).type
- const step = (data as EnvironmentLogs).details?.stage?.step
-
- const success = step === 'Deployed'
- const error = type === LogsType.ERROR || step === 'DeployedError'
-
- const indexClassName = `${
- error
- ? 'text-red-500 bg-neutral-550 group-hover:bg-neutral-650'
- : success
- ? 'text-green-500 bg-neutral-550 group-hover:bg-neutral-650'
- : 'bg-neutral-700 text-neutral-400 group-hover:bg-neutral-550'
- }`
-
- const colorsCellClassName = (date?: boolean) =>
- `${error ? 'text-red-500' : success ? 'text-green-500' : `${date ? 'text-neutral-100' : 'text-neutral-350'}`}`
-
- return (
-
-
-
- {dateFullFormat(data.timestamp, utc ? 'UTC' : undefined, 'dd MMM, HH:mm:ss.SS')}
-
-
-
- {type === LogsType.ERROR ? (
- {data.error?.user_log_message}
- ) : (
- {data.message?.safe_message}
- )}
-
-
-
-
- )
-}
-
-export default RowDeployment
diff --git a/libs/pages/logs/environment/src/lib/ui/sidebar-pipeline-item/sidebar-pipeline-item.tsx b/libs/pages/logs/environment/src/lib/ui/sidebar-pipeline-item/sidebar-pipeline-item.tsx
index 35031ef7733..8524cd245d3 100644
--- a/libs/pages/logs/environment/src/lib/ui/sidebar-pipeline-item/sidebar-pipeline-item.tsx
+++ b/libs/pages/logs/environment/src/lib/ui/sidebar-pipeline-item/sidebar-pipeline-item.tsx
@@ -1,11 +1,11 @@
import { type DeploymentStageWithServicesStatuses, type Status } from 'qovery-typescript-axios'
import { useContext, useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
+import { ServiceStageIdsContext } from '@qovery/domains/service-logs/feature'
import { type AnyService } from '@qovery/domains/services/data-access'
import { ServiceAvatar } from '@qovery/domains/services/feature'
import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL } from '@qovery/shared/routes'
import { BadgeDeploymentOrder, Icon, StatusChip } from '@qovery/shared/ui'
-import { ServiceStageIdsContext } from '../../feature/service-stage-ids-context/service-stage-ids-context'
export function mergeServices(
applications?: Status[],
diff --git a/libs/shared/console-shared/src/lib/layout-logs/buttons-actions-logs/buttons-actions-logs.tsx b/libs/shared/console-shared/src/lib/layout-logs/buttons-actions-logs/buttons-actions-logs.tsx
index 027556ad109..13af6465c5b 100644
--- a/libs/shared/console-shared/src/lib/layout-logs/buttons-actions-logs/buttons-actions-logs.tsx
+++ b/libs/shared/console-shared/src/lib/layout-logs/buttons-actions-logs/buttons-actions-logs.tsx
@@ -6,11 +6,10 @@ import { type LayoutLogsDataProps } from '../layout-logs'
export interface ButtonsActionsLogsProps {
refScrollSection: RefObject
data: LayoutLogsDataProps
- pauseLogs?: boolean
}
export function ButtonsActionsLogs(props: ButtonsActionsLogsProps) {
- const { refScrollSection, data, pauseLogs } = props
+ const { refScrollSection, data } = props
const downloadJSON = () => {
download(JSON.stringify(data?.items), `data-${Date.now()}.json`, 'text/json;charset=utf-8')
@@ -29,7 +28,7 @@ export function ButtonsActionsLogs(props: ButtonsActionsLogsProps) {
useEffect(() => {
// auto scroll when we add data
- !pauseLogs && forcedScroll && forcedScroll(true)
+ forcedScroll && forcedScroll(true)
}, [data])
return (
diff --git a/libs/shared/console-shared/src/lib/layout-logs/layout-logs.spec.tsx b/libs/shared/console-shared/src/lib/layout-logs/layout-logs.spec.tsx
index 054c39af7ea..210cb453544 100644
--- a/libs/shared/console-shared/src/lib/layout-logs/layout-logs.spec.tsx
+++ b/libs/shared/console-shared/src/lib/layout-logs/layout-logs.spec.tsx
@@ -5,7 +5,7 @@ import { LayoutLogs, type LayoutLogsProps } from './layout-logs'
describe('LayoutLogs', () => {
const props: LayoutLogsProps = {
- type: 'deployment',
+ type: 'infra',
data: {
loadingStatus: 'loaded',
items: clusterLogFactoryMock(2, true),
@@ -88,55 +88,4 @@ describe('LayoutLogs', () => {
expect(tabsLogs).toBeInTheDocument()
})
-
- it('should have a navigation', () => {
- props.data = {
- loadingStatus: 'loaded',
- items: [
- {
- created_at: 1667834316521,
- message: 'message',
- pod_name: 'app-z9d11ee4f-7d754477b6-k9sl7',
- version: '53deb16f853aef759b8be84fbeec96e9727',
- },
- ],
- }
- props.withLogsNavigation = true
-
- const { getByText } = renderWithProviders()
-
- getByText('Deployment logs')
- getByText('Service logs')
- })
-
- it('should have debug checkbox when debug is true', () => {
- props.data = {
- loadingStatus: 'loaded',
- items: [
- {
- created_at: 1667834316521,
- message: 'message',
- pod_name: 'app-z9d11ee4f-7d754477b6-k9sl7',
- version: '53deb16f853aef759b8be84fbeec96e9727',
- },
- {
- created_at: 1667834316521,
- message: 'message',
- pod_name: ' NGINX',
- version: '53deb16f853aef759b8be84fbeec96e9722',
- },
- ],
- }
- props.enabledNginx = true
- props.setEnabledNginx = jest.fn()
-
- renderWithProviders()
-
- screen.getByTestId('checkbox-debug')
- })
-
- it('should have progressing bar', () => {
- renderWithProviders()
- screen.getByRole('progressbar')
- })
})
diff --git a/libs/shared/console-shared/src/lib/layout-logs/layout-logs.tsx b/libs/shared/console-shared/src/lib/layout-logs/layout-logs.tsx
index 5870b22928e..a75c188ee5f 100644
--- a/libs/shared/console-shared/src/lib/layout-logs/layout-logs.tsx
+++ b/libs/shared/console-shared/src/lib/layout-logs/layout-logs.tsx
@@ -2,20 +2,13 @@ import {
type ClusterLogs,
type ClusterLogsError,
type ClusterLogsStepEnum,
- DatabaseModeEnum,
type EnvironmentLogs,
type EnvironmentLogsError,
- type ServiceDeploymentStatusEnum,
} from 'qovery-typescript-axios'
import { type ServiceLogResponseDto } from 'qovery-ws-typescript-axios'
-import { type Dispatch, type PropsWithChildren, type ReactNode, type SetStateAction, useRef, useState } from 'react'
-import { Link, useLocation, useParams } from 'react-router-dom'
-import { type AnyService, type Database } from '@qovery/domains/services/data-access'
-import { ServiceStateChip } from '@qovery/domains/services/feature'
+import { type PropsWithChildren, type ReactNode, useRef } from 'react'
import { type LoadingStatus } from '@qovery/shared/interfaces'
-// eslint-disable-next-line @nx/enforce-module-boundaries
-import { DEPLOYMENT_LOGS_URL, ENVIRONMENT_LOGS_URL, SERVICE_LOGS_URL } from '@qovery/shared/routes'
-import { Button, Checkbox, Icon, Tooltip } from '@qovery/shared/ui'
+import { Icon } from '@qovery/shared/ui'
import { scrollParentToChild } from '@qovery/shared/util-js'
import ButtonsActionsLogs from './buttons-actions-logs/buttons-actions-logs'
import { PlaceholderLogs } from './placeholder-logs/placeholder-logs'
@@ -27,29 +20,16 @@ export interface LayoutLogsDataProps {
items?: ClusterLogs[] | EnvironmentLogs[] | ServiceLogResponseDto[]
}
-export type logsType = 'infra' | 'live' | 'deployment'
+export type logsType = 'infra'
export interface LayoutLogsProps {
type: logsType
- service?: AnyService
data?: LayoutLogsDataProps
errors?: ErrorLogsProps[]
tabInformation?: ReactNode
withLogsNavigation?: boolean
- pauseLogs?: boolean
- setPauseLogs?: (pause: boolean) => void
lineNumbers?: boolean
- enabledNginx?: boolean
- setEnabledNginx?: (debugMode: boolean) => void
clusterBanner?: boolean
- countNginx?: number
- customPlaceholder?: string | ReactNode
- serviceDeploymentStatus?: ServiceDeploymentStatusEnum
- isProgressing?: boolean
- progressingMsg?: string
- newMessagesAvailable?: boolean
- setNewMessagesAvailable?: Dispatch>
- resetFilterPodName?: () => void
}
export interface ErrorLogsProps {
@@ -66,27 +46,11 @@ export function LayoutLogs({
children,
errors,
withLogsNavigation,
- pauseLogs,
- setPauseLogs,
lineNumbers,
- enabledNginx,
- setEnabledNginx,
clusterBanner,
- countNginx,
- customPlaceholder,
- service,
- serviceDeploymentStatus,
- isProgressing,
- progressingMsg,
- newMessagesAvailable,
- setNewMessagesAvailable,
- resetFilterPodName,
}: PropsWithChildren) {
- const location = useLocation()
const refScrollSection = useRef(null)
- const { organizationId = '', projectId = '', environmentId = '', serviceId = '' } = useParams()
-
const scrollToError = () => {
const section = refScrollSection.current
if (!section) return
@@ -95,65 +59,14 @@ export function LayoutLogs({
if (row) scrollParentToChild(section, row, 100)
}
- const LinkNavigation = (
- name: string,
- link: string,
- environmentId?: string,
- serviceId?: string,
- displayStatusChip = true
- ) => {
- const isActive = location.pathname.includes(link)
- return (
-
- {displayStatusChip && (
-
- )}
-
- {name}
-
- )
- }
-
return (
- {withLogsNavigation && (
-
- {LinkNavigation(
- 'Deployment logs',
- ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + DEPLOYMENT_LOGS_URL(serviceId),
- undefined,
- undefined,
- false
- )}
- {LinkNavigation(
- 'Service logs',
- ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + SERVICE_LOGS_URL(serviceId),
- environmentId,
- serviceId,
- (service as Database)?.mode !== DatabaseModeEnum.MANAGED
- )}
-
- )}
{!data || data?.items?.length === 0 || data?.hideLogs ? (
-
+
) : (
<>
)}
- {setEnabledNginx && (
-
-
-
- {enabledNginx && countNginx !== undefined ?
({countNginx}) : ''}
-
-
-
-
-
-
- )}
-
+
{
- if (
- !pauseLogs &&
- setPauseLogs &&
- refScrollSection.current &&
- refScrollSection.current.clientHeight !== refScrollSection.current.scrollHeight &&
- event.deltaY < 0
- ) {
- setPauseLogs(true)
- setNewMessagesAvailable?.(false)
- }
- }}
className={`mb-5 h-[calc(100%-20px)] w-full overflow-y-auto bg-neutral-700 pb-16 ${
lineNumbers
? 'before:absolute before:left-1 before:top-9 before:-z-[1] before:h-full before:w-10 before:bg-neutral-700'
: ''
} ${withLogsNavigation ? 'mt-[88px]' : 'mt-11'}`}
>
-
- {children}
- {isProgressing && (
-
- {pauseLogs ? 'Streaming paused' : progressingMsg}
- {!pauseLogs && (
- <>
-
-
-
- >
- )}
-
- )}
-
+
{children}
{tabInformation && (
)}
>
)}
- {pauseLogs && newMessagesAvailable && (
-
- )}
)
}
diff --git a/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.spec.tsx b/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.spec.tsx
index dda2b5e5737..69029217b1e 100644
--- a/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.spec.tsx
+++ b/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.spec.tsx
@@ -1,15 +1,10 @@
-import { ServiceDeploymentStatusEnum } from 'qovery-typescript-axios'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import PlaceholderLogs, { type PlaceholderLogsProps } from './placeholder-logs'
describe('PlaceholderLogs', () => {
const props: PlaceholderLogsProps = {
- type: 'deployment',
- serviceName: 'my-app',
+ type: 'infra',
loadingStatus: 'not loaded',
- customPlaceholder: custom placeholder
,
- itemsLength: 0,
- hideLogs: true,
}
it('should render successfully', () => {
@@ -17,28 +12,6 @@ describe('PlaceholderLogs', () => {
expect(baseElement).toBeTruthy()
})
- it('Deployment - display loader screen', () => {
- renderWithProviders()
- screen.getByTestId('spinner')
- })
-
- it('Deployment - should render a custom placeholder', () => {
- props.loadingStatus = 'loaded'
- renderWithProviders()
-
- screen.getByText('custom placeholder')
- })
-
- it('Deployment - should render a no log placeholder', () => {
- props.loadingStatus = 'loaded'
- props.customPlaceholder = undefined
- props.serviceDeploymentStatus = ServiceDeploymentStatusEnum.NEVER_DEPLOYED
- renderWithProviders()
-
- screen.getByText(props.serviceName as string)
- screen.getByText(/No logs on this execution for/i)
- })
-
it('Infra - should render a placeholder with spinner if logs not loaded', () => {
props.type = 'infra'
props.loadingStatus = 'not loaded'
diff --git a/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.tsx b/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.tsx
index 7b9b752e41d..9794d8c8352 100644
--- a/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.tsx
+++ b/libs/shared/console-shared/src/lib/layout-logs/placeholder-logs/placeholder-logs.tsx
@@ -1,84 +1,15 @@
-import { ServiceDeploymentStatusEnum } from 'qovery-typescript-axios'
-import { type ReactNode } from 'react'
-import { P, match } from 'ts-pattern'
import { type LoadingStatus } from '@qovery/shared/interfaces'
import { type logsType } from '../layout-logs'
import { LoaderPlaceholder } from './loader-placeholder/loader-placeholder'
export interface PlaceholderLogsProps {
type: logsType
- serviceName?: string
loadingStatus?: LoadingStatus
- customPlaceholder?: string | ReactNode
- serviceDeploymentStatus?: ServiceDeploymentStatusEnum
- itemsLength?: number
- hideLogs?: boolean
}
-export function PlaceholderLogs({
- type,
- serviceName,
- loadingStatus,
- customPlaceholder,
- serviceDeploymentStatus,
- itemsLength,
- hideLogs,
-}: PlaceholderLogsProps) {
- const deploymentPlaceholder = () => {
- const outOfDateOrUpToDate =
- serviceDeploymentStatus === ServiceDeploymentStatusEnum.NEVER_DEPLOYED ||
- serviceDeploymentStatus === ServiceDeploymentStatusEnum.UP_TO_DATE
-
- const displaySpinner = match({ loadingStatus, itemsLength, hideLogs, serviceDeploymentStatus })
- .with(
- {
- loadingStatus: P.not('loaded'),
- itemsLength: 0,
- hideLogs: false,
- serviceDeploymentStatus: P.nullish,
- },
- () => true
- )
- .otherwise(() => false)
-
- const displayPlaceholders = loadingStatus === 'loaded' && hideLogs
-
- if (displaySpinner) {
- // Display loader spinner
- return
- } else {
- if (displayPlaceholders) {
- return (
-
- {!customPlaceholder && outOfDateOrUpToDate ? (
- <>
- {/* Display message about no logs available */}
-
- No logs on this execution for {serviceName}.
-
- {serviceDeploymentStatus !== ServiceDeploymentStatusEnum.NEVER_DEPLOYED && (
-
- This service was deployed more than 30 days ago and thus no deployment logs are available.
-
- )}
- >
- ) : (
- // Display custom placeholder with list of history deployments
- customPlaceholder
- )}
-
- )
- }
-
- // Return the default loader spinner
- return
- }
- }
-
+export function PlaceholderLogs({ type, loadingStatus }: PlaceholderLogsProps) {
return (
- {type === 'deployment' && deploymentPlaceholder()}
-
{type === 'infra' && (
{!loadingStatus || loadingStatus === 'not loaded' ? (
diff --git a/libs/shared/ui/src/lib/components/status-chip/__snapshots__/status-chip.spec.tsx.snap b/libs/shared/ui/src/lib/components/status-chip/__snapshots__/status-chip.spec.tsx.snap
index 6cc898e3c54..b73a313682b 100644
--- a/libs/shared/ui/src/lib/components/status-chip/__snapshots__/status-chip.spec.tsx.snap
+++ b/libs/shared/ui/src/lib/components/status-chip/__snapshots__/status-chip.spec.tsx.snap
@@ -44,7 +44,7 @@ exports[`StatusChip should match snapshot for ERROR status 1`] = `
data-state="closed"
>
)
- .with('DEPLOYED', 'RUNNING', 'COMPLETED', () => )
+ .with('DEPLOYED', 'RUNNING', 'COMPLETED', 'SUCCESS', () => )
.with('RESTARTED', () => )
// spinner
.with(
@@ -70,7 +70,7 @@ export function StatusChip(props: StatusChipProps) {
.with('DELETING', () => )
// stopped
.with('STOPPED', () => )
- .with('CANCELED', () => )
+ .with('CANCELED', 'CANCEL', 'SKIP', () => )
.with('DELETED', () => )
// unknow / error / warning
.with('UNKNOWN', () => )
@@ -85,7 +85,7 @@ export function StatusChip(props: StatusChipProps) {
'INVALID_CREDENTIALS',
'RECAP',
() => (
-
+