UI – Add VPP features for iPadOS and iOS (#20755)
## Addresses #20467 – part 2 ### Aggregate software: #### Software titles <img width="1616" alt="sw-titles-updated" src="https://github.com/user-attachments/assets/0b9922c7-e36e-4d2f-b204-95c3cdf9b602"> #### Software versions <img width="1616" alt="Screenshot 2024-07-29 at 6 14 21 PM" src="https://github.com/user-attachments/assets/5a097700-cd6c-45b1-a21f-9d76a733f0ae"> #### Host software <img width="1616" alt="Screenshot 2024-07-29 at 6 23 01 PM" src="https://github.com/user-attachments/assets/84e18695-f47a-4022-bd53-7f5d37ce452a"> ### Add software modal (VPP) _screenshots use mocked data - UI is flexible enough to display cleanly before and after backend is in place:_ <img width="1339" alt="happy" src="https://github.com/user-attachments/assets/8900aa93-316c-4a09-8e5a-1a1e45b0c458"> #### No apps: <img width="1572" alt="Screenshot 2024-07-29 at 6 35 03 PM" src="https://github.com/user-attachments/assets/466b9b6c-4d3d-49dd-94a9-94e395d89cb7"> #### Not enabled: <img width="1572" alt="Screenshot 2024-07-29 at 6 37 45 PM" src="https://github.com/user-attachments/assets/9bcfd480-8741-4d95-ba3b-550dee4dc673"> #### Error: <img width="1572" alt="Screenshot 2024-07-29 at 6 39 39 PM" src="https://github.com/user-attachments/assets/e944dd40-676e-4aba-9cd9-49ff319bf402"> ### Vuln support – Not supported for now: _see above screenshots for `list` endpoints_ #### Software title detail <img width="1616" alt="Screenshot 2024-07-29 at 6 47 29 PM" src="https://github.com/user-attachments/assets/2e30fd0a-21e4-4d19-bf9b-71a994bfd0e7"> #### Software version and OS detail: <img width="1616" alt="Screenshot 2024-07-29 at 6 48 28 PM" src="https://github.com/user-attachments/assets/e8fec769-ba97-4b6b-b10c-9bb4c973c732"> <img width="1616" alt="Screenshot 2024-07-29 at 6 50 25 PM" src="https://github.com/user-attachments/assets/0ac15727-e0cb-447c-8758-c58b79656d1a"> - [x] Changes file added for user-visible changes in `changes/`, - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
co-authored by
Jacob Shandling
parent
9bb6ef15ae
commit
19a64941ba
@@ -0,0 +1 @@
|
||||
* Add UI features for managing Apple VPP apps for iPadOS and iOS hosts
|
||||
@@ -28,9 +28,10 @@ export const createMockVppInfo = (
|
||||
|
||||
const DEFAULT_MDM_APPLE_VPP_APP_MOCK: IVppApp = {
|
||||
name: "Test App",
|
||||
bundle_identifier: "com.test.app",
|
||||
icon_url: "https://via.placeholder.com/512",
|
||||
latest_version: "1.0",
|
||||
app_store_id: 1,
|
||||
app_store_id: "1",
|
||||
added: false,
|
||||
platform: "darwin",
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import classnames from "classnames";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Icon from "components/Icon";
|
||||
import Graphic from "components/Graphic";
|
||||
|
||||
const baseClass = "data-error";
|
||||
|
||||
@@ -14,6 +15,7 @@ interface IDataErrorProps {
|
||||
children?: React.ReactNode;
|
||||
card?: boolean;
|
||||
className?: string;
|
||||
useNew?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_DESCRIPTION = "Refresh the page or log in again.";
|
||||
@@ -24,8 +26,36 @@ const DataError = ({
|
||||
children,
|
||||
card,
|
||||
className,
|
||||
useNew = false,
|
||||
}: IDataErrorProps): JSX.Element => {
|
||||
const classes = classnames(baseClass, className);
|
||||
if (useNew) {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={`${baseClass}__${card ? "card" : "inner-new"}`}>
|
||||
<Graphic name="data-error" />
|
||||
<div className={`${baseClass}__header`}>
|
||||
Something's gone wrong.
|
||||
</div>
|
||||
{children || (
|
||||
<>
|
||||
<div className={`${baseClass}__data`}>Refresh to try again.</div>
|
||||
{!excludeIssueLink && (
|
||||
<div className={`${baseClass}__data`}>
|
||||
If this keeps happening please
|
||||
<CustomLink
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="file an issue"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
|
||||
@@ -32,4 +32,21 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// // // // // // // // // // // //
|
||||
// new version
|
||||
&__inner-new {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
.graphic {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
color: $core-fleet-black;
|
||||
font-size: $x-small;
|
||||
}
|
||||
&__header {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import React from "react";
|
||||
import { Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TextCell from "../TextCell";
|
||||
|
||||
interface ILinkCellProps {
|
||||
value: string | JSX.Element;
|
||||
value?: string | JSX.Element;
|
||||
path: string;
|
||||
className?: string;
|
||||
customOnClick?: (e: React.MouseEvent) => void;
|
||||
@@ -25,6 +26,10 @@ const LinkCell = ({
|
||||
title,
|
||||
tooltipContent,
|
||||
}: ILinkCellProps): JSX.Element => {
|
||||
// text cell with no value renders desired empty cell
|
||||
if (!value) {
|
||||
return <TextCell />;
|
||||
}
|
||||
const cellClasses = classnames(baseClass, className);
|
||||
|
||||
const onClick = (e: React.MouseEvent): void => {
|
||||
|
||||
@@ -51,14 +51,14 @@
|
||||
&__option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
margin-top: $pad-xsmall;
|
||||
font-size: $xx-small;
|
||||
white-space: normal;
|
||||
color: $core-fleet-blue;
|
||||
color: $ui-fleet-black-50;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,6 +18,7 @@ import EmptyTeams from "./EmptyTeams";
|
||||
import EmptyPacks from "./EmptyPacks";
|
||||
import EmptySchedule from "./EmptySchedule";
|
||||
import CollectingResults from "./CollectingResults";
|
||||
import DataError from "./DataError";
|
||||
|
||||
export const GRAPHIC_MAP = {
|
||||
// Empty state graphics
|
||||
@@ -43,6 +44,7 @@ export const GRAPHIC_MAP = {
|
||||
"file-vpp": FileVpp,
|
||||
// Other graphics
|
||||
"collecting-results": CollectingResults,
|
||||
"data-error": DataError,
|
||||
};
|
||||
|
||||
export type GraphicNames = keyof typeof GRAPHIC_MAP;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Platform } from "./platform";
|
||||
import { IPolicy } from "./policy";
|
||||
import { IQuery } from "./query";
|
||||
import { ISchedulableQueryStats } from "./schedulable_query";
|
||||
@@ -162,6 +163,7 @@ export interface IActivityDetails {
|
||||
stats?: ISchedulableQueryStats;
|
||||
software_title?: string;
|
||||
software_package?: string;
|
||||
platform?: Platform; // software platform
|
||||
status?: string;
|
||||
install_uuid?: string;
|
||||
self_service?: boolean;
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
export type AppleDisplayPlatform = "macOS" | "iOS" | "iPadOS";
|
||||
export type DisplayPlatform =
|
||||
| AppleDisplayPlatform
|
||||
| "Windows"
|
||||
| "Linux"
|
||||
| "ChromeOS";
|
||||
export const APPLE_PLATFORM_DISPLAY_NAMES = {
|
||||
darwin: "macOS",
|
||||
ios: "iOS",
|
||||
ipados: "iPadOS",
|
||||
} as const;
|
||||
|
||||
export type ApplePlatform = keyof typeof APPLE_PLATFORM_DISPLAY_NAMES;
|
||||
export type AppleDisplayPlatform = typeof APPLE_PLATFORM_DISPLAY_NAMES[keyof typeof APPLE_PLATFORM_DISPLAY_NAMES];
|
||||
|
||||
export const PLATFORM_DISPLAY_NAMES = {
|
||||
windows: "Windows",
|
||||
linux: "Linux",
|
||||
chrome: "ChromeOS",
|
||||
...APPLE_PLATFORM_DISPLAY_NAMES,
|
||||
} as const;
|
||||
|
||||
export type Platform = keyof typeof PLATFORM_DISPLAY_NAMES;
|
||||
export type DisplayPlatform = typeof PLATFORM_DISPLAY_NAMES[keyof typeof PLATFORM_DISPLAY_NAMES];
|
||||
export type QueryableDisplayPlatform = Exclude<
|
||||
DisplayPlatform,
|
||||
"iOS" | "iPadOS"
|
||||
>;
|
||||
|
||||
export type ApplePlatform = "darwin" | "ios" | "ipados";
|
||||
export type Platform = ApplePlatform | "windows" | "linux" | "chrome";
|
||||
export type QueryablePlatform = Exclude<Platform, "ios" | "ipados">;
|
||||
|
||||
export const SUPPORTED_PLATFORMS: QueryablePlatform[] = [
|
||||
@@ -20,6 +29,9 @@ export const SUPPORTED_PLATFORMS: QueryablePlatform[] = [
|
||||
"chrome",
|
||||
];
|
||||
|
||||
// TODO - add "iOS" and "iPadOS" once we support them
|
||||
export const VULN_SUPPORTED_PLATFORMS: Platform[] = ["darwin", "windows"];
|
||||
|
||||
export type SelectedPlatform = QueryablePlatform | "all";
|
||||
|
||||
export type SelectedPlatformString =
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface ISoftware {
|
||||
name: string; // e.g., "Figma.app"
|
||||
version: string; // e.g., "2.1.11"
|
||||
bundle_identifier?: string | null; // e.g., "com.figma.Desktop"
|
||||
source: string; // "apps" | "ipados" | "ios" | "programs" | ?
|
||||
source: string; // "apps" | "ipados_apps" | "ios_apps" | "programs" | ?
|
||||
generated_cpe: string;
|
||||
vulnerabilities: ISoftwareVulnerability[] | null;
|
||||
hosts_count?: number;
|
||||
@@ -148,8 +148,8 @@ export const SOURCE_TYPE_CONVERSION: Record<string, string> = {
|
||||
atom_packages: "Package (Atom)", // Atom packages were removed from software inventory. Mapping is maintained for backwards compatibility. (2023-12-04)
|
||||
python_packages: "Package (Python)",
|
||||
apps: "Application (macOS)",
|
||||
ios: "Application (iOS)",
|
||||
ipados: "Application (iPadOS)",
|
||||
ios_apps: "Application (iOS)",
|
||||
ipados_apps: "Application (iPadOS)",
|
||||
chrome_extensions: "Browser plugin", // chrome_extensions can include any chrome-based browser (e.g., edge), so we rely instead on the `browser` field computed by Fleet server and fallback to this value if it is not present.
|
||||
firefox_addons: "Browser plugin (Firefox)",
|
||||
safari_extensions: "Browser plugin (Safari)",
|
||||
|
||||
@@ -4,7 +4,10 @@ import { formatDistanceToNowStrict } from "date-fns";
|
||||
|
||||
import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity";
|
||||
import { getInstallStatusPredicate } from "interfaces/software";
|
||||
import { AppleDisplayPlatform } from "interfaces/platform";
|
||||
import {
|
||||
AppleDisplayPlatform,
|
||||
PLATFORM_DISPLAY_NAMES,
|
||||
} from "interfaces/platform";
|
||||
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
@@ -882,10 +885,13 @@ const TAGGED_TEMPLATES = {
|
||||
);
|
||||
},
|
||||
addedAppStoreApp: (activity: IActivity) => {
|
||||
const { software_title: swTitle, platform: swPlatform } =
|
||||
activity.details || {};
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
added <b>{activity.details?.software_title}</b> to{" "}
|
||||
added <b>{swTitle}</b>{" "}
|
||||
{swPlatform ? `(${PLATFORM_DISPLAY_NAMES[swPlatform]}) ` : ""}to{" "}
|
||||
{activity.details?.team_name ? (
|
||||
<>
|
||||
{" "}
|
||||
@@ -898,10 +904,13 @@ const TAGGED_TEMPLATES = {
|
||||
);
|
||||
},
|
||||
deletedAppStoreApp: (activity: IActivity) => {
|
||||
const { software_title: swTitle, platform: swPlatform } =
|
||||
activity.details || {};
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
deleted <b>{activity.details?.software_title}</b> from{" "}
|
||||
deleted <b>{swTitle}</b>{" "}
|
||||
{swPlatform ? `(${PLATFORM_DISPLAY_NAMES[swPlatform]}) ` : ""}from{" "}
|
||||
{activity.details?.team_name ? (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
@@ -108,7 +108,7 @@ const generateDefaultTableHeaders = (
|
||||
Cell: (cellProps: IVulnCellProps) => {
|
||||
const platform = cellProps.row.original.platform;
|
||||
if (platform !== "darwin" && platform !== "windows") {
|
||||
return <TextCell value="Not supported" grey italic />;
|
||||
return <TextCell value="Not supported" grey />;
|
||||
}
|
||||
return <VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { CONTACT_FLEET_LINK } from "utilities/constants";
|
||||
|
||||
import Button from "components/buttons/Button/Button";
|
||||
// @ts-ignore
|
||||
import StackedWhiteBoxes from "components/StackedWhiteBoxes";
|
||||
@@ -48,7 +50,7 @@ const NoAccessPage = ({ router, orgContactUrl }: INoAccessPageProps) => {
|
||||
<p>
|
||||
To get access,{" "}
|
||||
<CustomLink
|
||||
url={orgContactUrl || "https://fleetdm.com/contact"}
|
||||
url={orgContactUrl || CONTACT_FLEET_LINK}
|
||||
text="contact your administrator"
|
||||
/>
|
||||
.
|
||||
|
||||
@@ -11,64 +11,34 @@ import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { ignoreAxiosError } from "interfaces/errors";
|
||||
import {
|
||||
isLinuxLike,
|
||||
Platform,
|
||||
VULN_SUPPORTED_PLATFORMS,
|
||||
} from "interfaces/platform";
|
||||
|
||||
import osVersionsAPI, {
|
||||
IOSVersionResponse,
|
||||
IGetOsVersionQueryKey,
|
||||
} from "services/entities/operating_systems";
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { isLinuxLike } from "interfaces/platform";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS, SUPPORT_LINK } from "utilities/constants";
|
||||
import {
|
||||
DEFAULT_USE_QUERY_OPTIONS,
|
||||
PLATFORM_DISPLAY_NAMES,
|
||||
} from "utilities/constants";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import MainContent from "components/MainContent";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TeamsHeader from "components/TeamsHeader";
|
||||
import Card from "components/Card";
|
||||
|
||||
import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
|
||||
import SoftwareVulnerabilitiesTable from "../components/SoftwareVulnerabilitiesTable";
|
||||
import DetailsNoHosts from "../components/DetailsNoHosts";
|
||||
import { VulnsNotSupported } from "../components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable";
|
||||
|
||||
const baseClass = "software-os-details-page";
|
||||
|
||||
interface INotSupportedVulnProps {
|
||||
platform: string;
|
||||
}
|
||||
|
||||
const platformDisplayName = (platform: string) => {
|
||||
if (isLinuxLike(platform)) {
|
||||
return "Linux hosts";
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case "chrome":
|
||||
return "Chromebooks";
|
||||
case "ios":
|
||||
return "iPhones";
|
||||
case "ipados":
|
||||
return "iPads";
|
||||
default:
|
||||
return "this operating system";
|
||||
}
|
||||
};
|
||||
|
||||
const NotSupportedVuln = ({ platform }: INotSupportedVulnProps) => {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="Vulnerabilities are not supported for this type of host"
|
||||
info={
|
||||
<>
|
||||
Interested in vulnerability management for{" "}
|
||||
{platformDisplayName(platform)}?{" "}
|
||||
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoftwareOSDetailsRouteParams {
|
||||
id: string;
|
||||
team_id?: string;
|
||||
@@ -145,10 +115,13 @@ const SoftwareOSDetailsPage = ({
|
||||
}
|
||||
|
||||
if (
|
||||
osVersionDetails.platform !== "darwin" &&
|
||||
osVersionDetails.platform !== "windows"
|
||||
// TODO - detangle platform typing here
|
||||
!VULN_SUPPORTED_PLATFORMS.includes(osVersionDetails.platform as Platform)
|
||||
) {
|
||||
return <NotSupportedVuln platform={osVersionDetails.platform} />;
|
||||
const supportInterestText = isLinuxLike(osVersionDetails.platform)
|
||||
? "Linux"
|
||||
: PLATFORM_DISPLAY_NAMES[osVersionDetails.platform];
|
||||
return <VulnsNotSupported supportInterestText={supportInterestText} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -202,6 +202,9 @@ const SoftwareTitleDetailsPage = ({
|
||||
data={softwareTitle.versions ?? []}
|
||||
isLoading={isSoftwareTitleLoading}
|
||||
teamIdForApi={teamIdForApi}
|
||||
isIPadOSOrIOSApp={["ios_apps", "ipados_apps"].includes(
|
||||
softwareTitle.source
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
+8
-2
@@ -44,6 +44,7 @@ interface ISoftwareTitleDetailsTableProps {
|
||||
data: ISoftwareTitleVersion[];
|
||||
isLoading: boolean;
|
||||
teamIdForApi?: number;
|
||||
isIPadOSOrIOSApp: boolean;
|
||||
}
|
||||
|
||||
interface IRowProps extends Row {
|
||||
@@ -57,6 +58,7 @@ const SoftwareTitleDetailsTable = ({
|
||||
data,
|
||||
isLoading,
|
||||
teamIdForApi,
|
||||
isIPadOSOrIOSApp,
|
||||
}: ISoftwareTitleDetailsTableProps) => {
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = {
|
||||
@@ -74,8 +76,12 @@ const SoftwareTitleDetailsTable = ({
|
||||
|
||||
const softwareTableHeaders = useMemo(
|
||||
() =>
|
||||
generateSoftwareTitleDetailsTableConfig({ router, teamId: teamIdForApi }),
|
||||
[router, teamIdForApi]
|
||||
generateSoftwareTitleDetailsTableConfig({
|
||||
router,
|
||||
teamId: teamIdForApi,
|
||||
isIPadOSOrIOSApp,
|
||||
}),
|
||||
[router, teamIdForApi, isIPadOSOrIOSApp]
|
||||
);
|
||||
|
||||
const renderVersionsCount = () => (
|
||||
|
||||
+8
-3
@@ -17,6 +17,7 @@ import VulnerabilitiesCell from "../../components/VulnerabilitiesCell";
|
||||
interface ISoftwareTitleDetailsTableConfigProps {
|
||||
router: InjectedRouter;
|
||||
teamId?: number;
|
||||
isIPadOSOrIOSApp: boolean;
|
||||
}
|
||||
interface ICellProps {
|
||||
cell: {
|
||||
@@ -48,6 +49,7 @@ interface IVulnCellProps extends ICellProps {
|
||||
const generateSoftwareTitleDetailsTableConfig = ({
|
||||
router,
|
||||
teamId,
|
||||
isIPadOSOrIOSApp,
|
||||
}: ISoftwareTitleDetailsTableConfigProps) => {
|
||||
const tableHeaders = [
|
||||
{
|
||||
@@ -90,10 +92,13 @@ const generateSoftwareTitleDetailsTableConfig = ({
|
||||
// With the versions data, we can sum up the vulnerabilities to get the
|
||||
// total number of vulnerabilities for the software title
|
||||
accessor: "vulnerabilities",
|
||||
Cell: (cellProps: IVulnCellProps): JSX.Element => (
|
||||
<VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />
|
||||
Cell: (cellProps: IVulnCellProps): JSX.Element => {
|
||||
if (isIPadOSOrIOSApp) {
|
||||
return <TextCell value="Not supported" grey />;
|
||||
}
|
||||
return <VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />;
|
||||
// TODO: tooltip
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Hosts",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ISoftwareTitleDetails,
|
||||
isSoftwarePackage,
|
||||
} from "interfaces/software";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
/**
|
||||
* Generates the data needed to render the package card.
|
||||
@@ -18,9 +19,10 @@ export const getPackageCardInfo = (softwareTitle: ISoftwareTitleDetails) => {
|
||||
return {
|
||||
softwarePackage: isSoftwarePackage(packageData) ? packageData : undefined,
|
||||
name: softwareTitle.name,
|
||||
version: isSoftwarePackage(packageData)
|
||||
? packageData.version
|
||||
: packageData.latest_version,
|
||||
version:
|
||||
(isSoftwarePackage(packageData)
|
||||
? packageData.version
|
||||
: packageData.latest_version) || DEFAULT_EMPTY_CELL_VALUE,
|
||||
uploadedAt: isSoftwarePackage(packageData) ? packageData.uploaded_at : "",
|
||||
status: packageData.status,
|
||||
isSelfService: isSoftwarePackage(packageData)
|
||||
|
||||
+14
-15
@@ -2,13 +2,7 @@ import React from "react";
|
||||
import { CellProps, Column } from "react-table";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import {
|
||||
IAppStoreApp,
|
||||
ISoftware,
|
||||
ISoftwarePackage,
|
||||
ISoftwareTitle,
|
||||
formatSoftwareType,
|
||||
} from "interfaces/software";
|
||||
import { ISoftwareTitle, formatSoftwareType } from "interfaces/software";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
@@ -123,14 +117,6 @@ const generateTableHeaders = (
|
||||
},
|
||||
sortType: "caseInsensitive",
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
disableSortBy: true,
|
||||
accessor: "source",
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={formatSoftwareType(cellProps.row.original)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
@@ -139,6 +125,14 @@ const generateTableHeaders = (
|
||||
<VersionCell versions={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
disableSortBy: true,
|
||||
accessor: "source",
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={formatSoftwareType(cellProps.row.original)} />
|
||||
),
|
||||
},
|
||||
// the "vulnerabilities" accessor is used but the data is actually coming
|
||||
// from the version attribute. We do this as we already have a "versions"
|
||||
// attribute used for the "Version" column and we cannot reuse. This is a
|
||||
@@ -149,6 +143,11 @@ const generateTableHeaders = (
|
||||
Header: "Vulnerabilities",
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps: IVulnerabilitiesCellProps) => {
|
||||
if (
|
||||
["ios_apps", "ipados_apps"].includes(cellProps.row.original.source)
|
||||
) {
|
||||
return <TextCell value="Not supported" grey />;
|
||||
}
|
||||
const vulnerabilities = getVulnerabilities(
|
||||
cellProps.row.original.versions ?? []
|
||||
);
|
||||
|
||||
+16
-11
@@ -63,14 +63,6 @@ const generateTableHeaders = (
|
||||
},
|
||||
sortType: "caseInsensitive",
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
disableSortBy: true,
|
||||
accessor: "source",
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={formatSoftwareType(cellProps.row.original)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
disableSortBy: true,
|
||||
@@ -79,13 +71,26 @@ const generateTableHeaders = (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "Type",
|
||||
disableSortBy: true,
|
||||
accessor: "source",
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={formatSoftwareType(cellProps.row.original)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: "Vulnerabilities",
|
||||
disableSortBy: true,
|
||||
accessor: "vulnerabilities",
|
||||
Cell: (cellProps: IVulnerabilitiesCellProps) => (
|
||||
<VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />
|
||||
),
|
||||
Cell: (cellProps: IVulnerabilitiesCellProps) => {
|
||||
if (
|
||||
["ipados_apps", "ios_apps"].includes(cellProps.row.original.source)
|
||||
) {
|
||||
return <TextCell value="Not supported" grey />;
|
||||
}
|
||||
return <VulnerabilitiesCell vulnerabilities={cellProps.cell.value} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: (cellProps: ITableHeaderProps) => (
|
||||
|
||||
+19
-7
@@ -31,6 +31,7 @@ import Card from "components/Card";
|
||||
import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
|
||||
import SoftwareVulnerabilitiesTable from "../components/SoftwareVulnerabilitiesTable";
|
||||
import DetailsNoHosts from "../components/DetailsNoHosts";
|
||||
import { VulnsNotSupported } from "../components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable";
|
||||
|
||||
const baseClass = "software-version-details-page";
|
||||
|
||||
@@ -112,6 +113,23 @@ const SoftwareVersionDetailsPage = ({
|
||||
[handleTeamChange]
|
||||
);
|
||||
|
||||
const renderVulnTable = (swVersion: ISoftwareVersion) => {
|
||||
if (["ios_apps", "ipados_apps"].includes(swVersion.source)) {
|
||||
const supportInterestText =
|
||||
swVersion.source === "ios_apps" ? "iOS" : "iPadOS";
|
||||
return <VulnsNotSupported supportInterestText={supportInterestText} />;
|
||||
}
|
||||
return (
|
||||
<SoftwareVulnerabilitiesTable
|
||||
data={swVersion.vulnerabilities ?? []}
|
||||
itemName="software item"
|
||||
isLoading={isSoftwareVersionLoading}
|
||||
router={router}
|
||||
teamIdForApi={teamIdForApi}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (isSoftwareVersionLoading) {
|
||||
return <Spinner />;
|
||||
@@ -157,13 +175,7 @@ const SoftwareVersionDetailsPage = ({
|
||||
className={`${baseClass}__vulnerabilities-section`}
|
||||
>
|
||||
<h2 className="section__header">Vulnerabilities</h2>
|
||||
<SoftwareVulnerabilitiesTable
|
||||
data={softwareVersion.vulnerabilities ?? []}
|
||||
itemName="software item"
|
||||
isLoading={isSoftwareVersionLoading}
|
||||
router={router}
|
||||
teamIdForApi={teamIdForApi}
|
||||
/>
|
||||
{renderVulnTable(softwareVersion)}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,8 @@ import mdmAppleAPI, {
|
||||
} from "services/entities/mdm_apple";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
|
||||
import { PLATFORM_DISPLAY_NAMES } from "interfaces/platform";
|
||||
|
||||
import Card from "components/Card";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Spinner from "components/Spinner";
|
||||
@@ -29,7 +31,7 @@ const EnableVppCard = () => {
|
||||
<Card borderRadiusSize="medium">
|
||||
<div className={`${baseClass}__enable-vpp`}>
|
||||
<p className={`${baseClass}__enable-vpp-title`}>
|
||||
<b>Volume Purchasing Program (VPP) isn’t enabled.</b>
|
||||
<b>Volume Purchasing Program (VPP) isn't enabled</b>
|
||||
</p>
|
||||
<p className={`${baseClass}__enable-vpp-description`}>
|
||||
To add App Store apps, first enable VPP.
|
||||
@@ -66,6 +68,11 @@ const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
|
||||
name="vppApp"
|
||||
onChange={() => onSelect(app)}
|
||||
/>
|
||||
{app.platform && (
|
||||
<div className="app-platform">
|
||||
{PLATFORM_DISPLAY_NAMES[app.platform]}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -76,40 +83,20 @@ interface IVppAppListProps {
|
||||
onSelect: (app: IVppApp) => void;
|
||||
}
|
||||
|
||||
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => {
|
||||
const renderContent = () => {
|
||||
if (apps.length === 0) {
|
||||
return (
|
||||
<div className={`${baseClass}__no-software`}>
|
||||
<p className={`${baseClass}__no-software-title`}>
|
||||
You don't have any App Store apps
|
||||
</p>
|
||||
<p className={`${baseClass}__no-software-description`}>
|
||||
You must purchase apps in ABM. App Store apps that are already added
|
||||
to this team are not listed.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={`${baseClass}__list`}>
|
||||
{apps.map((app) => (
|
||||
<VppAppListItem
|
||||
key={app.app_store_id}
|
||||
app={app}
|
||||
selected={selectedApp?.app_store_id === app.app_store_id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__list-container`}>{renderContent()}</div>
|
||||
);
|
||||
};
|
||||
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => (
|
||||
<div className={`${baseClass}__list-container`}>
|
||||
<ul className={`${baseClass}__list`}>
|
||||
{apps.map((app) => (
|
||||
<VppAppListItem
|
||||
key={app.app_store_id}
|
||||
app={app}
|
||||
selected={selectedApp?.app_store_id === app.app_store_id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface IAppStoreVppProps {
|
||||
teamId: number;
|
||||
@@ -193,20 +180,41 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
|
||||
return <DataError className={`${baseClass}__error`} />;
|
||||
}
|
||||
|
||||
return vppApps ? (
|
||||
<VppAppList
|
||||
apps={vppApps}
|
||||
selectedApp={selectedApp}
|
||||
onSelect={onSelectApp}
|
||||
/>
|
||||
) : null;
|
||||
if (vppApps) {
|
||||
if (vppApps.length === 0) {
|
||||
return (
|
||||
<div className={`${baseClass}__no-software`}>
|
||||
<p className={`${baseClass}__no-software-title`}>
|
||||
You don't have any App Store apps
|
||||
</p>
|
||||
<p className={`${baseClass}__no-software-description`}>
|
||||
Add apps in{" "}
|
||||
<CustomLink url="https://business.apple.com" text="ABM" newTab />{" "}
|
||||
Apps that are already added to this team are not listed.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<VppAppList
|
||||
apps={vppApps}
|
||||
selectedApp={selectedApp}
|
||||
onSelect={onSelectApp}
|
||||
/>
|
||||
<div className={`${baseClass}__help-text`}>
|
||||
These apps were added in Apple Business Manager (ABM). To add more
|
||||
apps, head to{" "}
|
||||
<CustomLink url="https://business.apple.com" text="ABM" newTab />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Apple App Store apps purchased via Apple Business Manager.
|
||||
</p>
|
||||
{renderContent()}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
.app-store-vpp {
|
||||
margin-top: $pad-large;
|
||||
|
||||
&__description {
|
||||
margin: $pad-medium 0;
|
||||
}
|
||||
|
||||
&__list-container {
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
border-radius: $border-radius-medium;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
&__list {
|
||||
@@ -17,9 +14,16 @@
|
||||
}
|
||||
|
||||
&__list-item {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
align-items: center;
|
||||
padding: $pad-small $pad-medium;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
|
||||
.app-platform {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -32,8 +36,11 @@
|
||||
}
|
||||
|
||||
&__no-software {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
align-items: center;
|
||||
padding: $pad-xxlarge 48px;
|
||||
text-align: center;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
@@ -43,7 +50,7 @@
|
||||
}
|
||||
|
||||
&__no-software-description {
|
||||
margin-top: $pad-small;
|
||||
margin: 0;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
@@ -63,4 +70,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
@include help-text;
|
||||
.custom-link {
|
||||
font-size: $xx-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface IEmptySoftwareTableProps {
|
||||
isNotDetectingSoftware?: boolean;
|
||||
/** isCollectingSoftware is only used on the Dashboard page with a TODO to revisit */
|
||||
isCollectingSoftware?: boolean;
|
||||
isFilterVulnerable?: boolean;
|
||||
}
|
||||
|
||||
const generateTypeText = (
|
||||
@@ -39,7 +38,6 @@ const EmptySoftwareTable = ({
|
||||
isSoftwareDisabled,
|
||||
isNotDetectingSoftware,
|
||||
isCollectingSoftware,
|
||||
isFilterVulnerable,
|
||||
}: IEmptySoftwareTableProps): JSX.Element => {
|
||||
const softwareTypeText = generateTypeText(tableName, softwareFilter);
|
||||
|
||||
|
||||
+24
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
software/versions/:id > Vulnerabilities table
|
||||
software/os/:id > Vulnerabilities table
|
||||
*/
|
||||
software/versions/:id > Vulnerabilities table
|
||||
software/os/:id > Vulnerabilities table
|
||||
*/
|
||||
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import classnames from "classnames";
|
||||
@@ -11,7 +11,8 @@ import PATHS from "router/paths";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { ISoftwareVulnerability } from "interfaces/software";
|
||||
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
import { CONTACT_FLEET_LINK, GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
import { DisplayPlatform } from "interfaces/platform";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TableCount from "components/TableContainer/TableCount";
|
||||
@@ -26,6 +27,10 @@ interface INoVulnsDetectedProps {
|
||||
itemName: string;
|
||||
}
|
||||
|
||||
interface IVulnsNotSupportedProps {
|
||||
supportInterestText?: DisplayPlatform;
|
||||
}
|
||||
|
||||
const NoVulnsDetected = ({ itemName }: INoVulnsDetectedProps): JSX.Element => {
|
||||
return (
|
||||
<EmptyTable
|
||||
@@ -44,6 +49,21 @@ const NoVulnsDetected = ({ itemName }: INoVulnsDetectedProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
export const VulnsNotSupported = ({
|
||||
supportInterestText,
|
||||
}: IVulnsNotSupportedProps) => (
|
||||
<EmptyTable
|
||||
header="Vulnerabilities are not supported for this type of host"
|
||||
info={
|
||||
<>
|
||||
Interested in vulnerabilities in{" "}
|
||||
{supportInterestText ?? "this platform"}?{" "}
|
||||
<CustomLink url={CONTACT_FLEET_LINK} text="Let us know" newTab />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
interface ISoftwareVulnerabilitiesTableProps {
|
||||
data: ISoftwareVulnerability[];
|
||||
/** Name displayed on the empty state */
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const MacApp = (props: SVGProps<SVGSVGElement>) => (
|
||||
const AppleApp = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" {...props}>
|
||||
<path fill="#515774" d="M0 0h32v32H0z" />
|
||||
<path
|
||||
@@ -13,4 +13,4 @@ const MacApp = (props: SVGProps<SVGSVGElement>) => (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default MacApp;
|
||||
export default AppleApp;
|
||||
@@ -6,7 +6,7 @@ import ChromeApp from "./ChromeApp";
|
||||
import Excel from "./Excel";
|
||||
import Extension from "./Extension";
|
||||
import Firefox from "./Firefox";
|
||||
import MacApp from "./MacApp";
|
||||
import AppleApp from "./AppleApp";
|
||||
import MacOS from "./MacOS";
|
||||
import Package from "./Package";
|
||||
import Safari from "./Safari";
|
||||
@@ -65,7 +65,9 @@ const SOFTWARE_SOURCE_TO_ICON_MAP = {
|
||||
atom_packages: Package,
|
||||
python_packages: Package,
|
||||
homebrew_packages: Package,
|
||||
apps: MacApp,
|
||||
apps: AppleApp,
|
||||
ios_apps: AppleApp,
|
||||
ipados_apps: AppleApp,
|
||||
programs: WindowsApp,
|
||||
chrome_extensions: Extension,
|
||||
safari_extensions: Extension,
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
max-width: 520px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { CONTACT_FLEET_LINK } from "utilities/constants";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
@@ -188,11 +191,7 @@ const Smtp = ({
|
||||
<>
|
||||
To configure SMTP,{" "}
|
||||
<CustomLink
|
||||
url={
|
||||
isPremiumTier
|
||||
? "https://fleetdm.com/contact"
|
||||
: "https://fleetdm.com/slack"
|
||||
}
|
||||
url={isPremiumTier ? CONTACT_FLEET_LINK : "https://fleetdm.com/slack"}
|
||||
text="get help"
|
||||
newTab
|
||||
/>
|
||||
|
||||
@@ -428,7 +428,6 @@ const DeviceUserPage = ({
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
isMyDevicePage
|
||||
hostTeamId={host.team_id || 0}
|
||||
hostPlatform={host?.platform || ""}
|
||||
isSoftwareEnabled={isSoftwareEnabled}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -854,31 +854,29 @@ const HostDetailsPage = ({
|
||||
munki={macadmins?.munki}
|
||||
mdm={mdm}
|
||||
/>
|
||||
{!isIosOrIpadosHost && (
|
||||
<ActivityCard
|
||||
activeTab={activeActivityTab}
|
||||
activities={
|
||||
activeActivityTab === "past"
|
||||
? pastActivities
|
||||
: upcomingActivities
|
||||
}
|
||||
isLoading={
|
||||
activeActivityTab === "past"
|
||||
? pastActivitiesIsFetching
|
||||
: upcomingActivitiesIsFetching
|
||||
}
|
||||
isError={
|
||||
activeActivityTab === "past"
|
||||
? pastActivitiesIsError
|
||||
: upcomingActivitiesIsError
|
||||
}
|
||||
upcomingCount={upcomingActivities?.count || 0}
|
||||
onChangeTab={onChangeActivityTab}
|
||||
onNextPage={() => setActivityPage(activityPage + 1)}
|
||||
onPreviousPage={() => setActivityPage(activityPage - 1)}
|
||||
onShowDetails={onShowActivityDetails}
|
||||
/>
|
||||
)}
|
||||
<ActivityCard
|
||||
activeTab={activeActivityTab}
|
||||
activities={
|
||||
activeActivityTab === "past"
|
||||
? pastActivities
|
||||
: upcomingActivities
|
||||
}
|
||||
isLoading={
|
||||
activeActivityTab === "past"
|
||||
? pastActivitiesIsFetching
|
||||
: upcomingActivitiesIsFetching
|
||||
}
|
||||
isError={
|
||||
activeActivityTab === "past"
|
||||
? pastActivitiesIsError
|
||||
: upcomingActivitiesIsError
|
||||
}
|
||||
upcomingCount={upcomingActivities?.count || 0}
|
||||
onChangeTab={onChangeActivityTab}
|
||||
onNextPage={() => setActivityPage(activityPage + 1)}
|
||||
onPreviousPage={() => setActivityPage(activityPage - 1)}
|
||||
onShowDetails={onShowActivityDetails}
|
||||
/>
|
||||
{!isIosOrIpadosHost && (
|
||||
<AgentOptionsCard
|
||||
osqueryData={osqueryData}
|
||||
@@ -911,7 +909,6 @@ const HostDetailsPage = ({
|
||||
pathname={location.pathname}
|
||||
onShowSoftwareDetails={setSelectedSoftwareDetails}
|
||||
hostTeamId={host.team_id || 0}
|
||||
hostPlatform={host.platform}
|
||||
/>
|
||||
{host?.platform === "darwin" && macadmins?.munki?.version && (
|
||||
<MunkiIssuesCard
|
||||
|
||||
@@ -24,14 +24,12 @@
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
// Note: CSS for labels card spans the whole grid for iOS and iPadOS
|
||||
// because activity card is missing
|
||||
// No agent options card for i(Pad)OS, so extend Labels card vertically
|
||||
&__details-panel--ios-grid.react-tabs__tab-panel--selected {
|
||||
// Must be selected to show grid
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"about"
|
||||
"labels";
|
||||
"about about"
|
||||
"activity labels";
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
|
||||
&__empty-feed {
|
||||
min-height: 484px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
|
||||
&__empty-feed {
|
||||
min-height: 484px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
|
||||
@@ -12,15 +12,13 @@ import deviceAPI, {
|
||||
IGetDeviceSoftwareResponse,
|
||||
} from "services/entities/device_user";
|
||||
import { IHostSoftware, ISoftware } from "interfaces/software";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS, SUPPORT_LINK } from "utilities/constants";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import Card from "components/Card/Card";
|
||||
import DataError from "components/DataError";
|
||||
import Spinner from "components/Spinner";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import { generateSoftwareTableHeaders as generateHostSoftwareTableConfig } from "./HostSoftwareTableConfig";
|
||||
import { generateSoftwareTableHeaders as generateDeviceSoftwareTableConfig } from "./DeviceSoftwareTableConfig";
|
||||
@@ -42,7 +40,6 @@ interface IHostSoftwareProps {
|
||||
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
|
||||
pathname: string;
|
||||
hostTeamId: number;
|
||||
hostPlatform: string;
|
||||
onShowSoftwareDetails?: (software: IHostSoftware) => void;
|
||||
isSoftwareEnabled?: boolean;
|
||||
isMyDevicePage?: boolean;
|
||||
@@ -60,6 +57,7 @@ export const parseHostSoftwareQueryParams = (queryParams: {
|
||||
order_key?: string;
|
||||
order_direction?: "asc" | "desc";
|
||||
vulnerable?: string;
|
||||
available_for_install?: string;
|
||||
}) => {
|
||||
const searchQuery = queryParams?.query ?? DEFAULT_SEARCH_QUERY;
|
||||
const sortHeader = queryParams?.order_key ?? DEFAULT_SORT_HEADER;
|
||||
@@ -69,6 +67,7 @@ export const parseHostSoftwareQueryParams = (queryParams: {
|
||||
: DEFAULT_PAGE;
|
||||
const pageSize = DEFAULT_PAGE_SIZE;
|
||||
const vulnerable = queryParams.vulnerable === "true";
|
||||
const availableForInstall = queryParams.available_for_install === "true";
|
||||
|
||||
return {
|
||||
page,
|
||||
@@ -77,6 +76,7 @@ export const parseHostSoftwareQueryParams = (queryParams: {
|
||||
order_direction: sortDirection,
|
||||
per_page: pageSize,
|
||||
vulnerable,
|
||||
available_for_install: availableForInstall,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -88,7 +88,6 @@ const HostSoftware = ({
|
||||
queryParams,
|
||||
pathname,
|
||||
hostTeamId = 0,
|
||||
hostPlatform,
|
||||
onShowSoftwareDetails,
|
||||
isSoftwareEnabled = false,
|
||||
isMyDevicePage = false,
|
||||
@@ -105,8 +104,6 @@ const HostSoftware = ({
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const isIosOrIpadOs = hostPlatform === "ipados" || hostPlatform === "ios";
|
||||
|
||||
const {
|
||||
data: hostSoftwareRes,
|
||||
isLoading: hostSoftwareLoading,
|
||||
@@ -132,7 +129,7 @@ const HostSoftware = ({
|
||||
},
|
||||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: isSoftwareEnabled && !isMyDevicePage && !isIosOrIpadOs, // if disabled, we'll always show a generic "No software detected" message
|
||||
enabled: isSoftwareEnabled && !isMyDevicePage, // if disabled, we'll always show a generic "No software detected" message
|
||||
keepPreviousData: true,
|
||||
staleTime: 7000,
|
||||
}
|
||||
@@ -239,26 +236,22 @@ const HostSoftware = ({
|
||||
|
||||
const data = isMyDevicePage ? deviceSoftwareRes : hostSoftwareRes;
|
||||
|
||||
const getHostSoftwareFilterFromQueryParams = () => {
|
||||
const { vulnerable, available_for_install } = queryParams;
|
||||
if (available_for_install) {
|
||||
return "installableSoftware";
|
||||
}
|
||||
if (vulnerable) {
|
||||
return "vulnerableSoftware";
|
||||
}
|
||||
return "allSoftware";
|
||||
};
|
||||
|
||||
const renderHostSoftware = () => {
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (isIosOrIpadOs) {
|
||||
return (
|
||||
<EmptyTable
|
||||
header="Software is not supported for this host"
|
||||
info={
|
||||
<>
|
||||
Interested in viewing software for{" "}
|
||||
{hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "}
|
||||
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isError && <DataError />}
|
||||
@@ -275,7 +268,7 @@ const HostSoftware = ({
|
||||
searchQuery={queryParams.query}
|
||||
page={queryParams.page}
|
||||
pagePath={pathname}
|
||||
vulnerable={queryParams.vulnerable}
|
||||
hostSoftwareFilter={getHostSoftwareFilterFromQueryParams()}
|
||||
pathPrefix={pathname}
|
||||
/>
|
||||
)}
|
||||
|
||||
+41
-34
@@ -4,6 +4,9 @@ import { InjectedRouter } from "react-router";
|
||||
import { IGetHostSoftwareResponse } from "services/entities/hosts";
|
||||
import { IGetDeviceSoftwareResponse } from "services/entities/device_user";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
import { QueryParams } from "utilities/url";
|
||||
|
||||
import { ISoftwareDropdownFilterVal } from "pages/SoftwarePage/SoftwareTitles/SoftwareTable/helpers";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
@@ -17,20 +20,26 @@ const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
const baseClass = "host-software-table";
|
||||
|
||||
export const VULNERABLE_DROPDOWN_OPTIONS = [
|
||||
export const DROPDOWN_OPTIONS = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "All software",
|
||||
value: false,
|
||||
value: "allSoftware",
|
||||
helpText: "All software installed on your hosts.",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Vulnerable software",
|
||||
value: true,
|
||||
value: "vulnerableSoftware",
|
||||
helpText:
|
||||
"All software installed on your hosts with detected vulnerabilities.",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Available for install",
|
||||
value: "installableSoftware",
|
||||
helpText: "Software that can be installed on your hosts.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface IHostSoftwareTableProps {
|
||||
@@ -45,7 +54,7 @@ interface IHostSoftwareTableProps {
|
||||
pagePath: string;
|
||||
routeTemplate?: string;
|
||||
pathPrefix: string;
|
||||
vulnerable?: boolean;
|
||||
hostSoftwareFilter: ISoftwareDropdownFilterVal;
|
||||
}
|
||||
|
||||
const HostSoftwareTable = ({
|
||||
@@ -60,38 +69,45 @@ const HostSoftwareTable = ({
|
||||
pagePath,
|
||||
routeTemplate,
|
||||
pathPrefix,
|
||||
vulnerable,
|
||||
hostSoftwareFilter,
|
||||
}: IHostSoftwareTableProps) => {
|
||||
const handleVulnFilterDropdownChange = useCallback(
|
||||
(isFilterVulnerable: boolean) => {
|
||||
const handleFilterDropdownChange = useCallback(
|
||||
(val: ISoftwareDropdownFilterVal) => {
|
||||
const newParams: QueryParams = {
|
||||
query: searchQuery,
|
||||
order_key: sortHeader,
|
||||
order_direction: sortDirection,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
// mutually exclusive
|
||||
if (val === "installableSoftware") {
|
||||
newParams.available_for_install = true.toString();
|
||||
} else if (val === "vulnerableSoftware") {
|
||||
newParams.vulnerable = true.toString();
|
||||
}
|
||||
|
||||
const nextPath = getNextLocationPath({
|
||||
pathPrefix,
|
||||
routeTemplate,
|
||||
queryParams: {
|
||||
query: searchQuery,
|
||||
order_key: sortHeader,
|
||||
order_direction: sortDirection,
|
||||
page: 0,
|
||||
vulnerable: isFilterVulnerable.toString(),
|
||||
},
|
||||
queryParams: newParams,
|
||||
});
|
||||
router.replace(nextPath);
|
||||
},
|
||||
[pathPrefix, routeTemplate, router, searchQuery, sortDirection, sortHeader]
|
||||
);
|
||||
|
||||
const memoizedVulnFilterDropdown = useCallback(() => {
|
||||
const memoizedFilterDropdown = useCallback(() => {
|
||||
return (
|
||||
<Dropdown
|
||||
value={vulnerable}
|
||||
className={`${baseClass}__vuln_dropdown`}
|
||||
options={VULNERABLE_DROPDOWN_OPTIONS}
|
||||
value={hostSoftwareFilter}
|
||||
options={DROPDOWN_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={handleVulnFilterDropdownChange}
|
||||
onChange={handleFilterDropdownChange}
|
||||
tableFilterDropdown
|
||||
/>
|
||||
);
|
||||
}, [handleVulnFilterDropdownChange, vulnerable]);
|
||||
}, [handleFilterDropdownChange, hostSoftwareFilter]);
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
@@ -115,20 +131,16 @@ const HostSoftwareTable = ({
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData, changedParam: string) => {
|
||||
const newQueryParam: Record<
|
||||
string,
|
||||
string | number | boolean | undefined
|
||||
> = {
|
||||
const newQueryParam: QueryParams = {
|
||||
query: newTableQuery.searchQuery,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
||||
vulnerable,
|
||||
};
|
||||
|
||||
return newQueryParam;
|
||||
},
|
||||
[vulnerable]
|
||||
[]
|
||||
);
|
||||
|
||||
// TODO: Look into useDebounceCallback with dependencies
|
||||
@@ -167,13 +179,8 @@ const HostSoftwareTable = ({
|
||||
}, [count, isSoftwareNotDetected]);
|
||||
|
||||
const memoizedEmptyComponent = useCallback(() => {
|
||||
return (
|
||||
<EmptySoftwareTable
|
||||
isFilterVulnerable={vulnerable}
|
||||
isNotDetectingSoftware={searchQuery === ""}
|
||||
/>
|
||||
);
|
||||
}, [searchQuery, vulnerable]);
|
||||
return <EmptySoftwareTable isNotDetectingSoftware={searchQuery === ""} />;
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -191,7 +198,7 @@ const HostSoftwareTable = ({
|
||||
inputPlaceHolder="Search by name"
|
||||
onQueryChange={onQueryChange}
|
||||
emptyComponent={memoizedEmptyComponent}
|
||||
customControl={memoizedVulnFilterDropdown}
|
||||
customControl={memoizedFilterDropdown}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
searchable
|
||||
|
||||
@@ -20,6 +20,7 @@ const TEST_PROPS: ISoftwareSelfServiceProps = {
|
||||
order_direction: "asc",
|
||||
per_page: 10,
|
||||
vulnerable: true,
|
||||
available_for_install: false,
|
||||
},
|
||||
router: createMockRouter(),
|
||||
};
|
||||
@@ -93,6 +94,7 @@ describe("SelfService", () => {
|
||||
order_direction: "asc",
|
||||
per_page: 10,
|
||||
vulnerable: true,
|
||||
available_for_install: false,
|
||||
}}
|
||||
router={createMockRouter()}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.host-software-table {
|
||||
.controls {
|
||||
// vulnerable software dropdown filter
|
||||
// software filter
|
||||
.Select {
|
||||
.Select-menu-outer {
|
||||
width: 364px;
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
.Select > .Select-menu-outer {
|
||||
left: -186px;
|
||||
width: 360px;
|
||||
.dropdown__help-text {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
.Select-control {
|
||||
margin-top: 0;
|
||||
|
||||
+2
-5
@@ -10,6 +10,7 @@ import LogDestinationIndicator from "components/LogDestinationIndicator/LogDesti
|
||||
|
||||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import { CONTACT_FLEET_LINK } from "utilities/constants";
|
||||
|
||||
interface IManageQueryAutomationsModalProps {
|
||||
isUpdatingAutomations: boolean;
|
||||
@@ -171,11 +172,7 @@ const ManageQueryAutomationsModal = ({
|
||||
<p>Automations currently run on macOS, Windows, and Linux hosts.</p>
|
||||
<p>
|
||||
Interested in query automations for your Chromebooks?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/contact"
|
||||
text="Let us know"
|
||||
newTab
|
||||
/>
|
||||
<CustomLink url={CONTACT_FLEET_LINK} text="Let us know" newTab />
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<Button
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApplePlatform } from "interfaces/platform";
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
@@ -9,14 +10,15 @@ export interface IGetVppInfoResponse {
|
||||
|
||||
export interface IVppApp {
|
||||
name: string;
|
||||
bundle_identifier: string;
|
||||
icon_url: string;
|
||||
latest_version: string;
|
||||
app_store_id: number; // FIXME: This seems to be coming back as a string but we're not seeing TS errors because there's an implicit any cast
|
||||
app_store_id: string;
|
||||
added: boolean;
|
||||
platform: string; // "darwin" | "ios" | "ipados"
|
||||
platform: ApplePlatform;
|
||||
}
|
||||
|
||||
interface IGetVppAppsResponse {
|
||||
export interface IGetVppAppsResponse {
|
||||
app_store_apps: IVppApp[];
|
||||
}
|
||||
|
||||
@@ -67,10 +69,7 @@ export default {
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
|
||||
addVppApp: (teamId: number, appStoreId: number) => {
|
||||
// FIXME: API seems to expect app_store_id to be a string but we're not seeing TS errors because
|
||||
// there's an implicit any cast on getVppApps response that is being fed into the appStoreId
|
||||
// param (so it's being treated as a number even though it is really a string being passed in here)
|
||||
addVppApp: (teamId: number, appStoreId: string) => {
|
||||
const { MDM_APPLE_VPP_APPS } = endpoints;
|
||||
return sendRequest("POST", MDM_APPLE_VPP_APPS, {
|
||||
app_store_id: appStoreId,
|
||||
|
||||
@@ -50,9 +50,11 @@ export interface ISoftwareVersionsResponse {
|
||||
export interface ISoftwareTitleResponse {
|
||||
software_title: ISoftwareTitleDetails;
|
||||
}
|
||||
|
||||
export interface ISoftwareVersionResponse {
|
||||
software: ISoftwareVersion;
|
||||
}
|
||||
|
||||
export interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
|
||||
scope: "software-versions";
|
||||
}
|
||||
|
||||
@@ -59,8 +59,11 @@ export const HOST_STATUS_WEBHOOK_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [
|
||||
|
||||
export const GITHUB_NEW_ISSUE_LINK =
|
||||
"https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md";
|
||||
|
||||
export const SUPPORT_LINK = "https://fleetdm.com/support";
|
||||
|
||||
export const CONTACT_FLEET_LINK = "https://fleetdm.com/contact";
|
||||
|
||||
/** July 28, 2016 is the date of the initial commit to fleet/fleet. */
|
||||
export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user