From 49b86438bbe108f9c2fa8a9e606fc870733a2679 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Fri, 5 Jun 2026 09:18:02 -0500 Subject: [PATCH] feat: replace osquery column with agent column on hosts page (#44811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit for #44846 for #43458 - UPDATE: @noahtalerman: For the following story: - https://github.com/fleetdm/fleet/issues/44846 --- # Checklist for submitter - [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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## Summary by CodeRabbit ## Release Notes * **New Features** * Added a new Agent column on the Hosts page displaying Orbit version with tooltips showing Osquery, Orbit, and Fleet Desktop versions for comprehensive version visibility. * **Improvements** * Updated default column visibility on the Hosts page—Issues and Private IP columns are now hidden by default for a cleaner view. --------- Co-authored-by: Scott Gress --- changes/default-columns-agent | 2 + .../hosts/ManageHostsPage/HostTableConfig.tsx | 61 ++++++++++++++++--- server/datastore/mysql/hosts.go | 6 +- server/datastore/mysql/hosts_test.go | 55 +++++++++++++++++ 4 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 changes/default-columns-agent diff --git a/changes/default-columns-agent b/changes/default-columns-agent new file mode 100644 index 0000000000..b793ded528 --- /dev/null +++ b/changes/default-columns-agent @@ -0,0 +1,2 @@ +* Replaced the "Osquery" column with a richer "Agent" column on the Hosts page that shows Orbit version with a tooltip displaying osquery, Orbit, and Fleet Desktop versions. +* Hidden "Issues" and "Private IP address" columns by default for new Fleet instances. diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index dfe84cc9b2..e8c624e2a6 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -582,22 +582,61 @@ const allHostTableHeaders = (teamId?: number): IHostTableColumnConfig[] => [ ); }, }, - // Osquery + // Agent { - title: "Osquery", + title: "Agent", Header: (cellProps: IHostTableHeaderProps) => ( - + ), - accessor: "osquery_version", - id: "osquery_version", + accessor: (row) => row.orbit_version || row.osquery_version, + id: "agent", Cell: (cellProps: IHostTableStringCellProps) => { - if (isMobilePlatform(cellProps.row.original.platform)) { + const { + platform, + orbit_version, + osquery_version, + fleet_desktop_version, + } = cellProps.row.original; + + if (isMobilePlatform(platform)) { return NotSupported; } - return ; + + // Match the Host details Vitals card: treat a missing/empty orbit version + // (including the normalized "---" placeholder) as a vanilla osquery host. + const isChromeOrVanillaOsquery = + platform === "chrome" || + !orbit_version || + orbit_version === DEFAULT_EMPTY_CELL_VALUE; + + if (isChromeOrVanillaOsquery) { + return ; + } + + return ( + + osquery: {osquery_version} +
+ Orbit: {orbit_version} + {fleet_desktop_version && + fleet_desktop_version !== DEFAULT_EMPTY_CELL_VALUE && ( + <> +
+ Fleet Desktop: {fleet_desktop_version} + + )} + + } + > + {orbit_version} + + } + /> + ); }, }, // Last seen @@ -672,6 +711,8 @@ const defaultHiddenColumns = [ "device_mapping", "primary_mac", "public_ip", + "primary_ip", + "issues", "cpu_type", // TODO: should those be mdm.? "mdm.server_url", diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 0d8026dc3a..e81be56d41 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -72,6 +72,7 @@ var hostAllowedOrderKeys = common_mysql.OrderKeyAllowlist{ "public_ip": "h.public_ip", "last_enrolled_at": "h.last_enrolled_at", "last_restarted_at": "h.last_restarted_at", + "agent": "COALESCE(NULLIF(hoi.version, ''), h.osquery_version)", "orbit_version": "hoi.version", "fleet_desktop_version": "hoi.desktop_version", "issues": "host_issues.total_issues_count", @@ -1130,7 +1131,9 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt t.name AS team_name, COALESCE(hu.software_updated_at, h.created_at) AS software_updated_at, h.last_restarted_at, - h.timezone + h.timezone, + hoi.version AS orbit_version, + hoi.desktop_version AS fleet_desktop_version ` sql += hostMDMSelect @@ -1442,6 +1445,7 @@ func (ds *Datastore) applyHostFilters( LEFT JOIN host_updates hu ON (h.id = hu.host_id) LEFT JOIN teams t ON (h.team_id = t.id) LEFT JOIN host_disks hd ON hd.host_id = h.id + LEFT JOIN host_orbit_info hoi ON hoi.host_id = h.id %s %s %s diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index d14f8282d7..84691bc5b8 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -10132,6 +10132,61 @@ func testHostOrder(t *testing.T, ds *Datastore) { ) require.NoError(t, err) chk(hosts, "0003", "0004", "0001") + + // Test sorting by "agent". The agent order key is + // COALESCE(NULLIF(hoi.version, ''), h.osquery_version): orbit-enrolled hosts + // sort by their orbit version, while hosts with no orbit row OR an empty + // orbit version fall back to their osquery version (host_orbit_info.version + // is NOT NULL, so absent orbit info is stored as '' for some hosts). + // hostIDs[0] ("0001"): osquery 9.0.0, no orbit row -> effective 9.0.0 + // hostIDs[1] ("0004"): osquery 9.9.9, orbit 1.0.0 -> effective 1.0.0 (orbit wins) + // hostIDs[2] ("0003"): osquery 5.0.0, orbit '' -> effective 5.0.0 (NULLIF fallback) + _, err = ds.writer(ctx).Exec(`UPDATE hosts SET osquery_version = '9.0.0' WHERE id = ?`, hostIDs[0]) + require.NoError(t, err) + _, err = ds.writer(ctx).Exec(`UPDATE hosts SET osquery_version = '9.9.9' WHERE id = ?`, hostIDs[1]) + require.NoError(t, err) + _, err = ds.writer(ctx).Exec(`UPDATE hosts SET osquery_version = '5.0.0' WHERE id = ?`, hostIDs[2]) + require.NoError(t, err) + err = ds.SetOrUpdateHostOrbitInfo( + ctx, hostIDs[1], "1.0.0", sql.NullString{String: "1.0.0", Valid: true}, sql.NullBool{Bool: true, Valid: true}, + ) + require.NoError(t, err) + // Empty orbit version must still fall back to osquery_version (guards NULLIF); + // plain COALESCE would sort this host as '' and place it first. + err = ds.SetOrUpdateHostOrbitInfo( + ctx, hostIDs[2], "", sql.NullString{Valid: false}, sql.NullBool{Valid: false}, + ) + require.NoError(t, err) + + hosts, err = ds.ListHosts( + ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{ + ListOptions: fleet.ListOptions{ + OrderKey: "agent", + OrderDirection: fleet.OrderAscending, + }, + }, + ) + require.NoError(t, err) + chk(hosts, "0004", "0003", "0001") + + // ListHosts must also populate the orbit/desktop version fields the Agent + // column tooltip relies on (these were previously only loaded by ds.Host). + var orbitHost, vanillaHost *fleet.Host + for _, h := range hosts { + switch h.DisplayName() { + case "0004": + orbitHost = h + case "0001": + vanillaHost = h + } + } + require.NotNil(t, orbitHost) + require.NotNil(t, vanillaHost) + assert.Equal(t, new("1.0.0"), orbitHost.OrbitVersion) + assert.Equal(t, new("1.0.0"), orbitHost.DesktopVersion) + // Vanilla osquery host: no orbit info loaded. + assert.Nil(t, vanillaHost.OrbitVersion) + assert.Nil(t, vanillaHost.DesktopVersion) } func testHostIDsByOSID(t *testing.T, ds *Datastore) {