Skip to content

feat(environments): make environment list columns sortable - #3020

Open
RemiBonnet wants to merge 1 commit into
stagingfrom
feat/environment-list-sortable-columns
Open

RemiBonnet wants to merge 1 commit into
stagingfrom
feat/environment-list-sortable-columns

Conversation

@RemiBonnet

@RemiBonnet RemiBonnet commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Issue:

Makes the environment list table sortable by clicking on the column headers, as requested. Each click on a sortable header toggles the sort direction between ascending and descending.

Sortable columns

  • Environment – alphabetical (default order, ascending)
  • Last operation – by last deployment date
  • Cluster – alphabetical by cluster name
  • Last update – by updated_at

Implementation details

  • Adds a lightweight, self-contained sort state to EnvironmentSection (one per section) driving the existing custom TablePrimitives table.
  • Reuses the same header affordance as the service rows: a clickable header button with arrow-down (ascending) / arrow-up (descending) icons, so the behavior is consistent with the rest of the Console.
  • Exposes the current sort through aria-sort on the column headers for accessibility.
  • Preserves existing defaults: ephemeral environments still start sorted by the most recent operation; other sections keep the alphabetical order. A stable alphabetical tie-break keeps equal rows predictable.

Screenshots / Recordings

UI-only interaction change; header labels and layout are unchanged aside from the added sort arrows on click.

Testing

  • Changes tested locally in the relevant Console's pages and Storybooks
  • Added unit tests for the sorting behavior and aria-sort state (environment-section.spec.tsx)
  • yarn test / yarn format / yarn lint

Note: automated checks (yarn test/format/lint/typecheck) were not run in the authoring environment because the Node/Yarn toolchain is not available there. CI on this PR will validate them.

PR Checklist

  • Followed naming, styling, and TypeScript rules
  • Performed a self-review (diff inspected, no dead code)
  • Titled the PR using Conventional Commits with a scope
  • Kept only necessary comments, written in English
  • Involve a designer to validate the UI change
  • Covered new logic with unit tests

🤖 Generated with Claude Code


Summary by cubic

Makes the environment list table sortable by clicking on column headers, and enables the same sorting on the service list's Last operation column; each click toggles between ascending and descending.

  • Sortable environment columns: Environment (name), Last operation (last deployment date), Cluster, and Last update (updated_at); services sort by Last operation.
  • Reuses the console's existing sortable-header affordance with arrow icons and exposes sort state through aria-sort.
  • Keeps existing defaults: ephemeral environments start sorted by most recent operation; other sections keep alphabetical order with an alphabetical tie-break. Missing updated_at values sort as if updated just now.
  • Service list sorting reads last_deployment_date from deployment status so recent deployments order correctly.

Written for commit 1852c31. Summary will update on new commits.

Review in cubic

@nx-cloud

nx-cloud Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 1852c31

Command Status Duration Result
nx run console:build --parallel=3 --configurati... ✅ Succeeded 1m 17s View ↗
nx affected --target=test --parallel=3 --config... ✅ Succeeded 2m 44s View ↗
nx affected --target=lint --parallel=3 ✅ Succeeded 2m 30s View ↗
nx-cloud record -- yarn nx format:check ✅ Succeeded 6s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-09-25 14:36:08 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Confidence score: 3/5

  • In libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.spec.tsx, the unanchored role query also matches row links, so the assertion receives four links and the test fails — scope the query to the individual environment link.
  • In libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx, Last update sorting treats a missing updated_at as 1970 even though the row displays “just now,” so the sort order can disagree with what users see — use the display’s Date.now() fallback.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.spec.tsx">

<violation number="1" location="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.spec.tsx:171">
P2: This unanchored role query also matches each row’s `role="link"` in addition to its nested environment anchor, so the assertion receives four links and the new test fails. Restrict the accessible name to the individual environment anchors.</violation>
</file>

<file name="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx">

<violation number="1" location="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx:196">
P3: The Last update sort disagrees with the row display when `updated_at` is missing: the row shows “just now,” but this comparator sorts it as 1970. Use the same `Date.now()` fallback as the display.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

)

const getEnvironmentNames = () =>
screen.getAllByRole('link', { name: /alpha|beta/i }).map((link) => link.textContent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This unanchored role query also matches each row’s role="link" in addition to its nested environment anchor, so the assertion receives four links and the new test fails. Restrict the accessible name to the individual environment anchors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.spec.tsx, line 171:

<comment>This unanchored role query also matches each row’s `role="link"` in addition to its nested environment anchor, so the assertion receives four links and the new test fails. Restrict the accessible name to the individual environment anchors.</comment>

<file context>
@@ -157,6 +157,45 @@ describe('EnvironmentSection', () => {
+    )
+
+    const getEnvironmentNames = () =>
+      screen.getAllByRole('link', { name: /alpha|beta/i }).map((link) => link.textContent)
+
+    expect(getEnvironmentNames()).toEqual(['Alpha', 'Beta'])
</file context>
Suggested change
screen.getAllByRole('link', { name: /alpha|beta/i }).map((link) => link.textContent)
screen.getAllByRole('link', { name: /^(Alpha|Beta)$/i }).map((link) => link.textContent)

}

function lastUpdateTimestamp(overview: EnvironmentOverviewResponse) {
return overview.updated_at ? new Date(overview.updated_at).getTime() : 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Last update sort disagrees with the row display when updated_at is missing: the row shows “just now,” but this comparator sorts it as 1970. Use the same Date.now() fallback as the display.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx, line 196:

<comment>The Last update sort disagrees with the row display when `updated_at` is missing: the row shows “just now,” but this comparator sorts it as 1970. Use the same `Date.now()` fallback as the display.</comment>

<file context>
@@ -192,6 +192,70 @@ function lastOperationTimestamp(overview: EnvironmentOverviewResponse) {
 }
 
+function lastUpdateTimestamp(overview: EnvironmentOverviewResponse) {
+  return overview.updated_at ? new Date(overview.updated_at).getTime() : 0
+}
+
</file context>
Suggested change
return overview.updated_at ? new Date(overview.updated_at).getTime() : 0
return new Date(overview.updated_at ?? Date.now()).getTime()

@RemiBonnet
RemiBonnet force-pushed the feat/environment-list-sortable-columns branch from 1b1e094 to 71f741f Compare September 25, 2026 13:27

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and no new issues found across 2 files (changes from recent commits).

Confidence score: 4/5

  • In libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx, rows without updated_at get a different fallback timestamp on each comparator call, which can make their sort order inconsistent; use a stable fallback value for the comparison.

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.66667% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.17%. Comparing base (5322ea5) to head (1852c31).
⚠️ Report is 1 commits behind head on staging.

Files with missing lines Patch % Lines
...-table/environment-section/environment-section.tsx 84.61% 2 Missing and 2 partials ⚠️
...ices/feature/src/lib/service-list/service-list.tsx 33.33% 0 Missing and 2 partials ⚠️
...feature/src/lib/hooks/use-services/use-services.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           staging    #3020      +/-   ##
===========================================
- Coverage    50.84%   49.17%   -1.68%     
===========================================
  Files         1313      528     -785     
  Lines        28937    14358   -14579     
  Branches      8637     4499    -4138     
===========================================
- Hits         14713     7060    -7653     
+ Misses       11822     6151    -5671     
+ Partials      2402     1147    -1255     
Flag Coverage Δ
unittests 49.17% <76.66%> (-1.68%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Allow sorting the environment list by clicking on column headers, with a
click toggling the direction between ascending and descending. Sortable
columns: Environment (name), Last operation, Cluster and Last update.

This mirrors the sortable behavior already used on the service rows
(arrow-down for ascending, arrow-up for descending) and exposes the
current sort state via aria-sort for accessibility.

Defaults are preserved: ephemeral environments still start sorted by the
most recent operation, other sections keep the alphabetical order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@RemiBonnet
RemiBonnet force-pushed the feat/environment-list-sortable-columns branch from 71f741f to 1852c31 Compare September 25, 2026 14:28

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 5 reviewed files. 1 file intentionally excluded from review.

Confidence score: 3/5

  • In environment-section.tsx, the initially active sort is announced as unsorted until a header is clicked, so screen-reader users get incorrect sort status; pass the resolved default sort to SortableColumnHeader or initialize equivalent state.
  • In service-list.tsx, sortable service headers lack aria-sort, leaving screen-reader users unable to determine the sort direction. The spec checks icon classes rather than this accessibility contract, so assert the header’s aria-sort in service-list.spec.tsx.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx">

<violation number="1" location="libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx:284">
P2: The default-sorted column is announced as unsorted until the user clicks a header. Pass the resolved default sort to `SortableColumnHeader` (or initialize equivalent state) so `aria-sort` reports the actual active column and direction.</violation>
</file>

<file name="libs/domains/services/feature/src/lib/service-list/service-list.tsx">

<violation number="1" location="libs/domains/services/feature/src/lib/service-list/service-list.tsx:252">
P2: This newly sortable service column has no `aria-sort` state in the shared header branch, so screen readers cannot tell whether it is unsorted, ascending, or descending. Add the current sort state to the `Table.ColumnHeaderCell`, including `none` when inactive.</violation>
</file>

<file name="libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx">

<violation number="1" location="libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx:455">
P3: This test asserts the FontAwesome glyph classes, which are an implementation detail, while the feature's advertised accessibility contract (header aria-sort) goes unverified. Assert the header's aria-sort attribute (ascending/descending/none) instead of, or in addition to, the icon classes so the test keeps checking meaning when the icon library or glyph names change.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


// Ephemeral environments default to the most recent operation first, every other
// section keeps the historical alphabetical order. Clicking a header overrides this.
const [sort, setSort] = useState<EnvironmentSort | null>(null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The default-sorted column is announced as unsorted until the user clicks a header. Pass the resolved default sort to SortableColumnHeader (or initialize equivalent state) so aria-sort reports the actual active column and direction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx, line 284:

<comment>The default-sorted column is announced as unsorted until the user clicks a header. Pass the resolved default sort to `SortableColumnHeader` (or initialize equivalent state) so `aria-sort` reports the actual active column and direction.</comment>

<file context>
@@ -280,32 +281,32 @@ export function EnvironmentSection({
-      ? { column: 'last-operation', direction: 'desc' }
-      : { column: 'name', direction: 'asc' }
-  )
+  const [sort, setSort] = useState<EnvironmentSort | null>(null)
 
   const handleSort = useCallback((column: EnvironmentSortColumn) => {
</file context>

id: 'last_deployment',
header: 'Last operation',
enableColumnFilter: false,
enableSorting: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This newly sortable service column has no aria-sort state in the shared header branch, so screen readers cannot tell whether it is unsorted, ascending, or descending. Add the current sort state to the Table.ColumnHeaderCell, including none when inactive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/services/feature/src/lib/service-list/service-list.tsx, line 252:

<comment>This newly sortable service column has no `aria-sort` state in the shared header branch, so screen readers cannot tell whether it is unsorted, ascending, or descending. Add the current sort state to the `Table.ColumnHeaderCell`, including `none` when inactive.</comment>

<file context>
@@ -240,19 +240,27 @@ export function ServiceList({ className, containerClassName, environment, ...pro
+          id: 'last_deployment',
+          header: 'Last operation',
+          enableColumnFilter: false,
+          enableSorting: true,
+          sortingFn: 'basic',
+          sortDescFirst: false,
</file context>

.slice(1)
.map((row) => row.textContent?.match(/FRONT-END|back-end-A|CRONJOB|seed_script/)?.[0])

expect(lastOperationHeader.querySelector('.fa-arrow-down, .fa-arrow-up')).not.toBeInTheDocument()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test asserts the FontAwesome glyph classes, which are an implementation detail, while the feature's advertised accessibility contract (header aria-sort) goes unverified. Assert the header's aria-sort attribute (ascending/descending/none) instead of, or in addition to, the icon classes so the test keeps checking meaning when the icon library or glyph names change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx, line 455:

<comment>This test asserts the FontAwesome glyph classes, which are an implementation detail, while the feature's advertised accessibility contract (header aria-sort) goes unverified. Assert the header's aria-sort attribute (ascending/descending/none) instead of, or in addition to, the icon classes so the test keeps checking meaning when the icon library or glyph names change.</comment>

<file context>
@@ -443,6 +443,31 @@ describe('ServiceList', () => {
+        .slice(1)
+        .map((row) => row.textContent?.match(/FRONT-END|back-end-A|CRONJOB|seed_script/)?.[0])
+
+    expect(lastOperationHeader.querySelector('.fa-arrow-down, .fa-arrow-up')).not.toBeInTheDocument()
+    expect(getServiceNames()).toEqual(['FRONT-END', 'back-end-A', 'CRONJOB', 'seed_script'])
+
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant