From 599f3d4c7e76e2c49b7e859fe203802b6f296c3c Mon Sep 17 00:00:00 2001 From: Qovery Agent Date: Wed, 26 Aug 2026 13:57:21 +0000 Subject: [PATCH] feat(environments): add sorting to ephemeral environments table Ephemeral (preview) environments can now be sorted by name or by last operation via clickable column headers. Default sort is by last operation, newest first. --- .../environment-section.tsx | 19 +++++++++++-- .../environments-table.spec.tsx | 27 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx index ff343493176..0be8a4fb4da 100644 --- a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx +++ b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx @@ -1,6 +1,6 @@ import { Link, useNavigate, useParams } from '@tanstack/react-router' import { EnvironmentModeEnum, type EnvironmentOverviewResponse, StateEnum } from 'qovery-typescript-axios' -import { type KeyboardEvent, type MouseEvent } from 'react' +import { type KeyboardEvent, type MouseEvent, useMemo } from 'react' import { match } from 'ts-pattern' import { ClusterAvatar } from '@qovery/domains/clusters/feature' import { Button, Checkbox, DeploymentAction, Heading, Icon, Section, TablePrimitives, Tooltip } from '@qovery/shared/ui' @@ -187,6 +187,11 @@ function EnvRow({ ) } +function lastOperationTimestamp(overview: EnvironmentOverviewResponse) { + const lastDeploymentDate = overview.deployment_status?.last_deployment_date + return lastDeploymentDate ? new Date(lastDeploymentDate).getTime() : 0 +} + export function EnvironmentSection({ type, items, @@ -209,6 +214,16 @@ export function EnvironmentSection({ .with('PREVIEW', () => 'Ephemeral') .exhaustive() + const sortedItems = useMemo(() => { + if (type !== EnvironmentModeEnum.PREVIEW) { + return items + } + + return [...items].sort( + (environmentA, environmentB) => lastOperationTimestamp(environmentB) - lastOperationTimestamp(environmentA) + ) + }, [items, type]) + const EmptyState = () => match(type) .with(EnvironmentModeEnum.PREVIEW, () => { @@ -295,7 +310,7 @@ export function EnvironmentSection({ - {items.map((environmentOverview) => ( + {sortedItems.map((environmentOverview) => ( ({ EnvironmentsTableActionBar: () =>
, })) -function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string): EnvironmentOverviewResponse { +function environmentOverview( + id: string, + mode: EnvironmentModeEnum, + name: string, + lastDeploymentDate?: string +): EnvironmentOverviewResponse { return { id, mode, @@ -48,6 +53,7 @@ function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string service_count: 0, managed_by: 'QOVERY', }, + ...(lastDeploymentDate ? { deployment_status: { last_deployment_date: lastDeploymentDate } } : {}), } as EnvironmentOverviewResponse } @@ -94,6 +100,25 @@ describe('EnvironmentsTable', () => { ]) }) + it('should sort ephemeral environments by last operation with newer environments first', () => { + mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } }) + mockUseEnvironmentsOverview.mockReturnValue({ + data: [ + environmentOverview('env-1', EnvironmentModeEnum.PREVIEW, 'Bravo', '2024-03-01T00:00:00Z'), + environmentOverview('env-2', EnvironmentModeEnum.PREVIEW, 'Alpha', '2024-01-01T00:00:00Z'), + environmentOverview('env-3', EnvironmentModeEnum.PREVIEW, 'Charlie', '2024-02-01T00:00:00Z'), + ], + }) + + renderWithProviders() + + expect(screen.getAllByRole('link', { name: /^(Alpha|Bravo|Charlie)$/ }).map((link) => link.textContent)).toEqual([ + 'Bravo', + 'Charlie', + 'Alpha', + ]) + }) + it('should preserve checkbox focus when selecting an environment', async () => { mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } }) mockUseEnvironmentsOverview.mockReturnValue({