Fleet Premium to Sandbox (#11372)

## Addresses #9371 
### Adds a suite of UI logic for premium features in the Sandbox
environment

For reviewer: please review the work for the below 3 substasks, which
are the only remaining subtasks encompassed by this PR that have not yet
passed review individually:
  - #10822 (9)
  - #10823 (10)
  - #10824 (11)

## Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
This commit is contained in:
Jacob Shandling
2023-04-27 08:53:30 -07:00
committed by GitHub
co-authored by Jacob Shandling Martin Angers
parent 2a0f8d0701
commit cb58849d95
74 changed files with 1051 additions and 464 deletions
@@ -0,0 +1 @@
* Inform prospective customers that Teams is a Premium feature.
@@ -0,0 +1,2 @@
* Added "Premium Feature" icons for premium-only columns of the Vulnerabilities table when in
Sandbox mode
+2
View File
@@ -0,0 +1,2 @@
- Added a star to let a sandbox user know that the "Probability of exploit" column of the Manage
Software page is a premium feature
@@ -0,0 +1,2 @@
- In Sandbox, added "Premium Feature" icons for premium-only option to designate a policy as "Critical," as well
as copy to the tooltip above the icon next to policies designated "Critical" in the Manage policies table.
+1
View File
@@ -0,0 +1 @@
* Added a suite of UI logic for premium features in the Sandbox environment
+30
View File
@@ -0,0 +1,30 @@
import { IPolicyStats } from "interfaces/policy";
const DEFAULT_POLICY_MOCK: IPolicyStats = {
id: 1,
name: "Antivirus healthy (Linux)",
query:
"SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
critical: false,
description:
"Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
author_id: 1,
author_name: "Test User",
author_email: "test@user.com",
team_id: undefined,
resolution: "Ensure ClamAV and Freshclam are installed and running.",
platform: "linux" as const,
created_at: "2023-03-24T22:13:59Z",
updated_at: "2023-03-31T19:05:13Z",
passing_host_count: 0,
failing_host_count: 8,
webhook: "Off",
has_run: true,
osquery_policy_ms: 3600000,
};
const createMockPolicy = (overrides?: Partial<IPolicyStats>): IPolicyStats => {
return { ...DEFAULT_POLICY_MOCK, ...overrides };
};
export default createMockPolicy;
@@ -3,6 +3,7 @@
flex-direction: column;
padding: 0;
padding-bottom: 20px;
width: 650px;
p {
padding-top: $pad-small;
@@ -27,7 +28,6 @@
&__select-installer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
@@ -38,11 +38,9 @@
cursor: pointer;
font-size: $small;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-grow: 1;
padding: 16px;
gap: 16px;
width: 247px;
border: 1px solid #c5c7d1;
@@ -50,7 +48,6 @@
span {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
+4 -5
View File
@@ -1,10 +1,9 @@
.custom-link {
display: inline-flex;
align-items: center;
gap: $pad-xsmall;
&__no-wrap {
white-space: nowrap;
}
&__external-icon {
display: inline;
margin-left: $pad-xsmall;
}
}
@@ -0,0 +1,59 @@
import CustomLink from "components/CustomLink";
import Icon from "components/Icon";
import { uniqueId } from "lodash";
import React from "react";
import ReactTooltip, { Place } from "react-tooltip";
import { COLORS } from "styles/var/colors";
interface IPremiumFeatureIconWithTooltip {
tooltipPlace?: Place;
tooltipDelayHide?: number;
tooltipPositionOverrides?: {
leftAdj?: number;
topAdj?: number;
};
}
const PremiumFeatureIconWithTooltip = ({
tooltipPlace,
tooltipDelayHide = 100,
tooltipPositionOverrides,
}: IPremiumFeatureIconWithTooltip) => {
const [leftAdj, topAdj] = [
tooltipPositionOverrides?.leftAdj ?? 0,
tooltipPositionOverrides?.topAdj ?? 0,
];
const tipId = uniqueId();
return (
<span className="premium-icon-tip">
<span data-tip data-for={tipId}>
<Icon name="premium-feature" className="premium-feature-icon" />
</span>
<ReactTooltip
place={tooltipPlace ?? "top"}
type="dark"
effect="solid"
id={tipId}
backgroundColor={COLORS["tooltip-bg"]}
delayHide={tooltipDelayHide}
delayUpdate={500}
overridePosition={(pos: { left: number; top: number }) => {
return {
left: pos.left + leftAdj,
top: pos.top + topAdj,
};
}}
>
{`This is a Fleet Premium feature. `}
<CustomLink
url="https://fleetdm.com/upgrade"
text="Learn more"
newTab
multiline={false}
iconColor="core-fleet-white"
/>
</ReactTooltip>
</span>
);
};
export default PremiumFeatureIconWithTooltip;
@@ -0,0 +1,4 @@
.premium-icon-tip {
font-size: $x-small;
font-weight: normal;
}
@@ -0,0 +1 @@
export { default } from "./PremiumFeatureIconWithTooltip";
@@ -1,38 +0,0 @@
import classnames from "classnames";
import React from "react";
import CustomLink from "components/CustomLink";
interface ISandboxDemoMessageProps {
/** message to display in the sandbox error */
message: string;
/** UTM (Urchin Tracking Module) source text that is added to the demo link */
utmSource: string;
className?: string;
}
const baseClass = "sandbox-demo-message";
const SandboxDemoMessage = ({
message,
utmSource,
className,
}: ISandboxDemoMessageProps): JSX.Element => {
const classes = classnames(baseClass, className);
return (
<div className={classes}>
<h2 className={`${baseClass}__message`}>{message}</h2>
<p className={`${baseClass}__link-message`}>
Want to learn more?{" "}
<CustomLink
url={`https://calendly.com/fleetdm/demo?utm_source=${utmSource}`}
text={"Schedule a demo"}
newTab
/>
</p>
</div>
);
};
export default SandboxDemoMessage;
@@ -1 +0,0 @@
export { default } from "./SandboxDemoMessage";
@@ -0,0 +1,51 @@
import classnames from "classnames";
import React from "react";
import CustomLink from "components/CustomLink";
interface ISandboxMessageProps {
variant?: "demo" | "sales";
/** message to display in the sandbox error */
message: string;
/** UTM (Urchin Tracking Module) source text that is added to the demo link */
utmSource?: string;
className?: string;
}
const baseClass = "sandbox-message";
const SandboxMessage = ({
variant = "demo",
message,
utmSource,
className,
}: ISandboxMessageProps): JSX.Element => {
const classes = classnames(baseClass, className);
const variants = {
demo: (
<CustomLink
url={`https://calendly.com/fleetdm/demo?utm_source=${utmSource}`}
text={"Schedule a demo"}
newTab
/>
),
sales: (
<CustomLink
url={`https://fleetdm.com/upgrade`}
text={"Contact sales"}
newTab
/>
),
};
return (
<div className={classes}>
<h2 className={`${baseClass}__message`}>{message}</h2>
<p className={`${baseClass}__link-message`}>
Want to learn more? {variants[variant]}
</p>
</div>
);
};
export default SandboxMessage;
@@ -1,15 +1,14 @@
.sandbox-demo-message {
.sandbox-message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
align-items: center;
width: 350px;
margin: auto;
margin: $pad-xxlarge auto;
&__message {
font-size: $small;
font-weight: $bold;
margin: 0 0 $pad-large;
}
&__link-message {
@@ -0,0 +1 @@
export { default } from "./SandboxMessage";
@@ -1,5 +1,6 @@
import React, { useCallback } from "react";
import { kebabCase } from "lodash";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import { ButtonVariant } from "components/buttons/Button/Button";
import Button from "../../../buttons/Button";
@@ -19,6 +20,7 @@ export interface IActionButtonProps {
hideButton?: boolean | ((targetIds: number[]) => boolean);
icon?: string;
iconPosition?: string;
indicatePremiumFeature?: boolean;
}
function useActionCallback(
@@ -42,6 +44,7 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => {
hideButton,
icon,
iconPosition,
indicatePremiumFeature,
} = buttonProps;
const onButtonClick = useActionCallback(onActionButtonClick);
@@ -75,9 +78,19 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => {
return Boolean(hideButtonProp);
};
return isHidden(hideButton) ? null : (
if (isHidden(hideButton)) {
return null;
}
return (
<div className={`${baseClass} ${baseClass}__${kebabCase(name)}`}>
<Button onClick={() => onButtonClick(targetIds)} variant={variant}>
{indicatePremiumFeature && (
<PremiumFeatureIconWithTooltip tooltipDelayHide={500} />
)}
<Button
disabled={indicatePremiumFeature}
onClick={() => onButtonClick(targetIds)}
variant={variant}
>
<>
{iconPosition === "left" && iconLink && (
<img alt={`${name} icon`} src={iconLink} />
@@ -1,7 +1,7 @@
.action-button {
display: flex;
align-items: center;
img {
position: relative;
}
@@ -22,4 +22,10 @@
left: 1px;
}
}
.premium-icon-tip {
position: relative;
top: 3px;
padding-right: $pad-xsmall;
}
}
@@ -377,6 +377,7 @@ const DataTable = ({
hideButton,
icon,
iconPosition,
indicatePremiumFeature,
} = actionButtonProps;
return (
<div className={`${baseClass}__${kebabCase(name)}`}>
@@ -388,6 +389,7 @@ const DataTable = ({
targetIds={targetIds}
variant={variant}
hideButton={hideButton}
indicatePremiumFeature={indicatePremiumFeature}
icon={icon}
iconPosition={iconPosition}
/>
@@ -132,6 +132,14 @@ $shadow-transition-width: 10px;
border-right: none;
border-top-right-radius: 6px;
}
.column-header {
span {
display: flex;
align-items: center;
gap: 3px;
}
}
}
&.active-selection {
@@ -8,6 +8,8 @@ import {
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";
const generateDropdownOptions = (
teams: ITeamSummary[] | undefined,
@@ -40,6 +42,7 @@ interface ITeamsDropdownProps {
includeAll?: boolean; // Include the "All Teams" option;
includeNoTeams?: boolean;
isDisabled?: boolean;
isSandboxMode?: boolean;
onChange: (newSelectedValue: number) => void;
onOpen?: () => void;
onClose?: () => void;
@@ -52,7 +55,8 @@ const TeamsDropdown = ({
selectedTeamId,
includeAll = true,
includeNoTeams = false,
isDisabled,
isDisabled = false,
isSandboxMode = false,
onChange,
onOpen,
onClose,
@@ -72,23 +76,62 @@ const TeamsDropdown = ({
disabled: isDisabled || undefined,
});
return (
<div className={dropdownWrapperClasses}>
{teamOptions.length && (
const renderDropdown = () => {
if (isSandboxMode) {
const tooltipId = uniqueId();
return (
<>
<span data-tip data-for={tooltipId}>
<Dropdown
value={selectedValue}
placeholder="All teams"
options={teamOptions}
className={baseClass}
searchable={false}
disabled
/>
</span>
<ReactTooltip
type="light"
effect="solid"
id={tooltipId}
clickable
delayHide={200}
arrowColor="transparent"
overridePosition={(pos: { left: number; top: number }) => {
return {
left: pos.left - 150,
top: pos.top + 78,
};
}}
>
{`Teams allow you to segment hosts into specific groups of endpoints. This feature is included in Fleet Premium.`}
<br />
<a href="https://calendly.com/fleetdm/demo">
Contact us to learn more.
</a>
</ReactTooltip>
</>
);
}
if (teamOptions.length) {
return (
<Dropdown
value={selectedValue}
placeholder="All teams"
className={baseClass}
options={teamOptions}
searchable={false}
disabled={isDisabled || false}
disabled={isDisabled}
onChange={onChange}
onOpen={onOpen}
onClose={onClose}
/>
)}
</div>
);
);
}
};
return <div className={dropdownWrapperClasses}>{renderDropdown()}</div>;
};
export default TeamsDropdown;
@@ -3,6 +3,23 @@
cursor: auto;
opacity: 0.75;
}
.__react_component_tooltip {
width: 260px;
text-align: left;
line-height: 19px;
font-size: 14px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
border-radius: $border-radius;
font-feature-settings: "ss02" on, "salt" on, "ss01" on;
color: $ui-fleet-black-75;
padding: $pad-medium;
a {
font-size: inherit;
font-weight: normal;
color: $core-vibrant-blue;
}
}
}
.component__team-dropdown {
@@ -296,4 +296,10 @@
padding: 0 0 0 10px;
}
}
.premium-feature-icon {
position: relative;
top: 4px;
margin-left: 3px;
}
}
@@ -0,0 +1,39 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
interface ICriticalPolicyProps {
color?: Colors;
size?: IconSizes;
}
const CriticalPolicy = ({
color = "core-fleet-blue",
size = "small",
}: ICriticalPolicyProps) => {
return (
<svg
width={ICON_SIZES[size]}
height={ICON_SIZES[size]}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_210_11264)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.31628 0.0513167C8.11101 -0.0171056 7.88909 -0.0171056 7.68382 0.0513167L1.68382 2.05132L1.09751 2.24676L1.0101 2.85858C0.744777 4.71586 0.739144 7.58148 1.61486 10.1817C2.50082 12.8123 4.3438 15.2886 7.80394 15.9806L8.00005 16.0198L8.19617 15.9806C11.6563 15.2886 13.4993 12.8123 14.3852 10.1817C15.261 7.58148 15.2553 4.71586 14.99 2.85858L14.9026 2.24676L14.3163 2.05132L8.31628 0.0513167ZM3.51025 9.54333C2.84797 7.57686 2.76666 5.36854 2.91876 3.74786L8.00005 2.05409L13.0813 3.74786C13.2334 5.36854 13.1521 7.57686 12.4899 9.54333C11.7701 11.6806 10.4166 13.4188 8.00005 13.9772C5.58348 13.4188 4.23004 11.6806 3.51025 9.54333ZM11.0709 6.48649C11.3396 6.17124 11.3018 5.69787 10.9865 5.42919C10.6713 5.16051 10.1979 5.19826 9.92924 5.51351L7.45871 8.41227L6.03302 6.97231C5.74159 6.67797 5.26672 6.67561 4.97237 6.96704C4.67803 7.25847 4.67566 7.73334 4.9671 8.02769L6.9671 10.0477C7.11479 10.1969 7.31825 10.2773 7.52803 10.2695C7.73781 10.2616 7.9347 10.1663 8.07087 10.0065L11.0709 6.48649Z"
fill={COLORS[color]}
/>
</g>
<defs>
<clipPath id="clip0_210_11264">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default CriticalPolicy;
-44
View File
@@ -1,44 +0,0 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
import {
ICON_SIZES,
IconSizes,
ICON_SIZES_BASE14,
} from "styles/var/icon_sizes";
interface IPolicyProps {
color?: Colors;
size?: IconSizes;
}
const Policy = ({
color = "core-fleet-blue",
size = "small",
}: IPolicyProps) => {
return (
<svg
width={ICON_SIZES_BASE14[size]}
height={ICON_SIZES[size]}
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 16"
>
<g
clipPath="url(#a)"
fillRule="evenodd"
clipRule="evenodd"
fill={COLORS[color]}
>
<path d="m6.951 9.838 3.112-3.015a.89.89 0 0 0 .27-.634.876.876 0 0 0-.27-.634.91.91 0 0 0-.64-.258.925.925 0 0 0-.638.258L6.313 7.95l-1.1-1.065a.919.919 0 0 0-1.477.29.877.877 0 0 0 .2.979l1.737 1.683a.91.91 0 0 0 .639.258.925.925 0 0 0 .64-.258Z" />
<path d="M13.041 2.357v.001L7.345.067a.925.925 0 0 0-.69 0l-6.09 2.45a.906.906 0 0 0-.409.325A.882.882 0 0 0 0 3.34v2.98c0 2.066.634 4.083 1.82 5.796a10.637 10.637 0 0 0 4.84 3.82.926.926 0 0 0 .68 0 10.637 10.637 0 0 0 4.84-3.82A10.162 10.162 0 0 0 14 6.322V3.34a.88.88 0 0 0-.156-.499.905.905 0 0 0-.408-.325l-.395-.16Zm-.86 1.583v2.382a8.4 8.4 0 0 1-1.438 4.692A8.805 8.805 0 0 1 7 14.139a8.804 8.804 0 0 1-3.743-3.125 8.4 8.4 0 0 1-1.439-4.692V3.94L7 1.854l5.182 2.086Z" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h14v16H0z" />
</clipPath>
</defs>
</svg>
);
};
export default Policy;
+3 -3
View File
@@ -14,18 +14,18 @@ const PremiumFeature = ({ size = "medium" }: IPremiumFeatureProps) => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="17" height="17" fill="#E5E5E5" />
<rect width="17" height="17" fill="none" />
<rect
width="1400"
height="880"
transform="translate(-664 -401)"
fill="white"
fill="none"
/>
<rect
width="1400"
height="830"
transform="translate(-664 -351)"
fill="white"
fill="none"
/>
<g clipPath="url(#clip0_11687_321888)">
<mask id="path-1-inside-1_11687_321888" fill="white">
+2 -3
View File
@@ -2,6 +2,7 @@ import Alert from "./Alert";
import CalendarCheck from "./CalendarCheck";
import Check from "./Check";
import Chevron from "./Chevron";
import CriticalPolicy from "./CriticalPolicy";
import DownCaret from "./DownCaret";
import Ex from "./Ex";
import EmptyHosts from "./EmptyHosts";
@@ -29,8 +30,6 @@ import M1 from "./M1";
import Centos from "./Centos";
import Ubuntu from "./Ubuntu";
import Policy from "./Policy";
// Encircled
import ApplePurple from "./ApplePurple";
import LinuxGreen from "./LinuxGreen";
@@ -63,6 +62,7 @@ export const ICON_MAP = {
"calendar-check": CalendarCheck,
chevron: Chevron,
check: Check,
"critical-policy": CriticalPolicy,
"down-caret": DownCaret,
ex: Ex,
"empty-hosts": EmptyHosts,
@@ -98,7 +98,6 @@ export const ICON_MAP = {
m1: M1,
centos: Centos,
ubuntu: Ubuntu,
policy: Policy,
"premium-feature": PremiumFeature,
"darwin-purple": ApplePurple,
"windows-blue": WindowsBlue,
+2 -1
View File
@@ -8,6 +8,7 @@ export default PropTypes.shape({
export interface IDropdownOption {
disabled: boolean;
label: string;
label: string | JSX.Element;
value: string | number;
premiumOnly?: boolean;
}
+52 -33
View File
@@ -45,6 +45,7 @@ import CustomLink from "components/CustomLink";
import Dropdown from "components/forms/fields/Dropdown";
import MainContent from "components/MainContent";
import LastUpdatedText from "components/LastUpdatedText";
import SandboxGate from "components/Sandbox/SandboxGate";
import useInfoCard from "./components/InfoCard";
import MissingHosts from "./cards/MissingHosts";
import LowDiskSpaceHosts from "./cards/LowDiskSpaceHosts";
@@ -82,7 +83,6 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
isGlobalAdmin,
isGlobalMaintainer,
isPremiumTier,
isFreeTier,
isSandboxMode,
isOnGlobalTeam,
} = useContext(AppContext);
@@ -443,6 +443,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
showHostsUI={showHostsUI}
selectedPlatformLabelId={selectedPlatformLabelId}
currentTeamId={teamIdForApi}
isSandboxMode={isSandboxMode}
/>
),
});
@@ -457,6 +458,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
showHostsUI={showHostsUI}
selectedPlatformLabelId={selectedPlatformLabelId}
currentTeamId={teamIdForApi}
isSandboxMode={isSandboxMode}
/>
),
});
@@ -487,6 +489,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
<ActivityFeed
setShowActivityFeedTitle={setShowActivityFeedTitle}
isPremiumTier={isPremiumTier || false}
isSandboxMode={isSandboxMode}
/>
),
});
@@ -543,24 +546,30 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
),
});
const MDMCard = useInfoCard({
title: "Mobile device management (MDM)",
titleDetail: mdmTitleDetail,
showTitle: !isMacAdminsFetching,
description: (
<p>MDM is used to change settings and install software on your hosts.</p>
),
children: (
<Mdm
isFetching={isMdmFetching}
error={errorMdm}
mdmStatusData={mdmStatusData}
mdmSolutions={mdmSolutions}
selectedPlatformLabelId={selectedPlatformLabelId}
selectedTeamId={currentTeamId}
/>
),
});
const MDMCard = (
<SandboxGate>
{useInfoCard({
title: "Mobile device management (MDM)",
titleDetail: mdmTitleDetail,
showTitle: !isMacAdminsFetching,
description: (
<p>
MDM is used to change settings and install software on your hosts.
</p>
),
children: (
<Mdm
isFetching={isMdmFetching}
error={errorMdm}
mdmStatusData={mdmStatusData}
mdmSolutions={mdmSolutions}
selectedPlatformLabelId={selectedPlatformLabelId}
selectedTeamId={currentTeamId}
/>
),
})}
</SandboxGate>
);
const OperatingSystemsCard = useInfoCard({
title: "Operating systems",
@@ -647,6 +656,29 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
);
};
const renderDashboardHeader = () => {
if (isPremiumTier) {
if (userTeams) {
if (userTeams.length > 1 || isOnGlobalTeam) {
return (
<TeamsDropdown
selectedTeamId={currentTeamId}
currentUserTeams={userTeams}
onChange={handleTeamChange}
isSandboxMode={isSandboxMode}
/>
);
}
if (userTeams.length === 1) {
return <h1>{userTeams[0].name}</h1>;
}
}
// userTeams.length should have at least 1 element
return null;
}
// Free tier
return <h1>{config?.org_info.org_name}</h1>;
};
return !isRouteOk ? (
<Spinner />
) : (
@@ -655,20 +687,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>
{isFreeTier && <h1>{config?.org_info.org_name}</h1>}
{isPremiumTier &&
userTeams &&
(userTeams.length > 1 || isOnGlobalTeam) && (
<TeamsDropdown
selectedTeamId={currentTeamId}
currentUserTeams={userTeams}
onChange={handleTeamChange}
/>
)}
{isPremiumTier &&
!isOnGlobalTeam &&
userTeams &&
userTeams.length === 1 && <h1>{userTeams[0].name}</h1>}
{renderDashboardHeader()}
</div>
</div>
</div>
@@ -20,6 +20,7 @@ const baseClass = "activity-feed";
interface IActvityCardProps {
setShowActivityFeedTitle: (showActivityFeedTitle: boolean) => void;
isPremiumTier: boolean;
isSandboxMode?: boolean;
}
const DEFAULT_PAGE_SIZE = 8;
@@ -27,6 +28,7 @@ const DEFAULT_PAGE_SIZE = 8;
const ActivityFeed = ({
setShowActivityFeedTitle,
isPremiumTier,
isSandboxMode = false,
}: IActvityCardProps): JSX.Element => {
const [pageIndex, setPageIndex] = useState(0);
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
@@ -115,6 +117,7 @@ const ActivityFeed = ({
<ActivityItem
activity={activity}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
onDetailsClick={handleDetailsClick}
key={activity.id}
/>
@@ -9,9 +9,21 @@ import Avatar from "components/Avatar";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import ReactTooltip from "react-tooltip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
const baseClass = "activity-item";
const PREMIUM_ACTIVITIES = new Set([
"created_team",
"deleted_team",
"applied_spec_team",
"changed_user_team_role",
"deleted_user_team_role",
"read_host_disk_encryption_key",
"enabled_macos_disk_encryption",
"disabled_macos_disk_encryption",
]);
const getProfileMessageSuffix = (
isPremiumTier: boolean,
teamName?: string | null
@@ -499,6 +511,7 @@ const getDetail = (
interface IActivityItemProps {
activity: IActivity;
isPremiumTier: boolean;
isSandboxMode?: boolean;
/** A handler for handling clicking on the details of an activity. Not all
* activites have more details so this is optional. An example of additonal
@@ -510,6 +523,7 @@ interface IActivityItemProps {
const ActivityItem = ({
activity,
isPremiumTier,
isSandboxMode = false,
onDetailsClick = noop,
}: IActivityItemProps) => {
const { actor_email } = activity;
@@ -518,6 +532,8 @@ const ActivityItem = ({
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
const activityCreatedAt = new Date(activity.created_at);
const indicatePremiumFeature =
isSandboxMode && PREMIUM_ACTIVITIES.has(activity.type);
return (
<div className={baseClass}>
@@ -529,6 +545,7 @@ const ActivityItem = ({
/>
<div className={`${baseClass}__details`}>
<p>
{indicatePremiumFeature && <PremiumFeatureIconWithTooltip />}
<span className={`${baseClass}__details-topline`}>
{activity.type === ActivityType.UserLoggedIn ? (
<b>{activity.actor_email} </b>
@@ -30,6 +30,12 @@
&__details {
padding-left: $pad-large;
.premium-icon-tip {
position: relative;
top: 4px;
padding-right: $pad-xsmall;
}
p {
margin: 0;
line-height: 16px;
@@ -6,6 +6,7 @@ import { kebabCase } from "lodash";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
import { IconNames } from "components/icons";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
interface ISummaryTileProps {
count: number;
@@ -13,8 +14,10 @@ interface ISummaryTileProps {
showUI: boolean;
title: string;
iconName: IconNames;
tooltip?: string;
path: string;
tooltip?: string;
isSandboxMode?: boolean;
sandboxPremiumOnlyIcon?: boolean;
}
const baseClass = "summary-tile";
@@ -25,8 +28,10 @@ const SummaryTile = ({
showUI, // false on first load only
title,
iconName,
tooltip,
path,
tooltip,
isSandboxMode = false,
sandboxPremiumOnlyIcon = false,
}: ISummaryTileProps): JSX.Element => {
const numberWithCommas = (x: number): string => {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -64,6 +69,11 @@ const SummaryTile = ({
) : (
title
)}
{isSandboxMode && sandboxPremiumOnlyIcon && (
<PremiumFeatureIconWithTooltip
tooltipPositionOverrides={{ leftAdj: 2, topAdj: 5 }}
/>
)}
</div>
</div>
</>
@@ -32,5 +32,14 @@
width: 200px;
white-space: initial;
}
.premium-icon-tip {
margin-left: 3px;
.premium-feature-icon {
position: relative;
top: 5px;
}
}
}
}
@@ -14,6 +14,7 @@ interface IHostSummaryProps {
showHostsUI: boolean;
selectedPlatformLabelId?: number;
currentTeamId?: number;
isSandboxMode?: boolean;
}
const LowDiskSpaceHosts = ({
@@ -23,6 +24,7 @@ const LowDiskSpaceHosts = ({
showHostsUI,
selectedPlatformLabelId,
currentTeamId,
isSandboxMode = false,
}: IHostSummaryProps): JSX.Element => {
// build the manage hosts URL filtered by low disk space only
// currently backend cannot filter by both low disk space and label
@@ -46,6 +48,8 @@ const LowDiskSpaceHosts = ({
title="Low disk space hosts"
tooltip={`Hosts that have ${lowDiskSpaceGb} GB or less disk space available.`}
path={path}
isSandboxMode={isSandboxMode}
sandboxPremiumOnlyIcon
/>
</div>
);
@@ -13,6 +13,7 @@ interface IHostSummaryProps {
showHostsUI: boolean;
selectedPlatformLabelId?: number;
currentTeamId?: number;
isSandboxMode?: boolean;
}
const MissingHosts = ({
@@ -21,6 +22,7 @@ const MissingHosts = ({
showHostsUI,
selectedPlatformLabelId,
currentTeamId,
isSandboxMode = false,
}: IHostSummaryProps): JSX.Element => {
// build the manage hosts URL filtered by missing and platform
const queryParams = {
@@ -43,6 +45,8 @@ const MissingHosts = ({
title="Missing hosts"
tooltip="Hosts that have not been online in 30 days or more."
path={path}
isSandboxMode={isSandboxMode}
sandboxPremiumOnlyIcon
/>
</div>
);
@@ -68,6 +68,7 @@ const ManageControlsPage = ({
isOnGlobalTeam,
isPremiumTier,
isGlobalAdmin,
isSandboxMode,
} = useContext(AppContext);
const {
@@ -172,6 +173,7 @@ const ManageControlsPage = ({
onChange={handleTeamChange}
includeAll={false}
includeNoTeams
isSandboxMode={isSandboxMode}
/>
)}
{isPremiumTier &&
@@ -21,8 +21,6 @@ import Modal from "components/Modal";
import UserSettingsForm from "components/forms/UserSettingsForm";
import InfoBanner from "components/InfoBanner";
import SecretField from "components/EnrollSecrets/SecretField";
import SandboxGate from "components/Sandbox/SandboxGate";
import SandboxDemoMessage from "components/Sandbox/SandboxDemoMessage";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
import CustomLink from "components/CustomLink";
@@ -225,15 +223,7 @@ const UserSettingsPage = ({
return (
<>
<MainContent className={baseClass}>
<SandboxGate
fallbackComponent={() => (
<SandboxDemoMessage
className={`${baseClass}__sandboxMode`}
message="Account management is only available in self-managed Fleet"
utmSource="fleet-ui-my-account-page"
/>
)}
>
<>
<div className={`${baseClass}__manage`}>
<h1>My account</h1>
<UserSettingsForm
@@ -248,17 +238,15 @@ const UserSettingsPage = ({
{renderEmailModal()}
{renderPasswordModal()}
{renderApiTokenModal()}
</SandboxGate>
</>
</MainContent>
<SandboxGate>
<SidePanelContent>
<UserSidePanel
currentUser={currentUser}
onChangePassword={onShowPasswordModal}
onGetApiToken={onShowApiTokenModal}
/>
</SidePanelContent>
</SandboxGate>
<SidePanelContent>
<UserSidePanel
currentUser={currentUser}
onChangePassword={onShowPasswordModal}
onGetApiToken={onShowApiTokenModal}
/>
</SidePanelContent>
</>
);
};
@@ -9,8 +9,6 @@ import configAPI from "services/entities/config";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import deepDifference from "utilities/deep_difference";
import SandboxGate from "components/Sandbox/SandboxGate";
import SandboxDemoMessage from "components/Sandbox/SandboxDemoMessage";
import Spinner from "components/Spinner";
import SideNav from "../components/SideNav";
@@ -121,33 +119,23 @@ const OrgSettingsPage = ({ params }: IOrgSettingsPageProps) => {
<p className={`${baseClass}__page-description`}>
Set your organization information and configure SSO and SMTP
</p>
<SandboxGate
fallbackComponent={() => (
<SandboxDemoMessage
message="Organization settings are only available in self-managed Fleet"
utmSource="fleet-ui-organization-settings-page"
className={`${baseClass}__sandbox-demo-message`}
/>
)}
>
<SideNav
className={`${baseClass}__side-nav`}
navItems={navItems}
activeItem={currentFormSection.urlSection}
CurrentCard={
!isLoadingAppConfig && appConfig ? (
<CurrentCard
appConfig={appConfig}
handleSubmit={onFormSubmit}
isUpdatingSettings={isUpdatingSettings}
isPremiumTier={isPremiumTier}
/>
) : (
<Spinner />
)
}
/>
</SandboxGate>
<SideNav
className={`${baseClass}__side-nav`}
navItems={navItems}
activeItem={currentFormSection.urlSection}
CurrentCard={
!isLoadingAppConfig && appConfig ? (
<CurrentCard
appConfig={appConfig}
handleSubmit={onFormSubmit}
isUpdatingSettings={isUpdatingSettings}
isPremiumTier={isPremiumTier}
/>
) : (
<Spinner />
)
}
/>
</div>
);
};
@@ -11,23 +11,9 @@ import classnames from "classnames";
interface ISettingSubNavItem {
name: string;
pathname: string;
exclude?: boolean;
}
const settingsSubNav: ISettingSubNavItem[] = [
{
name: "Organization settings",
pathname: PATHS.ADMIN_SETTINGS,
},
{
name: "Integrations",
pathname: PATHS.ADMIN_INTEGRATIONS,
},
{
name: "Users",
pathname: PATHS.ADMIN_USERS,
},
];
interface ISettingsWrapperProp {
children: JSX.Element;
location: {
@@ -36,13 +22,6 @@ interface ISettingsWrapperProp {
router: InjectedRouter; // v3
}
const getTabIndex = (path: string): number => {
return settingsSubNav.findIndex((navItem) => {
// tab stays highlighted for paths that start with same pathname
return path.startsWith(navItem.pathname);
});
};
const baseClass = "settings-wrapper";
const SettingsWrapper = ({
@@ -52,18 +31,44 @@ const SettingsWrapper = ({
}: ISettingsWrapperProp): JSX.Element => {
const { isPremiumTier, isSandboxMode } = useContext(AppContext);
if (isPremiumTier && settingsSubNav.length === 3) {
settingsSubNav.push({
const settingsSubNav: ISettingSubNavItem[] = [
{
name: "Organization settings",
pathname: PATHS.ADMIN_SETTINGS,
exclude: isSandboxMode,
},
{
name: "Integrations",
pathname: PATHS.ADMIN_INTEGRATIONS,
},
{
name: "Users",
pathname: PATHS.ADMIN_USERS,
exclude: isSandboxMode,
},
{
name: "Teams",
pathname: PATHS.ADMIN_TEAMS,
});
}
exclude: !isPremiumTier,
},
];
const filteredSettingsSubNav = settingsSubNav.filter((navItem) => {
return !navItem.exclude;
});
const navigateToNav = (i: number): void => {
const navPath = settingsSubNav[i].pathname;
const navPath = filteredSettingsSubNav[i].pathname;
router.push(navPath);
};
const getTabIndex = (path: string): number => {
return filteredSettingsSubNav.findIndex((navItem) => {
// tab stays highlighted for paths that start with same pathname
return path.startsWith(navItem.pathname);
});
};
// we add a conditional sandbox-mode class here as we will need to make some
// styling changes on the settings page to have the sticky elements work
// with the sandbox mode expiry message
@@ -79,7 +84,7 @@ const SettingsWrapper = ({
onSelect={(i) => navigateToNav(i)}
>
<TabList>
{settingsSubNav.map((navItem) => {
{filteredSettingsSubNav.map((navItem) => {
// Bolding text when the tab is active causes a layout shift
// so we add a hidden pseudo element with the same text string
return (
@@ -18,6 +18,8 @@ import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import SandboxGate from "components/Sandbox/SandboxGate";
import SandboxMessage from "components/Sandbox/SandboxMessage";
import CreateTeamModal from "./components/CreateTeamModal";
import DeleteTeamModal from "./components/DeleteTeamModal";
@@ -254,62 +256,75 @@ const TeamManagementPage = (): JSX.Element => {
<p className={`${baseClass}__page-description`}>
Create, customize, and remove teams from Fleet.
</p>
{loadingTeamsError ? (
<TableDataError />
) : (
<TableContainer
columns={tableHeaders}
data={tableData}
isLoading={isFetchingTeams}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
inputPlaceHolder={"Search"}
actionButtonText={"Create team"}
actionButtonVariant={"brand"}
hideActionButton={teams && teams.length === 0 && searchString === ""}
onActionButtonClick={toggleCreateTeamModal}
onQueryChange={onQueryChange}
resultsTitle={"teams"}
emptyComponent={() =>
EmptyTable({
iconName: "empty-teams",
header: emptyState().header,
info: emptyState().info,
additionalInfo: emptyState().additionalInfo,
primaryButton: emptyState().primaryButton,
})
}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable={teams && teams.length > 0 && searchString !== ""}
isClientSidePagination
/>
)}
{showCreateTeamModal && (
<CreateTeamModal
onCancel={toggleCreateTeamModal}
onSubmit={onCreateSubmit}
backendValidators={backendValidators}
isUpdatingTeams={isUpdatingTeams}
/>
)}
{showDeleteTeamModal && (
<DeleteTeamModal
onCancel={toggleDeleteTeamModal}
onSubmit={onDeleteSubmit}
name={teamEditing?.name || ""}
isUpdatingTeams={isUpdatingTeams}
/>
)}
{showEditTeamModal && (
<EditTeamModal
onCancel={toggleEditTeamModal}
onSubmit={onEditSubmit}
defaultName={teamEditing?.name || ""}
backendValidators={backendValidators}
isUpdatingTeams={isUpdatingTeams}
/>
)}
<SandboxGate
fallbackComponent={() => (
<SandboxMessage
variant="sales"
message="Teams is only available in Fleet premium."
utmSource="fleet-ui-teams-page"
className={`${baseClass}__sandbox-message`}
/>
)}
>
{loadingTeamsError ? (
<TableDataError />
) : (
<TableContainer
columns={tableHeaders}
data={tableData}
isLoading={isFetchingTeams}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
inputPlaceHolder={"Search"}
actionButtonText={"Create team"}
actionButtonVariant={"brand"}
hideActionButton={
teams && teams.length === 0 && searchString === ""
}
onActionButtonClick={toggleCreateTeamModal}
onQueryChange={onQueryChange}
resultsTitle={"teams"}
emptyComponent={() =>
EmptyTable({
iconName: "empty-teams",
header: emptyState().header,
info: emptyState().info,
additionalInfo: emptyState().additionalInfo,
primaryButton: emptyState().primaryButton,
})
}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable={teams && teams.length > 0 && searchString !== ""}
isClientSidePagination
/>
)}
{showCreateTeamModal && (
<CreateTeamModal
onCancel={toggleCreateTeamModal}
onSubmit={onCreateSubmit}
backendValidators={backendValidators}
isUpdatingTeams={isUpdatingTeams}
/>
)}
{showDeleteTeamModal && (
<DeleteTeamModal
onCancel={toggleDeleteTeamModal}
onSubmit={onDeleteSubmit}
name={teamEditing?.name || ""}
isUpdatingTeams={isUpdatingTeams}
/>
)}
{showEditTeamModal && (
<EditTeamModal
onCancel={toggleEditTeamModal}
onSubmit={onEditSubmit}
defaultName={teamEditing?.name || ""}
backendValidators={backendValidators}
isUpdatingTeams={isUpdatingTeams}
/>
)}
</SandboxGate>
</div>
);
};
@@ -1,7 +1,7 @@
import React from "react";
import { InjectedRouter } from "react-router";
import SandboxGate from "components/Sandbox/SandboxGate";
import SandboxDemoMessage from "components/Sandbox/SandboxDemoMessage";
import SandboxMessage from "components/Sandbox/SandboxMessage";
import UsersTable from "./components/UsersTable";
const baseClass = "user-management";
@@ -19,10 +19,10 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
</p>
<SandboxGate
fallbackComponent={() => (
<SandboxDemoMessage
<SandboxMessage
message="User management is only available in self-managed Fleet"
utmSource="fleet-ui-users-page"
className={`${baseClass}__sandbox-demo-message`}
className={`${baseClass}__sandbox-message`}
/>
)}
>
@@ -0,0 +1,71 @@
import React from "react";
import Icon from "components/Icon";
export const LABEL_SLUG_PREFIX = "labels/";
export const DEFAULT_SORT_HEADER = "display_name";
export const DEFAULT_SORT_DIRECTION = "asc";
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_PAGE_INDEX = 0;
export const getHostSelectStatuses = (isSandboxMode = false) => {
return [
{
disabled: false,
label: "All hosts",
value: "",
helpText: "All hosts added to Fleet.",
},
{
disabled: false,
label: "Online hosts",
value: "online",
helpText: "Hosts that will respond to a live query.",
},
{
disabled: false,
label: "Offline hosts",
value: "offline",
helpText: "Hosts that wont respond to a live query.",
},
{
disabled: false,
label: isSandboxMode ? (
<span>
<span>Missing hosts</span>
<Icon name="premium-feature" className="premium-feature-icon" />
{/* <PremiumFeatureIconWithTooltip /> */}
</span>
) : (
"Missing hosts"
),
value: "missing",
helpText: "Hosts that have been offline for 30 days or more.",
},
{
disabled: false,
label: "New hosts",
value: "new",
helpText: "Hosts added to Fleet in the last 24 hours.",
},
];
};
export const MAC_SETTINGS_FILTER_OPTIONS = [
{
disabled: false,
label: "Latest",
value: "latest",
},
{
disabled: false,
label: "Pending",
value: "pending",
},
{
disabled: false,
label: "Failing",
value: "failing",
},
];
@@ -11,6 +11,7 @@ import { RouteProps } from "react-router/lib/Route";
import { find, isEmpty, isEqual, omit } from "lodash";
import { format } from "date-fns";
import FileSaver from "file-saver";
import classNames from "classnames";
import enrollSecretsAPI from "services/entities/enroll_secret";
import labelsAPI, { ILabelsResponse } from "services/entities/labels";
@@ -79,8 +80,8 @@ import {
DEFAULT_SORT_DIRECTION,
DEFAULT_PAGE_SIZE,
DEFAULT_PAGE_INDEX,
HOST_SELECT_STATUSES,
} from "./constants";
getHostSelectStatuses,
} from "./HostsPageConfig";
import { isAcceptableStatus, getNextLocationPath } from "./helpers";
import DeleteSecretModal from "../../../components/EnrollSecrets/DeleteSecretModal";
import SecretEditorModal from "../../../components/EnrollSecrets/SecretEditorModal";
@@ -1060,6 +1061,7 @@ const ManageHostsPage = ({
isDisabled={isLoadingHosts || isLoadingHostsCount} // TODO: why?
onChange={onTeamChange}
includeNoTeams
isSandboxMode={isSandboxMode}
/>
);
@@ -1292,12 +1294,16 @@ const ManageHostsPage = ({
? selectedLabel
: undefined;
const statusDropdownClassnames = classNames(
`${baseClass}__status_dropdown`,
{ [`${baseClass}__status-dropdown-sandbox`]: isSandboxMode }
);
return (
<div className={`${baseClass}__filter-dropdowns`}>
<Dropdown
value={status || ""}
className={`${baseClass}__status_dropdown`}
options={HOST_SELECT_STATUSES}
className={statusDropdownClassnames}
options={getHostSelectStatuses(isSandboxMode)}
searchable={false}
onChange={handleStatusDropdownChange}
/>
@@ -1388,6 +1394,7 @@ const ManageHostsPage = ({
variant: "text-icon",
icon: "transfer",
hideButton: !isPremiumTier || (!isGlobalAdmin && !isGlobalMaintainer),
indicatePremiumFeature: isPremiumTier && isSandboxMode,
},
];
@@ -1577,6 +1584,7 @@ const ManageHostsPage = ({
onChangeMacSettingsFilter={handleMacSettingsStatusDropdownChange}
onClickEditLabel={onEditLabelClick}
onClickDeleteLabel={toggleDeleteLabelModal}
isSandboxMode={isSandboxMode}
/>
{renderNoEnrollSecretBanner()}
{renderTable()}
@@ -109,6 +109,12 @@
.manage-hosts__filter-dropdowns {
display: flex;
margin-left: $pad-small;
.manage-hosts__status-dropdown-sandbox {
width: auto;
.Select-control {
width: 182px;
}
}
}
}
}
@@ -135,7 +141,7 @@
}
}
.table-container__header-left {
&__header-left {
order: 2;
display: flex;
flex-direction: column;
@@ -154,12 +160,19 @@
.form-field--dropdown,
.manage-hosts__label-filter-dropdown {
max-width: 366px;
flex: 1;
}
.manage-hosts__status_dropdown {
width: auto;
}
.manage-hosts__status-dropdown-sandbox {
.Select-control {
width: 100%;
}
}
}
}
}
@@ -3,15 +3,19 @@ import ReactTooltip from "react-tooltip";
import classnames from "classnames";
import Button from "components/buttons/Button";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import CloseIcon from "../../../../../../assets/images/icon-close-vibrant-blue-16x16@2x.png";
interface IFilterPillProps {
label: string;
onClear: () => void;
icon?: string;
tooltipDescription?: string | ReactNode;
premiumFeatureTooltipDelayHide?: number;
className?: string;
onClear: () => void;
isSandboxMode?: boolean;
sandboxPremiumOnlyIcon?: boolean;
}
const baseClass = "filter-pill";
@@ -20,8 +24,11 @@ const FilterPill = ({
label,
icon,
tooltipDescription,
premiumFeatureTooltipDelayHide,
className,
onClear,
isSandboxMode = false,
sandboxPremiumOnlyIcon = false,
}: IFilterPillProps) => {
const baseClasses = classnames(baseClass, className);
const labelClasses = classnames(`${baseClass}__label`, {
@@ -35,15 +42,23 @@ const FilterPill = ({
aria-label={`hosts filtered by ${label}`}
>
<>
<span
data-tip={tooltipDescription}
data-for={`filter-pill-tooltip-${label}`}
>
<span>
<div className={labelClasses}>
{icon && (
<img src={icon} alt="" data-testid={`${baseClass}__icon`} />
)}
{label}
{isSandboxMode && sandboxPremiumOnlyIcon && (
<PremiumFeatureIconWithTooltip
tooltipPositionOverrides={{ leftAdj: 120, topAdj: -3 }}
tooltipDelayHide={premiumFeatureTooltipDelayHide}
/>
)}
<span
data-tip={tooltipDescription}
data-for={`filter-pill-tooltip-${label}`}
>
{label}
</span>
<Button
className={`${baseClass}__clear-filter`}
onClick={onClear}
@@ -1,5 +1,4 @@
.filter-pill {
&__label {
display: inline-flex;
align-items: center;
@@ -29,5 +28,13 @@
margin: 0px;
}
}
.premium-icon-tip {
.premium-feature-icon {
position: relative;
top: 2px;
margin-right: 6px;
}
}
}
}
@@ -26,10 +26,9 @@ import {
import Dropdown from "components/forms/fields/Dropdown";
import Button from "components/buttons/Button";
import { MAC_SETTINGS_FILTER_OPTIONS } from "../../constants";
import FilterPill from "../FilterPill";
import PoliciesFilter from "../PoliciesFilter";
import { MAC_SETTINGS_FILTER_OPTIONS } from "../../HostsPageConfig";
import DiskEncryptionStatusFilter from "../DiskEncryptionStatusFilter";
import BootstrapPackageStatusFilter from "../BootstrapPackageStatusFilter/BootstrapPackageStatusFilter";
@@ -83,6 +82,7 @@ interface IHostsFilterBlockProps {
) => void;
onClickEditLabel: (evt: React.MouseEvent<HTMLButtonElement>) => void;
onClickDeleteLabel: () => void;
isSandboxMode?: boolean;
}
/**
@@ -120,6 +120,7 @@ const HostsFilterBlock = ({
onChangeMacSettingsFilter,
onClickEditLabel,
onClickDeleteLabel,
isSandboxMode = false,
}: IHostsFilterBlockProps) => {
const renderLabelFilterPill = () => {
if (selectedLabel) {
@@ -359,7 +360,10 @@ const HostsFilterBlock = ({
<FilterPill
label="Low disk space"
tooltipDescription={TooltipDescription}
premiumFeatureTooltipDelayHide={1000}
onClear={() => handleClearFilter(["low_disk_space"])}
isSandboxMode={isSandboxMode}
sandboxPremiumOnlyIcon
/>
);
};
@@ -1,59 +0,0 @@
import { MdmProfileStatus } from "interfaces/mdm";
export const LABEL_SLUG_PREFIX = "labels/";
export const DEFAULT_SORT_HEADER = "display_name";
export const DEFAULT_SORT_DIRECTION = "asc";
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_PAGE_INDEX = 0;
export const HOST_SELECT_STATUSES = [
{
disabled: false,
label: "All hosts",
value: "",
helpText: "All hosts added to Fleet.",
},
{
disabled: false,
label: "Online hosts",
value: "online",
helpText: "Hosts that will respond to a live query.",
},
{
disabled: false,
label: "Offline hosts",
value: "offline",
helpText: "Hosts that wont respond to a live query.",
},
{
disabled: false,
label: "Missing hosts",
value: "missing",
helpText: "Hosts that have been offline for 30 days or more.",
},
{
disabled: false,
label: "New hosts",
value: "new",
helpText: "Hosts added to Fleet in the last 24 hours.",
},
];
export const MAC_SETTINGS_FILTER_OPTIONS = [
{
disabled: false,
label: "Verifying",
value: MdmProfileStatus.VERIFYING,
},
{
disabled: false,
label: "Pending",
value: MdmProfileStatus.PENDING,
},
{
disabled: false,
label: "Failed",
value: MdmProfileStatus.FAILED,
},
];
@@ -31,6 +31,7 @@ const HostActionsDropdown = ({
isMdmEnabledAndConfigured = false,
isTeamAdmin = false,
isTeamMaintainer = false,
isSandboxMode = false,
} = useContext(AppContext);
const options = generateHostActionOptions({
@@ -46,6 +47,7 @@ const HostActionsDropdown = ({
isFleetMdm: mdmName === "Fleet",
isMdmEnabledAndConfigured,
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
isSandboxMode,
});
// No options to render. Exit early
@@ -1,11 +1,14 @@
import React from "react";
import { IDropdownOption } from "interfaces/dropdownOption";
import { cloneDeep } from "lodash";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
const DEFAULT_OPTIONS: IDropdownOption[] = [
{
label: "Transfer",
value: "transfer",
disabled: false,
premiumOnly: true,
},
{
label: "Query",
@@ -41,6 +44,7 @@ interface IHostActionConfigOptions {
isFleetMdm: boolean;
isMdmEnabledAndConfigured: boolean;
doesStoreEncryptionKey: boolean;
isSandboxMode: boolean;
}
const canTransferTeam = (config: IHostActionConfigOptions) => {
@@ -106,17 +110,33 @@ const filterOutOptions = (
const setOptionsAsDisabled = (
options: IDropdownOption[],
isHostOnline: boolean
isHostOnline: boolean,
isSandboxMode: boolean
) => {
if (!isHostOnline) {
const disableOptions = options.filter(
(option) => option.value === "query" || option.value === "mdmOff"
);
disableOptions.forEach((option) => {
const disableOptions = (optionsToDisable: IDropdownOption[]) => {
optionsToDisable.forEach((option) => {
option.disabled = true;
});
};
let optionsToDisable: IDropdownOption[] = [];
console.log("options to disable: ", optionsToDisable);
if (!isHostOnline) {
optionsToDisable = optionsToDisable.concat(
options.filter(
(option) => option.value === "query" || option.value === "mdmOff"
)
);
}
console.log("options to disable: ", optionsToDisable);
if (isSandboxMode) {
optionsToDisable = optionsToDisable.concat(
options.filter((option) => option.value === "transfer")
);
}
console.log("options to disable: ", optionsToDisable);
disableOptions(optionsToDisable);
return options;
};
@@ -133,6 +153,28 @@ export const generateHostActionOptions = (config: IHostActionConfigOptions) => {
if (options.length === 0) return options;
options = setOptionsAsDisabled(options, config.isHostOnline);
options = setOptionsAsDisabled(
options,
config.isHostOnline,
config.isSandboxMode
);
if (config.isSandboxMode) {
const premiumOnlyOptions: IDropdownOption[] = options.filter(
(option) => !!option.premiumOnly
);
premiumOnlyOptions.forEach((option) => {
option.label = (
<span>
{option.label}
<PremiumFeatureIconWithTooltip
tooltipPositionOverrides={{ leftAdj: 2 }}
/>
</span>
);
});
}
return options;
};
@@ -126,6 +126,7 @@ const HostDetailsPage = ({
isGlobalAdmin = false,
isGlobalObserver,
isPremiumTier = false,
isSandboxMode,
isOnlyObserver,
filteredHostsPath,
} = useContext(AppContext);
@@ -687,6 +688,7 @@ const HostDetailsPage = ({
diskEncryption={hostDiskEncryption}
bootstrapPackageData={bootstrapPackageData}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
isOnlyObserver={isOnlyObserver}
toggleOSPolicyModal={toggleOSPolicyModal}
toggleMacSettingsModal={toggleMacSettingsModal}
+1 -1
View File
@@ -40,7 +40,7 @@
.info-flex__data {
display: flex;
gap: 4px;
gap: $pad-xsmall;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -11,6 +11,7 @@ import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import StatusIndicator from "components/StatusIndicator";
import { IHostMacMdmProfile, BootstrapPackageStatus } from "interfaces/mdm";
import getHostStatusTooltipText from "pages/hosts/helpers";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import MacSettingsIndicator from "./MacSettingsIndicator";
import HostSummaryIndicator from "./HostSummaryIndicator";
@@ -33,6 +34,7 @@ interface IHostSummaryProps {
bootstrapPackageData?: IBootstrapPackageData;
diskEncryption?: IHostDiskEncryptionProps;
isPremiumTier?: boolean;
isSandboxMode?: boolean;
isOnlyObserver?: boolean;
toggleOSPolicyModal?: () => void;
toggleMacSettingsModal?: () => void;
@@ -52,6 +54,7 @@ const HostSummary = ({
bootstrapPackageData,
diskEncryption,
isPremiumTier,
isSandboxMode = false,
isOnlyObserver,
toggleOSPolicyModal,
toggleMacSettingsModal,
@@ -105,7 +108,9 @@ const HostSummary = ({
const renderIssues = () => (
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Issues</span>
<span className="info-flex__header">
Issues{isSandboxMode && <PremiumFeatureIconWithTooltip />}
</span>
<span className="info-flex__data">
<span
className="host-issue tooltip tooltip__tooltip-icon"
@@ -162,7 +167,7 @@ const HostSummary = ({
/>
</div>
{titleData.issues?.total_issues_count > 0 &&
{(titleData.issues?.total_issues_count > 0 || isSandboxMode) &&
isPremiumTier &&
renderIssues()}
@@ -257,7 +262,7 @@ const HostSummary = ({
);
return (
<>
<div className={baseClass}>
<div className="header title">
<div className="title__inner">
<div className="display-name-container">
@@ -279,7 +284,7 @@ const HostSummary = ({
<div className="section title">
<div className="title__inner">{renderSummary()}</div>
</div>
</>
</div>
);
};
@@ -0,0 +1,14 @@
.host-summary {
.info-flex {
&__header {
display: flex;
align-items: center;
gap: $pad-xxsmall;
max-height: 20px;
.premium-icon-tip {
position: relative;
top: 3px;
}
}
}
}
@@ -58,6 +58,7 @@ const ManagePolicyPage = ({
isOnGlobalTeam,
isFreeTier,
isPremiumTier,
isSandboxMode,
setConfig,
} = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
@@ -330,7 +331,6 @@ const ManagePolicyPage = ({
currentAutomatedPolicies = webhook?.policy_ids || [];
}
}
return !isRouteOk || (isPremiumTier && !userTeams) ? (
<Spinner />
) : (
@@ -347,6 +347,7 @@ const ManagePolicyPage = ({
currentUserTeams={userTeams || []}
selectedTeamId={currentTeamId}
onChange={onTeamChange}
isSandboxMode={isSandboxMode}
/>
)}
{isPremiumTier &&
@@ -417,6 +418,8 @@ const ManagePolicyPage = ({
canAddOrDeletePolicy={canAddOrDeletePolicy}
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
/>
))}
{!isAnyTeamSelected && globalPoliciesError && <TableDataError />}
@@ -433,6 +436,8 @@ const ManagePolicyPage = ({
canAddOrDeletePolicy={canAddOrDeletePolicy}
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
/>
))}
</div>
@@ -148,10 +148,11 @@
}
.critical-tooltip {
text-align: left;
font-weight: $regular;
}
.policy-icon {
.critical-policy-icon {
margin-left: 1px;
position: relative;
}
@@ -0,0 +1,55 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import createMockPolicy from "__mocks__/policyMock";
import PoliciesTable from "./PoliciesTable";
describe("Policies table", () => {
const testCriticalPolicy = createMockPolicy({ critical: true });
it("Renders a tooltip including 'Premium feature' copy for a critical policy in Sandbox mode", () => {
render(
<PoliciesTable
policiesList={[testCriticalPolicy]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
isSandboxMode
/>
);
expect(
screen.getByText("This policy has been marked as critical.", {
exact: false,
})
).toBeInTheDocument();
expect(
screen.getByText("This is a premium feature.", { exact: false })
).toBeInTheDocument();
});
it("Renders a tooltip excluding 'Premium feature' copy for a critical policy not in Sandbox mode", () => {
render(
<PoliciesTable
policiesList={[testCriticalPolicy]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
isSandboxMode={false}
/>
);
expect(
screen.getByText("This policy has been marked as critical.", {
exact: false,
})
).toBeInTheDocument();
expect(
screen.queryByText("This is a premium feature.", { exact: false })
).toBeNull();
});
});
@@ -31,6 +31,8 @@ interface IPoliciesTableProps {
tableType?: string;
currentTeam: ITeamSummary | undefined;
currentAutomatedPolicies?: number[];
isPremiumTier?: boolean;
isSandboxMode?: boolean;
}
const PoliciesTable = ({
@@ -42,6 +44,8 @@ const PoliciesTable = ({
tableType,
currentTeam,
currentAutomatedPolicies,
isPremiumTier,
isSandboxMode,
}: IPoliciesTableProps): JSX.Element => {
const { MANAGE_HOSTS } = paths;
@@ -121,11 +125,15 @@ const PoliciesTable = ({
) : (
<TableContainer
resultsTitle={"policies"}
columns={generateTableHeaders({
selectedTeamId: currentTeam?.id,
canAddOrDeletePolicy,
tableType,
})}
columns={generateTableHeaders(
{
selectedTeamId: currentTeam?.id,
canAddOrDeletePolicy,
tableType,
},
isPremiumTier,
isSandboxMode
)}
data={generateDataSet(
policiesList,
currentAutomatedPolicies,
@@ -91,11 +91,16 @@ const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateTableHeaders = (options: {
selectedTeamId?: number | null;
canAddOrDeletePolicy?: boolean;
tableType?: string;
}): IDataColumn[] => {
const generateTableHeaders = (
options: {
selectedTeamId?: number | null;
canAddOrDeletePolicy?: boolean;
tableType?: string;
},
isPremiumTier?: boolean,
isSandboxMode?: boolean
): IDataColumn[] => {
const { selectedTeamId, tableType, canAddOrDeletePolicy } = options;
const tableHeaders: IDataColumn[] = [
@@ -110,14 +115,17 @@ const generateTableHeaders = (options: {
value={
<>
<div className="policy-name-text">{cellProps.cell.value}</div>
{cellProps.row.original.critical && (
{isPremiumTier && cellProps.row.original.critical && (
<>
<span
className="tooltip-base"
data-tip
data-for={`critical-tooltip-${cellProps.row.original.id}`}
>
<Icon className="policy-icon" name="policy" />
<Icon
className="critical-policy-icon"
name="critical-policy"
/>
</span>
<ReactTooltip
className="critical-tooltip"
@@ -128,6 +136,12 @@ const generateTableHeaders = (options: {
backgroundColor="#3e4771"
>
This policy has been marked as critical.
{isSandboxMode && (
<>
<br />
This is a premium feature.
</>
)}
</ReactTooltip>
</>
)}
@@ -168,4 +168,12 @@
font-size: $x-small;
}
}
.critical-checkbox-wrapper {
display: flex;
align-items: center;
gap: $pad-xsmall;
.form-field--checkbox {
margin-bottom: 0;
}
}
}
@@ -16,6 +16,7 @@ import Button from "components/buttons/Button";
import Modal from "components/Modal";
import ReactTooltip from "react-tooltip";
import { platform } from "process";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
export interface INewPolicyModalProps {
baseClass: string;
@@ -49,7 +50,7 @@ const NewPolicyModal = ({
lastEditedQueryPlatform,
isUpdatingPolicy,
}: INewPolicyModalProps): JSX.Element => {
const { isPremiumTier } = useContext(AppContext);
const { isPremiumTier, isSandboxMode } = useContext(AppContext);
const {
lastEditedQueryName,
lastEditedQueryDescription,
@@ -142,21 +143,24 @@ const NewPolicyModal = ({
/>
{platformSelector.render()}
{isPremiumTier && (
<Checkbox
name="critical-policy"
onChange={(value: boolean) => setCritical(value)}
value={critical}
isLeftLabel
>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
}
isDelayed
<div className="critical-checkbox-wrapper">
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<Checkbox
name="critical-policy"
onChange={(value: boolean) => setCritical(value)}
value={critical}
isLeftLabel
>
Critical:
</TooltipWrapper>
</Checkbox>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
}
isDelayed
>
Critical:
</TooltipWrapper>
</Checkbox>
</div>
)}
<div className="modal-cta-wrap">
<span
@@ -27,6 +27,7 @@ import Checkbox from "components/forms/fields/Checkbox";
import TooltipWrapper from "components/TooltipWrapper";
import Spinner from "components/Spinner";
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import NewPolicyModal from "../NewPolicyModal";
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png";
@@ -114,6 +115,7 @@ const PolicyForm = ({
isGlobalMaintainer,
isOnGlobalTeam,
isPremiumTier,
isSandboxMode,
} = useContext(AppContext);
const debounceSQL = useDebouncedCallback((sql: string) => {
@@ -426,22 +428,30 @@ const PolicyForm = ({
const renderCriticalPolicy = () => {
return (
<Checkbox
name="critical-policy"
className="critical-policy"
onChange={(value: boolean) => setLastEditedQueryCritical(value)}
value={lastEditedQueryCritical}
isLeftLabel
>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
}
isDelayed
<div className="critical-checkbox-wrapper">
{isSandboxMode && (
<PremiumFeatureIconWithTooltip
tooltipDelayHide={500}
tooltipPositionOverrides={{ leftAdj: 84, topAdj: -4 }}
/>
)}
<Checkbox
name="critical-policy"
className="critical-policy"
onChange={(value: boolean) => setLastEditedQueryCritical(value)}
value={lastEditedQueryCritical}
isLeftLabel
>
Critical:
</TooltipWrapper>
</Checkbox>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
}
isDelayed
>
Critical:
</TooltipWrapper>
</Checkbox>
</div>
);
};
@@ -123,7 +123,12 @@ const ManageSchedulePage = ({
const { MANAGE_PACKS } = paths;
const handleAdvanced = () => router.push(MANAGE_PACKS);
const { isOnGlobalTeam, isPremiumTier, isFreeTier } = useContext(AppContext);
const {
isOnGlobalTeam,
isPremiumTier,
isFreeTier,
isSandboxMode,
} = useContext(AppContext);
const {
currentTeamId,
@@ -409,6 +414,7 @@ const ManageSchedulePage = ({
selectedTeamId={currentTeamId}
currentUserTeams={userTeams || []}
onChange={handleTeamChange}
isSandboxMode={isSandboxMode}
/>
)}
{isPremiumTier &&
@@ -583,8 +583,14 @@ const ManageSoftwarePage = ({
softwareCount;
const softwareTableHeaders = useMemo(
() => generateSoftwareTableHeaders(router, isPremiumTier, currentTeamId),
[isPremiumTier, router, currentTeamId]
() =>
generateSoftwareTableHeaders(
router,
isPremiumTier,
isSandboxMode,
currentTeamId
),
[isPremiumTier, isSandboxMode, router, currentTeamId]
);
const handleRowSelect = (row: IRowProps) => {
const hostsBySoftwareParams = {
@@ -623,6 +629,7 @@ const ManageSoftwarePage = ({
currentUserTeams={userTeams || []}
selectedTeamId={currentTeamId}
onChange={onTeamChange}
isSandboxMode={isSandboxMode}
/>
)}
{isPremiumTier &&
@@ -14,6 +14,7 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
@@ -87,7 +88,7 @@ const getMaxProbability = (vulns: IVulnerability[]) =>
0
);
const generateEPSSColumnHeader = () => {
const generateEPSSColumnHeader = (isSandboxMode = false) => {
return {
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
@@ -104,10 +105,13 @@ const generateEPSSColumnHeader = () => {
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
<>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
</>
);
},
disableSortBy: false,
@@ -185,6 +189,7 @@ const generateVulnColumnHeader = () => {
const generateTableHeaders = (
router: InjectedRouter,
isPremiumTier?: boolean,
isSandboxMode?: boolean,
teamId?: number
): Column[] => {
const softwareTableHeaders = [
@@ -234,7 +239,9 @@ const generateTableHeaders = (
<TextCell formatter={formatSoftwareType} value={cellProps.cell.value} />
),
},
isPremiumTier ? generateEPSSColumnHeader() : generateVulnColumnHeader(),
isPremiumTier
? generateEPSSColumnHeader(isSandboxMode)
: generateVulnColumnHeader(),
{
title: "Hosts",
Header: (cellProps: IHeaderProps): JSX.Element => (
@@ -18,7 +18,6 @@ import Spinner from "components/Spinner";
import BackLink from "components/BackLink";
import MainContent from "components/MainContent";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import Vulnerabilities from "./components/Vulnerabilities";
const baseClass = "software-details-page";
@@ -32,9 +31,13 @@ interface ISoftwareDetailsProps {
const SoftwareDetailsPage = ({
params: { software_id },
}: ISoftwareDetailsProps): JSX.Element => {
const { isPremiumTier, currentTeam, filteredSoftwarePath } = useContext(
AppContext
);
const {
isPremiumTier,
isSandboxMode,
currentTeam,
filteredSoftwarePath,
} = useContext(AppContext);
const handlePageError = useErrorHandler();
const { data: software, isFetching: isFetchingSoftware } = useQuery<
@@ -119,6 +122,7 @@ const SoftwareDetailsPage = ({
</div>
<Vulnerabilities
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
isLoading={isFetchingSoftware}
software={software}
/>
@@ -9,6 +9,7 @@ import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import CustomLink from "components/CustomLink";
import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
interface IHeaderProps {
column: {
@@ -61,7 +62,10 @@ const formatSeverity = (float: number | null) => {
return `${severity} (${float.toFixed(1)})`;
};
const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
const generateVulnTableHeaders = (
isPremiumTier: boolean,
isSandboxMode: boolean
): IDataColumn[] => {
const tableHeaders: IDataColumn[] = [
{
title: "Vunerability",
@@ -97,10 +101,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
<>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -123,10 +130,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
<>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -150,10 +160,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
<>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -174,10 +187,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
<>
{isSandboxMode && <PremiumFeatureIconWithTooltip />}
<HeaderCell
value={titleWithToolTip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
</>
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => {
@@ -86,4 +86,20 @@ describe("Vulnerabilities", () => {
expect(screen.queryByText("Critical", { exact: false })).toBeNull();
expect(screen.queryByText("ago", { exact: false })).toBeNull();
});
// Test for premium icons on column headers in Sandbox mode
it("Renders 4 'Premium feature' tooltips when in premium tier Sandbox mode", () => {
render(
<Vulnerabilities
isLoading={false}
isPremiumTier
isSandboxMode
software={mockSoftwareWithVuln}
/>
);
expect(
screen.getAllByText("This is a Fleet Premium feature.", { exact: false })
).toHaveLength(4);
});
});
@@ -14,6 +14,7 @@ const baseClass = "vulnerabilities";
interface IVulnerabilitiesProps {
isLoading: boolean;
isPremiumTier: boolean;
isSandboxMode?: boolean;
software: ISoftware;
}
@@ -38,11 +39,13 @@ const NoVulnsDetected = (): JSX.Element => {
const Vulnerabilities = ({
isLoading,
isPremiumTier,
isSandboxMode = false,
software,
}: IVulnerabilitiesProps): JSX.Element => {
const tableHeaders = useMemo(() => generateVulnTableHeaders(isPremiumTier), [
isPremiumTier,
]);
const tableHeaders = useMemo(
() => generateVulnTableHeaders(isPremiumTier, isSandboxMode),
[isPremiumTier, isSandboxMode]
);
return (
<div className="section section--vulnerabilities">
@@ -0,0 +1,20 @@
import React, { useContext } from "react";
import { useErrorHandler } from "react-error-boundary";
import { AppContext } from "context/app";
interface IExcludeInSandboxRoutesProps {
children: JSX.Element;
}
const ExcludeInSandboxRoutes = ({ children }: IExcludeInSandboxRoutesProps) => {
const handlePageError = useErrorHandler();
const { isSandboxMode } = useContext(AppContext);
if (isSandboxMode) {
handlePageError({ status: 403 });
return null;
}
return <>{children}</>;
};
export default ExcludeInSandboxRoutes;
@@ -0,0 +1 @@
export { default } from "./ExcludeInSandboxRoutes";
+22 -14
View File
@@ -55,7 +55,7 @@ import MacOSSetup from "pages/ManageControlsPage/MacOSSetup/MacOSSetup";
import PATHS from "router/paths";
import AppProvider from "context/app";
import AppProvider, { AppContext } from "context/app";
import RoutingProvider from "context/routing";
import AuthGlobalAdminRoutes from "./components/AuthGlobalAdminRoutes";
@@ -66,7 +66,9 @@ import AuthGlobalAdminMaintainerRoutes from "./components/AuthGlobalAdminMaintai
import AuthAnyMaintainerAnyAdminRoutes from "./components/AuthAnyMaintainerAnyAdminRoutes";
import AuthAnyMaintainerAdminObserverPlusRoutes from "./components/AuthAnyMaintainerAdminObserverPlusRoutes";
import PremiumRoutes from "./components/PremiumRoutes";
import ExcludeInSandboxRoutes from "./components/ExcludeInSandboxRoutes";
const isSandboxMode = { AppContext };
interface IAppWrapperProps {
children: JSX.Element;
}
@@ -105,17 +107,21 @@ const routes = (
<Route path="email/change/:token" component={EmailTokenRedirect} />
<Route path="logout" component={LogoutPage} />
<Route component={CoreLayout}>
<IndexRedirect to={"/dashboard"} />
<IndexRedirect to="/dashboard" />
<Route path="dashboard" component={DashboardPage}>
<Route path="linux" component={DashboardPage} />
<Route path="mac" component={DashboardPage} />
<Route path="windows" component={DashboardPage} />
</Route>
<Route path="settings" component={AuthAnyAdminRoutes}>
<IndexRedirect to={"organization"} />
<IndexRedirect
to={isSandboxMode ? "integrations" : "organization"}
/>
<Route component={SettingsWrapper}>
<Route component={AuthGlobalAdminRoutes}>
<Route path="organization" component={OrgSettingsPage} />
<Route component={ExcludeInSandboxRoutes}>
<Route path="organization" component={OrgSettingsPage} />
</Route>
<Route
path="organization/:section"
component={OrgSettingsPage}
@@ -125,7 +131,9 @@ const routes = (
path="integrations/:section"
component={AdminIntegrationsPage}
/>
<Route path="users" component={AdminUserManagementPage} />
<Route component={ExcludeInSandboxRoutes}>
<Route path="users" component={AdminUserManagementPage} />
</Route>
<Route component={PremiumRoutes}>
<Route path="teams" component={AdminTeamManagementPage} />
</Route>
@@ -140,12 +148,12 @@ const routes = (
<Redirect from="teams/:team_id/options" to="teams" />
</Route>
<Route path="labels">
<IndexRedirect to={"new"} />
<IndexRedirect to="new" />
<Route path=":label_id" component={LabelPage} />
<Route path="new" component={LabelPage} />
</Route>
<Route path="hosts">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManageHostsPage} />
<Route path="manage/labels/:label_id" component={ManageHostsPage} />
<Route path="manage/:active_label" component={ManageHostsPage} />
@@ -158,7 +166,7 @@ const routes = (
component={ManageHostsPage}
/>
<IndexRedirect to={":host_id"} />
<IndexRedirect to=":host_id" />
<Route component={HostDetailsPage}>
<Route path=":host_id" component={HostDetailsPage}>
<Route path="software" component={HostDetailsPage} />
@@ -169,7 +177,7 @@ const routes = (
</Route>
<Route path="controls" component={AuthAnyMaintainerAnyAdminRoutes}>
<IndexRedirect to={"mac-os-updates"} />
<IndexRedirect to="mac-os-updates" />
<Route component={ManageControlsPage}>
<Route path="mac-os-updates" component={MacOSUpdates} />
<Route path="mac-settings" component={MacOSSettings} />
@@ -180,13 +188,13 @@ const routes = (
</Route>
<Route path="software">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManageSoftwarePage} />
<Route path=":software_id" component={SoftwareDetailsPage} />
</Route>
<Route component={AuthGlobalAdminMaintainerRoutes}>
<Route path="packs">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManagePacksPage} />
<Route path="new" component={PackComposerPage} />
<Route path=":id">
@@ -197,14 +205,14 @@ const routes = (
</Route>
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
<Route path="schedule">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManageSchedulePage} />
<Redirect from="manage/teams" to="manage" />
<Redirect from="manage/teams/:team_id" to="manage" />
</Route>
</Route>
<Route path="queries">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManageQueriesPage} />
<Route component={AuthAnyMaintainerAdminObserverPlusRoutes}>
<Route path="new" component={QueryPage} />
@@ -212,7 +220,7 @@ const routes = (
<Route path=":id" component={QueryPage} />
</Route>
<Route path="policies">
<IndexRedirect to={"manage"} />
<IndexRedirect to="manage" />
<Route path="manage" component={ManagePoliciesPage} />
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
<Route path="new" component={PolicyPage} />
+1 -1
View File
@@ -21,7 +21,7 @@ export default {
ADMIN_INTEGRATIONS_TICKET_DESTINATIONS: `${URL_PREFIX}/settings/integrations/ticket-destinations`,
ADMIN_INTEGRATIONS_MDM: `${URL_PREFIX}/settings/integrations/mdm`,
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,
ADMIN_SETTINGS: `${URL_PREFIX}/settings/organization`,
ADMIN_SETTINGS: `${URL_PREFIX}/settings`,
ADMIN_SETTINGS_INFO: `${URL_PREFIX}/settings/organization/info`,
ADMIN_SETTINGS_WEBADDRESS: `${URL_PREFIX}/settings/organization/webaddress`,
ADMIN_SETTINGS_SSO: `${URL_PREFIX}/settings/organization/sso`,