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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user