From f292c7def448e87638914761e6634f48006a4e70 Mon Sep 17 00:00:00 2001 From: Rahul Raghunathan <88367811+rrahul-1@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:08:41 +0530 Subject: [PATCH] Add sortable 'Added to Fleet' column to hosts table (#50098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #50083 # Screenshot demonstrating the fix - Hosts page: image - Edit columns modal: image ## Note on sort direction The new "Added to Fleet" column renders as a "days ago" duration (same formatter as Last seen / Last fetched / Last restarted). To keep behavior consistent across all four time-ago columns on the hosts table, this PR applies the sort-direction inversion originally introduced for `last_restarted_at` in #14878 (fix for #13160) to all of them: - `seen_time` (Last seen) - `detail_updated_at` (Last fetched) - `last_restarted_at` (Last restarted) — unchanged behavior - `last_enrolled_at` (Added to Fleet) — new Arrow-down on any of these columns now sorts by the visible duration (biggest "days ago" first / oldest date first), rather than by the raw underlying timestamp. This is a user-facing behavior change on Last seen and Last fetched — please re-QA sort order on those two columns alongside the new one. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] QA'd all new/changed functionality manually ## Summary by CodeRabbit * **New Features** * Added an **Added to Fleet** column to the hosts table. * Displays when each host last enrolled with Fleet, with an explanatory tooltip. * The column is hidden by default and can be enabled through table settings. * Supports ascending and descending sorting. * **Bug Fixes** * Corrected descending sorting for **Last seen** and **Last fetched** to reflect the displayed host age. --------- Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> --- changes/50083-added-to-fleet-column | 2 ++ .../hosts/ManageHostsPage/HostTableConfig.tsx | 32 +++++++++++++++++++ .../hosts/ManageHostsPage/HostsPageConfig.tsx | 21 +++++++++++- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 18 ++++------- 4 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 changes/50083-added-to-fleet-column diff --git a/changes/50083-added-to-fleet-column b/changes/50083-added-to-fleet-column new file mode 100644 index 0000000000..2b14852f67 --- /dev/null +++ b/changes/50083-added-to-fleet-column @@ -0,0 +1,2 @@ +* Added a sortable "Added to Fleet" column to the hosts table, showing when each host last enrolled with Fleet. +* Fixed sort direction on the "Last seen" and "Last fetched" columns of the hosts table so that sort descending puts the oldest date (biggest "days ago" number) first, matching the visible duration rather than the underlying timestamp. diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 0a2ad02a7e..fccd076eb4 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -732,6 +732,37 @@ const allHostTableHeaders = (teamId?: number): IHostTableColumnConfig[] => [ ); }, }, + // Added to Fleet + { + title: "Added to Fleet", + Header: (cellProps: IHostTableHeaderProps) => { + const titleWithToolTip = ( + + The last time the
host enrolled with Fleet. + + } + > + Added to Fleet +
+ ); + return ( + + ); + }, + accessor: "last_enrolled_at", + id: "last_enrolled_at", + Cell: (cellProps: IHostTableStringCellProps) => ( + + ), + }, ]; const defaultHiddenColumns = [ @@ -752,6 +783,7 @@ const defaultHiddenColumns = [ "seen_time", "hardware_model", "hardware_serial", + "last_enrolled_at", ]; /** diff --git a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx index c2135e688c..3c52696fa2 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx @@ -1,4 +1,4 @@ -import { HOSTS_QUERY_PARAMS } from "services/entities/hosts"; +import { HOSTS_QUERY_PARAMS, ISortOption } from "services/entities/hosts"; export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [ "query", @@ -68,6 +68,25 @@ export const DEFAULT_SORT_DIRECTION = "asc"; export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PAGE_INDEX = 0; +// Columns rendered as "N days ago" durations. Sort direction is inverted at +// the API boundary so arrow-down = biggest days-ago (oldest date) first — +// matching the visible number rather than the underlying timestamp. +export const TIME_AGO_SORT_HEADERS = new Set([ + "seen_time", + "detail_updated_at", + "last_restarted_at", + "last_enrolled_at", +]); + +export const toApiSortBy = (sortBy: ISortOption[]): ISortOption[] => + sortBy.map((s) => { + if (!TIME_AGO_SORT_HEADERS.has(s.key)) return s; + return { + key: s.key, + direction: s.direction === "asc" ? "desc" : "asc", + }; + }); + export const hostSelectStatuses = (isPremiumTier: boolean) => { const baseStatuses = [ { diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index a4d2f3799e..39defbceb7 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -115,6 +115,7 @@ import { DEFAULT_SORT_DIRECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX, + toApiSortBy, hostSelectStatuses, MANAGE_HOSTS_PAGE_FILTER_KEYS, MANAGE_HOSTS_PAGE_LABEL_INCOMPATIBLE_QUERY_PARAMS, @@ -336,6 +337,7 @@ const ManageHostsPage = ({ ); const [searchQuery, setSearchQuery] = useState(initialQuery); const [sortBy, setSortBy] = useState(initialSortBy); + const apiSortBy = useMemo(() => toApiSortBy(sortBy), [sortBy]); const [tableQueryData, setTableQueryData] = useState(); const [isUpdating, setIsUpdating] = useState(false); @@ -668,7 +670,7 @@ const ManageHostsPage = ({ scope: "hosts", selectedLabels, globalFilter: searchQuery, - sortBy, + sortBy: apiSortBy, teamId: teamIdForApi, policyId, policyResponse, @@ -1188,18 +1190,10 @@ const ManageHostsPage = ({ let sort = sortBy; if (sortHeader) { - let direction = sortDirection; - if (sortHeader === "last_restarted_at") { - if (sortDirection === "asc") { - direction = "desc"; - } else { - direction = "asc"; - } - } sort = [ { key: sortHeader, - direction: direction || DEFAULT_SORT_DIRECTION, + direction: sortDirection || DEFAULT_SORT_DIRECTION, }, ]; } else if (!sortBy.length) { @@ -1780,7 +1774,7 @@ const ManageHostsPage = ({ let options = { selectedLabels, globalFilter: searchQuery, - sortBy, + sortBy: apiSortBy, teamId: teamIdForApi, policyId, policyResponse, @@ -1847,7 +1841,7 @@ const ManageHostsPage = ({ teamIdForApi, selectedLabels, searchQuery, - sortBy, + apiSortBy, policyId, policyResponse, macSettingsStatus,