feat: replace osquery column with agent column on hosts page (#44811)

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`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Scott Gress <scott@fleetdm.com>
This commit is contained in:
Josh Roskos
2026-06-05 09:18:02 -05:00
committed by GitHub
co-authored by Scott Gress
parent 457aad6db9
commit 49b86438bb
4 changed files with 113 additions and 11 deletions
+2
View File
@@ -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.
@@ -582,22 +582,61 @@ const allHostTableHeaders = (teamId?: number): IHostTableColumnConfig[] => [
);
},
},
// Osquery
// Agent
{
title: "Osquery",
title: "Agent",
Header: (cellProps: IHostTableHeaderProps) => (
<HeaderCell
value="Osquery"
isSortedDesc={cellProps.column.isSortedDesc}
/>
<HeaderCell value="Agent" isSortedDesc={cellProps.column.isSortedDesc} />
),
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 <TextCell value={cellProps.cell.value} />;
// 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 <TextCell value={osquery_version} />;
}
return (
<TextCell
value={
<TooltipWrapper
tipContent={
<>
osquery: {osquery_version}
<br />
Orbit: {orbit_version}
{fleet_desktop_version &&
fleet_desktop_version !== DEFAULT_EMPTY_CELL_VALUE && (
<>
<br />
Fleet Desktop: {fleet_desktop_version}
</>
)}
</>
}
>
{orbit_version}
</TooltipWrapper>
}
/>
);
},
},
// Last seen
@@ -672,6 +711,8 @@ const defaultHiddenColumns = [
"device_mapping",
"primary_mac",
"public_ip",
"primary_ip",
"issues",
"cpu_type",
// TODO: should those be mdm.<blah>?
"mdm.server_url",
+5 -1
View File
@@ -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
+55
View File
@@ -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) {