Enable ticket workflow for failing policies automation (#6066)
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
@@ -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", () => {
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
+329
-156
@@ -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>
|
||||
|
||||
+57
@@ -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
|
||||
<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,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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user