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:
co-authored by
Scott Gress
parent
457aad6db9
commit
49b86438bb
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user