-
Notifications
You must be signed in to change notification settings - Fork 1k
Make OS tables sortable by version #53013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ce8dbd1
Make OS tables sortable by version
kevinmcox e6ff4db
Reset Software > OS table's sort indicator on platform change
kevinmcox 430de2e
Disable OS version sort when viewing all platforms
kevinmcox c7e2f30
Merge remote-tracking branch 'upstream/main' into os-version-sort
kevinmcox 7468217
Update rest-api.md
kevinmcox 31f76d3
Merge remote-tracking branch 'upstream/main' into os-version-sort
kevinmcox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Made the Version column sortable on the dashboard "Operating systems" card and the Software > OS page once a specific platform is selected, instead of sorting only by host count. Versions sort numerically by segment (so, for example, "26.10" sorts after "26.6"), Windows feature-update codenames (e.g. "22H1") sort by year and half, and Ubuntu LTS releases (e.g. "22.04.9 LTS") sort numerically too, rather than as plain strings. Since comparing versions across different platforms isn't meaningful, sorting by version is disabled while viewing all platforms together (which still sorts by host count), and groups results by platform (most hosts first) for the one remaining case where a platform filter can still span multiple platforms (e.g. the dashboard's Linux tab). Added `version` as a valid `order_key` for `GET /api/v1/fleet/os_versions`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
frontend/pages/DashboardPage/cards/OperatingSystems/OSTableConfig.tests.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { IOperatingSystemVersion } from "interfaces/operating_system"; | ||
|
|
||
| import generateTableHeaders, { | ||
| compareOSTableVersions, | ||
| compareOSVersionStrings, | ||
| } from "./OSTableConfig"; | ||
|
|
||
| const makeRow = ( | ||
| platform: string, | ||
| version: string | ||
| ): IOperatingSystemVersion => ({ | ||
| os_version_id: 1, | ||
| name: `${platform} ${version}`, | ||
| name_only: platform, | ||
| version, | ||
| platform, | ||
| hosts_count: 0, | ||
| vulnerabilities: [], | ||
| kernels: [], | ||
| }); | ||
|
|
||
| // react-table negates a sortType's return value whenever `desc` is true, on | ||
| // top of whatever the sortType itself returns. This simulates that so tests | ||
| // assert on what actually ends up rendered, not the raw pre-negation value. | ||
| const renderedOrder = ( | ||
| rowA: IOperatingSystemVersion, | ||
| rowB: IOperatingSystemVersion, | ||
| desc: boolean, | ||
| totals?: Record<string, number> | ||
| ) => { | ||
| const raw = compareOSTableVersions(rowA, rowB, desc, totals); | ||
| return desc ? -raw : raw; | ||
| }; | ||
|
|
||
| describe("compareOSTableVersions", () => { | ||
| it("falls back to version comparison within the same platform, flipping with direction", () => { | ||
| const a = makeRow("darwin", "26.6"); | ||
| const b = makeRow("darwin", "26.10"); | ||
| expect(renderedOrder(a, b, false)).toEqual(-1); | ||
| expect(renderedOrder(a, b, true)).toEqual(1); | ||
| }); | ||
|
|
||
| it("orders platform groups by host total, most hosts first, regardless of direction", () => { | ||
| // Windows has fewer hosts but a "lower" version number than darwin — | ||
| // grouping should still put windows first because it has more hosts. | ||
| const windowsRow = makeRow("windows", "10.0.9200.100"); | ||
| const darwinRow = makeRow("darwin", "26.6"); | ||
| const totals = { windows: 3000, darwin: 200 }; | ||
|
|
||
| expect(renderedOrder(windowsRow, darwinRow, false, totals)).toBeLessThan(0); | ||
| expect(renderedOrder(darwinRow, windowsRow, false, totals)).toBeGreaterThan( | ||
| 0 | ||
| ); | ||
|
|
||
| // Direction toggle only flips within-group version order, not group | ||
| // order — windows must still sort first when desc is true. | ||
| expect(renderedOrder(windowsRow, darwinRow, true, totals)).toBeLessThan(0); | ||
| expect(renderedOrder(darwinRow, windowsRow, true, totals)).toBeGreaterThan( | ||
| 0 | ||
| ); | ||
| }); | ||
|
|
||
| it("falls back to comparing platform names when host totals tie, so groups stay together instead of relying on stable sort", () => { | ||
| const archRow = makeRow("arch", "rolling"); | ||
| const debianRow = makeRow("debian", "12"); | ||
| // No totals provided at all (both default to 0 — a tie). | ||
| const ascending = renderedOrder(archRow, debianRow, false); | ||
| const descending = renderedOrder(archRow, debianRow, true); | ||
|
|
||
| // Whatever the tiebreak order is, it must be consistent and stay the | ||
| // same regardless of direction — i.e. still deterministic, not a | ||
| // coincidence of input order. | ||
| expect(ascending).not.toEqual(0); | ||
| expect(ascending).toEqual(descending); | ||
| expect(renderedOrder(debianRow, archRow, false)).toEqual(-ascending); | ||
| }); | ||
| }); | ||
|
|
||
| describe("compareOSVersionStrings", () => { | ||
| it("compares numeric segments by magnitude, not lexically", () => { | ||
| expect(compareOSVersionStrings("26.6", "26.10")).toEqual(-1); | ||
| expect(compareOSVersionStrings("26.10", "26.6")).toEqual(1); | ||
| expect(compareOSVersionStrings("10.0.9200.100", "10.0.26200.8875")).toEqual( | ||
| -1 | ||
| ); | ||
| }); | ||
|
|
||
| it("compares Windows feature-update codenames by year and half", () => { | ||
| // The reason compareOSVersionStrings exists as its own function instead | ||
| // of reusing the shared frontend/utilities/helpers.tsx compareVersions: | ||
| // that helper doesn't understand this codename shape at all. | ||
| expect(compareOSVersionStrings("21H2", "22H1")).toEqual(-1); | ||
| expect(compareOSVersionStrings("22H1", "21H2")).toEqual(1); | ||
| expect(compareOSVersionStrings("22H1", "22H2")).toEqual(-1); | ||
| expect(compareOSVersionStrings("21H2", "21H2")).toEqual(0); | ||
| }); | ||
|
|
||
| it("treats a non-comparable version (e.g. Arch Linux's 'rolling') as older than any comparable version", () => { | ||
| expect(compareOSVersionStrings("rolling", "26.6")).toEqual(-1); | ||
| expect(compareOSVersionStrings("26.6", "rolling")).toEqual(1); | ||
| expect(compareOSVersionStrings("rolling", "rolling")).toEqual(0); | ||
| }); | ||
|
|
||
| it("strips Ubuntu's ' LTS' suffix before comparing numerically", () => { | ||
| // osquery's os_version table reports Ubuntu LTS releases with a literal | ||
| // " LTS" suffix (e.g. "22.04.9 LTS"), which Fleet stores verbatim. | ||
| // Without stripping it, these tie as non-comparable instead of | ||
| // comparing numerically. | ||
| expect(compareOSVersionStrings("22.04.9 LTS", "22.04.15 LTS")).toEqual(-1); | ||
| expect(compareOSVersionStrings("22.04.15 LTS", "22.04.9 LTS")).toEqual(1); | ||
| expect(compareOSVersionStrings("22.04.9 lts", "22.04.15 LTS")).toEqual(-1); | ||
| }); | ||
|
|
||
| it("treats strings that only coerce to a number via JS's loose Number() as non-comparable, not as valid segments", () => { | ||
| // Number("") === 0, Number("1.") === 1, Number(".1") === 0.1, etc. — a | ||
| // naive `.split(".").map(Number)` would silently accept these as valid | ||
| // version segments, unlike the strict server-side regex this mirrors. | ||
| expect(compareOSVersionStrings("", "26.6")).toEqual(-1); | ||
| expect(compareOSVersionStrings("26.", "26.6")).toEqual(-1); | ||
| expect(compareOSVersionStrings(".26", "26.6")).toEqual(-1); | ||
| expect(compareOSVersionStrings("+1.2", "26.6")).toEqual(-1); | ||
| expect(compareOSVersionStrings("1e2", "26.6")).toEqual(-1); | ||
| }); | ||
| }); | ||
|
|
||
| describe("generateTableHeaders", () => { | ||
| const findVersionColumn = (disableVersionSort?: boolean) => | ||
| generateTableHeaders(undefined, undefined, { disableVersionSort }).find( | ||
| (column) => column.accessor === "version" | ||
| ); | ||
|
|
||
| it("disables the Version column's sort when disableVersionSort is set, so 'All platforms' can't trigger a cross-platform version sort", () => { | ||
| expect(findVersionColumn(true)?.disableSortBy).toEqual(true); | ||
| }); | ||
|
|
||
| it("leaves the Version column sortable once a specific platform is selected", () => { | ||
| expect(findVersionColumn(false)?.disableSortBy).toEqual(false); | ||
| expect(findVersionColumn(undefined)?.disableSortBy).toEqual(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.