mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Enhanced UI for host operating systems to include additional information for Windows and macOS (#7201)
This commit is contained in:
parent
7c793fdc7d
commit
8d4ad6ce9f
13 changed files with 353 additions and 112 deletions
|
|
@ -2,6 +2,7 @@
|
|||
kernel version.
|
||||
- Updated `GET /os_versions` endpoint to include new request and response fields.
|
||||
- Updated `GET /hosts` endpoint to add `operating_system_id` query parameter.
|
||||
- Enhanced UI for host operating systems to include additional information for Windows and macOS.
|
||||
|
||||
Note that the operating systems data and the aggregated stats are updated lazily to prevent a long
|
||||
database migration when upgrading to this Fleet version - the data will be updated as hosts send
|
||||
|
|
|
|||
|
|
@ -20,5 +20,12 @@ describe("Dashboard", () => {
|
|||
});
|
||||
cy.getAttached(".operating-systems").should("exist");
|
||||
});
|
||||
it("displays operating systems card if Windows platform is selected", () => {
|
||||
cy.getAttached(".homepage__platform_dropdown").click();
|
||||
cy.getAttached(".Select-menu-outer").within(() => {
|
||||
cy.findAllByText("Windows").click();
|
||||
});
|
||||
cy.getAttached(".operating-systems").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
export interface IOperatingSystemVersion {
|
||||
id: number;
|
||||
os_id: number;
|
||||
name: string;
|
||||
name_only: string;
|
||||
version: string;
|
||||
platform: string;
|
||||
hosts_count: number;
|
||||
}
|
||||
|
||||
export const OS_VENDOR_BY_PLATFORM: Record<string, string> = {
|
||||
darwin: "Apple",
|
||||
windows: "Microsoft",
|
||||
} as const;
|
||||
|
||||
export const OS_END_OF_LIFE_LINK_BY_PLATFORM: Record<string, string> = {
|
||||
darwin: "https://endoflife.date/macos",
|
||||
windows: "https://endoflife.date/windows",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -304,8 +304,8 @@ const Homepage = (): JSX.Element => {
|
|||
<OperatingSystems
|
||||
currentTeamId={currentTeam?.id}
|
||||
selectedPlatform={selectedPlatform as IOsqueryPlatform}
|
||||
setShowOperatingSystemsUI={setShowOperatingSystemsUI}
|
||||
showOperatingSystemsUI={showOperatingSystemsUI}
|
||||
showTitle={showOperatingSystemsUI}
|
||||
setShowTitle={setShowOperatingSystemsUI}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
@ -336,7 +336,9 @@ const Homepage = (): JSX.Element => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const windowsLayout = () => null;
|
||||
const windowsLayout = () => (
|
||||
<div className={`${baseClass}__section`}>{OperatingSystemsCard}</div>
|
||||
);
|
||||
const linuxLayout = () => null;
|
||||
|
||||
const renderCards = () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import {
|
||||
OS_END_OF_LIFE_LINK_BY_PLATFORM,
|
||||
OS_VENDOR_BY_PLATFORM,
|
||||
} from "interfaces/operating_system";
|
||||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import operatingSystemsAPI, {
|
||||
IOperatingSystemsResponse,
|
||||
import {
|
||||
getOSVersions,
|
||||
IGetOSVersionsQueryKey,
|
||||
IOSVersionsResponse,
|
||||
OS_VERSIONS_API_SUPPORTED_PLATFORMS,
|
||||
} from "services/entities/operating_systems";
|
||||
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
|
||||
|
||||
|
|
@ -12,19 +19,19 @@ import Spinner from "components/Spinner";
|
|||
import TableDataError from "components/DataError";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
|
||||
import ExternalURLIcon from "../../../../../assets/images/icon-external-url-12x12@2x.png";
|
||||
|
||||
import generateTableHeaders from "./OperatingSystemsTableConfig";
|
||||
|
||||
interface IOperatingSystemsCardProps {
|
||||
currentTeamId: number | undefined;
|
||||
selectedPlatform: IOsqueryPlatform;
|
||||
showOperatingSystemsUI: boolean;
|
||||
setShowOperatingSystemsUI: (showOperatingSystemsTitle: boolean) => void;
|
||||
showTitle: boolean;
|
||||
setShowTitle: (showTitle: boolean) => void;
|
||||
setTitleDetail?: (content: JSX.Element | string | null) => void;
|
||||
setTitleDescription?: (content: JSX.Element | string | null) => void;
|
||||
}
|
||||
|
||||
// TODO: add platforms to this constant as new ones are supported
|
||||
const OS_API_SUPPORTED_PLATFORMS: IOsqueryPlatform[] = ["darwin"];
|
||||
|
||||
const DEFAULT_SORT_DIRECTION = "desc";
|
||||
const DEFAULT_SORT_HEADER = "hosts_count";
|
||||
const PAGE_SIZE = 8;
|
||||
|
|
@ -32,9 +39,9 @@ const baseClass = "operating-systems";
|
|||
|
||||
const EmptyOperatingSystems = (platform: IOsqueryPlatform): JSX.Element => (
|
||||
<div className={`${baseClass}__empty-os`}>
|
||||
<h1>{`No ${
|
||||
PLATFORM_DISPLAY_NAMES[platform] || "supported"
|
||||
} operating systems detected`}</h1>
|
||||
<h1>{`No${
|
||||
` ${PLATFORM_DISPLAY_NAMES[platform]}` || ""
|
||||
} operating systems detected.`}</h1>
|
||||
<p>
|
||||
{`Did you add ${`${PLATFORM_DISPLAY_NAMES[platform]} ` || ""}hosts to
|
||||
Fleet? Try again in about an hour as the system catches up.`}
|
||||
|
|
@ -45,66 +52,92 @@ const EmptyOperatingSystems = (platform: IOsqueryPlatform): JSX.Element => (
|
|||
const OperatingSystems = ({
|
||||
currentTeamId,
|
||||
selectedPlatform,
|
||||
showOperatingSystemsUI,
|
||||
setShowOperatingSystemsUI,
|
||||
showTitle,
|
||||
setShowTitle,
|
||||
setTitleDetail,
|
||||
setTitleDescription,
|
||||
}: IOperatingSystemsCardProps): JSX.Element => {
|
||||
const { data: osInfo, error: errorOS, isFetching } = useQuery<
|
||||
IOperatingSystemsResponse,
|
||||
const { data: osInfo, error, isFetching } = useQuery<
|
||||
IOSVersionsResponse,
|
||||
Error,
|
||||
IOperatingSystemsResponse,
|
||||
Array<{
|
||||
scope: string;
|
||||
platform: IOsqueryPlatform;
|
||||
teamId: number | undefined;
|
||||
}>
|
||||
IOSVersionsResponse,
|
||||
IGetOSVersionsQueryKey[]
|
||||
>(
|
||||
[
|
||||
{
|
||||
scope: "os_version",
|
||||
scope: "os_versions",
|
||||
platform: selectedPlatform,
|
||||
teamId: currentTeamId,
|
||||
},
|
||||
],
|
||||
({ queryKey: [{ platform, teamId }] }) => {
|
||||
return operatingSystemsAPI.getVersions({
|
||||
return getOSVersions({
|
||||
platform,
|
||||
teamId,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: OS_API_SUPPORTED_PLATFORMS.includes(selectedPlatform),
|
||||
enabled: OS_VERSIONS_API_SUPPORTED_PLATFORMS.includes(selectedPlatform),
|
||||
staleTime: 10000,
|
||||
keepPreviousData: true,
|
||||
onSuccess: (data) => {
|
||||
setShowOperatingSystemsUI(true);
|
||||
setTitleDetail &&
|
||||
setTitleDetail(
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={data.counts_updated_at}
|
||||
whatToRetrieve={"operating systems"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
setShowOperatingSystemsUI(true);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const description =
|
||||
OS_VENDOR_BY_PLATFORM[selectedPlatform] &&
|
||||
OS_END_OF_LIFE_LINK_BY_PLATFORM[selectedPlatform] ? (
|
||||
<p>
|
||||
{OS_VENDOR_BY_PLATFORM[selectedPlatform]} releases updates and fixes for
|
||||
supported operating systems.{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={OS_END_OF_LIFE_LINK_BY_PLATFORM[selectedPlatform]}
|
||||
>
|
||||
See supported operating systems <img src={ExternalURLIcon} alt="" />
|
||||
</a>
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
const titleDetail = osInfo?.counts_updated_at ? (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={osInfo?.counts_updated_at}
|
||||
whatToRetrieve={"operating systems"}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetching) {
|
||||
setShowTitle(false);
|
||||
setTitleDescription?.(null);
|
||||
setTitleDetail?.(null);
|
||||
return;
|
||||
}
|
||||
setShowTitle(true);
|
||||
if (osInfo?.os_versions?.length) {
|
||||
setTitleDescription?.(description);
|
||||
setTitleDetail?.(titleDetail);
|
||||
return;
|
||||
}
|
||||
setTitleDescription?.(null);
|
||||
setTitleDetail?.(null);
|
||||
}, [isFetching, osInfo, setTitleDescription, setTitleDetail]);
|
||||
|
||||
const tableHeaders = generateTableHeaders();
|
||||
const showPaginationControls = (osInfo?.os_versions?.length || 0) > 8;
|
||||
|
||||
// Renders opaque information as host information is loading
|
||||
const opacity = showOperatingSystemsUI ? { opacity: 1 } : { opacity: 0 };
|
||||
const opacity = isFetching || !showTitle ? { opacity: 0 } : { opacity: 1 };
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{!showOperatingSystemsUI && !errorOS && (
|
||||
{isFetching && (
|
||||
<div className="spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div style={opacity}>
|
||||
{errorOS ? (
|
||||
{error ? (
|
||||
<TableDataError card />
|
||||
) : (
|
||||
<TableContainer
|
||||
|
|
@ -120,8 +153,10 @@ const OperatingSystems = ({
|
|||
isAllPagesSelected={false}
|
||||
disableCount
|
||||
disableActionButton
|
||||
isClientSidePagination
|
||||
isClientSidePagination={showPaginationControls}
|
||||
disablePagination={!showPaginationControls}
|
||||
pageSize={PAGE_SIZE}
|
||||
highlightOnHover
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
import Chevron from "../../../../../assets/images/icon-chevron-right-blue-16x16@2x.png";
|
||||
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
value: string;
|
||||
|
|
@ -32,12 +34,19 @@ interface IDataColumn {
|
|||
disableSortBy?: boolean;
|
||||
}
|
||||
|
||||
const osTableHeaders = [
|
||||
const defaultTableHeaders = [
|
||||
{
|
||||
title: "Name",
|
||||
Header: "Name",
|
||||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
accessor: "name_only",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
accessor: "version",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
},
|
||||
{
|
||||
|
|
@ -50,12 +59,32 @@ const osTableHeaders = [
|
|||
),
|
||||
disableSortBy: false,
|
||||
accessor: "hosts_count",
|
||||
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
return (
|
||||
<span className="hosts-cell__wrapper">
|
||||
<span className="hosts-cell__count">
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
</span>
|
||||
<span className="hosts-cell__link">
|
||||
<Link
|
||||
to={`${PATHS.MANAGE_HOSTS}?operating_system_id=${cellProps.row.original.os_id}`}
|
||||
className="hosts-link"
|
||||
>
|
||||
<span className="link-text">View all hosts</span>
|
||||
<img
|
||||
alt="link to hosts filtered by operating system ID"
|
||||
src={Chevron}
|
||||
/>
|
||||
</Link>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const generateTableHeaders = (): IDataColumn[] => {
|
||||
return osTableHeaders;
|
||||
return defaultTableHeaders;
|
||||
};
|
||||
|
||||
export default generateTableHeaders;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,8 @@
|
|||
margin-top: $pad-large;
|
||||
position: relative;
|
||||
|
||||
.data-table__wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.component__tabs-wrapper .table-container__header {
|
||||
display: none;
|
||||
}
|
||||
&__empty-os {
|
||||
margin: $pad-medium auto 0;
|
||||
margin: 0 auto;
|
||||
|
||||
h1 {
|
||||
font-size: $small;
|
||||
|
|
@ -24,40 +18,58 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
.data-table-container {
|
||||
.data-table__table {
|
||||
table-layout: fixed;
|
||||
|
||||
thead {
|
||||
.hosts_count__header {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
width: 84px;
|
||||
}
|
||||
}
|
||||
.data-table__wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
tbody {
|
||||
.name__cell,
|
||||
.version__cell {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.id__cell {
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
}
|
||||
.vulnerabilities__cell {
|
||||
img {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hosts-cell__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-link {
|
||||
color: $core-vibrant-blue;
|
||||
visibility: hidden;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
padding-right: $pad-xxsmall;
|
||||
}
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.hosts-link {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.count-loading {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
.count-error {
|
||||
color: $ui-error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,19 @@ const baseClass = "homepage-info-card";
|
|||
|
||||
const useInfoCard = ({
|
||||
title,
|
||||
description,
|
||||
description: defaultDescription,
|
||||
children,
|
||||
action,
|
||||
total_host_count,
|
||||
showTitle,
|
||||
showTitle = true,
|
||||
}: IInfoCardProps): JSX.Element => {
|
||||
const [actionLink, setActionURL] = useState<string | null>(null);
|
||||
const [titleDetail, setTitleDetail] = useState<JSX.Element | string | null>(
|
||||
null
|
||||
);
|
||||
const [description, setDescription] = useState<JSX.Element | string | null>(
|
||||
defaultDescription || null
|
||||
);
|
||||
|
||||
const renderAction = () => {
|
||||
if (action) {
|
||||
|
|
@ -77,6 +80,7 @@ const useInfoCard = ({
|
|||
if (React.isValidElement(child)) {
|
||||
child = React.cloneElement(child, {
|
||||
setTitleDetail,
|
||||
setTitleDescription: setDescription,
|
||||
setActionURL,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@
|
|||
&__section-description {
|
||||
font-size: $x-small;
|
||||
|
||||
p {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $x-small;
|
||||
color: $core-vibrant-blue;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ import hostsAPI, {
|
|||
import hostCountAPI, {
|
||||
IHostCountLoadOptions,
|
||||
} from "services/entities/host_count";
|
||||
import {
|
||||
getOSVersions,
|
||||
IGetOSVersionsQueryKey,
|
||||
IOSVersionsResponse,
|
||||
} from "services/entities/operating_systems";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
|
@ -32,6 +37,7 @@ import {
|
|||
import { IApiError } from "interfaces/errors";
|
||||
import { IHost } from "interfaces/host";
|
||||
import { ILabel, ILabelFormData } from "interfaces/label";
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { ITeam } from "interfaces/team";
|
||||
|
|
@ -262,6 +268,10 @@ const ManageHostsPage = ({
|
|||
queryParams?.software_id !== undefined
|
||||
? parseInt(queryParams?.software_id, 10)
|
||||
: undefined;
|
||||
const operatingSystemId =
|
||||
queryParams?.operating_system_id !== undefined
|
||||
? parseInt(queryParams?.operating_system_id, 10)
|
||||
: undefined;
|
||||
const { active_label: activeLabel, label_id: labelID } = routeParams;
|
||||
|
||||
// ===== filter matching
|
||||
|
|
@ -356,6 +366,17 @@ const ManageHostsPage = ({
|
|||
}
|
||||
);
|
||||
|
||||
const { data: osVersions } = useQuery<
|
||||
IOSVersionsResponse,
|
||||
Error,
|
||||
IOperatingSystemVersion[],
|
||||
IGetOSVersionsQueryKey[]
|
||||
>([{ scope: "os_versions" }], () => getOSVersions(), {
|
||||
enabled: !!queryParams?.operating_system_id,
|
||||
keepPreviousData: true,
|
||||
select: (data) => data.os_versions,
|
||||
});
|
||||
|
||||
const toggleDeleteSecretModal = () => {
|
||||
// open and closes delete modal
|
||||
setShowDeleteSecretModal(!showDeleteSecretModal);
|
||||
|
|
@ -454,6 +475,10 @@ const ManageHostsPage = ({
|
|||
options.teamId = queryParams.team_id;
|
||||
}
|
||||
|
||||
if (queryParams.operating_system_id) {
|
||||
options.operatingSystemId = queryParams.operating_system_id;
|
||||
}
|
||||
|
||||
try {
|
||||
const { count: returnedHostCount } = await hostCountAPI.load(options);
|
||||
setFilteredHostCount(returnedHostCount);
|
||||
|
|
@ -509,6 +534,7 @@ const ManageHostsPage = ({
|
|||
policyId,
|
||||
policyResponse,
|
||||
softwareId,
|
||||
operatingSystemId,
|
||||
page: tableQueryData ? tableQueryData.pageIndex : 0,
|
||||
perPage: tableQueryData ? tableQueryData.pageSize : 100,
|
||||
device_mapping: true,
|
||||
|
|
@ -602,6 +628,17 @@ const ManageHostsPage = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleClearOSFilter = () => {
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_HOSTS,
|
||||
routeTemplate,
|
||||
routeParams,
|
||||
queryParams: omit(queryParams, ["operating_system_id"]),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearSoftwareFilter = () => {
|
||||
router.replace(PATHS.MANAGE_HOSTS);
|
||||
setCurrentTeam(undefined);
|
||||
|
|
@ -735,7 +772,9 @@ const ManageHostsPage = ({
|
|||
if (softwareId && !policyId) {
|
||||
newQueryParams.software_id = softwareId;
|
||||
}
|
||||
|
||||
if (operatingSystemId && !softwareId && !policyId) {
|
||||
newQueryParams.operating_system_id = operatingSystemId;
|
||||
}
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_HOSTS,
|
||||
|
|
@ -752,6 +791,7 @@ const ManageHostsPage = ({
|
|||
policyId,
|
||||
queryParams,
|
||||
softwareId,
|
||||
operatingSystemId,
|
||||
sortBy,
|
||||
]
|
||||
);
|
||||
|
|
@ -1063,6 +1103,53 @@ const ManageHostsPage = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const renderOSFilterBlock = () => {
|
||||
const os = osVersions?.find((v) => v.os_id === operatingSystemId);
|
||||
if (!os) {
|
||||
return <></>;
|
||||
}
|
||||
const { name, name_only, version } = os;
|
||||
const buttonText =
|
||||
name_only || version
|
||||
? `${name_only || ""} ${version || ""}`
|
||||
: `${name || ""}`;
|
||||
return (
|
||||
<div className={`${baseClass}__software-filter-block`}>
|
||||
<div>
|
||||
<span
|
||||
data-tip
|
||||
data-for="software-filter-tooltip"
|
||||
data-tip-disable={!name_only || !version || !name}
|
||||
>
|
||||
<div className={`${baseClass}__software-filter-name-card tooltip`}>
|
||||
{buttonText}
|
||||
<Button
|
||||
className={`${baseClass}__clear-policies-filter`}
|
||||
onClick={handleClearOSFilter}
|
||||
variant={"small-text-icon"}
|
||||
title={buttonText}
|
||||
>
|
||||
<img src={CloseIcon} alt="Remove os filter" />
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id="software-filter-tooltip"
|
||||
data-html
|
||||
>
|
||||
<span className={`tooltip__tooltip-text`}>
|
||||
{`Hosts with ${name_only || name}`},<br />
|
||||
{version && `${version} installed`}
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPoliciesFilterBlock = () => (
|
||||
<div className={`${baseClass}__policies-filter-block`}>
|
||||
<PoliciesFilter
|
||||
|
|
@ -1384,7 +1471,7 @@ const ManageHostsPage = ({
|
|||
selectedLabel &&
|
||||
selectedLabel.type !== "all" &&
|
||||
selectedLabel.type !== "status";
|
||||
if (policyId || softwareId || showSelectedLabel) {
|
||||
if (policyId || softwareId || showSelectedLabel || operatingSystemId) {
|
||||
return (
|
||||
<div className={`${baseClass}__labels-active-filter-wrap`}>
|
||||
{showSelectedLabel && renderHeaderLabelBlock()}
|
||||
|
|
@ -1396,6 +1483,11 @@ const ManageHostsPage = ({
|
|||
!policyId &&
|
||||
!showSelectedLabel &&
|
||||
renderSoftwareFilterBlock()}
|
||||
{!!operatingSystemId &&
|
||||
!policyId &&
|
||||
!softwareId &&
|
||||
!showSelectedLabel &&
|
||||
renderOSFilterBlock()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1495,14 +1587,18 @@ const ManageHostsPage = ({
|
|||
!isHostsLoading &&
|
||||
teamSync
|
||||
) {
|
||||
const { software_id, policy_id } = queryParams || {};
|
||||
const includesSoftwareOrPolicyFilter = !!(software_id || policy_id);
|
||||
const { software_id, policy_id, operating_system_id } = queryParams || {};
|
||||
const includesSoftwareOrPolicyOrOSFilter = !!(
|
||||
software_id ||
|
||||
policy_id ||
|
||||
operating_system_id
|
||||
);
|
||||
|
||||
return (
|
||||
<NoHosts
|
||||
toggleAddHostsModal={toggleAddHostsModal}
|
||||
canEnrollHosts={canEnrollHosts}
|
||||
includesSoftwareOrPolicyFilter={includesSoftwareOrPolicyFilter}
|
||||
includesSoftwareOrPolicyFilter={includesSoftwareOrPolicyOrOSFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface IHostCountLoadOptions {
|
|||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
softwareId?: number;
|
||||
operatingSystemId?: number;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
@ -29,6 +30,7 @@ export default {
|
|||
const policyResponse = options?.policyResponse || null;
|
||||
const selectedLabels = options?.selectedLabels || [];
|
||||
const softwareId = options?.softwareId || null;
|
||||
const operatingSystemId = options?.operatingSystemId || null;
|
||||
|
||||
const labelPrefix = "labels/";
|
||||
|
||||
|
|
@ -68,6 +70,10 @@ export default {
|
|||
queryString += `&software_id=${softwareId}`;
|
||||
}
|
||||
|
||||
if (!label && !policyId && !softwareId && operatingSystemId) {
|
||||
queryString += `&operating_system_id=${operatingSystemId}`;
|
||||
}
|
||||
|
||||
// Append query string to endpoint route after slicing off the leading ampersand
|
||||
const path = `${HOSTS_COUNT}${queryString && `?${queryString.slice(1)}`}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface ILoadHostsOptions {
|
|||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
softwareId?: number;
|
||||
operatingSystemId?: number;
|
||||
device_mapping?: boolean;
|
||||
columns?: string;
|
||||
visibleColumns?: string;
|
||||
|
|
@ -34,6 +35,7 @@ export interface IExportHostsOptions {
|
|||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
softwareId?: number;
|
||||
operatingSystemId?: number;
|
||||
device_mapping?: boolean;
|
||||
columns?: string;
|
||||
visibleColumns?: string;
|
||||
|
|
@ -104,6 +106,19 @@ const getSoftwareParam = (
|
|||
return label === undefined && policyId === undefined ? softwareId : undefined;
|
||||
};
|
||||
|
||||
const getOperatingSystemParam = (
|
||||
label?: string,
|
||||
policyId?: number,
|
||||
softwareId?: number,
|
||||
operatingSystemId?: number
|
||||
) => {
|
||||
return label === undefined &&
|
||||
policyId === undefined &&
|
||||
softwareId === undefined
|
||||
? operatingSystemId
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export default {
|
||||
destroy: (host: IHost) => {
|
||||
const { HOSTS } = endpoints;
|
||||
|
|
@ -202,6 +217,7 @@ export default {
|
|||
policyId,
|
||||
policyResponse = "passing",
|
||||
softwareId,
|
||||
operatingSystemId,
|
||||
device_mapping,
|
||||
selectedLabels,
|
||||
sortBy,
|
||||
|
|
@ -221,6 +237,13 @@ export default {
|
|||
policy_id: policyParams.policy_id,
|
||||
policy_response: policyParams.policy_response,
|
||||
software_id: getSoftwareParam(label, policyId, softwareId),
|
||||
operating_system_id: getOperatingSystemParam(
|
||||
label,
|
||||
policyId,
|
||||
softwareId,
|
||||
operatingSystemId
|
||||
),
|
||||
|
||||
status: getStatusParam(selectedLabels),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,30 +5,39 @@ import { IOperatingSystemVersion } from "interfaces/operating_system";
|
|||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
export interface IOperatingSystemsResponse {
|
||||
// TODO: add platforms to this constant as new ones are supported
|
||||
export const OS_VERSIONS_API_SUPPORTED_PLATFORMS: IOsqueryPlatform[] = [
|
||||
"darwin",
|
||||
"windows",
|
||||
];
|
||||
|
||||
export interface IGetOSVersionsRequest {
|
||||
id?: number;
|
||||
platform?: IOsqueryPlatform;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export interface IGetOSVersionsQueryKey extends IGetOSVersionsRequest {
|
||||
scope: string;
|
||||
}
|
||||
export interface IOSVersionsResponse {
|
||||
counts_updated_at: string;
|
||||
os_versions: IOperatingSystemVersion[];
|
||||
}
|
||||
|
||||
interface IGetVersionParams {
|
||||
platform: IOsqueryPlatform;
|
||||
teamId?: number;
|
||||
}
|
||||
export const getOSVersions = async ({
|
||||
id,
|
||||
platform,
|
||||
teamId,
|
||||
}: IGetOSVersionsRequest = {}): Promise<IOSVersionsResponse> => {
|
||||
const { OS_VERSIONS } = endpoints;
|
||||
const queryParams = { id, platform, team_id: teamId };
|
||||
const queryString = buildQueryStringFromParams(queryParams);
|
||||
const path = `${OS_VERSIONS}?${queryString}`;
|
||||
|
||||
return sendRequest("GET", path);
|
||||
};
|
||||
|
||||
export default {
|
||||
getVersions: async ({
|
||||
platform,
|
||||
teamId,
|
||||
}: IGetVersionParams): Promise<IOperatingSystemsResponse> => {
|
||||
const { OS_VERSIONS } = endpoints;
|
||||
const queryParams = { platform, team_id: teamId };
|
||||
const queryString = buildQueryStringFromParams(queryParams);
|
||||
const path = `${OS_VERSIONS}?${queryString}`;
|
||||
|
||||
try {
|
||||
return sendRequest("GET", path);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getOSVersions,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue