Fleet Calendar feature: Updates to manage automations modal (#17652)

This commit is contained in:
RachelElysia 2024-03-20 16:07:27 -04:00 committed by Victor Lyuboslavsky
parent d97e32fc21
commit 4db06f2cbb
No known key found for this signature in database
16 changed files with 185 additions and 291 deletions

View file

@ -41,7 +41,7 @@ import TableDataError from "components/DataError";
import MainContent from "components/MainContent";
import PoliciesTable from "./components/PoliciesTable";
import ManagePolicyAutomationsModal from "./components/ManagePolicyAutomationsModal";
import OtherWorkflowsModal from "./components/OtherWorkflowsModal";
import AddPolicyModal from "./components/AddPolicyModal";
import DeletePolicyModal from "./components/DeletePolicyModal";
@ -129,7 +129,6 @@ const ManagePolicyPage = ({
const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
false
);
const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false);
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
const [showDeletePolicyModal, setShowDeletePolicyModal] = useState(false);
@ -477,10 +476,6 @@ const ManagePolicyPage = ({
const toggleManageAutomationsModal = () =>
setShowManageAutomationsModal(!showManageAutomationsModal);
const togglePreviewPayloadModal = useCallback(() => {
setShowPreviewPayloadModal(!showPreviewPayloadModal);
}, [setShowPreviewPayloadModal, showPreviewPayloadModal]);
const toggleAddPolicyModal = () => setShowAddPolicyModal(!showAddPolicyModal);
const toggleDeletePolicyModal = () =>
@ -796,15 +791,13 @@ const ManagePolicyPage = ({
</div>
)}
{config && automationsConfig && showManageAutomationsModal && (
<ManagePolicyAutomationsModal
<OtherWorkflowsModal
automationsConfig={automationsConfig}
availableIntegrations={config.integrations}
availablePolicies={availablePoliciesForAutomation}
isUpdatingAutomations={isUpdatingAutomations}
showPreviewPayloadModal={showPreviewPayloadModal}
onExit={toggleManageAutomationsModal}
handleSubmit={handleUpdateAutomations}
togglePreviewPayloadModal={togglePreviewPayloadModal}
/>
)}
{showAddPolicyModal && (

View file

@ -0,0 +1,64 @@
import React, { useContext } from "react";
import { syntaxHighlight } from "utilities/helpers";
import { AppContext } from "context/app";
import { IPolicyWebhookPreviewPayload } from "interfaces/policy";
const baseClass = "example-payload";
interface IHostPreview {
id: number;
display_name: string;
url: string;
}
interface IExamplePayload {
timestamp: string;
policy: IPolicyWebhookPreviewPayload;
hosts: IHostPreview[];
}
const ExamplePayload = (): JSX.Element => {
const { isFreeTier } = useContext(AppContext);
const json: IExamplePayload = {
timestamp: "0000-00-00T00:00:00Z",
policy: {
id: 1,
name: "Is Gatekeeper enabled?",
query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
description: "Checks if gatekeeper is enabled on macOS devices.",
author_id: 1,
author_name: "John",
author_email: "john@example.com",
resolution: "Turn on Gatekeeper feature in System Preferences.",
passing_host_count: 2000,
failing_host_count: 300,
critical: false,
},
hosts: [
{
id: 1,
display_name: "macbook-1",
url: "https://fleet.example.com/hosts/1",
},
{
id: 2,
display_name: "macbbook-2",
url: "https://fleet.example.com/hosts/2",
},
],
};
if (isFreeTier) {
delete json.policy.critical;
}
return (
<div className={baseClass}>
<pre>POST https://server.com/example</pre>
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />
</div>
);
};
export default ExamplePayload;

View file

@ -0,0 +1,9 @@
.example-payload {
display: flex;
flex-direction: column;
gap: $pad-large;
pre {
margin: 0;
}
}

View file

@ -0,0 +1 @@
export { default } from "./ExamplePayload";

View file

@ -1,28 +1,24 @@
import React, { useContext } from "react";
import { AppContext } from "context/app";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import { IIntegrationType } from "interfaces/integration";
import Card from "components/Card";
import JiraPreview from "../../../../../../assets/images/jira-policy-automation-preview-400x419@2x.png";
import ZendeskPreview from "../../../../../../assets/images/zendesk-policy-automation-preview-400x515@2x.png";
import JiraPreviewPremium from "../../../../../../assets/images/jira-policy-automation-preview-premium-400x316@2x.png";
import ZendeskPreviewPremium from "../../../../../../assets/images/zendesk-policy-automation-preview-premium-400x483@2x.png";
const baseClass = "preview-ticket-modal";
const baseClass = "example-ticket";
interface IPreviewTicketModalProps {
interface IExampleTicketProps {
integrationType?: IIntegrationType;
onCancel: () => void;
}
const PreviewTicketModal = ({
const ExampleTicket = ({
integrationType,
onCancel,
}: IPreviewTicketModalProps): JSX.Element => {
}: IExampleTicketProps): JSX.Element => {
const { isPremiumTier } = useContext(AppContext);
const screenshot =
@ -41,30 +37,10 @@ const PreviewTicketModal = ({
);
return (
<Modal
title="Example ticket"
onExit={onCancel}
className={baseClass}
width="large"
>
<div className={`${baseClass}`}>
<p className="automations-learn-more">
Want to learn more about how automations in Fleet work?{" "}
<CustomLink
url="https://fleetdm.com/docs/using-fleet/automations"
text=" Check out the Fleet documentation"
newTab
/>
</p>
<div className={`${baseClass}__example`}>{screenshot}</div>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done
</Button>
</div>
</div>
</Modal>
<Card className={baseClass} color="gray">
{screenshot}
</Card>
);
};
export default PreviewTicketModal;
export default ExampleTicket;

View file

@ -0,0 +1,10 @@
.example-ticket {
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&__screenshot {
max-width: 400px;
}
}

View file

@ -0,0 +1 @@
export { default } from "./ExampleTicket";

View file

@ -1 +0,0 @@
export { default } from "./ManagePolicyAutomationsModal";

View file

@ -19,22 +19,21 @@ import Dropdown from "components/forms/fields/Dropdown";
import InputField from "components/forms/fields/InputField";
import Radio from "components/forms/fields/Radio";
import validUrl from "components/forms/validators/valid_url";
import RevealButton from "components/buttons/RevealButton";
import CustomLink from "components/CustomLink";
import ExampleTicket from "../ExampleTicket";
import ExamplePayload from "../ExamplePayload";
import PreviewPayloadModal from "../PreviewPayloadModal";
import PreviewTicketModal from "../PreviewTicketModal";
interface IManagePolicyAutomationsModalProps {
interface IOtherWorkflowsModalProps {
automationsConfig: IAutomationsConfig | ITeamAutomationsConfig;
availableIntegrations: IIntegrations;
availablePolicies: IPolicy[];
isUpdatingAutomations: boolean;
showPreviewPayloadModal: boolean;
onExit: () => void;
handleSubmit: (formData: {
webhook_settings: Pick<IWebhookSettings, "failing_policies_webhook">;
integrations: IIntegrations;
}) => void;
togglePreviewPayloadModal: () => void;
}
interface ICheckedPolicy {
@ -83,18 +82,16 @@ const useCheckboxListStateManagement = (
return { policyItems, updatePolicyItems };
};
const baseClass = "manage-policy-automations-modal";
const baseClass = "other-workflows-modal";
const ManagePolicyAutomationsModal = ({
const OtherWorkflowsModal = ({
automationsConfig,
availableIntegrations,
availablePolicies,
isUpdatingAutomations,
showPreviewPayloadModal,
onExit,
handleSubmit,
togglePreviewPayloadModal: togglePreviewModal,
}: IManagePolicyAutomationsModalProps): JSX.Element => {
}: IOtherWorkflowsModalProps): JSX.Element => {
const {
webhook_settings: { failing_policies_webhook: webhook },
} = automationsConfig;
@ -131,6 +128,9 @@ const ManagePolicyAutomationsModal = ({
IIntegration | undefined
>(serverEnabledIntegration);
const [showExamplePayload, setShowExamplePayload] = useState(false);
const [showExampleTicket, setShowExampleTicket] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const { policyItems, updatePolicyItems } = useCheckboxListStateManagement(
@ -218,13 +218,6 @@ const ManagePolicyAutomationsModal = ({
z.group_id === selectedIntegration?.group_id,
})) || null;
// if (
// !isPolicyAutomationsEnabled ||
// (!isWebhookEnabled && !selectedIntegration)
// ) {
// newPolicyIds = [];
// }
const updatedEnabledPoliciesAcrossPages = () => {
if (webhook.policy_ids) {
// Array of policy ids on the page
@ -297,34 +290,52 @@ const ManagePolicyAutomationsModal = ({
placeholder="https://server.com/example"
tooltip="Provide a URL to deliver a webhook request to."
/>
<Button type="button" variant="text-link" onClick={togglePreviewModal}>
Preview payload
</Button>
<RevealButton
isShowing={showExamplePayload}
className={baseClass}
hideText="Hide example payload"
showText="Show example payload"
caretPosition="after"
onClick={() => setShowExamplePayload(!showExamplePayload)}
/>
{showExamplePayload && <ExamplePayload />}
</>
);
};
const renderIntegrations = () => {
return jira?.length || zendesk?.length ? (
<div className={`${baseClass}__integrations`}>
<Dropdown
options={dropdownOptions}
onChange={onSelectIntegration}
placeholder="Select integration"
value={
selectedIntegration?.group_id || selectedIntegration?.project_key
}
label="Integration"
error={errors.integration}
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
hint={
"For each policy, Fleet will create a ticket with a list of the failing hosts."
}
<>
<div className={`${baseClass}__integrations`}>
<Dropdown
options={dropdownOptions}
onChange={onSelectIntegration}
placeholder="Select integration"
value={
selectedIntegration?.group_id || selectedIntegration?.project_key
}
label="Integration"
error={errors.integration}
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
hint={
"For each policy, Fleet will create a ticket with a list of the failing hosts."
}
/>
</div>
<RevealButton
isShowing={showExampleTicket}
className={baseClass}
hideText={"Hide example ticket"}
showText={"Show example ticket"}
caretPosition="after"
onClick={() => setShowExampleTicket(!showExampleTicket)}
/>
<Button type="button" variant="text-link" onClick={togglePreviewModal}>
Preview ticket
</Button>
</div>
{showExampleTicket && (
<ExampleTicket
integrationType={getIntegrationType(selectedIntegration)}
/>
)}
</>
) : (
<div className={`form-field ${baseClass}__no-integrations`}>
<div className="form-field__label">You have no integrations.</div>
@ -338,22 +349,10 @@ const ManagePolicyAutomationsModal = ({
);
};
const renderPreview = () =>
!isWebhookEnabled ? (
<PreviewTicketModal
integrationType={getIntegrationType(selectedIntegration)}
onCancel={togglePreviewModal}
/>
) : (
<PreviewPayloadModal onCancel={togglePreviewModal} />
);
return showPreviewPayloadModal ? (
renderPreview()
) : (
return (
<Modal
onExit={onExit}
title="Manage automations"
title="Other workflows"
className={baseClass}
width="large"
>
@ -372,12 +371,32 @@ const ManagePolicyAutomationsModal = ({
isPolicyAutomationsEnabled ? "enabled" : "disabled"
}`}
>
<div className={`form-field ${baseClass}__workflow`}>
<div className="form-field__label">Workflow</div>
<Radio
className={`${baseClass}__radio-input`}
label="Ticket"
id="ticket-radio-btn"
checked={!isWebhookEnabled}
value="ticket"
name="ticket"
onChange={onChangeRadio}
/>
<Radio
className={`${baseClass}__radio-input`}
label="Webhook"
id="webhook-radio-btn"
checked={isWebhookEnabled}
value="webhook"
name="webhook"
onChange={onChangeRadio}
/>
</div>
{isWebhookEnabled ? renderWebhook() : renderIntegrations()}
<div className="form-field">
{availablePolicies?.length ? (
<>
<div className="form-field__label">
Choose which policies you would like to listen to:
</div>
<div className="form-field__label">Policies:</div>
{policyItems &&
policyItems.map((policyItem) => {
const { isChecked, name, id } = policyItem;
@ -405,28 +424,14 @@ const ManagePolicyAutomationsModal = ({
</>
)}
</div>
<div className={`form-field ${baseClass}__workflow`}>
<div className="form-field__label">Workflow</div>
<Radio
className={`${baseClass}__radio-input`}
label="Ticket"
id="ticket-radio-btn"
checked={!isWebhookEnabled}
value="ticket"
name="ticket"
onChange={onChangeRadio}
<p className={`${baseClass}__help-text`}>
The workflow will be triggered when hosts fail these policies.{" "}
<CustomLink
url="https://www.fleetdm.com/learn-more-about/policy-automations"
text="Learn more"
newTab
/>
<Radio
className={`${baseClass}__radio-input`}
label="Webhook"
id="webhook-radio-btn"
checked={isWebhookEnabled}
value="webhook"
name="webhook"
onChange={onChangeRadio}
/>
</div>
{isWebhookEnabled ? renderWebhook() : renderIntegrations()}
</p>
</div>
<div className="modal-cta-wrap">
<Button
@ -447,4 +452,4 @@ const ManagePolicyAutomationsModal = ({
);
};
export default ManagePolicyAutomationsModal;
export default OtherWorkflowsModal;

View file

@ -1,12 +1,9 @@
.manage-policy-automations-modal {
.other-workflows-modal {
pre,
code {
background-color: $ui-off-white;
color: $core-fleet-blue;
border: 1px solid $ui-fleet-black-10;
border-radius: $border-radius;
padding: 7px $pad-medium;
margin: $pad-large 0 0 44px;
}
&__error {
@ -22,4 +19,8 @@
&__no-integrations a {
display: block;
}
&__help-text {
@include help-text;
}
}

View file

@ -0,0 +1 @@
export { default } from "./OtherWorkflowsModal";

View file

@ -1,97 +0,0 @@
import React, { useContext } from "react";
import { syntaxHighlight } from "utilities/helpers";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import { AppContext } from "context/app";
import { IPolicyWebhookPreviewPayload } from "interfaces/policy";
const baseClass = "preview-data-modal";
interface IPreviewPayloadModalProps {
onCancel: () => void;
}
interface IHostPreview {
id: number;
display_name: string;
url: string;
}
interface IPreviewPayload {
timestamp: string;
policy: IPolicyWebhookPreviewPayload;
hosts: IHostPreview[];
}
const PreviewPayloadModal = ({
onCancel,
}: IPreviewPayloadModalProps): JSX.Element => {
const { isFreeTier } = useContext(AppContext);
const json: IPreviewPayload = {
timestamp: "0000-00-00T00:00:00Z",
policy: {
id: 1,
name: "Is Gatekeeper enabled?",
query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
description: "Checks if gatekeeper is enabled on macOS devices.",
author_id: 1,
author_name: "John",
author_email: "john@example.com",
resolution: "Turn on Gatekeeper feature in System Preferences.",
passing_host_count: 2000,
failing_host_count: 300,
critical: false,
},
hosts: [
{
id: 1,
display_name: "macbook-1",
url: "https://fleet.example.com/hosts/1",
},
{
id: 2,
display_name: "macbbook-2",
url: "https://fleet.example.com/hosts/2",
},
],
};
if (isFreeTier) {
delete json.policy.critical;
}
return (
<Modal
title="Example payload"
onExit={onCancel}
onEnter={onCancel}
className={baseClass}
>
<div className={`${baseClass}__preview-modal`}>
<p>
Want to learn more about how automations in Fleet work?{" "}
<CustomLink
url="https://fleetdm.com/docs/using-fleet/automations"
text="Check out the Fleet documentation"
newTab
/>
</p>
<div className={`${baseClass}__payload-request-preview`}>
<pre>POST https://server.com/example</pre>
</div>
<div className={`${baseClass}__payload-webhook-preview`}>
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />
</div>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done
</Button>
</div>
</div>
</Modal>
);
};
export default PreviewPayloadModal;

View file

@ -1,54 +0,0 @@
.preview-payload-modal {
&__sandbox-info {
margin-top: $pad-medium;
p {
margin: 0;
margin-bottom: $pad-medium;
}
p:last-child {
margin-bottom: 0;
}
}
&__info-header {
font-weight: $bold;
}
&__advanced-options-button {
margin: $pad-medium 0;
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
.downcaret {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 2px;
}
}
.upcaret {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(180deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 4px;
margin-left: 14px;
}
}
.Select-value-label {
font-size: $small;
}
}

View file

@ -1 +0,0 @@
export { default } from "./PreviewPayloadModal";

View file

@ -1,13 +0,0 @@
.preview-ticket-modal {
&__example {
display: flex;
justify-content: center;
}
&__screenshot {
width: 400px;
height: auto;
border-radius: 8px;
filter: drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.1));
}
}

View file

@ -1 +0,0 @@
export { default } from "./PreviewTicketModal";