Skip to content

feat(ui): add column sorting to backend-ready data-tables#6558

Merged
otavio merged 2 commits into
masterfrom
feat/datatable-sorting
Jun 26, 2026
Merged

feat(ui): add column sorting to backend-ready data-tables#6558
otavio merged 2 commits into
masterfrom
feat/datatable-sorting

Conversation

@luannmoreira

Copy link
Copy Markdown
Member

What

Adds user-controllable column sorting to four console data-tables whose API endpoints already accept sort_by/order_by: Devices, Containers, API Keys, and the billing Device Chooser ("All Devices" tab). Frontend-only — no backend change.

Why

These lists rendered a DataTable with no sorting, even though the component and the backing endpoints already supported it. Only the admin Devices list was wired up. This brings the user-facing tables in line.

Fixes: shellhub-io/team#165

Changes

Each table mirrors the existing admin/devices pattern — local sortBy/orderBy state, a handleSort toggle (same field flips asc⇄desc; switching field resets direction; every sort resets to page 1), sortField/sortOrder/onSort passed to DataTable, and the data hook forwarding sort_by/order_by to the SDK query.

  • Devices (pages/devices, hooks/useDevices): sortable Hostname (name), Last Seen (last_seen); default last_seen desc.
  • Containers (pages/containers, hooks/useContainers): sortable Hostname (name), Last Seen (last_seen); default last_seen desc.
  • API Keys (pages/team/ApiKeysTab, hooks/useApiKeys): sortable Name (name), Created (created_at), Expires (expires_in); default created_at desc. Field names match the backend sorter (api/routes/api-key.go, api/services/api-key.go).
  • Device Chooser (components/billing/DeviceChooserDialog, "All" tab): sortable Hostname, Last Seen.

Column keys were aligned to the backend sort fields (hostnamename, createdcreated_at, expiresexpires_in) because DataTable passes the column key as the sort field.

Testing

TDD (red→green) per table. New/updated test files for all four assert that sortable headers render as controls and that clicking them drives the hook with the expected sortBy/orderBy and toggles direction.

cd ui/apps/console && npm run test -- <the four table test files> → 72 pass; tsc --noEmit and eslint clean.

Non-sortable derived columns (tags, online indicator, role) are intentionally left non-sortable. Tables whose endpoints lack sort params (Sessions, Firewall Rules, Public Keys, several admin lists) are out of scope and tracked separately in shellhub-io/team#165.

Wire user-controllable sort into the Devices, Containers, API Keys, and
Device Chooser tables, mirroring the existing admin/devices pattern. Each
clickable header toggles asc/desc, resets the page to 1, and forwards
sort_by/order_by to the SDK query. The backing endpoints already accept
these params, so no backend change is needed.

Column keys were aligned to the backend sort fields (hostname -> name,
created -> created_at, expires -> expires_in) since DataTable passes the
column key as the sort field.

Fixes: shellhub-io/team#165
@luannmoreira luannmoreira requested a review from a team as a code owner June 25, 2026 22:29
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Claude finished @luannmoreira's task in 12m 56s —— View job


Code Review Complete

Reviewed 11 files across code quality, security, testing, TypeScript patterns, and architecture — no new issues found. The code looks good as-is.

Verified:

  • Sort parameter flow is safe: values originate from hardcoded column keys, and the Go backend validates sort_by against an allowlist (name, last_seen, created_at, expires_in per endpoint) with order_by constrained to asc/desc.
  • No any types, no XSS vectors, no injection risk.
  • Tests cover default sort state and toggle behavior for all four tables.
  • Column key renames (hostnamename, createdcreated_at, expiresexpires_in) correctly match the backend sort fields.
  • No pkg/ changes, so no cross-repo impact on cloud/.

If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42

Copy link
Copy Markdown
Member

Solid change overall. I was just thinking about extracting that sorting logic into a shared hook, because most of it is repeated in the pages: the field and order useStates, the order toggle, and the default order being asc on name fields.

This would reduce the code repetition and also make it easier to integrate into other tables in the future, if any other gets the sorting logic in the back-end. Do you think it's valid?

@otavio

otavio commented Jun 26, 2026

Copy link
Copy Markdown
Member

Solid change overall. I was just thinking about extracting that sorting logic into a shared hook, because most of it is repeated in the pages: the field and order useStates, the order toggle, and the default order being asc on name fields.

This would reduce the code repetition and also make it easier to integrate into other tables in the future, if any other gets the sorting logic in the back-end. Do you think it's valid?

@luannmoreira Please verify if we can implement it this way, so we avoid duplicating all that.

The sort field/order state and the asc/desc toggle (defaulting to
ascending on name fields) were duplicated across the devices,
containers, API keys, device-chooser, and admin devices tables. Move
the logic into a generic useTableSort hook so new sortable tables reuse
it instead of copying the handler.
@luannmoreira

Copy link
Copy Markdown
Member Author

Good call @luizhf42 — addressed in 64c3e4e.

Extracted the repeated state into a generic useTableSort<TField> hook (src/hooks/useTableSort.ts) that owns the sortBy/orderBy state, the asc/desc toggle, and the default-ascending-on-name rule. It takes defaultField, an optional defaultOrder (defaults to desc), and an onSortChange callback for the setPage(1) reset:

const { sortBy, orderBy, handleSort } = useTableSort<SortField>({
  defaultField: "last_seen",
  onSortChange: () => setPage(1),
});

Migrated all 5 call sites — Devices, Containers, API Keys, Device Chooser, and the pre-existing admin Devices table (the pattern this PR mirrored), so there's no stray copy left. Behavior is unchanged; the existing sort tests (72 across the 4 suites) still pass. New sortable tables can now just reuse the hook.

@otavio

otavio commented Jun 26, 2026

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @otavio's task in 23m 7s —— View job


Code Review Complete

Reviewed 13 files across code quality, security, testing, TypeScript patterns, and architecture — no new issues found. The code looks good as-is.

Verified:

  • Sort parameter flow is safe: values originate from hardcoded column keys, and the Go backend validates sort_by against allowlists (name, last_seen, created_at, expires_in per endpoint) with order_by constrained to asc/desc.
  • No any types, no XSS vectors, no injection risk.
  • The as TField cast in useTableSort is a pragmatic bridge between the generic hook and DataTable's onSort: (field: string) => void interface — not a type safety gap.
  • Tests cover default sort state and toggle behavior for all four tables.
  • Column key renames (hostnamename, createdcreated_at, expiresexpires_in) correctly match the backend sort fields.
  • No pkg/ changes — zero cross-repo impact on cloud/.
  • The shared useTableSort hook cleanly replaces the duplicated sort logic from all 5 call sites with identical behavior.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@otavio otavio merged commit f7974d5 into master Jun 26, 2026
14 checks passed
@otavio otavio deleted the feat/datatable-sorting branch June 26, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants