diff --git a/assets/images/ticket-policies-jira-screenshot-400x419@2x.png b/assets/images/ticket-policies-jira-screenshot-400x419@2x.png new file mode 100644 index 0000000000..13b6128e48 Binary files /dev/null and b/assets/images/ticket-policies-jira-screenshot-400x419@2x.png differ diff --git a/assets/images/ticket-policies-zendesk-screenshot-400x515@2x.png b/assets/images/ticket-policies-zendesk-screenshot-400x515@2x.png new file mode 100644 index 0000000000..f9f5cf1549 Binary files /dev/null and b/assets/images/ticket-policies-zendesk-screenshot-400x515@2x.png differ diff --git a/changes/issue-5441-policies-integrations b/changes/issue-5441-policies-integrations new file mode 100644 index 0000000000..f01028584d --- /dev/null +++ b/changes/issue-5441-policies-integrations @@ -0,0 +1 @@ +* Update UI to enable ticket workflow for failing policies automation \ No newline at end of file diff --git a/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index ad17a5b27b..ac0993b7ed 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -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", () => { diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index d11b0b534c..5e7c69c34b 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -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" +>; diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts index 7e9bd4266f..b2a21ff8ad 100644 --- a/frontend/interfaces/integration.ts +++ b/frontend/interfaces/integration.ts @@ -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; diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts index c86b26aace..6f7f14ed78 100644 --- a/frontend/interfaces/team.ts +++ b/frontend/interfaces/team.ts @@ -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 */ diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 7eb6def1ca..7b4ef86951 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -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(); - 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( + ["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( + ["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 ? ( ) : ( @@ -369,7 +372,7 @@ const ManagePolicyPage = ({ {showCtaButtons && (
{canManageAutomations && - !isFetchingWebhooks && + automationsConfig && !isFetchingGlobalPolicies && (
)} - {showManageAutomationsModal && ( + {config && automationsConfig && showManageAutomationsModal && ( )} {showAddPolicyModal && ( diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 8f3253392b..ddfe97b385 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -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(() => { - 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( - 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(enableFailingPoliciesWebhook); + isPolicyAutomationsEnabled, + setIsPolicyAutomationsEnabled, + ] = useState( + !!webhook.enable_failing_policies_webhook || !!serverEnabledIntegration + ); + + const [isWebhookEnabled, setIsWebhookEnabled] = useState( + !isPolicyAutomationsEnabled || webhook.enable_failing_policies_webhook + ); + + const [destinationUrl, setDestinationUrl] = useState( + 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 | 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 ; - } + const renderWebhook = () => { + return ( +
+ + +
+ ); + }; - return ( - + const renderIntegrations = () => { + return jira?.length || zendesk?.length ? ( +
+ + +
+ ) : ( +
+
+ You have no integrations. +
+
+ + Add integration + +
+
+ ); + }; + + const renderPreview = () => + !isWebhookEnabled ? ( + + ) : ( + + ); + + return showPreviewModal ? ( + renderPreview() + ) : ( + <> {isAutomationsLoading ? ( @@ -180,9 +338,9 @@ const ManageAutomationsModal = ({
- setPolicyAutomationEnabled(!policyAutomationEnabled) + setIsPolicyAutomationsEnabled(!isPolicyAutomationsEnabled) } inactiveText={"Policy automations disabled"} activeText={"Policy automations enabled"} @@ -190,65 +348,80 @@ const ManageAutomationsModal = ({
- {availablePolicies && availablePolicies.length > 0 ? ( -
-

- - Choose which policies you would like to listen to: - -

- {policyItems && - policyItems.map((policyItem) => { - const { isChecked, name, id } = policyItem; - return ( -
- updatePolicyItems(policyItem.id)} - > - {name} - -
- ); - })} -
- ) : ( -
- You have no policies. -

Add a policy to turn on automations.

-
- )} - - +
+ {availablePolicies?.length ? ( +
+

+ {errors.policyItems ? ( + + {errors.policyItems} + + ) : ( + + Choose which policies you would like to listen to: + + )} +

+ {policyItems && + policyItems.map((policyItem) => { + const { isChecked, name, id } = policyItem; + return ( +
+ { + updatePolicyItems(policyItem.id); + !isChecked && + setErrors((errs) => + omit(errs, "policyItems") + ); + }} + > + {name} + +
+ ); + })} +
+ ) : ( +
+ You have no policies. +

Add a policy to turn on automations.

+
+ )} +
+
+ Workflow + + +
+ {isWebhookEnabled ? renderWebhook() : renderIntegrations()}
- {!policyAutomationEnabled && ( + {!isPolicyAutomationsEnabled && (
)}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/PreviewTicketModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/PreviewTicketModal.tsx new file mode 100644 index 0000000000..a5fbba7346 --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/PreviewTicketModal.tsx @@ -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 ( + +
+

+ Want to learn more about how automations in Fleet work?{" "} + + Check out the Fleet documentation  + + +

+
+ Example policies automation ticket +
+
+ +
+
+
+ ); +}; + +export default PreviewTicketModal; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/_styles.scss new file mode 100644 index 0000000000..6130ec3f25 --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/_styles.scss @@ -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; + } +} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/index.ts b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/index.ts new file mode 100644 index 0000000000..4d8716d447 --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/PreviewTicketModal/index.ts @@ -0,0 +1 @@ +export { default } from "./PreviewTicketModal"; diff --git a/frontend/services/entities/config.ts b/frontend/services/entities/config.ts index 51a186b609..aeeb459836 100644 --- a/frontend/services/entities/config.ts +++ b/frontend/services/entities/config.ts @@ -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"; diff --git a/frontend/services/entities/teams.ts b/frontend/services/entities/teams.ts index 6535893103..9213f523cd 100644 --- a/frontend/services/entities/teams.ts +++ b/frontend/services/entities/teams.ts @@ -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 => { 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, + teamId?: number + ): Promise => { if (typeof teamId === "undefined") { return Promise.reject("Invalid usage: missing team id"); } const { TEAMS } = endpoints; const path = `${TEAMS}/${teamId}`; + const requestBody: Record = {}; + 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;