Add platform filtering to Software > OS table (#21797)

#20385

See notes on that issue for API limitations (which is why Windows and
macOS are the only platforms listed).

Will move out of draft after adding the changes file and tests.

# 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/Committing-Changes.md#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman
2024-10-16 10:02:06 -05:00
committed by GitHub
parent 43060bfdc8
commit d0040e7622
10 changed files with 115 additions and 5 deletions
+1
View File
@@ -0,0 +1 @@
Allow filtering Software OS view to show only OSes from a particular platform (Windows, macOS, Linux, etc.)
@@ -11,6 +11,7 @@ import {
import TableDataError from "components/DataError";
import Spinner from "components/Spinner";
import { SelectedPlatform } from "interfaces/platform";
import SoftwareOSTable from "./SoftwareOSTable";
@@ -25,6 +26,7 @@ interface ISoftwareOSProps {
currentPage: number;
teamId?: number;
resetPageIndex: boolean;
platform: SelectedPlatform;
}
const SoftwareOS = ({
@@ -36,12 +38,14 @@ const SoftwareOS = ({
currentPage,
teamId,
resetPageIndex,
platform,
}: ISoftwareOSProps) => {
const queryParams = {
page: currentPage,
per_page: perPage,
order_direction: orderDirection,
order_key: orderKey,
platform: platform === "all" ? undefined : platform,
teamId,
};
@@ -85,6 +89,7 @@ const SoftwareOS = ({
teamId={teamId}
isLoading={isFetching}
resetPageIndex={resetPageIndex}
platform={platform}
/>
</div>
);
@@ -64,6 +64,7 @@ describe("Software operating systems table", () => {
screen.getByText("No operating systems detected")
).toBeInTheDocument();
expect(screen.getByText("0 items")).toBeInTheDocument();
expect(screen.getByText("All platforms")).toBeInTheDocument();
expect(screen.queryByText("Search")).toBeNull();
expect(screen.queryByText("Updated")).toBeNull();
});
@@ -8,6 +8,8 @@ import PATHS from "router/paths";
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import CustomLink from "components/CustomLink";
import TableContainer from "components/TableContainer";
import LastUpdatedText from "components/LastUpdatedText";
@@ -20,6 +22,7 @@ import { IOSVersionsResponse } from "services/entities/operating_systems";
import generateTableConfig from "pages/DashboardPage/cards/OperatingSystems/OSTableConfig";
import { buildQueryStringFromParams } from "utilities/url";
import { getNextLocationPath } from "utilities/helpers";
import { SelectedPlatform } from "interfaces/platform";
const baseClass = "software-os-table";
@@ -40,8 +43,47 @@ interface ISoftwareOSTableProps {
teamId?: number;
isLoading: boolean;
resetPageIndex: boolean;
platform?: SelectedPlatform;
}
const PLATFORM_FILTER_OPTIONS = [
{
disabled: false,
label: "All platforms",
value: "all",
},
{
disabled: false,
label: "macOS",
value: "darwin",
},
{
disabled: false,
label: "Windows",
value: "windows",
},
{
disabled: false,
label: "Linux",
value: "linux",
},
{
disabled: false,
label: "ChromeOS",
value: "chrome",
},
{
disabled: false,
label: "iOS",
value: "ios",
},
{
disabled: false,
label: "iPadOS",
value: "ipados",
},
];
const SoftwareOSTable = ({
router,
isSoftwareEnabled,
@@ -53,6 +95,7 @@ const SoftwareOSTable = ({
teamId,
isLoading,
resetPageIndex,
platform,
}: ISoftwareOSTableProps) => {
const determineQueryParamChange = useCallback(
(newTableQuery: ITableQueryData) => {
@@ -64,13 +107,15 @@ const SoftwareOSTable = ({
return val !== orderKey;
case "pageIndex":
return val !== currentPage;
case "platform":
return val !== platform;
default:
return false;
}
});
return changedEntry?.[0] ?? "";
},
[currentPage, orderDirection, orderKey]
[platform, currentPage, orderDirection, orderKey]
);
const generateNewQueryParams = useCallback(
@@ -92,7 +137,7 @@ const SoftwareOSTable = ({
const changedParam = determineQueryParamChange(newTableQuery);
// if nothing has changed, don't update the route. this can happen when
// this handler is called on the inital render.
// this handler is called on the initial render.
if (changedParam === "") return;
const newRoute = getNextLocationPath({
@@ -163,6 +208,34 @@ const SoftwareOSTable = ({
);
};
const handlePlatformFilterDropdownChange = (platformSelected: string) => {
router?.replace(
getNextLocationPath({
pathPrefix: PATHS.SOFTWARE_OS,
queryParams: {
team_id: teamId,
order_direction: orderDirection,
order_key: orderKey,
page: 0,
platform: platformSelected,
},
})
);
};
const renderPlatformDropdown = () => {
return (
<Dropdown
value={platform || "all"}
className={`${baseClass}__platform-dropdown`}
options={PLATFORM_FILTER_OPTIONS}
searchable={false}
onChange={handlePlatformFilterDropdownChange}
tableFilterDropdown
/>
);
};
return (
<div className={baseClass}>
<TableContainer
@@ -184,6 +257,8 @@ const SoftwareOSTable = ({
pageSize={perPage}
showMarkAllPages={false}
isAllPagesSelected={false}
customControl={renderPlatformDropdown}
customFiltersButton={() => <></>}
disableNextPage={!data?.meta.has_next_results}
searchable={false}
onQueryChange={onQueryChange}
@@ -1,4 +1,17 @@
.software-os-table {
&__platform-dropdown {
width: 175px;
.Select-menu-outer {
width: 160px;
max-height: min-content;
.Select-menu {
max-height: none;
}
}
}
.hosts-cell__wrapper {
display: flex;
align-items: center;
@@ -14,6 +14,7 @@ import {
IZendeskJiraIntegrations,
} from "interfaces/integration";
import { APP_CONTEXT_ALL_TEAMS_ID, ITeamConfig } from "interfaces/team";
import { SelectedPlatform } from "interfaces/platform";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
import configAPI from "services/entities/config";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
@@ -114,6 +115,7 @@ interface ISoftwarePageProps {
query?: string;
order_key?: string;
order_direction?: "asc" | "desc";
platform?: SelectedPlatform;
};
hash?: string;
};
@@ -148,6 +150,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
queryParams && queryParams.page
? parseInt(queryParams.page, 10)
: DEFAULT_PAGE;
const platform = queryParams?.platform || "all";
// TODO: move these down into the Software Titles component.
const query = queryParams && queryParams.query ? queryParams.query : "";
const showExploitedVulnerabilitiesOnly =
@@ -436,6 +439,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
currentPage: page,
teamId: teamIdForApi,
// TODO: move down into the Software Titles component
platform,
query,
showExploitedVulnerabilitiesOnly,
softwareFilter,
@@ -370,7 +370,7 @@ const SoftwareTable = ({
inputPlaceHolder="Search by name or vulnerability (CVE)"
onQueryChange={onQueryChange}
// additionalQueries serves as a trigger for the useDeepEffect hook
// to fire onQueryChange for events happeing outside of
// to fire onQueryChange for events happening outside of
// the TableContainer.
// additionalQueries={softwareFilter}
customControl={showFilterHeaders ? renderCustomControls : undefined}
@@ -148,7 +148,7 @@ const QueriesTable = ({
queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)();
// Source of truth is state held within TableContainer. That state is initialized using URL
// params, then subsquent updates to that state are pushed to the URL.
// params, then subsequent updates to that state are pushed to the URL.
const searchQuery = initialSearchQuery;
const platform = initialPlatform;
const page = initialPage;
+1 -1
View File
@@ -4691,7 +4691,7 @@ func (ds *Datastore) OSVersions(
// filter by platform, name, and version
var filtered []fleet.OSVersion
for _, os := range counts {
if (platform == nil || *platform == os.Platform) && (name == nil || version == nil || (*name == os.NameOnly && *version == os.Version)) {
if (platform == nil || *platform == os.Platform || (*platform == "linux" && fleet.IsLinux(os.Platform))) && (name == nil || version == nil || (*name == os.NameOnly && *version == os.Version)) {
filtered = append(filtered, os)
}
}
+11
View File
@@ -6392,6 +6392,17 @@ func testOSVersions(t *testing.T, ds *Datastore) {
}
require.Equal(t, expected, osVersions.OSVersions)
// filter by Linux pseudo-platform
platform = "linux"
osVersions, err = ds.OSVersions(ctx, nil, &platform, nil, nil)
require.NoError(t, err)
expected = []fleet.OSVersion{
{HostsCount: 1, Name: "CentOS 8.0.0", NameOnly: "CentOS", Version: "8.0.0", Platform: "rhel", OSVersionID: 4},
{HostsCount: 2, Name: "Ubuntu 20.4.0", NameOnly: "Ubuntu", Version: "20.4.0", Platform: "ubuntu", OSVersionID: 5},
}
require.Equal(t, expected, osVersions.OSVersions)
// filter by operating system name and version
osVersions, err = ds.OSVersions(ctx, nil, nil, ptr.String("Ubuntu"), ptr.String("20.4.0"))
require.NoError(t, err)