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:
co-authored by
Jacob Shandling
Martin Angers
parent
2a0f8d0701
commit
cb58849d95
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
* Added a suite of UI logic for premium features in the Sandbox environment
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
+3
-4
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,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,
|
||||
|
||||
@@ -8,6 +8,7 @@ export default PropTypes.shape({
|
||||
|
||||
export interface IDropdownOption {
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
label: string | JSX.Element;
|
||||
value: string | number;
|
||||
premiumOnly?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 won’t 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -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 won’t 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,
|
||||
},
|
||||
];
|
||||
+2
@@ -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
|
||||
|
||||
+49
-7
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+55
@@ -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();
|
||||
});
|
||||
});
|
||||
+13
-5
@@ -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,
|
||||
|
||||
+21
-7
@@ -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}
|
||||
/>
|
||||
|
||||
+33
-17
@@ -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 => {
|
||||
|
||||
+16
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+6
-3
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user