diff --git a/changes/10808-premium-feature-to-teams-dropdown-and-button b/changes/10808-premium-feature-to-teams-dropdown-and-button
new file mode 100644
index 0000000000..3ce202706b
--- /dev/null
+++ b/changes/10808-premium-feature-to-teams-dropdown-and-button
@@ -0,0 +1 @@
+* Inform prospective customers that Teams is a Premium feature.
diff --git a/changes/10816-software-details-premium-sandbox b/changes/10816-software-details-premium-sandbox
new file mode 100644
index 0000000000..5fef9d4c49
--- /dev/null
+++ b/changes/10816-software-details-premium-sandbox
@@ -0,0 +1,2 @@
+* Added "Premium Feature" icons for premium-only columns of the Vulnerabilities table when in
+Sandbox mode
diff --git a/changes/10817-prob-of-exploit-is-premium b/changes/10817-prob-of-exploit-is-premium
new file mode 100644
index 0000000000..b3dd56b8f4
--- /dev/null
+++ b/changes/10817-prob-of-exploit-is-premium
@@ -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
diff --git a/changes/10818-premium-feature-UI-elements-to-critical-policy b/changes/10818-premium-feature-UI-elements-to-critical-policy
new file mode 100644
index 0000000000..68cb85cdbf
--- /dev/null
+++ b/changes/10818-premium-feature-UI-elements-to-critical-policy
@@ -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.
diff --git a/changes/9371-premium-to-sandbox-story b/changes/9371-premium-to-sandbox-story
new file mode 100644
index 0000000000..cac126a05a
--- /dev/null
+++ b/changes/9371-premium-to-sandbox-story
@@ -0,0 +1 @@
+* Added a suite of UI logic for premium features in the Sandbox environment
diff --git a/frontend/__mocks__/policyMock.ts b/frontend/__mocks__/policyMock.ts
new file mode 100644
index 0000000000..71ba8f0f9b
--- /dev/null
+++ b/frontend/__mocks__/policyMock.ts
@@ -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 => {
+ return { ...DEFAULT_POLICY_MOCK, ...overrides };
+};
+
+export default createMockPolicy;
diff --git a/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss
index 939d0f9a5c..13c0984e13 100644
--- a/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss
+++ b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss
@@ -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;
diff --git a/frontend/components/CustomLink/_styles.scss b/frontend/components/CustomLink/_styles.scss
index 1685ee2d5c..6441107696 100644
--- a/frontend/components/CustomLink/_styles.scss
+++ b/frontend/components/CustomLink/_styles.scss
@@ -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;
- }
}
diff --git a/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx b/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx
new file mode 100644
index 0000000000..12fa73b863
--- /dev/null
+++ b/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx
@@ -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 (
+
+
+
+
+ {
+ return {
+ left: pos.left + leftAdj,
+ top: pos.top + topAdj,
+ };
+ }}
+ >
+ {`This is a Fleet Premium feature. `}
+
+
+
+ );
+};
+
+export default PremiumFeatureIconWithTooltip;
diff --git a/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss b/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss
new file mode 100644
index 0000000000..b5e7416867
--- /dev/null
+++ b/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss
@@ -0,0 +1,4 @@
+.premium-icon-tip {
+ font-size: $x-small;
+ font-weight: normal;
+}
diff --git a/frontend/components/PremiumFeatureIconWithTooltip/index.ts b/frontend/components/PremiumFeatureIconWithTooltip/index.ts
new file mode 100644
index 0000000000..f2c2609222
--- /dev/null
+++ b/frontend/components/PremiumFeatureIconWithTooltip/index.ts
@@ -0,0 +1 @@
+export { default } from "./PremiumFeatureIconWithTooltip";
diff --git a/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx b/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx
deleted file mode 100644
index a6e26d79da..0000000000
--- a/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx
+++ /dev/null
@@ -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 (
-
-
{message}
-
- Want to learn more?{" "}
-
-
-
- );
-};
-
-export default SandboxDemoMessage;
diff --git a/frontend/components/Sandbox/SandboxDemoMessage/index.ts b/frontend/components/Sandbox/SandboxDemoMessage/index.ts
deleted file mode 100644
index d3149383e3..0000000000
--- a/frontend/components/Sandbox/SandboxDemoMessage/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./SandboxDemoMessage";
diff --git a/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx b/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx
new file mode 100644
index 0000000000..e8d77c4f6e
--- /dev/null
+++ b/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx
@@ -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: (
+
+ ),
+ sales: (
+
+ ),
+ };
+
+ return (
+
+
{message}
+
+ Want to learn more? {variants[variant]}
+
+
+ );
+};
+
+export default SandboxMessage;
diff --git a/frontend/components/Sandbox/SandboxDemoMessage/_styles.scss b/frontend/components/Sandbox/SandboxMessage/_styles.scss
similarity index 78%
rename from frontend/components/Sandbox/SandboxDemoMessage/_styles.scss
rename to frontend/components/Sandbox/SandboxMessage/_styles.scss
index 19668e629e..08972a570e 100644
--- a/frontend/components/Sandbox/SandboxDemoMessage/_styles.scss
+++ b/frontend/components/Sandbox/SandboxMessage/_styles.scss
@@ -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 {
diff --git a/frontend/components/Sandbox/SandboxMessage/index.ts b/frontend/components/Sandbox/SandboxMessage/index.ts
new file mode 100644
index 0000000000..5fd7435569
--- /dev/null
+++ b/frontend/components/Sandbox/SandboxMessage/index.ts
@@ -0,0 +1 @@
+export { default } from "./SandboxMessage";
diff --git a/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx b/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx
index 6c5f771fda..0466c52158 100644
--- a/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx
+++ b/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx
@@ -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 (
-
);
diff --git a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx
index 3686192a34..7f5252ffcc 100644
--- a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx
+++ b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx
@@ -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 &&
diff --git a/frontend/pages/UserSettingsPage/UserSettingsPage.tsx b/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
index ff54aeee06..16fbcb9c8b 100644
--- a/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
+++ b/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
@@ -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 (
<>
- (
-
- )}
- >
+ <>
My account
+ >
-
-
-
-
-
+
+
+
>
);
};
diff --git a/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx
index a5f02f2280..ee53d76670 100644
--- a/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx
+++ b/frontend/pages/admin/OrgSettingsPage/OrgSettingsPage.tsx
@@ -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) => {
Set your organization information and configure SSO and SMTP
- (
-
- )}
- >
-
- ) : (
-
- )
- }
- />
-
+
+ ) : (
+
+ )
+ }
+ />
);
};
diff --git a/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx b/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx
index 84de8f94fc..75a6ed0823 100644
--- a/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx
+++ b/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx
@@ -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)}
>
- {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 (
diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
index 88966bb6fe..fbf96a5579 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
@@ -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 => {
Create, customize, and remove teams from Fleet.
- {loadingTeamsError ? (
-
- ) : (
-
- 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 && (
-
- )}
- {showDeleteTeamModal && (
-
- )}
- {showEditTeamModal && (
-
- )}
+ (
+
+ )}
+ >
+ {loadingTeamsError ? (
+
+ ) : (
+
+ 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 && (
+
+ )}
+ {showDeleteTeamModal && (
+
+ )}
+ {showEditTeamModal && (
+
+ )}
+
);
};
diff --git a/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx b/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx
index f7d06958d3..4f4190de78 100644
--- a/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx
+++ b/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx
@@ -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 => {
(
-
)}
>
diff --git a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx
new file mode 100644
index 0000000000..7b768baf0d
--- /dev/null
+++ b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx
@@ -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 ? (
+
+ Missing hosts
+
+ {/* */}
+
+ ) : (
+ "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",
+ },
+];
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index 5c6985d24c..76f6e8ada4 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -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 (
@@ -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()}
diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss
index ea35043650..f25f18a4b7 100644
--- a/frontend/pages/hosts/ManageHostsPage/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss
@@ -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%;
+ }
+ }
}
}
}
diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
index 3251236ab2..6e1f79b39c 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
@@ -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}`}
>
<>
-
+
{icon && (

)}
- {label}
+ {isSandboxMode && sandboxPremiumOnlyIcon && (
+
+ )}
+
+ {label}
+
void;
onClickEditLabel: (evt: React.MouseEvent) => 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 = ({
handleClearFilter(["low_disk_space"])}
+ isSandboxMode={isSandboxMode}
+ sandboxPremiumOnlyIcon
/>
);
};
diff --git a/frontend/pages/hosts/ManageHostsPage/constants.ts b/frontend/pages/hosts/ManageHostsPage/constants.ts
deleted file mode 100644
index 86d3840e74..0000000000
--- a/frontend/pages/hosts/ManageHostsPage/constants.ts
+++ /dev/null
@@ -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,
- },
-];
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
index 5b8e2c144a..03b2f8c7a0 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
@@ -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
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx
similarity index 70%
rename from frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts
rename to frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx
index 816dc61e77..ed0dd99a25 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx
@@ -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 = (
+
+ {option.label}
+
+
+ );
+ });
+ }
+
return options;
};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 7a387e6b45..9634e47f81 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -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}
diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss
index d4379f879d..2f51005169 100644
--- a/frontend/pages/hosts/details/_styles.scss
+++ b/frontend/pages/hosts/details/_styles.scss
@@ -40,7 +40,7 @@
.info-flex__data {
display: flex;
- gap: 4px;
+ gap: $pad-xsmall;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
index 704cb6c487..3bbda7a66a 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
@@ -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 = () => (
-
Issues
+
+ Issues{isSandboxMode && }
+
- {titleData.issues?.total_issues_count > 0 &&
+ {(titleData.issues?.total_issues_count > 0 || isSandboxMode) &&
isPremiumTier &&
renderIssues()}
@@ -257,7 +262,7 @@ const HostSummary = ({
);
return (
- <>
+
@@ -279,7 +284,7 @@ const HostSummary = ({
- >
+
);
};
diff --git a/frontend/pages/hosts/details/cards/HostSummary/_styles.scss b/frontend/pages/hosts/details/cards/HostSummary/_styles.scss
new file mode 100644
index 0000000000..7973e6c848
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/HostSummary/_styles.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index 6832435368..a44d5e5181 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -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) ? (
) : (
@@ -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 &&
}
@@ -433,6 +436,8 @@ const ManagePolicyPage = ({
canAddOrDeletePolicy={canAddOrDeletePolicy}
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
+ isPremiumTier={isPremiumTier}
+ isSandboxMode={isSandboxMode}
/>
))}
diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
index fdb32f1271..0f27d983df 100644
--- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss
+++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
@@ -148,10 +148,11 @@
}
.critical-tooltip {
+ text-align: left;
font-weight: $regular;
}
- .policy-icon {
+ .critical-policy-icon {
margin-left: 1px;
position: relative;
}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx
new file mode 100644
index 0000000000..4df81802f5
--- /dev/null
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx
@@ -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(
+
{}}
+ 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(
+ {}}
+ 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();
+ });
+});
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
index e09de2228d..ab32667f84 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
@@ -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 = ({
) : (
{
// 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={
<>
{cellProps.cell.value}
- {cellProps.row.original.critical && (
+ {isPremiumTier && cellProps.row.original.critical && (
<>
-
+
This policy has been marked as critical.
+ {isSandboxMode && (
+ <>
+
+ This is a premium feature.
+ >
+ )}
>
)}
diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss
index 39b79bf061..af237e4497 100644
--- a/frontend/pages/policies/PolicyPage/_styles.scss
+++ b/frontend/pages/policies/PolicyPage/_styles.scss
@@ -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;
+ }
+ }
}
diff --git a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx
index 4417dd4068..141b91992a 100644
--- a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx
+++ b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx
@@ -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 && (
- setCritical(value)}
- value={critical}
- isLeftLabel
- >
- If automations are turned on, this
information is included."
- }
- isDelayed
+
+ {isSandboxMode &&
}
+
setCritical(value)}
+ value={critical}
+ isLeftLabel
>
- Critical:
-
-
+
If automations are turned on, this
information is included."
+ }
+ isDelayed
+ >
+ Critical:
+
+
+
)}
{
@@ -426,22 +428,30 @@ const PolicyForm = ({
const renderCriticalPolicy = () => {
return (
- setLastEditedQueryCritical(value)}
- value={lastEditedQueryCritical}
- isLeftLabel
- >
- If automations are turned on, this
information is included."
- }
- isDelayed
+
+ {isSandboxMode && (
+
+ )}
+
setLastEditedQueryCritical(value)}
+ value={lastEditedQueryCritical}
+ isLeftLabel
>
- Critical:
-
-
+
If automations are turned on, this
information is included."
+ }
+ isDelayed
+ >
+ Critical:
+
+
+
);
};
diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
index a6a0f56a0c..a5bef3546e 100644
--- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
+++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
@@ -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 &&
diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
index a467a7209d..258a5b7492 100644
--- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
+++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
@@ -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 &&
diff --git a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
index a0360366ab..7da6142dff 100644
--- a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
+++ b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
@@ -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 = () => {
);
return (
-
+ <>
+ {isSandboxMode && }
+
+ >
);
},
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 = (
),
},
- isPremiumTier ? generateEPSSColumnHeader() : generateVulnColumnHeader(),
+ isPremiumTier
+ ? generateEPSSColumnHeader(isSandboxMode)
+ : generateVulnColumnHeader(),
{
title: "Hosts",
Header: (cellProps: IHeaderProps): JSX.Element => (
diff --git a/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx b/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
index 3c9558ccea..82c8216b23 100644
--- a/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
+++ b/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
@@ -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 = ({
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx
index 35f632f0d1..5bc2dd9adc 100644
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx
+++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx
@@ -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[] => {
);
return (
-
+ <>
+ {isSandboxMode && }
+
+ >
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -123,10 +130,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
);
return (
-
+ <>
+ {isSandboxMode && }
+
+ >
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -150,10 +160,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
);
return (
-
+ <>
+ {isSandboxMode && }
+
+ >
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => (
@@ -174,10 +187,13 @@ const generateVulnTableHeaders = (isPremiumTier: boolean): IDataColumn[] => {
);
return (
-
+ <>
+ {isSandboxMode && }
+
+ >
);
},
Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => {
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
index e4fe41b12f..bd98579dfe 100644
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
+++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
@@ -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(
+
+ );
+
+ expect(
+ screen.getAllByText("This is a Fleet Premium feature.", { exact: false })
+ ).toHaveLength(4);
+ });
});
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
index d8ec822d1e..138996eeb5 100644
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
+++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
@@ -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 (
diff --git a/frontend/router/components/ExcludeInSandboxRoutes/ExcludeInSandboxRoutes.tsx b/frontend/router/components/ExcludeInSandboxRoutes/ExcludeInSandboxRoutes.tsx
new file mode 100644
index 0000000000..5b8e2e9b40
--- /dev/null
+++ b/frontend/router/components/ExcludeInSandboxRoutes/ExcludeInSandboxRoutes.tsx
@@ -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;
diff --git a/frontend/router/components/ExcludeInSandboxRoutes/index.ts b/frontend/router/components/ExcludeInSandboxRoutes/index.ts
new file mode 100644
index 0000000000..9b6b52e6e3
--- /dev/null
+++ b/frontend/router/components/ExcludeInSandboxRoutes/index.ts
@@ -0,0 +1 @@
+export { default } from "./ExcludeInSandboxRoutes";
diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx
index bf6917c9f4..afb47e6c55 100644
--- a/frontend/router/index.tsx
+++ b/frontend/router/index.tsx
@@ -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 = (
-
+
-
+
-
+
+
+
-
+
+
+
@@ -140,12 +148,12 @@ const routes = (
-
+
-
+
@@ -158,7 +166,7 @@ const routes = (
component={ManageHostsPage}
/>
-
+
@@ -169,7 +177,7 @@ const routes = (
-
+
@@ -180,13 +188,13 @@ const routes = (
-
+
-
+
@@ -197,14 +205,14 @@ const routes = (
-
+
-
+
@@ -212,7 +220,7 @@ const routes = (
-
+
diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts
index 873c665c26..bdc702ca40 100644
--- a/frontend/router/paths.ts
+++ b/frontend/router/paths.ts
@@ -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`,