From cb58849d9520c0915c98313bafa560b2653bc1fb Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 27 Apr 2023 08:53:30 -0700 Subject: [PATCH] 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 Co-authored-by: Martin Angers --- ...emium-feature-to-teams-dropdown-and-button | 1 + .../10816-software-details-premium-sandbox | 2 + changes/10817-prob-of-exploit-is-premium | 2 + ...ium-feature-UI-elements-to-critical-policy | 2 + changes/9371-premium-to-sandbox-story | 1 + frontend/__mocks__/policyMock.ts | 30 +++++ .../DownloadInstallers/_styles.scss | 7 +- frontend/components/CustomLink/_styles.scss | 9 +- .../PremiumFeatureIconWithTooltip.tsx | 59 ++++++++ .../_styles.scss | 4 + .../PremiumFeatureIconWithTooltip/index.ts | 1 + .../SandboxDemoMessage/SandboxDemoMessage.tsx | 38 ------ .../Sandbox/SandboxDemoMessage/index.ts | 1 - .../Sandbox/SandboxMessage/SandboxMessage.tsx | 51 +++++++ .../_styles.scss | 7 +- .../Sandbox/SandboxMessage/index.ts | 1 + .../DataTable/ActionButton/ActionButton.tsx | 17 ++- .../DataTable/ActionButton/_styles.scss | 8 +- .../TableContainer/DataTable/DataTable.tsx | 2 + .../TableContainer/DataTable/_styles.scss | 8 ++ .../TeamsDropdown/TeamsDropdown.tsx | 59 ++++++-- .../components/TeamsDropdown/_styles.scss | 17 +++ .../forms/fields/Dropdown/_styles.scss | 6 + frontend/components/icons/CriticalPolicy.tsx | 39 ++++++ frontend/components/icons/Policy.tsx | 44 ------ frontend/components/icons/PremiumFeature.tsx | 6 +- frontend/components/icons/index.ts | 5 +- frontend/interfaces/dropdownOption.ts | 3 +- .../pages/DashboardPage/DashboardPage.tsx | 85 +++++++----- .../cards/ActivityFeed/ActivityFeed.tsx | 3 + .../ActivityItem/ActivityItem.tsx | 17 +++ .../ActivityFeed/ActivityItem/_styles.scss | 6 + .../HostsSummary/SummaryTile/SummaryTile.tsx | 14 +- .../HostsSummary/SummaryTile/_styles.scss | 9 ++ .../LowDiskSpaceHosts/LowDiskSpaceHosts.tsx | 4 + .../cards/MissingHosts/MissingHosts.tsx | 4 + .../ManageControlsPage/ManageControlsPage.tsx | 2 + .../UserSettingsPage/UserSettingsPage.tsx | 30 ++--- .../admin/OrgSettingsPage/OrgSettingsPage.tsx | 46 +++---- .../admin/SettingsWrapper/SettingsWrapper.tsx | 61 +++++---- .../TeamManagementPage/TeamManagementPage.tsx | 127 ++++++++++-------- .../UserManagementPage/UserManagementPage.tsx | 6 +- .../hosts/ManageHostsPage/HostsPageConfig.tsx | 71 ++++++++++ .../hosts/ManageHostsPage/ManageHostsPage.tsx | 16 ++- .../pages/hosts/ManageHostsPage/_styles.scss | 15 ++- .../components/FilterPill/FilterPill.tsx | 27 +++- .../components/FilterPill/_styles.scss | 9 +- .../HostsFilterBlock/HostsFilterBlock.tsx | 8 +- .../pages/hosts/ManageHostsPage/constants.ts | 59 -------- .../HostActionsDropdown.tsx | 2 + .../{helpers.ts => helpers.tsx} | 56 +++++++- .../HostDetailsPage/HostDetailsPage.tsx | 2 + frontend/pages/hosts/details/_styles.scss | 2 +- .../details/cards/HostSummary/HostSummary.tsx | 13 +- .../details/cards/HostSummary/_styles.scss | 14 ++ .../ManagePoliciesPage/ManagePoliciesPage.tsx | 7 +- .../policies/ManagePoliciesPage/_styles.scss | 3 +- .../PoliciesTable/PoliciesTable.tests.tsx | 55 ++++++++ .../PoliciesTable/PoliciesTable.tsx | 18 ++- .../PoliciesTable/PoliciesTableConfig.tsx | 28 +++- .../pages/policies/PolicyPage/_styles.scss | 8 ++ .../NewPolicyModal/NewPolicyModal.tsx | 34 ++--- .../components/PolicyForm/PolicyForm.tsx | 40 +++--- .../ManageSchedulePage/ManageSchedulePage.tsx | 8 +- .../ManageSoftwarePage/ManageSoftwarePage.tsx | 11 +- .../SoftwareTableConfig.tsx | 19 ++- .../SoftwareDetailsPage.tsx | 12 +- .../Vulnerabilities/VulnTableConfig.tsx | 50 ++++--- .../Vulnerabilities/Vulnerabilities.tests.tsx | 16 +++ .../Vulnerabilities/Vulnerabilities.tsx | 9 +- .../ExcludeInSandboxRoutes.tsx | 20 +++ .../ExcludeInSandboxRoutes/index.ts | 1 + frontend/router/index.tsx | 36 +++-- frontend/router/paths.ts | 2 +- 74 files changed, 1051 insertions(+), 464 deletions(-) create mode 100644 changes/10808-premium-feature-to-teams-dropdown-and-button create mode 100644 changes/10816-software-details-premium-sandbox create mode 100644 changes/10817-prob-of-exploit-is-premium create mode 100644 changes/10818-premium-feature-UI-elements-to-critical-policy create mode 100644 changes/9371-premium-to-sandbox-story create mode 100644 frontend/__mocks__/policyMock.ts create mode 100644 frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx create mode 100644 frontend/components/PremiumFeatureIconWithTooltip/_styles.scss create mode 100644 frontend/components/PremiumFeatureIconWithTooltip/index.ts delete mode 100644 frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx delete mode 100644 frontend/components/Sandbox/SandboxDemoMessage/index.ts create mode 100644 frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx rename frontend/components/Sandbox/{SandboxDemoMessage => SandboxMessage}/_styles.scss (78%) create mode 100644 frontend/components/Sandbox/SandboxMessage/index.ts create mode 100644 frontend/components/icons/CriticalPolicy.tsx delete mode 100644 frontend/components/icons/Policy.tsx create mode 100644 frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx delete mode 100644 frontend/pages/hosts/ManageHostsPage/constants.ts rename frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/{helpers.ts => helpers.tsx} (70%) create mode 100644 frontend/pages/hosts/details/cards/HostSummary/_styles.scss create mode 100644 frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx create mode 100644 frontend/router/components/ExcludeInSandboxRoutes/ExcludeInSandboxRoutes.tsx create mode 100644 frontend/router/components/ExcludeInSandboxRoutes/index.ts 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} +