From 210d17546742f1daaec76b4faee355399e6662ef Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Tue, 10 Feb 2026 16:10:04 +0100 Subject: [PATCH 1/8] feat(new-nav): add environment overview --- .../select-commit-modal.tsx | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/libs/domains/services/feature/src/lib/select-commit-modal/select-commit-modal.tsx b/libs/domains/services/feature/src/lib/select-commit-modal/select-commit-modal.tsx index 588f7e28d32..4ecc0d475f7 100644 --- a/libs/domains/services/feature/src/lib/select-commit-modal/select-commit-modal.tsx +++ b/libs/domains/services/feature/src/lib/select-commit-modal/select-commit-modal.tsx @@ -76,8 +76,8 @@ export function SelectCommitModal({ return (
-

{title}

-

{description}

+

{title}

+

{description}

{children}
@@ -88,14 +88,11 @@ export function SelectCommitModal({ {Object.entries(filterCommits).map(([date, commits]) => (
-
- +
+ {pluralize(commits.length, 'Commit')} on {dateToFormat(date, 'MMM dd, yyyy')}
-
+
{commits.map( ( { author_name, author_avatar_url, commit_page_url, created_at, git_commit_id, message }, @@ -109,10 +106,10 @@ export function SelectCommitModal({
@@ -179,12 +174,12 @@ export function SelectCommitModal({ ) : ( -
- -

No result for this search

+
+ +

No result for this search

)} -
+
From 60be49e52a2dabd7adca8fc508e0460b8cace3ec Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Tue, 10 Feb 2026 16:49:23 +0100 Subject: [PATCH 2/8] impr: color tokens for uninstall modal --- .../service-action-toolbar.tsx | 14 ++++++------- .../service-remove-modal.tsx | 20 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx index 8347dee2f00..fcd956a1d08 100644 --- a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx +++ b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx @@ -728,10 +728,10 @@ function MenuOtherActions({ description: 'Choose how to remove this service', entities: [
-
+
- {service.name} + {service.name}
, ], actions: [ @@ -739,20 +739,20 @@ function MenuOtherActions({ id: 'uninstall', title: 'Uninstall', description: ( -
+
Stop and remove the service but keep all Qovery configuration, data and settings.
You can easily reinstall or redeploy later with the same configuration.
- What's deleted: + What's deleted:
  • All service data
- What's kept: + What's kept:
  • Qovery configuration
  • Environment variables
  • @@ -775,14 +775,14 @@ function MenuOtherActions({ id: 'delete', title: 'Delete permanently', description: ( -
    +
    Permanently remove the service and all associated data.
    This action cannot be undone.
    - What's deleted: + What's deleted:
    • All service data
    • Qovery configuration
    • diff --git a/libs/domains/services/feature/src/lib/service-remove-modal/service-remove-modal.tsx b/libs/domains/services/feature/src/lib/service-remove-modal/service-remove-modal.tsx index c508b9b6d90..d9fa70fb22f 100644 --- a/libs/domains/services/feature/src/lib/service-remove-modal/service-remove-modal.tsx +++ b/libs/domains/services/feature/src/lib/service-remove-modal/service-remove-modal.tsx @@ -51,9 +51,9 @@ export function ServiceRemoveModal({ return (
      -

      {title}

      +

      {title}

      -
      +
      {description ? ( description ) : ( @@ -77,18 +77,18 @@ export function ServiceRemoveModal({ ))} @@ -135,16 +135,16 @@ export function ServiceRemoveModal({
      - This action cannot be undone, type " + This action cannot be undone, type " {selectedAction?.id} - " to confirm. + " to confirm.
      Date: Tue, 10 Feb 2026 17:00:29 +0100 Subject: [PATCH 3/8] impr: color token tweaks --- .../service-links-popover.tsx | 36 +++++++------------ .../src/lib/service-list/service-list.tsx | 2 +- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-links-popover/service-links-popover.tsx b/libs/domains/services/feature/src/lib/service-links-popover/service-links-popover.tsx index abf254cebe4..1ce3d2b7ffc 100644 --- a/libs/domains/services/feature/src/lib/service-links-popover/service-links-popover.tsx +++ b/libs/domains/services/feature/src/lib/service-links-popover/service-links-popover.tsx @@ -65,16 +65,16 @@ export function ServiceLinksPopover({ {children}
      -

      +

      {filteredLinks?.length ?? 0} {pluralize(filteredLinks?.length ?? 0, 'link')} attached

      {serviceType !== 'HELM' && ( - + Customize @@ -84,12 +84,9 @@ export function ServiceLinksPopover({
        {nginxLinks.map((link: LinkProps) => (
      • - +
      -
      {link.internal_port}
      +
      {link.internal_port}
      ))} {nginxLinks.length > 0 && gatewayApiLinks.length > 0 && ( <> -
    • -
    • +
    • +
    • Gateway API / Envoy stack more info @@ -125,11 +122,7 @@ export function ServiceLinksPopover({ classNameContent="max-w-xs" > - +
    • @@ -137,12 +130,9 @@ export function ServiceLinksPopover({ )} {gatewayApiLinks.map((link: LinkProps) => (
    • - +
    • -
      {link.internal_port}
      +
      {link.internal_port}
      ))} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index fbf2b015936..402dda10807 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -255,7 +255,7 @@ function ServiceNameCell({ {'auto_deploy' in service && service.auto_deploy && ( - + )} From ef88b460ce969bbafd8761ce5bc902a869ac8787 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Tue, 10 Feb 2026 17:44:36 +0100 Subject: [PATCH 4/8] impr: color token tweaks --- libs/shared/ui/src/lib/components/radio-group/radio-group.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared/ui/src/lib/components/radio-group/radio-group.tsx b/libs/shared/ui/src/lib/components/radio-group/radio-group.tsx index 68786a3becd..716ded7676c 100644 --- a/libs/shared/ui/src/lib/components/radio-group/radio-group.tsx +++ b/libs/shared/ui/src/lib/components/radio-group/radio-group.tsx @@ -67,7 +67,7 @@ const RadioGroupItem = forwardRef, R return ( - {variant === 'check' && } + {variant === 'check' && } ) From 092623bded7334e7f0024f9d34129dd810f9c77c Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Tue, 24 Feb 2026 11:36:24 +0100 Subject: [PATCH 5/8] Update color tokens --- .../services/feature/src/lib/service-list/service-list.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index 402dda10807..73051589eaf 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -228,6 +228,7 @@ function ServiceNameCell({ e.stopPropagation()} @@ -509,6 +510,7 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro {gitRepository.branch && gitRepository.url && ( - + Date: Tue, 24 Feb 2026 13:25:21 +0100 Subject: [PATCH 6/8] Updating the min-width of the status cells --- .../src/lib/service-list/service-list.tsx | 91 ++++++++++--------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index 73051589eaf..cab48e03a32 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -17,6 +17,7 @@ import { type Database, type Environment, type HelmSourceRepositoryResponse, + type ListServicesByEnvironmentId200ResponseResultsInner, type Status, } from 'qovery-typescript-axios' import { ServiceSubActionDto } from 'qovery-ws-typescript-axios' @@ -399,11 +400,13 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro }, cell: (info) => { return ( - +
      + +
      ) }, }), @@ -415,6 +418,8 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro filterFn: 'arrIncludesSome', size: 15, cell: (info) => { + const Wrapper = ({ children }: { children: React.ReactNode }) =>
      {children}
      + const service = info.row.original const link = match(service) .with( @@ -450,46 +455,50 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro if (checkRunningStatusClosed) { return ( - - e.stopPropagation()} - className="gap-2 whitespace-nowrap text-sm" - size="md" - color="neutral" - variant="outline" - radius="full" - > - - Status unavailable - - + + + e.stopPropagation()} + className="gap-2 whitespace-nowrap text-sm" + size="md" + color="neutral" + variant="outline" + radius="full" + > + + Status unavailable + + + ) } return ( - - - e.stopPropagation()} - className="gap-2 whitespace-nowrap text-sm" - size="md" - color="neutral" - variant="outline" - radius="full" - > - s.deploymentStatus?.state) - .otherwise((s) => s.runningStatus?.state)} - /> - {value} - - - + + + + e.stopPropagation()} + className="gap-2 whitespace-nowrap text-sm" + size="md" + color="neutral" + variant="outline" + radius="full" + > + s.deploymentStatus?.state) + .otherwise((s) => s.runningStatus?.state)} + /> + {value} + + + + ) }, }), From 407f224c9313841d1ba7a95879e5580817ca8074 Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Tue, 24 Feb 2026 17:53:37 +0100 Subject: [PATCH 7/8] Splitting components of the Services list table --- .../service-list/service-list-cells/index.ts | 4 + .../service-last-deployment-cell.tsx | 38 ++ .../service-list-cells/service-name-cell.tsx | 248 +++++++ .../service-running-status-cell.tsx | 109 +++ .../service-version-cell.tsx | 220 ++++++ .../src/lib/service-list/service-list.tsx | 625 +----------------- 6 files changed, 650 insertions(+), 594 deletions(-) create mode 100644 libs/domains/services/feature/src/lib/service-list/service-list-cells/index.ts create mode 100644 libs/domains/services/feature/src/lib/service-list/service-list-cells/service-last-deployment-cell.tsx create mode 100644 libs/domains/services/feature/src/lib/service-list/service-list-cells/service-name-cell.tsx create mode 100644 libs/domains/services/feature/src/lib/service-list/service-list-cells/service-running-status-cell.tsx create mode 100644 libs/domains/services/feature/src/lib/service-list/service-list-cells/service-version-cell.tsx diff --git a/libs/domains/services/feature/src/lib/service-list/service-list-cells/index.ts b/libs/domains/services/feature/src/lib/service-list/service-list-cells/index.ts new file mode 100644 index 00000000000..f0cbe4b47a6 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-list/service-list-cells/index.ts @@ -0,0 +1,4 @@ +export * from './service-name-cell' +export * from './service-version-cell' +export * from './service-last-deployment-cell' +export * from './service-running-status-cell' diff --git a/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-last-deployment-cell.tsx b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-last-deployment-cell.tsx new file mode 100644 index 00000000000..c7ad4e7fa1a --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-last-deployment-cell.tsx @@ -0,0 +1,38 @@ +import { type AnyService } from '@qovery/domains/services/data-access' +import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL } from '@qovery/shared/routes' +import { Icon, Link, Tooltip } from '@qovery/shared/ui' +import { dateUTCString, timeAgo } from '@qovery/shared/util-dates' + +type ServiceLastDeploymentCellProps = { + service: AnyService + organizationId: string + projectId: string + environmentId: string +} + +export function ServiceLastDeploymentCell({ + service, + organizationId, + projectId, + environmentId, +}: ServiceLastDeploymentCellProps) { + const value = 'deploymentStatus' in service ? service.deploymentStatus.last_deployment_date : undefined + const linkLog = + ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + + DEPLOYMENT_LOGS_VERSION_URL(service?.id, service?.deploymentStatus?.execution_id) + + return value ? ( + event.stopPropagation()} + > + + {timeAgo(new Date(value))} + + + + ) : ( + - + ) +} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-name-cell.tsx b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-name-cell.tsx new file mode 100644 index 00000000000..8f071e24522 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-name-cell.tsx @@ -0,0 +1,248 @@ +import { useNavigate } from '@tanstack/react-router' +import { type Environment, ServiceTypeEnum, type Status } from 'qovery-typescript-axios' +import { match } from 'ts-pattern' +import { type AnyService } from '@qovery/domains/services/data-access' +import { + APPLICATION_GENERAL_URL, + APPLICATION_URL, + DATABASE_GENERAL_URL, + DATABASE_URL, + DEPLOYMENT_LOGS_VERSION_URL, + ENVIRONMENT_LOGS_URL, + SERVICES_GENERAL_URL, +} from '@qovery/shared/routes' +import { AnimatedGradientText, Badge, Button, Icon, Link, Tooltip } from '@qovery/shared/ui' +import { formatCronExpression, pluralize, upperCaseFirstLetter } from '@qovery/shared/util-js' +import ServiceActionToolbar from '../../service-action-toolbar/service-action-toolbar' +import { ServiceAvatar } from '../../service-avatar/service-avatar' +import ServiceLinksPopover from '../../service-links-popover/service-links-popover' +import ServiceTemplateIndicator from '../../service-template-indicator/service-template-indicator' + +export function ServiceNameCell({ + service, + environment, + deploymentStatus, +}: { + service: AnyService + environment: Environment + deploymentStatus?: Status +}) { + const navigate = useNavigate() + + const deploymentRequestsCount = Number(deploymentStatus?.deployment_requests_count) + + const serviceLink = match(service) + .with( + { serviceType: ServiceTypeEnum.DATABASE }, + ({ id }) => + DATABASE_URL(environment.organization.id, environment.project.id, service.environment.id, id) + + DATABASE_GENERAL_URL + ) + .otherwise( + ({ id }) => + APPLICATION_URL(environment.organization.id, environment.project.id, service.environment.id, id) + + SERVICES_GENERAL_URL + ) + + const LinkDeploymentStatus = () => { + const environmentLog = ENVIRONMENT_LOGS_URL(environment.organization.id, environment.project.id, environment.id) + const deploymentLog = DEPLOYMENT_LOGS_VERSION_URL(service.id, deploymentStatus?.execution_id) + // const precheckLog = ENVIRONMENT_PRE_CHECK_LOGS_URL(deploymentStatus?.execution_id ?? '') + + return match(deploymentStatus?.state) + .with('DEPLOYMENT_QUEUED', 'DELETE_QUEUED', 'STOP_QUEUED', 'RESTART_QUEUED', (s) => ( + {upperCaseFirstLetter(s).replace('_', ' ')}... + )) + .with('CANCELED', () => Last deployment aborted) + .with('DEPLOYING', 'RESTARTING', 'BUILDING', 'DELETING', 'CANCELING', 'STOPPING', (s) => ( + e.stopPropagation()} + > + + + {upperCaseFirstLetter(s)}... + + + + )) + .with('DEPLOYMENT_ERROR', 'DELETE_ERROR', 'STOP_ERROR', 'RESTART_ERROR', 'BUILD_ERROR', () => ( + e.stopPropagation()} + > + Last deployment failed + + + )) + .otherwise(() => null) + } + + return ( +
      + + + + + {match(service) + .with({ serviceType: 'DATABASE' }, (db) => { + return ( + + + + e.stopPropagation()} + > + {db.name} + + + + + + ) + }) + .with({ serviceType: 'JOB' }, (job) => { + const schedule = match(job) + .with( + { job_type: 'CRON' }, + ({ schedule }) => + `Triggered: ${formatCronExpression(schedule.cronjob?.scheduled_at)} (${schedule.cronjob?.timezone})` + ) + .with({ job_type: 'LIFECYCLE' }, ({ schedule }) => { + const actions = [ + schedule.on_start && 'Deploy', + schedule.on_stop && 'Stop', + schedule.on_delete && 'Delete', + ] + .filter(Boolean) + .join(' - ') + return actions ? `Triggered on: ${actions}` : undefined + }) + .exhaustive() + + return ( + + + + e.stopPropagation()} + > + {service.name} + + + + + + + + + + + ) + }) + .otherwise(() => ( + + + e.stopPropagation()} + > + {service.name} + + + + + ))} + + {deploymentRequestsCount > 0 && ( + + + + {deploymentRequestsCount} + + + )} + +
      +
      + {'auto_deploy' in service && service.auto_deploy && ( + + + + + + )} +
      e.stopPropagation()}> + + + +
      +
      +
      e.stopPropagation()}> + undefined) + .with( + { serviceType: 'DATABASE', mode: 'CONTAINER' }, + () => () => + navigate({ + to: + DATABASE_URL(environment.organization.id, environment.project.id, environment.id, service.id) + + DATABASE_GENERAL_URL, + state: { + hasShell: true, + } as any, + }) + ) + .otherwise( + () => () => + navigate({ + to: + APPLICATION_URL(environment.organization.id, environment.project.id, environment.id, service.id) + + APPLICATION_GENERAL_URL, + state: { + hasShell: true, + } as any, + }) + )} + /> +
      +
      +
      + ) +} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-running-status-cell.tsx b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-running-status-cell.tsx new file mode 100644 index 00000000000..2b3e5d2826e --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-running-status-cell.tsx @@ -0,0 +1,109 @@ +import { ServiceTypeEnum } from 'qovery-typescript-axios' +import { ServiceSubActionDto } from 'qovery-ws-typescript-axios' +import { match } from 'ts-pattern' +import { type AnyService } from '@qovery/domains/services/data-access' +import { + APPLICATION_URL, + CLUSTER_URL, + DATABASE_GENERAL_URL, + DATABASE_URL, + SERVICES_GENERAL_URL, +} from '@qovery/shared/routes' +import { Link, Skeleton, StatusChip, Tooltip } from '@qovery/shared/ui' +import useCheckRunningStatusClosed from '../../hooks/use-check-running-status-closed/use-check-running-status-closed' + +type ServiceRunningStatusCellProps = { + service: AnyService + organizationId: string + projectId: string + environment: Environment + clusterId: string +} + +export function ServiceRunningStatusCell({ + service, + organizationId, + projectId, + environment, + clusterId, +}: ServiceRunningStatusCellProps) { + const stateLabel = service.runningStatus.stateLabel + + const { data: checkRunningStatusClosed } = useCheckRunningStatusClosed({ + clusterId, + environmentId: environment.id, + }) + + const Wrapper = ({ children }: { children: React.ReactNode }) =>
      {children}
      + + const link = match(service) + .with( + { serviceType: ServiceTypeEnum.DATABASE }, + ({ id }) => DATABASE_URL(organizationId, projectId, environment.id, id) + DATABASE_GENERAL_URL + ) + .otherwise(({ id }) => APPLICATION_URL(organizationId, projectId, environment.id, id) + SERVICES_GENERAL_URL) + + const value = match(service.runningStatus.triggered_action) + .with({ sub_action: ServiceSubActionDto.TERRAFORM_PLAN_ONLY }, () => 'Plan ' + stateLabel.toLowerCase()) + .with({ sub_action: ServiceSubActionDto.TERRAFORM_PLAN_AND_APPLY }, () => 'Apply ' + stateLabel.toLowerCase()) + .with( + { sub_action: ServiceSubActionDto.TERRAFORM_MIGRATE_STATE }, + () => 'Migrate state ' + stateLabel.toLowerCase() + ) + .with( + { sub_action: ServiceSubActionDto.TERRAFORM_FORCE_UNLOCK_STATE }, + () => 'Force unlock ' + stateLabel.toLowerCase() + ) + .with({ sub_action: ServiceSubActionDto.TERRAFORM_DESTROY }, () => 'Destroy ' + stateLabel.toLocaleString()) + .with({ sub_action: ServiceSubActionDto.NONE }, () => stateLabel) + .with(undefined, () => stateLabel) + .exhaustive() + + if (checkRunningStatusClosed) { + return ( + + + e.stopPropagation()} + className="gap-2 whitespace-nowrap text-sm" + size="md" + color="neutral" + variant="outline" + radius="full" + > + + Status unavailable + + + + ) + } + + return ( + + + + e.stopPropagation()} + className="gap-2 whitespace-nowrap text-sm" + size="md" + color="neutral" + variant="outline" + radius="full" + > + s.deploymentStatus?.state) + .otherwise((s) => s.runningStatus?.state)} + /> + {value} + + + + + ) +} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-version-cell.tsx b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-version-cell.tsx new file mode 100644 index 00000000000..8ba9a9defc2 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-list/service-list-cells/service-version-cell.tsx @@ -0,0 +1,220 @@ +import { + type ApplicationGitRepository, + type ContainerResponse, + type HelmSourceRepositoryResponse, +} from 'qovery-typescript-axios' +import { P, match } from 'ts-pattern' +import { + type AnyService, + type Application, + type Database, + type Helm, + type Job, + type Terraform, +} from '@qovery/domains/services/data-access' +import { + IconEnum, + isHelmGitSource, + isHelmRepositorySource, + isJobContainerSource, + isJobGitSource, +} from '@qovery/shared/enums' +import { ExternalLink, Icon, Tooltip, Truncate } from '@qovery/shared/ui' +import { buildGitProviderUrl } from '@qovery/shared/util-git' +import { containerRegistryKindToIcon } from '@qovery/shared/util-js' +import LastCommit from '../../last-commit/last-commit' +import LastVersion from '../../last-version/last-version' + +type ServiceVersionCellProps = { + service: AnyService + organizationId: string + projectId: string +} + +export function ServiceVersionCell({ service, organizationId, projectId }: ServiceVersionCellProps) { + const gitInfo = (service: Application | Job | Helm | Terraform, gitRepository?: ApplicationGitRepository) => + gitRepository && ( +
      e.stopPropagation()}> +
      + + + + + + + {gitRepository.branch && gitRepository.url && ( + + + + + + + )} +
      + +
      + ) + const containerInfo = (containerImage?: Pick) => + containerImage && ( +
      e.stopPropagation()}> +
      + + + + {containerImage.registry.name.length >= 20 && ( + <> + {containerImage.registry.name}
      + + )}{' '} + {containerImage.registry.url} +
      + } + > + + {containerImage.registry.name.length >= 20 ? ( + + ) : ( + containerImage.registry.name.toLowerCase() + )} + + + + + + + +
      + {(service.serviceType === 'CONTAINER' || + (service.serviceType === 'JOB' && isJobContainerSource(service.source))) && ( + + )} +
      + ) + + const datasourceInfo = (datasource?: Pick) => + datasource && ( +
      + + + {datasource.type.toLowerCase().replace('sql', 'SQL').replace('db', 'DB')} + + + v + {datasource.version} + +
      + ) + + const helmInfo = (helmRepository?: HelmSourceRepositoryResponse) => + helmRepository && ( +
      +
      e.stopPropagation()}> + + + + {helmRepository.repository?.name.length > 20 && ( + <> + {helmRepository.repository?.name}
      + + )} + {helmRepository.repository?.url} +
      + } + > + + {helmRepository.repository?.name.length > 20 ? ( + + ) : ( + helmRepository.repository?.name.toLowerCase() + )} + + + +
      + + + + +
      +
      + {service.serviceType === 'HELM' && ( + + )} +
      + ) + + const cell = match({ service }) + .with({ service: P.intersection({ serviceType: 'JOB' }, { source: P.when(isJobGitSource) }) }, ({ service }) => { + const { + source: { docker }, + } = service + return gitInfo(service, docker?.git_repository) + }) + .with( + { service: P.intersection({ serviceType: 'JOB' }, { source: P.when(isJobContainerSource) }) }, + ({ + service: { + source: { image }, + }, + }) => containerInfo(image) + ) + .with({ service: { serviceType: 'APPLICATION' } }, ({ service }) => gitInfo(service, service.git_repository)) + .with({ service: { serviceType: 'CONTAINER' } }, ({ service: { image_name, tag, registry } }) => + containerInfo({ image_name, tag, registry }) + ) + .with({ service: { serviceType: 'DATABASE' } }, ({ service: { accessibility, mode, type, version } }) => + datasourceInfo({ accessibility, mode, type, version }) + ) + .with({ service: P.intersection({ serviceType: 'HELM' }, { source: P.when(isHelmGitSource) }) }, ({ service }) => { + const { + source: { git }, + } = service + return gitInfo(service, git?.git_repository) + }) + .with( + { service: P.intersection({ serviceType: 'HELM' }, { source: P.when(isHelmRepositorySource) }) }, + ({ + service: { + source: { repository }, + }, + }) => helmInfo(repository) + ) + .with({ service: { serviceType: 'TERRAFORM' } }, ({ service }) => { + return gitInfo(service, service?.terraform_files_source?.git?.git_repository) + }) + .exhaustive() + return cell +} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index cab48e03a32..039fa0a5956 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -11,311 +11,32 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table' -import { - type ApplicationGitRepository, - type ContainerResponse, - type Database, - type Environment, - type HelmSourceRepositoryResponse, - type ListServicesByEnvironmentId200ResponseResultsInner, - type Status, -} from 'qovery-typescript-axios' -import { ServiceSubActionDto } from 'qovery-ws-typescript-axios' +import { type Environment } from 'qovery-typescript-axios' import { type ComponentProps, Fragment, useMemo, useState } from 'react' -import { P, match } from 'ts-pattern' -import { - type AnyService, - type Application, - type Helm, - type Job, - type Terraform, -} from '@qovery/domains/services/data-access' -import { - IconEnum, - ServiceTypeEnum, - isHelmGitSource, - isHelmRepositorySource, - isJobContainerSource, - isJobGitSource, -} from '@qovery/shared/enums' +import { match } from 'ts-pattern' +import { ServiceTypeEnum } from '@qovery/shared/enums' import { - APPLICATION_GENERAL_URL, APPLICATION_URL, - CLUSTER_URL, DATABASE_GENERAL_URL, DATABASE_URL, - DEPLOYMENT_LOGS_VERSION_URL, - ENVIRONMENT_LOGS_URL, SERVICES_GENERAL_URL, SERVICES_NEW_URL, SERVICES_URL, } from '@qovery/shared/routes' -import { - AnimatedGradientText, - Badge, - Button, - Checkbox, - EmptyState, - ExternalLink, - Icon, - Link, - Skeleton, - StatusChip, - TableFilter, - TablePrimitives, - Tooltip, - Truncate, -} from '@qovery/shared/ui' -import { dateUTCString, timeAgo } from '@qovery/shared/util-dates' -import { buildGitProviderUrl } from '@qovery/shared/util-git' -import { - containerRegistryKindToIcon, - formatCronExpression, - pluralize, - twMerge, - upperCaseFirstLetter, -} from '@qovery/shared/util-js' -import { useCheckRunningStatusClosed } from '../hooks/use-check-running-status-closed/use-check-running-status-closed' -import { useServices } from '../hooks/use-services/use-services' -import { LastCommit } from '../last-commit/last-commit' -import LastVersion from '../last-version/last-version' -import { ServiceActionToolbar } from '../service-action-toolbar/service-action-toolbar' +import { Checkbox, EmptyState, Icon, Link, TableFilter, TablePrimitives, Truncate } from '@qovery/shared/ui' +import { twMerge } from '@qovery/shared/util-js' +import useServices from '../hooks/use-services/use-services' import { ServiceAvatar } from '../service-avatar/service-avatar' -import { ServiceLinksPopover } from '../service-links-popover/service-links-popover' -import { ServiceTemplateIndicator } from '../service-template-indicator/service-template-indicator' import { ServiceListActionBar } from './service-list-action-bar' +import { + ServiceLastDeploymentCell, + ServiceNameCell, + ServiceRunningStatusCell, + ServiceVersionCell, +} from './service-list-cells' const { Table } = TablePrimitives -function ServiceNameCell({ - service, - environment, - deploymentStatus, -}: { - service: AnyService - environment: Environment - deploymentStatus?: Status -}) { - const navigate = useNavigate() - - const deploymentRequestsCount = Number(deploymentStatus?.deployment_requests_count) - - const serviceLink = match(service) - .with( - { serviceType: ServiceTypeEnum.DATABASE }, - ({ id }) => - DATABASE_URL(environment.organization.id, environment.project.id, service.environment.id, id) + - DATABASE_GENERAL_URL - ) - .otherwise( - ({ id }) => - APPLICATION_URL(environment.organization.id, environment.project.id, service.environment.id, id) + - SERVICES_GENERAL_URL - ) - - const LinkDeploymentStatus = () => { - const environmentLog = ENVIRONMENT_LOGS_URL(environment.organization.id, environment.project.id, environment.id) - const deploymentLog = DEPLOYMENT_LOGS_VERSION_URL(service.id, deploymentStatus?.execution_id) - // const precheckLog = ENVIRONMENT_PRE_CHECK_LOGS_URL(deploymentStatus?.execution_id ?? '') - - return match(deploymentStatus?.state) - .with('DEPLOYMENT_QUEUED', 'DELETE_QUEUED', 'STOP_QUEUED', 'RESTART_QUEUED', (s) => ( - {upperCaseFirstLetter(s).replace('_', ' ')}... - )) - .with('CANCELED', () => Last deployment aborted) - .with('DEPLOYING', 'RESTARTING', 'BUILDING', 'DELETING', 'CANCELING', 'STOPPING', (s) => ( - e.stopPropagation()} - > - - - {upperCaseFirstLetter(s)}... - - - - )) - .with('DEPLOYMENT_ERROR', 'DELETE_ERROR', 'STOP_ERROR', 'RESTART_ERROR', 'BUILD_ERROR', () => ( - e.stopPropagation()} - > - Last deployment failed - - - )) - .otherwise(() => null) - } - - return ( -
      - - - - - {match(service) - .with({ serviceType: 'DATABASE' }, (db) => { - return ( - - - - e.stopPropagation()} - > - {db.name} - - - - - - ) - }) - .with({ serviceType: 'JOB' }, (job) => { - const schedule = match(job) - .with( - { job_type: 'CRON' }, - ({ schedule }) => - `Triggered: ${formatCronExpression(schedule.cronjob?.scheduled_at)} (${schedule.cronjob?.timezone})` - ) - .with({ job_type: 'LIFECYCLE' }, ({ schedule }) => { - const actions = [ - schedule.on_start && 'Deploy', - schedule.on_stop && 'Stop', - schedule.on_delete && 'Delete', - ] - .filter(Boolean) - .join(' - ') - return actions ? `Triggered on: ${actions}` : undefined - }) - .exhaustive() - - return ( - - - - e.stopPropagation()} - > - {service.name} - - - - - - - - - - - ) - }) - .otherwise(() => ( - - - e.stopPropagation()} - > - {service.name} - - - - - ))} - - {deploymentRequestsCount > 0 && ( - - - - {deploymentRequestsCount} - - - )} - -
      -
      - {'auto_deploy' in service && service.auto_deploy && ( - - - - - - )} -
      e.stopPropagation()}> - - - -
      -
      -
      e.stopPropagation()}> - undefined) - .with( - { serviceType: 'DATABASE', mode: 'CONTAINER' }, - () => () => - navigate({ - to: - DATABASE_URL(environment.organization.id, environment.project.id, environment.id, service.id) + - DATABASE_GENERAL_URL, - state: { - hasShell: true, - } as any, - }) - ) - .otherwise( - () => () => - navigate({ - to: - APPLICATION_URL(environment.organization.id, environment.project.id, environment.id, service.id) + - APPLICATION_GENERAL_URL, - state: { - hasShell: true, - } as any, - }) - )} - /> -
      -
      -
      - ) -} - export interface ServiceListProps extends ComponentProps { environment: Environment } @@ -327,10 +48,7 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro const projectId = environment.project.id || '' const { data: services = [] } = useServices({ environmentId, suspense: true }) - const { data: checkRunningStatusClosed } = useCheckRunningStatusClosed({ - clusterId, - environmentId, - }) + const [sorting, setSorting] = useState([]) const [rowSelection, setRowSelection] = useState({}) const navigate = useNavigate() @@ -418,87 +136,14 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro filterFn: 'arrIncludesSome', size: 15, cell: (info) => { - const Wrapper = ({ children }: { children: React.ReactNode }) =>
      {children}
      - - const service = info.row.original - const link = match(service) - .with( - { serviceType: ServiceTypeEnum.DATABASE }, - ({ id }) => DATABASE_URL(organizationId, projectId, environmentId, id) + DATABASE_GENERAL_URL - ) - .otherwise(({ id }) => APPLICATION_URL(organizationId, projectId, environmentId, id) + SERVICES_GENERAL_URL) - - const value = match(service.runningStatus.triggered_action) - .with( - { sub_action: ServiceSubActionDto.TERRAFORM_PLAN_ONLY }, - () => 'Plan ' + info.getValue()?.toLowerCase() - ) - .with( - { sub_action: ServiceSubActionDto.TERRAFORM_PLAN_AND_APPLY }, - () => 'Apply ' + info.getValue()?.toLowerCase() - ) - .with( - { sub_action: ServiceSubActionDto.TERRAFORM_MIGRATE_STATE }, - () => 'Migrate state ' + info.getValue()?.toLowerCase() - ) - .with( - { sub_action: ServiceSubActionDto.TERRAFORM_FORCE_UNLOCK_STATE }, - () => 'Force unlock ' + info.getValue()?.toLowerCase() - ) - .with( - { sub_action: ServiceSubActionDto.TERRAFORM_DESTROY }, - () => 'Destroy ' + info.getValue()?.toLocaleString() - ) - .with({ sub_action: ServiceSubActionDto.NONE }, () => info.getValue()) - .with(undefined, () => info.getValue()) - .exhaustive() - - if (checkRunningStatusClosed) { - return ( - - - e.stopPropagation()} - className="gap-2 whitespace-nowrap text-sm" - size="md" - color="neutral" - variant="outline" - radius="full" - > - - Status unavailable - - - - ) - } - return ( - - - - e.stopPropagation()} - className="gap-2 whitespace-nowrap text-sm" - size="md" - color="neutral" - variant="outline" - radius="full" - > - s.deploymentStatus?.state) - .otherwise((s) => s.runningStatus?.state)} - /> - {value} - - - - + ) }, }), @@ -508,201 +153,9 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro enableSorting: false, size: 30, cell: (info) => { - const service = info.row.original - - const gitInfo = (service: Application | Job | Helm | Terraform, gitRepository?: ApplicationGitRepository) => - gitRepository && ( -
      e.stopPropagation()}> -
      - - - - - - - {gitRepository.branch && gitRepository.url && ( - - - - - - - )} -
      - -
      - ) - const containerInfo = (containerImage?: Pick) => - containerImage && ( -
      e.stopPropagation()}> -
      - - - - {containerImage.registry.name.length >= 20 && ( - <> - {containerImage.registry.name}
      - - )}{' '} - {containerImage.registry.url} -
      - } - > - - {containerImage.registry.name.length >= 20 ? ( - - ) : ( - containerImage.registry.name.toLowerCase() - )} - - - - - - - -
      - {(service.serviceType === 'CONTAINER' || - (service.serviceType === 'JOB' && isJobContainerSource(service.source))) && ( - - )} -
      - ) - - const datasourceInfo = (datasource?: Pick) => - datasource && ( -
      - - - {datasource.type.toLowerCase().replace('sql', 'SQL').replace('db', 'DB')} - - - v - {datasource.version} - -
      - ) - - const helmInfo = (helmRepository?: HelmSourceRepositoryResponse) => - helmRepository && ( -
      -
      e.stopPropagation()}> - - - - {helmRepository.repository?.name.length > 20 && ( - <> - {helmRepository.repository?.name}
      - - )} - {helmRepository.repository?.url} -
      - } - > - - {helmRepository.repository?.name.length > 20 ? ( - - ) : ( - helmRepository.repository?.name.toLowerCase() - )} - - - -
      - - - - -
      -
      - {service.serviceType === 'HELM' && ( - - )} -
      - ) - - const cell = match({ service }) - .with( - { service: P.intersection({ serviceType: 'JOB' }, { source: P.when(isJobGitSource) }) }, - ({ service }) => { - const { - source: { docker }, - } = service - return gitInfo(service, docker?.git_repository) - } - ) - .with( - { service: P.intersection({ serviceType: 'JOB' }, { source: P.when(isJobContainerSource) }) }, - ({ - service: { - source: { image }, - }, - }) => containerInfo(image) - ) - .with({ service: { serviceType: 'APPLICATION' } }, ({ service }) => - gitInfo(service, service.git_repository) - ) - .with({ service: { serviceType: 'CONTAINER' } }, ({ service: { image_name, tag, registry } }) => - containerInfo({ image_name, tag, registry }) - ) - .with({ service: { serviceType: 'DATABASE' } }, ({ service: { accessibility, mode, type, version } }) => - datasourceInfo({ accessibility, mode, type, version }) - ) - .with( - { service: P.intersection({ serviceType: 'HELM' }, { source: P.when(isHelmGitSource) }) }, - ({ service }) => { - const { - source: { git }, - } = service - return gitInfo(service, git?.git_repository) - } - ) - .with( - { service: P.intersection({ serviceType: 'HELM' }, { source: P.when(isHelmRepositorySource) }) }, - ({ - service: { - source: { repository }, - }, - }) => helmInfo(repository) - ) - .with({ service: { serviceType: 'TERRAFORM' } }, ({ service }) => { - return gitInfo(service, service?.terraform_files_source?.git?.git_repository) - }) - .exhaustive() - return cell + return ( + + ) }, }), columnHelper.accessor('deploymentStatus.last_deployment_date', { @@ -711,34 +164,18 @@ export function ServiceList({ className, environment, ...props }: ServiceListPro enableSorting: true, size: 3, cell: (info) => { - const service = info.row.original - const value = info.getValue() - const linkLog = - ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + - DEPLOYMENT_LOGS_VERSION_URL(service?.id, service?.deploymentStatus?.execution_id) - - return value ? ( - event.stopPropagation()} - > - - {timeAgo(new Date(value))} - - - - ) : ( - - + return ( + ) }, }), ], - [columnHelper, organizationId, projectId, environmentId, navigate] + [columnHelper, environment, clusterId, organizationId, projectId, environmentId] ) const table = useReactTable({ From 2fe49fec7b377cc5bcfc1b8e58c60661cb53621e Mon Sep 17 00:00:00 2001 From: Romain Billard Date: Wed, 25 Feb 2026 10:12:57 +0100 Subject: [PATCH 8/8] Fix outdated snapshots --- .../select-commit-modal.spec.tsx.snap | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/libs/domains/services/feature/src/lib/select-commit-modal/__snapshots__/select-commit-modal.spec.tsx.snap b/libs/domains/services/feature/src/lib/select-commit-modal/__snapshots__/select-commit-modal.spec.tsx.snap index d94b144d09d..dd18b7f1802 100644 --- a/libs/domains/services/feature/src/lib/select-commit-modal/__snapshots__/select-commit-modal.spec.tsx.snap +++ b/libs/domains/services/feature/src/lib/select-commit-modal/__snapshots__/select-commit-modal.spec.tsx.snap @@ -9,12 +9,12 @@ exports[`SelectCommitModal should match snapshot 1`] = ` class="flex flex-col gap-2 text-sm" >

      Deploy other version

      Type a version to deploy

      @@ -54,21 +54,21 @@ exports[`SelectCommitModal should match snapshot 1`] = ` class="pl-2" >