Enable ticket workflow for failing policies automation (#6066)

This commit is contained in:
gillespi314
2022-06-13 18:21:24 -05:00
committed by GitHub
parent 7621a0b290
commit 21837ed8ac
14 changed files with 597 additions and 267 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

+1
View File
@@ -0,0 +1 @@
* Update UI to enable ticket workflow for failing policies automation
@@ -400,6 +400,29 @@ describe("Policies flow (seeded)", () => {
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-checkbox__input").should("be.checked");
});
// reset slider for subsequent tests
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider").click();
});
cy.findByRole("button", { name: /^Save$/ }).click();
});
it("creates a failing policies integration", () => {
cy.getAttached(".button-wrap").within(() => {
cy.findByRole("button", { name: /manage automations/i }).click();
});
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider").click();
cy.getAttached(".fleet-checkbox__input").check({ force: true });
});
cy.getAttached("#ticket-radio-btn").next().click();
cy.findByText(/you have no integrations/i).should("exist");
cy.getAttached(".manage-automations-modal__add-integration-link").click();
// should be redirected to integrations settings page
cy.getAttached(".table-container").within(() => {
cy.findByText(/set up integration/i).should("exist");
});
// TODO: add tests for selecting integration
});
});
describe("Platform compatibility", () => {
+12 -5
View File
@@ -181,11 +181,7 @@ export interface IConfig {
// vulnerability_settings: {
// databases_path: string;
// };
webhook_settings: {
host_status_webhook: IWebhookHostStatus;
failing_policies_webhook: IWebhookFailingPolicies;
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
};
webhook_settings: IWebhookSettings;
integrations: IIntegrations;
logging: {
debug: boolean;
@@ -210,3 +206,14 @@ export interface IConfig {
};
};
}
export interface IWebhookSettings {
failing_policies_webhook: IWebhookFailingPolicies;
host_status_webhook: IWebhookHostStatus;
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
}
export type IAutomationsConfig = Pick<
IConfig,
"webhook_settings" | "integrations"
>;
+3
View File
@@ -3,6 +3,7 @@ export interface IJiraIntegration {
username: string;
api_token: string;
project_key: string;
enable_failing_policies?: boolean;
enable_software_vulnerabilities?: boolean;
}
@@ -11,6 +12,7 @@ export interface IZendeskIntegration {
email: string;
api_token: string;
group_id: number;
enable_failing_policies?: boolean;
enable_software_vulnerabilities?: boolean;
}
@@ -21,6 +23,7 @@ export interface IIntegration {
api_token: string;
project_key?: string;
group_id?: number;
enable_failing_policies?: boolean;
enable_software_vulnerabilities?: boolean;
originalIndex?: number;
type?: string;
+16 -6
View File
@@ -1,5 +1,7 @@
import PropTypes from "prop-types";
import enrollSecretInterface, { IEnrollSecret } from "./enroll_secret";
import { IIntegrations } from "./integration";
import { IWebhookFailingPolicies } from "./webhook";
export default PropTypes.shape({
id: PropTypes.number.isRequired,
@@ -24,7 +26,7 @@ export interface ITeamSummary {
}
/**
* The shape of a team entity
* The shape of a team entity excluding integrations and webhook settings
*/
export interface ITeam extends ITeamSummary {
uuid?: string;
@@ -34,19 +36,27 @@ export interface ITeam extends ITeamSummary {
agent_options?: {
[key: string]: any;
};
webhook_settings?: {
[key: string]: any;
};
user_count?: number;
host_count?: number;
secrets?: IEnrollSecret[];
role?: string; // role value is included when the team is in the context of a user
}
export interface ILoadTeamResponse {
team: ITeam;
/**
* The integrations and webhook settings of a team
*/
export interface ITeamAutomationsConfig {
webhook_settings: {
failing_policies_webhook: IWebhookFailingPolicies;
};
integrations: IIntegrations;
}
/**
* The shape of a team entity including integrations and webhook settings
*/
export type ITeamConfig = ITeam & ITeamAutomationsConfig;
/**
* The shape of a new member to add to a team
*/
@@ -1,22 +1,22 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { InjectedRouter } from "react-router/lib/Router";
import { get, has, noop } from "lodash";
import { noop } from "lodash";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import { IAutomationsConfig, IConfig } from "interfaces/config";
import { IPolicyStats, ILoadAllPoliciesResponse } from "interfaces/policy";
import { IWebhookFailingPolicies } from "interfaces/webhook";
import { IConfig } from "interfaces/config";
import { ITeam, ILoadTeamResponse } from "interfaces/team";
import { ITeamAutomationsConfig, ITeamConfig } from "interfaces/team";
import PATHS from "router/paths";
import configAPI from "services/entities/config";
import globalPoliciesAPI from "services/entities/global_policies";
import teamPoliciesAPI from "services/entities/team_policies";
import teamsAPI from "services/entities/teams";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import Button from "components/buttons/Button";
import RevealButton from "components/buttons/RevealButton";
@@ -86,13 +86,6 @@ const ManagePolicyPage = ({
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
const [
failingPoliciesWebhook,
setFailingPoliciesWebhook,
] = useState<IWebhookFailingPolicies>();
const [currentAutomatedPolicies, setCurrentAutomatedPolicies] = useState<
number[]
>();
useEffect(() => {
setLastEditedQueryPlatform(null);
@@ -112,7 +105,7 @@ const ManagePolicyPage = ({
{
enabled: !!availableTeams,
select: (data) => data.policies,
staleTime: 3000,
staleTime: 5000,
}
);
@@ -127,6 +120,7 @@ const ManagePolicyPage = ({
{
enabled: !!availableTeams && isPremiumTier && !!teamId,
select: (data) => data.policies,
staleTime: 5000,
}
);
@@ -134,35 +128,35 @@ const ManagePolicyPage = ({
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
const canManageAutomations = isGlobalAdmin || isTeamAdmin;
const { isFetching: isFetchingWebhooks, refetch: refetchWebhooks } = useQuery<
IConfig | ILoadTeamResponse,
Error,
IConfig | ITeam
>(
["webhooks", teamId],
const {
data: config,
isFetching: isFetchingConfig,
refetch: refetchConfig,
} = useQuery<IConfig, Error>(
["config"],
() => {
return teamId ? teamsAPI.load(teamId) : configAPI.loadAll();
return configAPI.loadAll();
},
{
enabled: canAddOrRemovePolicy,
select: (data) => {
if (has(data, "team")) {
return get(data, "team");
}
return data;
},
onSuccess: (data) => {
setFailingPoliciesWebhook(
data.webhook_settings?.failing_policies_webhook
);
setCurrentAutomatedPolicies(
data.webhook_settings?.failing_policies_webhook.policy_ids
);
if (has(data, "org_info")) {
setConfig(data as IConfig);
}
setConfig(data);
},
staleTime: 5000,
}
);
const {
data: teamConfig,
isFetching: isFetchingTeamConfig,
refetch: refetchTeamConfig,
} = useQuery<ILoadTeamResponse, Error, ITeamConfig>(
["teams", teamId],
() => teamsAPI.load(teamId),
{
enabled: !!teamId && canAddOrRemovePolicy,
select: (data) => data.team,
staleTime: 5000,
}
);
@@ -207,30 +201,15 @@ const ManagePolicyPage = ({
const toggleShowInheritedPolicies = () =>
setShowInheritedPolicies(!showInheritedPolicies);
const onCreateWebhookSubmit = async ({
destination_url,
policy_ids,
enable_failing_policies_webhook,
}: IWebhookFailingPolicies) => {
const handleUpdateAutomations = async (
requestBody: IAutomationsConfig | ITeamAutomationsConfig
) => {
setIsAutomationsLoading(true);
try {
const api = teamId ? teamsAPI : configAPI;
const secondParam = teamId || undefined;
const data = {
webhook_settings: {
failing_policies_webhook: {
destination_url,
policy_ids,
enable_failing_policies_webhook,
},
},
};
setIsAutomationsLoading(true);
const request = api.update(data, secondParam);
await request.then(() => {
renderFlash("success", "Successfully updated policy automations.");
});
await (teamId
? teamsAPI.update(requestBody, teamId)
: configAPI.update(requestBody));
renderFlash("success", "Successfully updated policy automations.");
} catch {
renderFlash(
"error",
@@ -239,7 +218,8 @@ const ManagePolicyPage = ({
} finally {
toggleManageAutomationsModal();
setIsAutomationsLoading(false);
refetchWebhooks();
refetchConfig();
teamId && refetchTeamConfig();
}
};
@@ -338,6 +318,29 @@ const ManagePolicyPage = ({
const showCtaButtons =
(!!teamId && teamPolicies) || (!teamId && globalPolicies);
const automationsConfig = teamId ? teamConfig : config;
// NOTE: backend uses webhook_settings to store automated policy ids for both webhooks and integrations
let currentAutomatedPolicies: number[] = [];
if (automationsConfig) {
const {
webhook_settings: { failing_policies_webhook: webhook },
integrations,
} = automationsConfig;
let isIntegrationEnabled = false;
if (integrations) {
const { jira, zendesk } = integrations;
isIntegrationEnabled =
!!jira?.find((j) => j.enable_failing_policies) ||
!!zendesk?.find((z) => z.enable_failing_policies);
}
if (isIntegrationEnabled || webhook?.enable_failing_policies_webhook) {
currentAutomatedPolicies = webhook?.policy_ids || [];
}
}
return !availableTeams ? (
<Spinner />
) : (
@@ -369,7 +372,7 @@ const ManagePolicyPage = ({
{showCtaButtons && (
<div className={`${baseClass} button-wrap`}>
{canManageAutomations &&
!isFetchingWebhooks &&
automationsConfig &&
!isFetchingGlobalPolicies && (
<Button
onClick={toggleManageAutomationsModal}
@@ -410,12 +413,16 @@ const ManagePolicyPage = ({
{!!teamId && teamPoliciesError && <TableDataError />}
{!!teamId &&
!teamPoliciesError &&
(isFetchingTeamPolicies && isFetchingWebhooks ? (
(isFetchingTeamPolicies ? (
<Spinner />
) : (
<PoliciesListWrapper
policiesList={teamPolicies || []}
isLoading={isFetchingTeamPolicies && isFetchingWebhooks}
isLoading={
isFetchingTeamPolicies ||
isFetchingTeamConfig ||
isFetchingConfig
}
onAddPolicyClick={onAddPolicyClick}
onRemovePoliciesClick={onRemovePoliciesClick}
canAddOrRemovePolicy={canAddOrRemovePolicy}
@@ -431,7 +438,7 @@ const ManagePolicyPage = ({
) : (
<PoliciesListWrapper
policiesList={globalPolicies || []}
isLoading={isFetchingGlobalPolicies && isFetchingWebhooks}
isLoading={isFetchingGlobalPolicies || isFetchingConfig}
onAddPolicyClick={onAddPolicyClick}
onRemovePoliciesClick={onRemovePoliciesClick}
canAddOrRemovePolicy={canAddOrRemovePolicy}
@@ -467,37 +474,27 @@ const ManagePolicyPage = ({
<Spinner />
) : (
<PoliciesListWrapper
isLoading={isFetchingGlobalPolicies && isFetchingWebhooks}
isLoading={isFetchingGlobalPolicies}
policiesList={globalPolicies || []}
onRemovePoliciesClick={noop}
resultsTitle="policies"
canAddOrRemovePolicy={canAddOrRemovePolicy}
tableType="inheritedPolicies"
currentTeam={currentTeam}
currentAutomatedPolicies={currentAutomatedPolicies}
/>
))}
</div>
)}
{showManageAutomationsModal && (
{config && automationsConfig && showManageAutomationsModal && (
<ManageAutomationsModal
onCancel={toggleManageAutomationsModal}
onCreateWebhookSubmit={onCreateWebhookSubmit}
togglePreviewPayloadModal={togglePreviewPayloadModal}
showPreviewPayloadModal={showPreviewPayloadModal}
automationsConfig={automationsConfig}
availableIntegrations={config.integrations}
availablePolicies={availablePoliciesForAutomation}
currentAutomatedPolicies={currentAutomatedPolicies || []}
currentDestinationUrl={
(failingPoliciesWebhook &&
failingPoliciesWebhook.destination_url) ||
""
}
enableFailingPoliciesWebhook={
(failingPoliciesWebhook &&
failingPoliciesWebhook.enable_failing_policies_webhook) ||
false
}
isAutomationsLoading={isAutomationsLoading}
showPreviewPayloadModal={showPreviewPayloadModal}
onExit={toggleManageAutomationsModal}
handleSubmit={handleUpdateAutomations}
togglePreviewPayloadModal={togglePreviewPayloadModal}
/>
)}
{showAddPolicyModal && (
@@ -1,9 +1,12 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router";
import { isEmpty, noop, omit } from "lodash";
import { IAutomationsConfig } from "interfaces/config";
import { IIntegration, IIntegrations } from "interfaces/integration";
import { IPolicy } from "interfaces/policy";
import { IWebhookFailingPolicies } from "interfaces/webhook";
import useDeepEffect from "hooks/useDeepEffect";
import { size } from "lodash";
import { ITeamAutomationsConfig } from "interfaces/team";
import PATHS from "router/paths";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
@@ -11,20 +14,24 @@ import Slider from "components/forms/fields/Slider";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Radio from "components/forms/fields/Radio";
import Spinner from "components/Spinner";
import PreviewPayloadModal from "../PreviewPayloadModal";
import PreviewTicketModal from "../PreviewTicketModal";
interface IManageAutomationsModalProps {
onCancel: () => void;
onCreateWebhookSubmit: (formData: IWebhookFailingPolicies) => void;
togglePreviewPayloadModal: () => void;
showPreviewPayloadModal: boolean;
automationsConfig: IAutomationsConfig | ITeamAutomationsConfig;
availableIntegrations: IIntegrations;
availablePolicies: IPolicy[];
currentAutomatedPolicies?: number[];
currentDestinationUrl?: string;
enableFailingPoliciesWebhook: boolean;
isAutomationsLoading: boolean;
showPreviewPayloadModal: boolean;
onExit: () => void;
handleSubmit: (formData: IAutomationsConfig | ITeamAutomationsConfig) => void;
togglePreviewPayloadModal: () => void;
}
interface ICheckedPolicy {
@@ -33,146 +40,297 @@ interface ICheckedPolicy {
isChecked: boolean;
}
const findEnabledIntegration = ({ jira, zendesk }: IIntegrations) => {
return (
jira?.find((j) => j.enable_failing_policies) ||
zendesk?.find((z) => z.enable_failing_policies)
);
};
const getIntegrationType = (integration?: IIntegration) => {
return (
(!!integration?.group_id && "zendesk") ||
(!!integration?.project_key && "jira") ||
undefined
);
};
const useCheckboxListStateManagement = (
allPolicies: IPolicy[],
automatedPolicies: number[] | undefined
) => {
const [policyItems, setPolicyItems] = useState<ICheckedPolicy[]>(() => {
return (
allPolicies &&
allPolicies.map((policy) => {
return {
name: policy.name,
id: policy.id,
isChecked:
!!automatedPolicies && automatedPolicies.includes(policy.id),
};
})
);
return allPolicies.map(({ name, id }) => ({
name,
id,
isChecked: !!automatedPolicies?.includes(id),
}));
});
const updatePolicyItems = (policyId: number) => {
setPolicyItems((prevState) => {
const selectedPolicy = policyItems.find(
(policy) => policy.id === policyId
);
const updatedPolicy = selectedPolicy && {
...selectedPolicy,
isChecked: !!selectedPolicy && !selectedPolicy.isChecked,
};
// this is replacing the policy object with the updatedPolicy we just created.
const newState = prevState.map((currentPolicy) => {
return currentPolicy.id === policyId && updatedPolicy
? updatedPolicy
: currentPolicy;
});
return newState;
});
setPolicyItems((prevItems) =>
prevItems.map((policy) =>
policy.id !== policyId
? policy
: { ...policy, isChecked: !policy.isChecked }
)
);
};
return { policyItems, updatePolicyItems };
};
const validateWebhookURL = (url: string) => {
const errors: { [key: string]: string } = {};
if (url === "") {
errors.url = "Please add a destination URL";
}
const valid = !size(errors);
return { valid, errors };
};
const baseClass = "manage-automations-modal";
const ManageAutomationsModal = ({
onCancel: onReturnToApp,
onCreateWebhookSubmit,
togglePreviewPayloadModal,
showPreviewPayloadModal,
automationsConfig,
availableIntegrations,
availablePolicies,
currentAutomatedPolicies,
currentDestinationUrl,
enableFailingPoliciesWebhook,
isAutomationsLoading,
showPreviewPayloadModal: showPreviewModal,
onExit,
handleSubmit,
togglePreviewPayloadModal: togglePreviewModal,
}: IManageAutomationsModalProps): JSX.Element => {
const [destination_url, setDestinationUrl] = useState<string>(
currentDestinationUrl || ""
const {
webhook_settings: { failing_policies_webhook: webhook },
} = automationsConfig;
const { jira, zendesk } = availableIntegrations || {};
const allIntegrations: IIntegration[] = [];
jira && allIntegrations.push(...jira);
zendesk && allIntegrations.push(...zendesk);
const dropdownOptions = allIntegrations.map(
({ group_id, project_key, url }) => ({
value: group_id || project_key,
label: `${url} - ${group_id || project_key}`,
})
);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const serverEnabledIntegration = findEnabledIntegration(
automationsConfig.integrations
);
const [
policyAutomationEnabled,
setPolicyAutomationEnabled,
] = useState<boolean>(enableFailingPoliciesWebhook);
isPolicyAutomationsEnabled,
setIsPolicyAutomationsEnabled,
] = useState<boolean>(
!!webhook.enable_failing_policies_webhook || !!serverEnabledIntegration
);
const [isWebhookEnabled, setIsWebhookEnabled] = useState(
!isPolicyAutomationsEnabled || webhook.enable_failing_policies_webhook
);
const [destinationUrl, setDestinationUrl] = useState<string>(
webhook.destination_url || ""
);
const [selectedIntegration, setSelectedIntegration] = useState<
IIntegration | undefined
>(serverEnabledIntegration);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const { policyItems, updatePolicyItems } = useCheckboxListStateManagement(
availablePolicies,
currentAutomatedPolicies
(isPolicyAutomationsEnabled && webhook?.policy_ids) || []
);
useDeepEffect(() => {
if (destination_url) {
setErrors({});
}
}, [destination_url]);
const onURLChange = (value: string) => {
const onChangeUrl = (value: string) => {
setDestinationUrl(value);
setErrors((errs) => omit(errs, "url"));
};
const handleSaveAutomation = (
const onChangeRadio = (val: string) => {
switch (val) {
case "webhook":
setIsWebhookEnabled(true);
setSelectedIntegration(undefined);
break;
case "ticket":
setIsWebhookEnabled(false);
break;
default:
noop();
}
};
const onSelectIntegration = (selected: string | number) => {
setSelectedIntegration(
allIntegrations.find(
({ group_id, project_key }) =>
group_id === selected || project_key === selected
)
);
};
const onClickSave = (
evt: React.MouseEvent<HTMLFormElement> | KeyboardEvent
) => {
evt.preventDefault();
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
setErrors({
...errors,
...newErrors,
let newPolicyIds: number[] = [];
policyItems?.forEach((p) => p.isChecked && newPolicyIds.push(p.id));
const newErrors = { ...errors };
if (!newPolicyIds.length) {
newErrors.policyItems =
"Please choose at least one policy you want to listen to:";
} else {
delete newErrors.policyItems;
}
if (isWebhookEnabled && !destinationUrl) {
newErrors.url = "Please add a destination URL";
} else {
delete newErrors.url;
}
if (!isEmpty(newErrors)) {
setErrors(newErrors);
return;
}
const newJira =
availableIntegrations.jira?.map((j) => ({
...j,
enable_failing_policies:
isPolicyAutomationsEnabled &&
!isWebhookEnabled &&
j.project_key === selectedIntegration?.project_key,
})) || null;
const newZendesk =
availableIntegrations.zendesk?.map((z) => ({
...z,
enable_failing_policies:
isPolicyAutomationsEnabled &&
!isWebhookEnabled &&
z.group_id === selectedIntegration?.group_id,
})) || null;
if (
!isPolicyAutomationsEnabled ||
(!isWebhookEnabled && !selectedIntegration)
) {
newPolicyIds = [];
}
// NOTE: backend uses webhook_settings to store automated policy ids for both webhooks and integrations
const newWebhook = {
failing_policies_webhook: {
destination_url: destinationUrl,
policy_ids: newPolicyIds,
enable_failing_policies_webhook:
isPolicyAutomationsEnabled && isWebhookEnabled,
},
};
handleSubmit({
webhook_settings: newWebhook,
integrations: {
jira: newJira,
zendesk: newZendesk,
},
});
const policy_ids =
policyItems &&
policyItems
.filter((policy) => policy.isChecked)
.map((policy) => policy.id);
// URL validation only needed if at least one policy is checked
if (valid || !enableFailingPoliciesWebhook) {
onCreateWebhookSubmit({
destination_url,
policy_ids,
enable_failing_policies_webhook: policyAutomationEnabled,
});
}
setErrors(newErrors);
};
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
handleSaveAutomation(event);
onClickSave(event);
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [handleSaveAutomation]);
}, [onClickSave]);
if (showPreviewPayloadModal) {
return <PreviewPayloadModal onCancel={togglePreviewPayloadModal} />;
}
const renderWebhook = () => {
return (
<div className={`${baseClass}__webhook`}>
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destinationUrl}
onChange={onChangeUrl}
error={errors.url}
hint={
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
}
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>
</div>
);
};
return (
<Modal
onExit={onReturnToApp}
title={"Manage automations"}
className={baseClass}
>
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"}
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
hint={
"For each policy, Fleet will create a ticket with a list of the failing hosts."
}
/>
<Button type="button" variant="text-link" onClick={togglePreviewModal}>
Preview ticket
</Button>
</div>
) : (
<div className={`${baseClass}__no-integrations`}>
<div>
<b>You have no integrations.</b>
</div>
<div className={`${baseClass}__no-integration--cta`}>
<Link
to={PATHS.ADMIN_INTEGRATIONS}
className={`${baseClass}__add-integration-link`}
>
<span>Add integration</span>
</Link>
</div>
</div>
);
};
const renderPreview = () =>
!isWebhookEnabled ? (
<PreviewTicketModal
type={
getIntegrationType(selectedIntegration) ||
(zendesk.length && "zendesk") ||
"jira"
}
onCancel={togglePreviewModal}
/>
) : (
<PreviewPayloadModal onCancel={togglePreviewModal} />
);
return showPreviewModal ? (
renderPreview()
) : (
<Modal onExit={onExit} title={"Manage automations"} className={baseClass}>
<>
{isAutomationsLoading ? (
<Spinner />
@@ -180,9 +338,9 @@ const ManageAutomationsModal = ({
<div className={baseClass}>
<div className={`${baseClass}__software-select-items`}>
<Slider
value={policyAutomationEnabled}
value={isPolicyAutomationsEnabled}
onChange={() =>
setPolicyAutomationEnabled(!policyAutomationEnabled)
setIsPolicyAutomationsEnabled(!isPolicyAutomationsEnabled)
}
inactiveText={"Policy automations disabled"}
activeText={"Policy automations enabled"}
@@ -190,65 +348,80 @@ const ManageAutomationsModal = ({
</div>
<div className={`${baseClass}__overlay-container`}>
<div className={`${baseClass}__policy-automation-enabled`}>
{availablePolicies && availablePolicies.length > 0 ? (
<div className={`${baseClass}__policy-select-items`}>
<p>
<strong>
Choose which policies you would like to listen to:
</strong>
</p>
{policyItems &&
policyItems.map((policyItem) => {
const { isChecked, name, id } = policyItem;
return (
<div key={id} className={`${baseClass}__team-item`}>
<Checkbox
value={isChecked}
name={name}
onChange={() => updatePolicyItems(policyItem.id)}
>
{name}
</Checkbox>
</div>
);
})}
</div>
) : (
<div className={`${baseClass}__no-policies`}>
<b>You have no policies.</b>
<p>Add a policy to turn on automations.</p>
</div>
)}
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destination_url}
onChange={onURLChange}
error={errors.url}
hint={
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
}
placeholder={"https://server.com/example"}
tooltip="Provide a URL to deliver a webhook request to."
/>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
>
Preview payload
</Button>
<div className={`${baseClass}__select`}>
{availablePolicies?.length ? (
<div className={`${baseClass}__policy-select-items`}>
<p>
{errors.policyItems ? (
<span className="form-field__label--error">
{errors.policyItems}
</span>
) : (
<strong>
Choose which policies you would like to listen to:
</strong>
)}
</p>
{policyItems &&
policyItems.map((policyItem) => {
const { isChecked, name, id } = policyItem;
return (
<div key={id} className={`${baseClass}__team-item`}>
<Checkbox
value={isChecked}
name={name}
onChange={() => {
updatePolicyItems(policyItem.id);
!isChecked &&
setErrors((errs) =>
omit(errs, "policyItems")
);
}}
>
{name}
</Checkbox>
</div>
);
})}
</div>
) : (
<div className={`${baseClass}__no-policies`}>
<b>You have no policies.</b>
<p>Add a policy to turn on automations.</p>
</div>
)}
</div>
<div className={`${baseClass}__workflow`}>
Workflow
<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>
{!policyAutomationEnabled && (
{!isPolicyAutomationsEnabled && (
<div className={`${baseClass}__overlay`} />
)}
</div>
<div className="modal-cta-wrap">
<Button
className={`${baseClass}__btn`}
onClick={onReturnToApp}
onClick={onExit}
variant="inverse"
>
Cancel
@@ -257,7 +430,7 @@ const ManageAutomationsModal = ({
className={`${baseClass}__btn`}
type="submit"
variant="brand"
onClick={handleSaveAutomation}
onClick={onClickSave}
>
Save
</Button>
@@ -0,0 +1,57 @@
import React from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
// @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
import JiraTicket from "../../../../../../assets/images/ticket-policies-jira-screenshot-400x419@2x.png";
import ZendeskTicket from "../../../../../../assets/images/ticket-policies-zendesk-screenshot-400x515@2x.png";
const baseClass = "preview-ticket-modal";
interface IPreviewTicketModalProps {
type?: "jira" | "zendesk";
onCancel: () => void;
}
const PreviewTicketModal = ({
type,
onCancel,
}: IPreviewTicketModalProps): JSX.Element => {
return (
<Modal title={"Example ticket"} onExit={onCancel} className={baseClass}>
<div className={`${baseClass}`}>
<p>
Want to learn more about how automations in Fleet work?{" "}
<a
href="https://fleetdm.com/docs/using-fleet/automations"
target="_blank"
rel="noopener noreferrer"
>
Check out the Fleet documentation&nbsp;
<FleetIcon name="external-link" />
</a>
</p>
<div className={`${baseClass}__example`}>
<img
className={`${baseClass}__screenshot`}
alt="Example policies automation ticket"
src={type === "zendesk" ? ZendeskTicket : JiraTicket}
/>
</div>
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="brand"
>
Done
</Button>
</div>
</div>
</Modal>
);
};
export default PreviewTicketModal;
@@ -0,0 +1,34 @@
.preview-ticket-modal {
&__example {
display: flex;
justify-content: center;
padding: 24px 0;
}
&__screenshot {
width: 400px;
height: auto;
border-radius: 8px;
filter: drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.1));
}
a {
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
text-decoration: none;
}
&__info-header {
font-weight: $bold;
}
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
}
&__btn {
margin-left: 12px;
}
}
@@ -0,0 +1 @@
export { default } from "./PreviewTicketModal";
-1
View File
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import sendMockRequest from "services/mock_service";
import endpoints from "utilities/endpoints";
import { IConfig } from "interfaces/config";
+43 -18
View File
@@ -1,9 +1,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import { IEnrollSecret } from "interfaces/enroll_secret";
import { INewMembersBody, IRemoveMembersBody, ITeam } from "interfaces/team";
import endpoints from "utilities/endpoints";
import { IWebhook } from "interfaces/webhook";
import { pick } from "lodash";
import { IEnrollSecret } from "interfaces/enroll_secret";
import {
INewMembersBody,
IRemoveMembersBody,
ITeamConfig,
} from "interfaces/team";
interface ILoadTeamsParams {
page?: number;
@@ -11,22 +16,22 @@ interface ILoadTeamsParams {
globalFilter?: string;
}
/**
* The response body expected for the "Get team" endpoint.
* See https://fleetdm.com/docs/using-fleet/rest-api#get-team
*/
export interface ILoadTeamResponse {
team: ITeamConfig;
}
export interface ILoadTeamsResponse {
teams: ITeam[];
teams: ITeamConfig[];
}
export interface ITeamFormData {
name: string;
}
interface ITeamWebhooks {
webhook_settings: {
[key: string]: IWebhook;
};
}
type ITeamUpdateData = ITeamFormData | ITeamWebhooks;
export default {
create: (formData: ITeamFormData) => {
const { TEAMS } = endpoints;
@@ -39,7 +44,7 @@ export default {
return sendRequest("DELETE", path);
},
load: (teamId: number) => {
load: (teamId: number): Promise<ILoadTeamResponse> => {
const { TEAMS } = endpoints;
const path = `${TEAMS}/${teamId}`;
@@ -64,18 +69,38 @@ export default {
return sendRequest("GET", path);
},
update: (updateParams: ITeamUpdateData, teamId?: number) => {
// we are grouping this update with the config api update function
// on the ManagePoliciesPage to streamline updating the
// webhook settings globally or for a team - see ManagePoliciesPage line 208
update: (
{ name, webhook_settings, integrations }: Partial<ITeamConfig>,
teamId?: number
): Promise<ITeamConfig> => {
if (typeof teamId === "undefined") {
return Promise.reject("Invalid usage: missing team id");
}
const { TEAMS } = endpoints;
const path = `${TEAMS}/${teamId}`;
const requestBody: Record<string, unknown> = {};
if (name) {
requestBody.name = name;
}
if (webhook_settings) {
requestBody.webhook_settings = webhook_settings;
}
if (integrations) {
const { jira, zendesk } = integrations;
const teamIntegrationProps = [
"enable_failing_policies",
"group_id",
"project_key",
"url",
];
requestBody.integrations = {
jira: jira?.map((j) => pick(j, teamIntegrationProps)),
zendesk: zendesk?.map((z) => pick(z, teamIntegrationProps)),
};
}
return sendRequest("PATCH", path, updateParams);
return sendRequest("PATCH", path, requestBody);
},
addMembers: (teamId: number, newMembers: INewMembersBody) => {
const { TEAMS_MEMBERS } = endpoints;