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) {