From b3b107512ec3d8ec32c3391a8798e31bddc3369f Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 30 Nov 2021 12:12:53 -0500 Subject: [PATCH] Host Details Page: OS policy creator (#3109) --- changes/issue-2596-os-policy-hdp | 1 + cypress/integration/free/maintainer.spec.ts | 3 + cypress/integration/free/observer.spec.ts | 2 + cypress/integration/premium/admin.spec.ts | 2 + .../integration/premium/maintainer.spec.ts | 2 + cypress/integration/premium/observer.spec.ts | 8 +- .../hosts/HostDetailsPage/HostDetailsPage.tsx | 180 ++++++++++++++++-- .../pages/hosts/HostDetailsPage/_styles.scss | 56 +++++- .../pages/policies/PolicyPage/PolicyPage.tsx | 6 +- .../NewPolicyModal/NewPolicyModal.tsx | 14 +- 10 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 changes/issue-2596-os-policy-hdp diff --git a/changes/issue-2596-os-policy-hdp b/changes/issue-2596-os-policy-hdp new file mode 100644 index 0000000000..28efd12a18 --- /dev/null +++ b/changes/issue-2596-os-policy-hdp @@ -0,0 +1 @@ +* Users can create an operating system policy from the host details page \ No newline at end of file diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index b4c5b4104e..78589eac15 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -56,6 +56,9 @@ describe( }); cy.contains("button", /transfer/i).should("not.exist"); + // See and select operating system + // TODO + // Test commented out // Pending fix to prevent consistent failing in GitHub diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index 107679173a..dbf065cf98 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -54,6 +54,8 @@ describe("Free tier - Observer user", () => { cy.contains("button", /delete/i).should("not.exist"); cy.contains("button", /query/i).click(); cy.contains("button", /create custom query/i).should("not.exist"); + // See but not select operating system + // TODO // Queries pages: Observer can or cannot run UI cy.visit("/queries/manage"); diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 6dee5d42f5..f42daae7b1 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -63,6 +63,8 @@ describe( cy.get(".transfer-action-btn").click(); cy.findByText(/transferred to oranges/i).should("exist"); cy.findByText(/team/i).next().contains("Oranges"); + // See and select operating system + // TODO // TODO - Fix tests according to improved query experience - MP // On the Queries - new / edit / run page, they should… diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts index 4c1f766adf..b86cc40c82 100644 --- a/cypress/integration/premium/maintainer.spec.ts +++ b/cypress/integration/premium/maintainer.spec.ts @@ -59,6 +59,8 @@ describe( cy.contains("button", /delete/i).should("exist"); cy.contains("button", /query/i).click(); cy.contains("button", /create custom query/i).should("exist"); + // See and select operating system + // TODO // Query pages: Can see teams UI for create, edit, and run query cy.visit("/queries/manage"); diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts index 3292b5dcd7..6f4d44b44c 100644 --- a/cypress/integration/premium/observer.spec.ts +++ b/cypress/integration/premium/observer.spec.ts @@ -21,6 +21,9 @@ describe("Premium tier - Observer user", () => { cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting cy.contains("All hosts"); + // Not see the "Manage enroll secret” button + cy.contains("button", /manage enroll secret/i).should("not.exist"); + cy.get("thead").within(() => { cy.findByText(/team/i).should("exist"); }); @@ -37,9 +40,8 @@ describe("Premium tier - Observer user", () => { cy.contains("button", /delete/i).should("not.exist"); cy.contains("button", /query/i).click(); cy.contains("button", /create custom query/i).should("not.exist"); - - // Not see the "Manage enroll secret” button - cy.contains("button", /manage enroll secret/i).should("not.exist"); + // See and not select operating system + // TODO // TODO - Fix tests according to improved query experience - MP // Query pages: Can see team in select targets dropdown diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx index 80f366f4db..8ab45d32b2 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx @@ -7,12 +7,15 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import classnames from "classnames"; import { isEmpty, pick, reduce } from "lodash"; +// @ts-ignore +import { stringToClipboard } from "utilities/copy_text"; import PATHS from "router/paths"; import hostAPI from "services/entities/hosts"; import queryAPI from "services/entities/queries"; import teamAPI from "services/entities/teams"; import { AppContext } from "context/app"; +import { PolicyContext } from "context/policy"; import { IHost, IPackStats } from "interfaces/host"; import { IQueryStats } from "interfaces/query_stats"; import { ISoftware } from "interfaces/software"; @@ -26,6 +29,8 @@ import { renderFlash } from "redux/nodes/notifications/actions"; import permissionUtils from "utilities/permissions"; import ReactTooltip from "react-tooltip"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; import Spinner from "components/Spinner"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; @@ -33,7 +38,6 @@ import SoftwareVulnerabilities from "pages/hosts/HostDetailsPage/SoftwareVulnCou import TableContainer from "components/TableContainer"; import TabsWrapper from "components/TabsWrapper"; import InfoBanner from "components/InfoBanner"; - import { Accordion, AccordionItem, @@ -70,10 +74,12 @@ import PolicyFailingCount from "./HostPoliciesTable/PolicyFailingCount"; import { isValidPolicyResponse } from "../ManageHostsPage/helpers"; import BackChevron from "../../../../assets/images/icon-chevron-down-9x6@2x.png"; +import CopyIcon from "../../../../assets/images/icon-copy-clipboard-fleet-blue-20x20@2x.png"; import DeleteIcon from "../../../../assets/images/icon-action-delete-14x14@2x.png"; -import TransferIcon from "../../../../assets/images/icon-action-transfer-16x16@2x.png"; -import QueryIcon from "../../../../assets/images/icon-action-query-16x16@2x.png"; import IssueIcon from "../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png"; +import QueryIcon from "../../../../assets/images/icon-action-query-16x16@2x.png"; +import QuestionIcon from "../../../../assets/images/icon-question-16x16@2x.png"; +import TransferIcon from "../../../../assets/images/icon-action-transfer-16x16@2x.png"; const baseClass = "host-details"; @@ -107,6 +113,12 @@ const HostDetailsPage = ({ isGlobalMaintainer, currentUser, } = useContext(AppContext); + const { + setLastEditedQueryName, + setLastEditedQueryDescription, + setLastEditedQueryBody, + setPolicyTeamId, + } = useContext(PolicyContext); const canTransferTeam = isPremiumTier && (isGlobalAdmin || isGlobalMaintainer); @@ -132,23 +144,11 @@ const HostDetailsPage = ({ const [showPolicyDetailsModal, setPolicyDetailsModal] = useState( false ); + const [showOSPolicyModal, setShowOSPolicyModal] = useState(false); const [selectedPolicy, setSelectedPolicy] = useState( null ); - const togglePolicyDetailsModal = useCallback( - (policy: IHostPolicy) => { - setPolicyDetailsModal(!showPolicyDetailsModal); - setSelectedPolicy(policy); - }, - [showPolicyDetailsModal, setPolicyDetailsModal, setSelectedPolicy] - ); - - const onCancelPolicyDetailsModal = useCallback(() => { - setPolicyDetailsModal(!showPolicyDetailsModal); - setSelectedPolicy(null); - }, [showPolicyDetailsModal, setPolicyDetailsModal, setSelectedPolicy]); - const [refetchStartTime, setRefetchStartTime] = useState(null); const [ showRefetchLoadingSpinner, @@ -160,6 +160,7 @@ const HostDetailsPage = ({ const [softwareSearchString, setSoftwareSearchString] = useState(""); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); + const [copyMessage, setCopyMessage] = useState(""); const { data: fleetQueries, error: fleetQueriesError } = useQuery< IFleetQueriesResponse, @@ -344,6 +345,16 @@ const HostDetailsPage = ({ ]) ); + const operatingSystem = host?.os_version.slice( + 0, + host?.os_version.lastIndexOf(" ") + ); + const operatingSystemVersion = host?.os_version.slice( + host?.os_version.lastIndexOf(" ") + 1 + ); + const osPolicyLabel = `Is ${operatingSystem}, version ${operatingSystemVersion} installed?`; + const osPolicy = `SELECT 1 from os_version WHERE name = '${operatingSystem}' AND major || ',' || minor || '.' || patch = '${operatingSystemVersion}';`; + const aboutData = normalizeEmptyValues( pick(host, [ "seen_time", @@ -363,6 +374,36 @@ const HostDetailsPage = ({ ]) ); + const togglePolicyDetailsModal = useCallback( + (policy: IHostPolicy) => { + setPolicyDetailsModal(!showPolicyDetailsModal); + setSelectedPolicy(policy); + }, + [showPolicyDetailsModal, setPolicyDetailsModal, setSelectedPolicy] + ); + + const toggleOSPolicyModal = useCallback(() => { + setShowOSPolicyModal(!showOSPolicyModal); + }, [showOSPolicyModal, setShowOSPolicyModal]); + + const onCancelPolicyDetailsModal = useCallback(() => { + setPolicyDetailsModal(!showPolicyDetailsModal); + setSelectedPolicy(null); + }, [showPolicyDetailsModal, setPolicyDetailsModal, setSelectedPolicy]); + + const onCreateNewPolicy = () => { + const { NEW_POLICY } = PATHS; + host?.team_name + ? setLastEditedQueryName(`${osPolicyLabel} (${host.team_name})`) + : setLastEditedQueryName(osPolicyLabel); + setPolicyTeamId(host?.team_id ? host?.team_id : 0); + setLastEditedQueryDescription( + "Returns yes or no for detecting operating system and version" + ); + setLastEditedQueryBody(osPolicy); + router.replace(NEW_POLICY); + }; + const onDestroyHost = async () => { if (host) { try { @@ -444,6 +485,39 @@ const HostDetailsPage = ({ setUsersSearchString(searchQuery); }, []); + const renderOsPolicyLabel = () => { + const onCopyOsPolicy = (evt: React.MouseEvent) => { + evt.preventDefault(); + + stringToClipboard(osPolicy) + .then(() => setCopyMessage("Copied!")) + .catch(() => setCopyMessage("Copy failed")); + + // Clear message after 1 second + setTimeout(() => setCopyMessage(""), 1000); + + return false; + }; + + return ( +
+ {osPolicyLabel}{" "} + + + {copyMessage && {`${copyMessage} `}} + + + +
+ ); + }; + const renderDeleteHostModal = () => ( ); + const renderOSPolicyModal = () => ( + setShowOSPolicyModal(false)} + className={`${baseClass}__modal`} + > + <> +

+ + {titleData.os_version}{" "} + + + Reported {humanHostDetailUpdated(titleData.detail_updated_at)} + +

+ + Example policy: + {" "} + + host issue + + + + A policy is a yes or no question +
you can ask all your devices. +
+
+ +
+ + +
+ +
+ ); + const renderActionButtons = () => { const isOnline = host?.status === "online"; @@ -1038,7 +1171,19 @@ const HostDetailsPage = ({
OS - {titleData.os_version} + + {isOnlyObserver ? ( + `${titleData.os_version}` + ) : ( + + )} +
Osquery @@ -1182,6 +1327,7 @@ const HostDetailsPage = ({ policy={selectedPolicy} /> )} + {showOSPolicyModal && renderOSPolicyModal()}
); }; diff --git a/frontend/pages/hosts/HostDetailsPage/_styles.scss b/frontend/pages/hosts/HostDetailsPage/_styles.scss index d1838b8490..27bf646cf9 100644 --- a/frontend/pages/hosts/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/HostDetailsPage/_styles.scss @@ -373,7 +373,8 @@ display: flex; flex-direction: row-reverse; - .button--alert { + .button--alert, + .button--brand { margin-left: 15px; } } @@ -722,4 +723,57 @@ .software-last-used-muted { color: $ui-fleet-black-50; } + + &__os-modal-title { + font-size: $medium; + font-weight: $bold; + } + &__os-modal-updated { + font-style: italic; + } + &__os-modal-example-title { + font-size: $x-small; + font-weight: $bold; + } + &__os-policy { + .form-field__label { + font-weight: normal; + } + #os-policy { + color: $core-fleet-purple; + line-height: 20px; + font-size: $x-small; + font-family: "SourceCodePro", $monospace; + min-height: 55px; + padding-right: $pad-xxlarge; + } + } + + &__os-policy-copy-icon { + color: $core-vibrant-blue; + margin-left: $pad-small; + margin-right: $pad-medium; + } + + .buttons { + display: flex; + align-items: center; + position: absolute; + right: 25px; + + span { + font-weight: $regular; + } + + a { + display: flex; + align-items: center; + } + } + + &__button-wrap { + display: flex; + justify-content: flex-end; + margin: $pad-xxlarge 0 0; + } } diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx index 74aab808ca..c1e0e83497 100644 --- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx +++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx @@ -50,6 +50,9 @@ const PolicyPage = ({ const { selectedOsqueryTable, setSelectedOsqueryTable, + lastEditedQueryName, + lastEditedQueryDescription, + lastEditedQueryBody, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, @@ -123,9 +126,6 @@ const PolicyPage = ({ detectIsFleetQueryRunnable(); !!policyIdForEdit && refetchStoredPolicy(); - setLastEditedQueryName(DEFAULT_POLICY.name); - setLastEditedQueryDescription(DEFAULT_POLICY.description); - setLastEditedQueryBody(DEFAULT_POLICY.query); }, []); useEffect(() => { diff --git a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx index aee963cdcc..bacd66e86d 100644 --- a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx +++ b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; +import React, { useState, useContext } from "react"; import { size } from "lodash"; import { IPolicyFormData } from "interfaces/policy"; import { useDeepEffect } from "utilities/hooks"; - +import { PolicyContext } from "context/policy"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import Button from "components/buttons/Button"; @@ -33,8 +33,14 @@ const NewPolicyModal = ({ onCreatePolicy, setIsNewPolicyModalOpen, }: INewPolicyModalProps): JSX.Element => { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); + const { lastEditedQueryName, lastEditedQueryDescription } = useContext( + PolicyContext + ); + + const [name, setName] = useState(lastEditedQueryName || ""); + const [description, setDescription] = useState( + lastEditedQueryDescription || "" + ); const [errors, setErrors] = useState<{ [key: string]: string }>({}); useDeepEffect(() => {