From d885758a6a4f08b2e1354bb16fc426f80081d4c5 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:04:41 -0400 Subject: [PATCH] UI: Settings > Integrations tab, Software Vulnerabilities Webhook v. Integration (#4874) --- assets/images/icon-jira-24x24@2x.png | Bin 0 -> 1367 bytes .../issue-2936-ui-includes-jira-integration | 1 + cypress/integration/free/admin.spec.ts | 8 +- cypress/integration/free/maintainer.spec.ts | 16 +- cypress/integration/premium/admin.spec.ts | 7 +- .../EnrollSecretRow/EnrollSecretRow.tsx | 4 +- .../components/forms/FormField/_styles.scss | 2 + .../forms/UserSettingsForm/_styles.scss | 2 - .../admin/AppConfigForm/AppConfigForm.tsx | 4 +- frontend/context/notification.tsx | 4 +- frontend/interfaces/config.ts | 2 + frontend/interfaces/integration.ts | 33 ++ .../pages/admin/AppSettingsPage/_styles.scss | 3 - .../IntegrationsPage/IntegrationsPage.tsx | 373 ++++++++++++++++++ .../IntegrationsTableConfig.tsx | 140 +++++++ .../pages/admin/IntegrationsPage/_styles.scss | 116 ++++++ .../CreateIntegrationModal.tsx | 70 ++++ .../CreateIntegrationModal/_styles.scss | 32 ++ .../CreateIntegrationModal/index.ts | 1 + .../DeleteIntegrationModal.tsx | 62 +++ .../DeleteIntegrationModal/_styles.scss | 15 + .../DeleteIntegrationModal/index.ts | 1 + .../EditIntegrationModal.tsx | 58 +++ .../EditIntegrationModal/_styles.scss | 11 + .../components/EditIntegrationModal/index.ts | 1 + .../IntegrationForm/IntegrationForm.tsx | 190 +++++++++ .../components/IntegrationForm/_styles.scss | 135 +++++++ .../components/IntegrationForm/index.ts | 1 + .../pages/admin/IntegrationsPage/index.ts | 1 + .../admin/SettingsWrapper/SettingsWrapper.tsx | 6 +- .../MembersPage/MembersPage.tsx | 12 +- .../TeamManagementPage/TeamManagementPage.tsx | 12 +- .../components/UserForm/_styles.scss | 5 - .../ManageAutomationsModal/_styles.scss | 5 - .../ManageSoftwarePage/ManageSoftwarePage.tsx | 79 ++-- .../ManageAutomationsModal.tsx | 287 ++++++++++++-- .../ManageAutomationsModal/_styles.scss | 30 +- frontend/router/index.tsx | 5 + frontend/router/paths.ts | 1 + frontend/services/entities/config.ts | 1 + .../services/mock_service/mocks/config.ts | 34 +- .../services/mock_service/mocks/responses.ts | 258 ++++++++---- frontend/utilities/constants.ts | 7 + 43 files changed, 1801 insertions(+), 234 deletions(-) create mode 100644 assets/images/icon-jira-24x24@2x.png create mode 100644 changes/issue-2936-ui-includes-jira-integration create mode 100644 frontend/interfaces/integration.ts create mode 100644 frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/IntegrationsTableConfig.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/_styles.scss create mode 100644 frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/CreateIntegrationModal.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/_styles.scss create mode 100644 frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/index.ts create mode 100644 frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/_styles.scss create mode 100644 frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/index.ts create mode 100644 frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/EditIntegrationModal.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/_styles.scss create mode 100644 frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/index.ts create mode 100644 frontend/pages/admin/IntegrationsPage/components/IntegrationForm/IntegrationForm.tsx create mode 100644 frontend/pages/admin/IntegrationsPage/components/IntegrationForm/_styles.scss create mode 100644 frontend/pages/admin/IntegrationsPage/components/IntegrationForm/index.ts create mode 100644 frontend/pages/admin/IntegrationsPage/index.ts diff --git a/assets/images/icon-jira-24x24@2x.png b/assets/images/icon-jira-24x24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f7fca518d7c9d134d8892d29339dc0c67de69b82 GIT binary patch literal 1367 zcmV-d1*rOoP)_nu7odlE%FD9NfgGQ*;6xVu-$b^-~-N}kh0Vgj7hN@ zFy8-lnKN265~d$CfwzIdjq{dD!Wa}ELWm70Ck?JFp1rV?oSK>GxvT(gNdc>I;}qWr z2ru|`##r*H>FLKVV^3Wf2TrjV(AWT#MJz6H(*!~S26hZMjb;F`ng00pyI}GkD5Xgq zHZzQVcc43Xk8#+H%fv_qX6K(t6Ux>p=ShlD5lTTi&cOTP#A#>)&t~r}b3%%Y^CiwX z=#MrbY#Ws84sIYOoFZqTi>u14p0A}{+b#;nNlGC=VtSE|0D)y!IIwW$49uRM3)SAJ z*(ve})L*x8hNT`h+~CN(-}{{-H{h^V!-OP5wKpnGkqo46sAa}@kuv6CLv_H6VbHCk zHiTun*@QZp0Hwk;JOWOk5V~k7vzdT26;Q+*k}fWAG&&j?*}_I6q*3zDVHw$|&cF2H zK2B&U2&rLk3N-?fLz+og$Ml}mmN8}ez0QxY(Y^1Fhn6cbT#3Ph*}8{P9K3CFyRlaG{B z6~Zp;Z@gf2S6j5ytLSqBpY{%fR9Kt>N1z9rAbB{kHcnMadBIj@{&=+=i`>Da6lo;F z;uL7WC161}HhrGt<3tL4O605gx#~&Pw25JGQXD>c&QebYRS7geT$q9gqTIw$Ki#)Z z9hVsjheCx-ufELLLwqZOp4;cf9DZ&?NMTH$nH;j`EOnxbvtLygiLy5^&91m%mKwfF zgu=JGH_3yb3Ow+Fgpy$1%T(z59<+pYbl}>A)>`+q=Rf^t(Q!nc5m?a;>i*Z+LofaU zm);7tg#p>!T>qDN;6#@CT3ta7H^Z3dl4T1h>{+CG>e|j_O3e*Su`7D1eKc#ESZ3aw za?FH{pDn>5Gbng1HmJgXKdqG)-m>=4>LvWT03n1pUBcKvT)1}NG@U$#x1!JMK)p~~ z@%m-rnds7w2=?_m*U3(pXYo*#hQw)lI`LuU;6X})g0=zAV5=;<%Dy=g3Y(s1K?Dth z)4jjSQ2)Q?;nG2sxMuXQBkJ6=^m4`x?20j_)N?~boQy;Q61YoIt5iI;I%D2`ah2S*nabY1IWyD`nxXfF zqTp1k+=u;g8HXERSfjJ(^ROW%%T$Ul54~43^l#!QIB`swRKK*cjSkMpt}Mxh%#9Mf z*EWoxusF3Rtj^C&oTTtXw(;F7Bk$71vb8t}r$)UFfA8&r-Du*Lh1vp6GO&3$mMz9u zcIF1*q_(%uF}vT+8l75g6?$8o~mxBvaK zyj82+&%_{9daAUBTSwPHi4r_mNpt`t{=>wIB<|K`~KVwz~8i6(lo ZdIT{IDmSh$^Q!;=002ovPDHLkV1oT2j)wpM literal 0 HcmV?d00001 diff --git a/changes/issue-2936-ui-includes-jira-integration b/changes/issue-2936-ui-includes-jira-integration new file mode 100644 index 000000000..860096284 --- /dev/null +++ b/changes/issue-2936-ui-includes-jira-integration @@ -0,0 +1 @@ +* Admin users can set jira integrations and software vulnerabilities to jira in the UI \ No newline at end of file diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index 31af6b285..71ce3dafa 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -197,12 +197,15 @@ describe( cy.loginWithCySession("anna@organization.com", "user123#"); cy.visit("/software/manage"); }); - it("allows global admin to update software vulnerability automation", () => { + it("allows global admin to create webhook software vulnerability automation", () => { cy.getAttached(".manage-software-page__header-wrap").within(() => { - cy.findByRole("button", { name: /manage automations/i }).click(); + cy.findByRole("button", { + name: /manage automations/i, + }).click(); }); cy.getAttached(".manage-automations-modal").within(() => { cy.getAttached(".fleet-slider").click(); + cy.getAttached("#webhook-radio-btn").next().click(); }); cy.getAttached("#webhook-url").click().type("www.foo.com/bar"); cy.findByRole("button", { name: /^Save$/ }).click(); @@ -215,6 +218,7 @@ describe( }); cy.getAttached(".manage-automations-modal").within(() => { cy.getAttached(".fleet-slider--active").should("exist"); + cy.getAttached("#webhook-url").should("exist"); }); }); }); diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index 52822a592..994d24d5f 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -187,18 +187,18 @@ describe( }); }); describe("Manage software page", () => { - beforeEach(() => { - cy.loginWithCySession("mary@organization.com", "user123#"); - cy.visit("/software/manage"); - }); - it("allows maintainer to click 'Manage automations' button", () => { - it("manages software automations when all teams selected", () => { + beforeEach(() => cy.visit("/software/manage")); + it("should restrict global maintainer from 'Manage automations' button", () => { + it("hides manages software automations when all teams selected", () => { cy.getAttached(".manage-software-page__header-wrap").within(() => { cy.getAttached(".Select").within(() => { cy.findByText(/all teams/i).should("exist"); }); - cy.findByRole("button", { name: /manage automations/i }).click(); - cy.findByRole("button", { name: /cancel/i }).click(); + cy.getAttached(".manage-software-page__header-wrap").within(() => { + cy.findByRole("button", { + name: /manage automations/i, + }).should("not.exist"); + }); }); }); it("hides manage automations button when all teams not selected", () => { diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 414445d5a..050a803a9 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -116,15 +116,13 @@ describe("Premium tier - Admin user", () => { }); describe("Manage software page", () => { beforeEach(() => cy.visit("/software/manage")); - it("allows global admin to update software vulnerability automation", () => { + it("allows global admin to create webhook software vulnerability automation", () => { cy.getAttached(".manage-software-page__header-wrap").within(() => { - cy.getAttached(".Select").within(() => { - cy.findByText(/all teams/i).should("exist"); - }); cy.findByRole("button", { name: /manage automations/i }).click(); }); cy.getAttached(".manage-automations-modal").within(() => { cy.getAttached(".fleet-slider").click(); + cy.getAttached("#webhook-radio-btn").next().click(); }); cy.getAttached("#webhook-url").click().type("www.foo.com/bar"); cy.findByRole("button", { name: /^Save$/ }).click(); @@ -137,6 +135,7 @@ describe("Premium tier - Admin user", () => { }); cy.getAttached(".manage-automations-modal").within(() => { cy.getAttached(".fleet-slider--active").should("exist"); + cy.getAttached("#webhook-url").should("exist"); }); }); it("hides manage automations button since all teams not selected", () => { diff --git a/frontend/components/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx b/frontend/components/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx index d05320a63..b3ee9a6ca 100644 --- a/frontend/components/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx +++ b/frontend/components/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx @@ -101,7 +101,7 @@ const EnrollSecretRow = ({ type={showSecret ? "text" : "password"} value={secret.secret} /> - {toggleSecretEditorModal && toggleDeleteSecretModal ? ( + {toggleSecretEditorModal && toggleDeleteSecretModal && ( <> - ) : null} + )} ); }; diff --git a/frontend/components/forms/FormField/_styles.scss b/frontend/components/forms/FormField/_styles.scss index 38793e86b..21fa5cac8 100644 --- a/frontend/components/forms/FormField/_styles.scss +++ b/frontend/components/forms/FormField/_styles.scss @@ -13,8 +13,10 @@ color: $core-vibrant-red; } + // so tooltips won't be triggered with whitespace &[data-has-tooltip="true"] { margin-bottom: $pad-small; + display: inline-flex; } } diff --git a/frontend/components/forms/UserSettingsForm/_styles.scss b/frontend/components/forms/UserSettingsForm/_styles.scss index 66d26c1ee..a09520949 100644 --- a/frontend/components/forms/UserSettingsForm/_styles.scss +++ b/frontend/components/forms/UserSettingsForm/_styles.scss @@ -6,8 +6,6 @@ .form-field__label { font-size: $x-small; font-weight: $bold; - // so tooltips won't be triggered with whitespace - display: inline-block; } &__email-hint { diff --git a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tsx b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tsx index dcb272fd4..45a9ff9c6 100644 --- a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tsx +++ b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tsx @@ -39,7 +39,7 @@ import { const baseClass = "app-config-form"; -const AppConfigFormFunctional = ({ +const AppConfigForm = ({ appConfig, handleSubmit, }: IAppConfigFormProps): JSX.Element => { @@ -977,4 +977,4 @@ const AppConfigFormFunctional = ({ ); }; -export default AppConfigFormFunctional; +export default AppConfigForm; diff --git a/frontend/context/notification.tsx b/frontend/context/notification.tsx index 9cfff52ee..3cfde94d4 100644 --- a/frontend/context/notification.tsx +++ b/frontend/context/notification.tsx @@ -10,7 +10,7 @@ type InitialStateType = { notification: INotification | null; renderFlash: ( alertType: "success" | "error" | "warning-filled" | null, - message: string | null, + message: JSX.Element | string | null, undoAction?: (evt: React.MouseEvent) => void ) => void; hideFlash: () => void; @@ -57,7 +57,7 @@ const NotificationProvider = ({ children }: Props) => { notification: state.notification, renderFlash: ( alertType: "success" | "error" | "warning-filled" | null, - message: string | null, + message: JSX.Element | string | null, undoAction?: (evt: React.MouseEvent) => void ) => { dispatch({ diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index 942d3bdeb..480d8bb6c 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -6,6 +6,7 @@ import { IWebhookSoftwareVulnerabilities, } from "interfaces/webhook"; import PropTypes from "prop-types"; +import { IIntegrations } from "./integration"; export default PropTypes.shape({ org_name: PropTypes.string, @@ -178,6 +179,7 @@ export interface IConfig { failing_policies_webhook: IWebhookFailingPolicies; vulnerabilities_webhook: IWebhookSoftwareVulnerabilities; }; + integrations: IIntegrations; logging: { debug: boolean; json: boolean; diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts new file mode 100644 index 000000000..95867e260 --- /dev/null +++ b/frontend/interfaces/integration.ts @@ -0,0 +1,33 @@ +export interface IJiraIntegration { + url: string; + username: string; + password: string; + project_key: string; + enable_software_vulnerabilities?: boolean; + index?: number; +} + +export interface IJiraIntegrationIndexed extends IJiraIntegration { + index: number; +} + +export interface IJiraIntegrationFormData { + url: string; + username: string; + password: string; + projectKey: string; + enableSoftwareVulnerabilities?: boolean; +} + +export interface IJiraIntegrationFormErrors { + url?: string | null; + username?: string | null; + password?: string | null; + projectKey?: string | null; +} + +export interface IIntegrations { + jira: IJiraIntegration[]; +} + +export type IIntegration = IJiraIntegration; diff --git a/frontend/pages/admin/AppSettingsPage/_styles.scss b/frontend/pages/admin/AppSettingsPage/_styles.scss index 44c9f4fdf..958d0e8eb 100644 --- a/frontend/pages/admin/AppSettingsPage/_styles.scss +++ b/frontend/pages/admin/AppSettingsPage/_styles.scss @@ -59,10 +59,7 @@ } } - // so tooltips won't be triggered with whitespace .form-field__label { - display: inline-block; - .buttons { top: 22px; } diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx new file mode 100644 index 000000000..ba8dd694c --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx @@ -0,0 +1,373 @@ +import React, { useState, useContext, useCallback } from "react"; +import { useQuery } from "react-query"; + +import { NotificationContext } from "context/notification"; +import { IConfig } from "interfaces/config"; +import { + IJiraIntegration, + IJiraIntegrationIndexed, + IJiraIntegrationFormErrors, +} from "interfaces/integration"; +import { IApiError } from "interfaces/errors"; + +import Button from "components/buttons/Button"; +// @ts-ignore +import FleetIcon from "components/icons/FleetIcon"; +import { DEFAULT_CREATE_INTEGRATION_ERRORS } from "utilities/constants"; + +import configAPI from "services/entities/config"; + +import TableContainer from "components/TableContainer"; +import TableDataError from "components/TableDataError"; +import AddIntegrationModal from "./components/CreateIntegrationModal"; +import DeleteIntegrationModal from "./components/DeleteIntegrationModal"; +import EditIntegrationModal from "./components/EditIntegrationModal"; + +import { + generateTableHeaders, + generateDataSet, +} from "./IntegrationsTableConfig"; + +const baseClass = "integrations-management"; +const noIntegrationsClass = "no-integrations"; + +const VALIDATION_FAILED_ERROR = + "There was a problem with the information you provided."; +const BAD_REQUEST_ERROR = + "Invalid login credentials or Jira URL. Please correct and try again."; +const UNKNOWN_ERROR = + "We experienced an error when attempting to connect to Jira. Please try again later."; + +const IntegrationsPage = (): JSX.Element => { + const { renderFlash } = useContext(NotificationContext); + + const [showAddIntegrationModal, setShowAddIntegrationModal] = useState(false); + const [showDeleteIntegrationModal, setShowDeleteIntegrationModal] = useState( + false + ); + const [showEditIntegrationModal, setShowEditIntegrationModal] = useState( + false + ); + const [ + integrationEditing, + setIntegrationEditing, + ] = useState(); + const [integrationsIndexed, setIntegrationsIndexed] = useState< + IJiraIntegrationIndexed[] + >(); + const [backendValidators, setBackendValidators] = useState<{ + [key: string]: string; + }>({}); + const [ + createIntegrationError, + setCreateIntegrationError, + ] = useState(DEFAULT_CREATE_INTEGRATION_ERRORS); + const [testingConnection, setTestingConnection] = useState(false); + + const { + data: integrations, + isLoading: isLoadingIntegrations, + error: loadingIntegrationsError, + refetch: refetchIntegrations, + } = useQuery( + ["integrations"], + () => configAPI.loadAll(), + { + select: (data: IConfig) => { + return data.integrations.jira; + }, + onSuccess: (data) => { + if (data) { + const addIndex = data.map((integration, index) => { + return { ...integration, index }; + }); + setIntegrationsIndexed(addIndex); + } + }, + } + ); + + const toggleAddIntegrationModal = useCallback(() => { + setShowAddIntegrationModal(!showAddIntegrationModal); + setBackendValidators({}); + }, [ + showAddIntegrationModal, + setShowAddIntegrationModal, + setBackendValidators, + ]); + + const toggleDeleteIntegrationModal = useCallback( + (integration?: IJiraIntegrationIndexed) => { + setShowDeleteIntegrationModal(!showDeleteIntegrationModal); + integration + ? setIntegrationEditing(integration) + : setIntegrationEditing(undefined); + }, + [ + showDeleteIntegrationModal, + setShowDeleteIntegrationModal, + setIntegrationEditing, + ] + ); + + const toggleEditIntegrationModal = useCallback( + (integration?: IJiraIntegrationIndexed) => { + setShowEditIntegrationModal(!showEditIntegrationModal); + setBackendValidators({}); + integration + ? setIntegrationEditing(integration) + : setIntegrationEditing(undefined); + }, + [ + showEditIntegrationModal, + setShowEditIntegrationModal, + setIntegrationEditing, + setBackendValidators, + ] + ); + + const onCreateSubmit = useCallback( + (jiraIntegrationSubmitData: IJiraIntegration[]) => { + setTestingConnection(true); + configAPI + .update({ integrations: { jira: jiraIntegrationSubmitData } }) + .then(() => { + renderFlash( + "success", + <> + Successfully added{" "} + + { + jiraIntegrationSubmitData[ + jiraIntegrationSubmitData.length - 1 + ].url + } + + + ); + setBackendValidators({}); + toggleAddIntegrationModal(); + refetchIntegrations(); + }) + .catch((createError: { data: IApiError }) => { + if (createError.data.message.includes("Validation Failed")) { + renderFlash("error", VALIDATION_FAILED_ERROR); + } + if (createError.data.message.includes("Bad request")) { + renderFlash("error", BAD_REQUEST_ERROR); + } + if (createError.data.message.includes("Unknown Error")) { + renderFlash("error", UNKNOWN_ERROR); + } else { + renderFlash( + "error", + <> + Could not add{" "} + + { + jiraIntegrationSubmitData[ + jiraIntegrationSubmitData.length - 1 + ].url + } + + . Please try again. + + ); + toggleAddIntegrationModal(); + } + }) + .finally(() => { + setTestingConnection(false); + }); + }, + [toggleAddIntegrationModal] + ); + + const onDeleteSubmit = useCallback(() => { + if (integrationEditing) { + integrations?.splice(integrationEditing.index, 1); + configAPI + .update({ integrations: { jira: integrations } }) + .then(() => { + renderFlash( + "success", + <> + Successfully deleted {integrationEditing.url} + + ); + }) + .catch(() => { + renderFlash( + "error", + <> + Could not delete {integrationEditing.url}. Please try + again. + + ); + }) + .finally(() => { + refetchIntegrations(); + toggleDeleteIntegrationModal(); + }); + } + }, [integrationEditing, toggleDeleteIntegrationModal]); + + const onEditSubmit = useCallback( + (jiraIntegrationSubmitData: IJiraIntegration[]) => { + if (integrationEditing) { + setTestingConnection(true); + configAPI + .update({ integrations: { jira: jiraIntegrationSubmitData } }) + .then(() => { + renderFlash( + "success", + <> + Successfully edited{" "} + + {jiraIntegrationSubmitData[integrationEditing?.index].url} + + + ); + setBackendValidators({}); + setTestingConnection(false); + setShowEditIntegrationModal(false); + refetchIntegrations(); + }) + .catch((editError: { data: IApiError }) => { + if (editError.data.message.includes("Validation Failed")) { + renderFlash("error", VALIDATION_FAILED_ERROR); + } + if (editError.data.message.includes("Bad request")) { + renderFlash("error", BAD_REQUEST_ERROR); + } + if (editError.data.message.includes("Unknown Error")) { + renderFlash("error", UNKNOWN_ERROR); + } else { + renderFlash( + "error", + <> + Could not edit {integrationEditing?.url}. Please try + again. + + ); + } + }) + .finally(() => { + setTestingConnection(false); + }); + } + }, + [integrationEditing, toggleEditIntegrationModal] + ); + + const onActionSelection = ( + action: string, + integration: IJiraIntegrationIndexed + ): void => { + switch (action) { + case "edit": + toggleEditIntegrationModal(integration); + break; + case "delete": + toggleDeleteIntegrationModal(integration); + break; + default: + } + }; + + const NoIntegrationsComponent = () => { + return ( +
+
+
+

Set up integrations

+

+ Create tickets automatically when Fleet detects new + vulnerabilities. +

+

+ Want to learn more?  + + Read about automations  + + +

+ +
+
+
+ ); + }; + + const tableHeaders = generateTableHeaders(onActionSelection); + const tableData = integrationsIndexed + ? generateDataSet(integrationsIndexed) + : []; + + return ( +
+

+ Add or edit integrations to create tickets when Fleet detects new + vulnerabilities. +

+ {loadingIntegrationsError ? ( + + ) : ( + + )} + {showAddIntegrationModal && ( + + )} + {showDeleteIntegrationModal && ( + + )} + {showEditIntegrationModal && ( + + )} +
+ ); +}; + +export default IntegrationsPage; diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationsTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationsTableConfig.tsx new file mode 100644 index 000000000..03b1c8478 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/IntegrationsTableConfig.tsx @@ -0,0 +1,140 @@ +import React from "react"; + +import TextCell from "components/TableContainer/DataTable/TextCell"; +import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; + +import { + IJiraIntegration, + IJiraIntegrationIndexed, +} from "interfaces/integration"; +import { IDropdownOption } from "interfaces/dropdownOption"; + +import JiraIcon from "../../../../assets/images/icon-jira-24x24@2x.png"; + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface IRowProps { + row: { + original: IJiraIntegrationIndexed; + }; +} +interface ICellProps extends IRowProps { + cell: { + value: string; + }; +} + +interface IDropdownCellProps extends IRowProps { + cell: { + value: IDropdownOption[]; + }; +} + +interface IDataColumn { + title: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: + | ((props: ICellProps) => JSX.Element) + | ((props: IDropdownCellProps) => JSX.Element); + disableHidden?: boolean; + disableSortBy?: boolean; + sortType?: string; +} + +export interface IIntegrationTableData extends IJiraIntegration { + actions: IDropdownOption[]; + name: string; +} + +// NOTE: cellProps come from react-table +// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties +const generateTableHeaders = ( + actionSelectHandler: ( + value: string, + integration: IJiraIntegrationIndexed + ) => void +): IDataColumn[] => { + return [ + { + title: "", + Header: "", + disableSortBy: true, + sortType: "caseInsensitive", + accessor: "hosts", + Cell: () => jira-icon, + }, + { + title: "Name", + Header: "Name", + disableSortBy: true, + sortType: "caseInsensitive", + accessor: "name", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + title: "Actions", + Header: "", + disableSortBy: true, + accessor: "actions", + Cell: (cellProps: IDropdownCellProps) => ( + + actionSelectHandler(value, cellProps.row.original) + } + placeholder={"Actions"} + /> + ), + }, + ]; +}; + +// NOTE: may need current user ID later for permission on actions. +const generateActionDropdownOptions = (): IDropdownOption[] => { + return [ + { + label: "Edit", + disabled: false, + value: "edit", + }, + { + label: "Delete", + disabled: false, + value: "delete", + }, + ]; +}; + +const enhanceIntegrationData = ( + integrations: IJiraIntegrationIndexed[] +): IIntegrationTableData[] => { + return Object.values(integrations).map((integration) => { + return { + url: integration.url, + username: integration.username, + password: integration.password, + project_key: integration.project_key, + actions: generateActionDropdownOptions(), + enable_software_vulnerabilities: + integration.enable_software_vulnerabilities, + name: `${integration.url} - ${integration.project_key}`, + index: integration.index, + }; + }); +}; + +const generateDataSet = ( + integrations: IJiraIntegrationIndexed[] +): IIntegrationTableData[] => { + return [...enhanceIntegrationData(integrations)]; +}; + +export { generateTableHeaders, generateDataSet }; diff --git a/frontend/pages/admin/IntegrationsPage/_styles.scss b/frontend/pages/admin/IntegrationsPage/_styles.scss new file mode 100644 index 000000000..1a7900d84 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/_styles.scss @@ -0,0 +1,116 @@ +.integrations-management { + padding: 0; + &__page-description { + font-size: $x-small; + color: $core-fleet-black; + @include sticky-settings-description; + padding-bottom: $pad-medium; + } +} + +.team-details__team-details { + .form-field--dropdown { + margin-bottom: 0; + } +} + +.team-details.team-select-open { + .component__tabs-wrapper { + position: relative; + z-index: 2; + } + .members { + position: relative; + z-index: 1; + } +} + +.no-integrations { + display: flex; + flex-direction: column; + align-items: center; + margin-top: $pad-xxxlarge; + + a { + font-size: $x-small; + color: $core-vibrant-blue; + font-weight: $bold; + text-decoration: none; + + img { + width: 12px; + height: 12px; + margin-left: 7px; + } + } + + h1 { + font-size: $large; + font-weight: $regular; + line-height: normal; + letter-spacing: normal; + color: $core-fleet-black; + } + + h2 { + font-size: $small; + font-weight: $bold; + margin: 0 0 $pad-large; + line-height: 20px; + color: $core-fleet-black; + } + + ul { + margin: 0; + padding: 0; + color: $core-fleet-black; + list-style: none; + + li { + &::before { + content: "•"; + color: $core-vibrant-blue; + margin-right: $pad-medium; + } + } + } + + &__inner { + display: flex; + flex-direction: row; + + h1 { + font-size: $small; + font-weight: $bold; + margin-bottom: $pad-medium; + } + + img { + width: 176px; + margin-right: $pad-xlarge; + } + + p { + color: $core-fleet-black; + font-weight: $regular; + font-size: $x-small; + margin: 0; + margin-bottom: $pad-large; + } + + .no-filter-results { + display: flex; + flex-direction: column; + width: 350px; + } + } + + &__inner-text { + width: 350px; + } +} +.hosts__cell { + img { + width: 24px; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/CreateIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/CreateIntegrationModal.tsx new file mode 100644 index 000000000..ee956c325 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/CreateIntegrationModal.tsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from "react"; + +import Modal from "components/Modal"; +import InfoBanner from "components/InfoBanner/InfoBanner"; +// @ts-ignore +import FleetIcon from "components/icons/FleetIcon"; +import Spinner from "components/Spinner"; +import { IJiraIntegration } from "interfaces/integration"; +import IntegrationForm from "../IntegrationForm"; + +const baseClass = "create-integration-modal"; + +interface ICreateIntegrationModalProps { + onCancel: () => void; + onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void; + serverErrors?: { base: string; email: string }; + backendValidators: { [key: string]: string }; + integrations: IJiraIntegration[]; + testingConnection: boolean; +} + +const CreateIntegrationModal = ({ + onCancel, + onSubmit, + backendValidators, + integrations, + testingConnection, +}: ICreateIntegrationModalProps): JSX.Element => { + const [errors, setErrors] = useState<{ [key: string]: string }>( + backendValidators + ); + + useEffect(() => { + setErrors(backendValidators); + }, [backendValidators]); + + return ( + + {testingConnection ? ( +
+ Testing connection to Jira + +
+ ) : ( + <> + +

+ Fleet supports Jira as a ticket destination.  + + Suggest a new destination  + + +

+
+ + + )} +
+ ); +}; + +export default CreateIntegrationModal; diff --git a/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/_styles.scss new file mode 100644 index 000000000..1be947db9 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/_styles.scss @@ -0,0 +1,32 @@ +.create-integration-modal { + a { + font-size: $x-small; + color: $core-vibrant-blue; + font-weight: $bold; + text-decoration: none; + + img { + width: 12px; + height: 12px; + margin-left: 7px; + } + } + + &__sandbox-info { + margin: $pad-medium 0; + } + + &__info-header { + margin: 0; + } + + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + margin-top: $pad-xxlarge; + } + + &__btn { + margin-left: 12px; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/index.ts b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/index.ts new file mode 100644 index 000000000..a27d02c19 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/CreateIntegrationModal/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateIntegrationModal"; diff --git a/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx new file mode 100644 index 000000000..e77d878d2 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx @@ -0,0 +1,62 @@ +import React, { useEffect } from "react"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; + +const baseClass = "delete-integration-modal"; + +interface IDeleteIntegrationModalProps { + url: string; + onSubmit: () => void; + onCancel: () => void; +} + +const DeleteIntegrationModal = ({ + url, + onSubmit, + onCancel, +}: IDeleteIntegrationModalProps): JSX.Element => { + useEffect(() => { + const listener = (event: KeyboardEvent) => { + if (event.code === "Enter" || event.code === "NumpadEnter") { + event.preventDefault(); + onSubmit(); + } + }; + document.addEventListener("keydown", listener); + return () => { + document.removeEventListener("keydown", listener); + }; + }, []); + + return ( + +
+

+ This action will delete the{" "} + {url} integration. +

+

The automations that use this integration will be turned off.

+
+ + +
+
+
+ ); +}; + +export default DeleteIntegrationModal; diff --git a/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/_styles.scss new file mode 100644 index 000000000..bd50b391b --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/_styles.scss @@ -0,0 +1,15 @@ +.delete-integration-modal { + &__url { + font-weight: $bold; + } + + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + margin-top: $pad-xxlarge; + } + + &__btn { + margin-left: 12px; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/index.ts b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/index.ts new file mode 100644 index 000000000..5ec8181b2 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/DeleteIntegrationModal/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteIntegrationModal"; diff --git a/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/EditIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/EditIntegrationModal.tsx new file mode 100644 index 000000000..56f642fe0 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/EditIntegrationModal.tsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from "react"; + +import Modal from "components/Modal"; +import Spinner from "components/Spinner"; +// @ts-ignore +import { + IJiraIntegration, + IJiraIntegrationIndexed, +} from "interfaces/integration"; +import IntegrationForm from "../IntegrationForm"; + +const baseClass = "edit-team-modal"; + +interface IEditIntegrationModalProps { + onCancel: () => void; + onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void; + backendValidators: { [key: string]: string }; + integrations: IJiraIntegration[]; + integrationEditing?: IJiraIntegrationIndexed; + testingConnection: boolean; +} + +const EditIntegrationModal = ({ + onCancel, + onSubmit, + backendValidators, + integrations, + integrationEditing, + testingConnection, +}: IEditIntegrationModalProps): JSX.Element => { + const [errors, setErrors] = useState<{ [key: string]: string }>( + backendValidators + ); + + useEffect(() => { + setErrors(backendValidators); + }, [backendValidators]); + + return ( + + {testingConnection ? ( +
+ Testing connection to Jira + +
+ ) : ( + + )} +
+ ); +}; + +export default EditIntegrationModal; diff --git a/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/_styles.scss new file mode 100644 index 000000000..60adcf5f0 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/_styles.scss @@ -0,0 +1,11 @@ +.edit-team-modal { + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + margin-top: $pad-xxlarge; + } + + &__btn { + margin-left: 12px; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/index.ts b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/index.ts new file mode 100644 index 000000000..24716991b --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/EditIntegrationModal/index.ts @@ -0,0 +1 @@ +export { default } from "./EditIntegrationModal"; diff --git a/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/IntegrationForm.tsx b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/IntegrationForm.tsx new file mode 100644 index 000000000..28c7ddeaa --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/IntegrationForm.tsx @@ -0,0 +1,190 @@ +import React, { FormEvent, useState } from "react"; +import ReactTooltip from "react-tooltip"; + +import { + IJiraIntegration, + IJiraIntegrationFormData, + IJiraIntegrationIndexed, +} from "interfaces/integration"; + +import Button from "components/buttons/Button"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; + +const baseClass = "integration-form"; + +interface IIntegrationFormProps { + onCancel: () => void; + onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void; + integrationEditing?: IJiraIntegrationIndexed; + integrations: IJiraIntegration[]; +} + +interface IFormField { + name: string; + value: string; +} + +const IntegrationForm = ({ + onCancel, + onSubmit, + integrationEditing, + integrations, +}: IIntegrationFormProps): JSX.Element => { + const [formData, setFormData] = useState({ + url: integrationEditing?.url || "", + username: integrationEditing?.username || "", + password: integrationEditing?.password || "", + projectKey: integrationEditing?.project_key || "", + enableSoftwareVulnerabilities: + integrationEditing?.enable_software_vulnerabilities || false, + }); + + const { url, username, password, projectKey } = formData; + + const onInputChange = ({ name, value }: IFormField) => { + setFormData({ ...formData, [name]: value }); + }; + + // IntegrationForm component can be used to create a new jira integration or edit an existing jira integration so submitData will be assembled accordingly + const createSubmitData = (): IJiraIntegration[] => { + let jiraIntegrationSubmitData = integrations; + + if (integrationEditing) { + // Edit existing integration using array replacement + jiraIntegrationSubmitData.splice(integrationEditing.index, 1, { + url, + username, + password, + project_key: projectKey, + }); + } else { + // Create new integration at end of array + jiraIntegrationSubmitData = [ + ...jiraIntegrationSubmitData, + { + url, + username, + password, + project_key: projectKey, + }, + ]; + } + + return jiraIntegrationSubmitData; + }; + + const onFormSubmit = (evt: FormEvent): void => { + evt.preventDefault(); + + return onSubmit(createSubmitData()); + }; + + return ( +
+ + \ + in which the issues are created. \ + " + } + /> + + \ + Jira. Your project key is in URL. For example, in
\ + “jira.example.com/projects/JRAEXAMPLE,”
\ + “JRAEXAMPLE” is your project key. \ + " + } + /> +
+
+ +
{" "} + +
+ Complete all fields to save the integration +
+
+ +
+ + ); +}; + +export default IntegrationForm; diff --git a/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/_styles.scss b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/_styles.scss new file mode 100644 index 000000000..42cb58b9a --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/_styles.scss @@ -0,0 +1,135 @@ +.integration-form { + a { + color: $core-vibrant-blue; + font-size: $x-small; + font-weight: $bold; + text-decoration: none; + } + + img { + width: 12px; + height: 12px; + margin-left: 6px; + } + + &__new-user-container { + margin-top: $pad-large; + margin-bottom: $pad-large; + } + + &__sso-input { + margin-top: $pad-large; + margin-bottom: $pad-large; + + .fleet-checkbox { + margin-top: 5px; + + &__label { + font-size: $x-small; + font-weight: $bold; + color: $core-fleet-black; + } + } + } + + .sso-disabled { + width: 375px; + } + + .invite-disabled { + width: 100px; + } + + .password-tooltip-text { + width: 310px; + } + + .current-team { + margin-top: 4px; + margin-bottom: 24px; + } + + &__label { + color: $core-fleet-black; + font-size: $x-small; + font-weight: $bold; + margin-bottom: $pad-small; + } + + &__user-permissions-info { + display: flex; + flex-direction: column; + margin-bottom: $pad-xlarge; + + p { + margin: 0 0 $pad-medium 0; + } + } + + &__selected-teams-container { + margin-bottom: $pad-xxlarge; + } + + &__radio-input { + margin-bottom: $pad-medium; + + &.disabled { + .radio__control { + background-color: $ui-fleet-black-25; + } + .radio__label { + color: $ui-fleet-black-50; + font-style: italic; + } + } + } + + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + } + + &__btn { + margin-left: 12px; + } + + &__invite-admin { + margin-bottom: 8px; + } + + &__password { + width: 98%; + float: left; + padding: 0 $pad-medium 0 0; + box-sizing: border-box; + + .input-icon-field { + width: 100%; + } + } + + &__details { + float: right; + width: 2%; + + .icon-tooltip { + margin-top: 12px; + } + + .hint { + color: $core-fleet-black; + + &--brand { + color: $core-vibrant-blue; + } + } + } + + .sublabel { + margin: 0px; + } + + &__tooltip-text { + width: 300px; + } +} diff --git a/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/index.ts b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/index.ts new file mode 100644 index 000000000..c78397843 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/components/IntegrationForm/index.ts @@ -0,0 +1 @@ +export { default } from "./IntegrationForm"; diff --git a/frontend/pages/admin/IntegrationsPage/index.ts b/frontend/pages/admin/IntegrationsPage/index.ts new file mode 100644 index 000000000..952bd2682 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./IntegrationsPage"; diff --git a/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx b/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx index 6f26f64e2..bce452525 100644 --- a/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx +++ b/frontend/pages/admin/SettingsWrapper/SettingsWrapper.tsx @@ -16,6 +16,10 @@ const settingsSubNav: ISettingSubNavItem[] = [ name: "Organization settings", pathname: PATHS.ADMIN_SETTINGS, }, + { + name: "Integrations", + pathname: PATHS.ADMIN_INTEGRATIONS, + }, { name: "Users", pathname: PATHS.ADMIN_USERS, @@ -45,7 +49,7 @@ const SettingsWrapper = ({ }: ISettingsWrapperProp): JSX.Element => { const { isPremiumTier } = useContext(AppContext); - if (isPremiumTier && settingsSubNav.length === 2) { + if (isPremiumTier && settingsSubNav.length === 3) { settingsSubNav.push({ name: "Teams", pathname: PATHS.ADMIN_TEAMS, diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx index c81bcf03e..e04758a5b 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx @@ -452,7 +452,7 @@ const MembersPage = ({ onCreateNewMember={toggleCreateMemberModal} /> ) : null} - {showEditUserModal ? ( + {showEditUserModal && ( - ) : null} - {showCreateUserModal ? ( + )} + {showCreateUserModal && ( - ) : null} - {showRemoveMemberModal && currentTeam ? ( + )} + {showRemoveMemberModal && currentTeam && ( - ) : null} + )} ); }; diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx index f963ad2d5..e73b62cdc 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx @@ -248,28 +248,28 @@ const TeamManagementPage = (): JSX.Element => { disablePagination /> )} - {showCreateTeamModal ? ( + {showCreateTeamModal && ( - ) : null} - {showDeleteTeamModal ? ( + )} + {showDeleteTeamModal && ( - ) : null} - {showEditTeamModal ? ( + )} + {showEditTeamModal && ( - ) : null} + )} ); }; diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss index c3eea90d5..39491bbe9 100644 --- a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss +++ b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss @@ -132,9 +132,4 @@ &__tooltip-text { width: 300px; } - - // so tooltips won't be triggered with whitespace - .form-field__label { - display: inline-block; - } } diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss index 363e1c596..e68c9aa1d 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss @@ -41,11 +41,6 @@ opacity: 75%; } - // so tooltips won't be triggered with whitespace - .form-field__label { - display: inline-block; - } - .form-field__label--error { color: $ui-error; } diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx index 409f9e7b9..082def44a 100644 --- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx +++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx @@ -6,6 +6,7 @@ import { useDebouncedCallback } from "use-debounce"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; import { IConfig } from "interfaces/config"; +import { IJiraIntegration } from "interfaces/integration"; import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore import configAPI from "services/entities/config"; import softwareAPI, { @@ -42,6 +43,15 @@ interface IManageSoftwarePageProps { search: string; }; } + +interface ISoftwareAutomations { + webhook_settings: { + vulnerabilities_webhook: IWebhookSoftwareVulnerabilities; + }; + integrations: { + jira: IJiraIntegration[]; + }; +} interface IHeaderButtonsState extends ITeamsDropdownState { isLoading: boolean; } @@ -66,9 +76,9 @@ const ManageSoftwarePage = ({ const [isSoftwareEnabled, setIsSoftwareEnabled] = useState(); const [ - softwareVulnerabilitiesWebhook, - setSoftwareVulnerabilitiesWebhook, - ] = useState(); + isVulnerabilityAutomationsEnabled, + setIsVulnerabilityAutomationsEnabled, + ] = useState(); const [filterVuln, setFilterVuln] = useState( location?.query?.vulnerable || false ); @@ -91,6 +101,18 @@ const ManageSoftwarePage = ({ const { data: config } = useQuery(["config"], configAPI.loadAll, { onSuccess: (data) => { setIsSoftwareEnabled(data?.host_settings?.enable_software_inventory); + let jiraIntegrationEnabled = false; + if (data.integrations.jira) { + jiraIntegrationEnabled = data?.integrations.jira.some( + (integration: any) => { + return integration.enable_software_vulnerabilities; + } + ); + } + setIsVulnerabilityAutomationsEnabled( + data?.webhook_settings?.vulnerabilities_webhook + .enable_vulnerabilities_webhook || jiraIntegrationEnabled + ); }, }); @@ -168,18 +190,18 @@ const ManageSoftwarePage = ({ const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer; - const { isLoading: isLoadingConfig, refetch: refetchConfig } = useQuery< - IConfig, - Error - >(["config"], () => configAPI.loadAll(), { - enabled: canAddOrRemoveSoftwareWebhook, - onSuccess: (data) => { - setSoftwareVulnerabilitiesWebhook( - data.webhook_settings.vulnerabilities_webhook - ); - setConfig(data); - }, - }); + const { + data: softwareVulnerabilitiesWebhook, + isLoading: isLoadingSoftwareVulnerabilitiesWebhook, + refetch: refetchSoftwareVulnerabilitiesWebhook, + } = useQuery( + ["config"], + () => configAPI.loadAll(), + { + enabled: canAddOrRemoveSoftwareWebhook, + select: (data: IConfig) => data.webhook_settings.vulnerabilities_webhook, + } + ); const onQueryChange = useDebouncedCallback( async ({ @@ -212,19 +234,11 @@ const ManageSoftwarePage = ({ toggleManageAutomationsModal(); }; - const onCreateWebhookSubmit = async ({ - destination_url, - enable_vulnerabilities_webhook, - }: IWebhookSoftwareVulnerabilities) => { + const onCreateWebhookSubmit = async ( + configSoftwareAutomations: ISoftwareAutomations + ) => { try { - const request = configAPI.update({ - webhook_settings: { - vulnerabilities_webhook: { - destination_url, - enable_vulnerabilities_webhook, - }, - }, - }); + const request = configAPI.update(configSoftwareAutomations); await request.then(() => { renderFlash( "success", @@ -238,7 +252,7 @@ const ManageSoftwarePage = ({ ); } finally { toggleManageAutomationsModal(); - refetchConfig(); + refetchSoftwareVulnerabilitiesWebhook(); } }; @@ -250,7 +264,7 @@ const ManageSoftwarePage = ({ state: IHeaderButtonsState ): JSX.Element | null => { if ( - (state.isGlobalAdmin || state.isGlobalMaintainer) && + state.isGlobalAdmin && (!state.isPremiumTier || state.teamId === 0) && !state.isLoading ) { @@ -297,12 +311,12 @@ const ManageSoftwarePage = ({ buttons={(state) => renderHeaderButtons({ ...state, - isLoading: isLoadingConfig, + isLoading: isLoadingSoftwareVulnerabilitiesWebhook, }) } /> ); - }, [router, location, isLoadingConfig]); + }, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]); const renderSoftwareCount = useCallback(() => { const count = softwareCount; @@ -469,6 +483,9 @@ const ManageSoftwarePage = ({ onCreateWebhookSubmit={onCreateWebhookSubmit} togglePreviewPayloadModal={togglePreviewPayloadModal} showPreviewPayloadModal={showPreviewPayloadModal} + softwareVulnerabilityAutomationEnabled={ + isVulnerabilityAutomationsEnabled + } softwareVulnerabilityWebhookEnabled={ softwareVulnerabilitiesWebhook && softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 4955b77d4..e944c5865 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,22 +1,46 @@ import React, { useState } from "react"; +import { useQuery } from "react-query"; +import { Link } from "react-router"; +import PATHS from "router/paths"; + +import { + IJiraIntegration, + IJiraIntegrationIndexed, +} from "interfaces/integration"; +import { IConfig } from "interfaces/config"; +import configAPI from "services/entities/config"; + +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; import Slider from "components/forms/fields/Slider"; +import Radio from "components/forms/fields/Radio"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; import { useDeepEffect } from "utilities/hooks"; -import { size } from "lodash"; +import _, { size } from "lodash"; import PreviewPayloadModal from "../PreviewPayloadModal"; +interface ISoftwareAutomations { + webhook_settings: { + vulnerabilities_webhook: IWebhookSoftwareVulnerabilities; + }; + integrations: { + jira: IJiraIntegration[]; + }; +} + interface IManageAutomationsModalProps { onCancel: () => void; - onCreateWebhookSubmit: (formData: IWebhookSoftwareVulnerabilities) => void; + onCreateWebhookSubmit: (formData: ISoftwareAutomations) => void; togglePreviewPayloadModal: () => void; showPreviewPayloadModal: boolean; + softwareVulnerabilityAutomationEnabled?: boolean; softwareVulnerabilityWebhookEnabled?: boolean; currentDestinationUrl?: string; } @@ -39,6 +63,7 @@ const ManageAutomationsModal = ({ onCreateWebhookSubmit, togglePreviewPayloadModal, showPreviewPayloadModal, + softwareVulnerabilityAutomationEnabled, softwareVulnerabilityWebhookEnabled, currentDestinationUrl, }: IManageAutomationsModalProps): JSX.Element => { @@ -46,11 +71,20 @@ const ManageAutomationsModal = ({ currentDestinationUrl || "" ); const [errors, setErrors] = useState<{ [key: string]: string }>({}); - const [ softwareAutomationsEnabled, setSoftwareAutomationsEnabled, - ] = useState(softwareVulnerabilityWebhookEnabled || false); + ] = useState(softwareVulnerabilityAutomationEnabled || false); + const [jiraEnabled, setJiraEnabled] = useState( + !softwareVulnerabilityWebhookEnabled + ); + const [integrationsIndexed, setIntegrationsIndexed] = useState< + IJiraIntegrationIndexed[] + >(); + const [ + selectedIntegration, + setSelectedIntegration, + ] = useState(); useDeepEffect(() => { if (destination_url) { @@ -58,6 +92,30 @@ const ManageAutomationsModal = ({ } }, [destination_url]); + const { data: integrations } = useQuery( + ["integrations"], + () => configAPI.loadAll(), + { + select: (data: IConfig) => { + return data.integrations.jira; + }, + onSuccess: (data) => { + if (data) { + const addIndex = data.map((integration, index) => { + return { ...integration, index }; + }); + setIntegrationsIndexed(addIndex); + const currentSelectedJiraIntegration = addIndex.find( + (integration) => { + return integration.enable_software_vulnerabilities === true; + } + ); + setSelectedIntegration(currentSelectedJiraIntegration); + } + }, + } + ); + const onURLChange = (value: string) => { setDestinationUrl(value); }; @@ -65,21 +123,175 @@ const ManageAutomationsModal = ({ const handleSaveAutomation = (evt: React.MouseEvent) => { evt.preventDefault(); - const { valid, errors: newErrors } = validateWebhookURL(destination_url); + const { valid: validUrl, errors: newErrors } = validateWebhookURL( + destination_url + ); setErrors({ ...errors, ...newErrors, }); - // URL validation only needed if software automation is checked - if (valid || !softwareAutomationsEnabled) { - onCreateWebhookSubmit({ - destination_url, - enable_vulnerabilities_webhook: softwareAutomationsEnabled, - }); + // Original config keys for software automation (webhook_settings, integrations) + const configSoftwareAutomations: ISoftwareAutomations = { + webhook_settings: { + vulnerabilities_webhook: { + destination_url, + enable_vulnerabilities_webhook: softwareVulnerabilityWebhookEnabled, + }, + }, + integrations: { + jira: integrations || [], + }, + }; - onReturnToApp(); - } + const updateSoftwareAutomation = () => { + if (!softwareAutomationsEnabled) { + // set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false + configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = false; + const disableAllJira = configSoftwareAutomations.integrations.jira.map( + (integration) => { + return { ...integration, enable_software_vulnerabilities: false }; + } + ); + configSoftwareAutomations.integrations.jira = disableAllJira; + return; + } + if (!jiraEnabled) { + if (!validUrl) { + return; + } + // set enable_vulnerabilities_webhook to true and all jira.enable_software_vulnerabilities to false + configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = true; + const disableAllJira = configSoftwareAutomations.integrations.jira.map( + (integration) => { + return { + ...integration, + enable_software_vulnerabilities: false, + }; + } + ); + configSoftwareAutomations.integrations.jira = disableAllJira; + return; + } + // set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false + // except the one jira integration selected + configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = false; + const enableSelectedJiraIntegrationOnly = configSoftwareAutomations.integrations.jira.map( + (integration, index) => { + return { + ...integration, + enable_software_vulnerabilities: + index === selectedIntegration?.index, + }; + } + ); + configSoftwareAutomations.integrations.jira = enableSelectedJiraIntegrationOnly; + }; + + updateSoftwareAutomation(); + onCreateWebhookSubmit(configSoftwareAutomations); + onReturnToApp(); + }; + + const createIntegrationDropdownOptions = () => { + const integrationOptions = integrationsIndexed?.map((i) => { + return { + value: String(i.index), + label: `${i.url} - ${i.project_key}`, + }; + }); + return integrationOptions; + }; + + const onChangeSelectIntegration = (selectIntegrationIndex: string) => { + const integrationWithIndex: + | IJiraIntegrationIndexed + | undefined = integrationsIndexed?.find( + (integ: IJiraIntegrationIndexed) => + integ.index === parseInt(selectIntegrationIndex, 10) + ); + setSelectedIntegration(integrationWithIndex); + }; + + const onRadioChange = (jira: boolean): ((evt: string) => void) => { + return () => { + setJiraEnabled(jira); + }; + }; + + const renderTicket = () => { + return ( +
+
+

+ A ticket will be created in your Integration if a detected + vulnerability (CVE) was published in the last 30 days. +

+
+ {integrationsIndexed && integrationsIndexed.length > 0 ? ( + + ) : ( +
+
+ You have no integrations. +
+
+ + Add integration + +
+
+ )} +
+ ); + }; + + const renderWebhook = () => { + return ( +
+
+

+ A request will be sent to your configured Destination URL if + a detected vulnerability (CVE) was published in the last 30 days. +

+
+ + +
+ ); }; if (showPreviewPayloadModal) { @@ -105,34 +317,28 @@ const ManageAutomationsModal = ({
-
-

- A request will be sent to your configured Destination URL{" "} - if a detected vulnerability (CVE) was published in the last 30 - days. -

+
+ Workflow + +
- - + {jiraEnabled ? renderTicket() : renderWebhook()}
{!softwareAutomationsEnabled && (
@@ -151,6 +357,7 @@ const ManageAutomationsModal = ({ type="submit" variant="brand" onClick={handleSaveAutomation} + disabled={jiraEnabled && !selectedIntegration} > Save diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss index d4ba0a65a..74f6bee05 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss @@ -47,12 +47,32 @@ } } - // so tooltips won't be triggered with whitespace - .form-field__label { - display: inline-block; - } - .form-field__label--error { color: $ui-error; } + + &__workflow { + margin-top: $medium; + } + + &__radio-input { + margin: $small 0; + } + + &__no-integration--cta { + margin-top: $pad-medium; + } + + &__add-integration-link { + font-size: $x-small; + color: $core-vibrant-blue; + font-weight: $bold; + text-decoration: none; + margin-top: $pad-large; + + &:hover, + &:focus-visible { + text-decoration: underline; + } + } } diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index e2fa6ef71..45198f96d 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -13,6 +13,7 @@ import { Provider } from "react-redux"; import { syncHistoryWithStore } from "react-router-redux"; import AdminAppSettingsPage from "pages/admin/AppSettingsPage"; +import AdminIntegrationsPage from "pages/admin/IntegrationsPage"; import AdminUserManagementPage from "pages/admin/UserManagementPage"; import AdminTeamManagementPage from "pages/admin/TeamManagementPage"; import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper"; @@ -104,6 +105,10 @@ const routes = ( + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index e71e11d9f..ac3b8989b 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -8,6 +8,7 @@ export default { HOME: `${URL_PREFIX}/dashboard`, ADMIN_USERS: `${URL_PREFIX}/settings/users`, ADMIN_SETTINGS: `${URL_PREFIX}/settings/organization`, + ADMIN_INTEGRATIONS: `${URL_PREFIX}/settings/integrations`, ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`, ALL_PACKS: `${URL_PREFIX}/packs/all`, EDIT_PACK: (packId: number): string => { diff --git a/frontend/services/entities/config.ts b/frontend/services/entities/config.ts index 59e4e7281..6f37707e7 100644 --- a/frontend/services/entities/config.ts +++ b/frontend/services/entities/config.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest from "services"; +import sendMockRequest from "services/mock_service"; import endpoints from "fleet/endpoints"; import { IConfig } from "interfaces/config"; diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 735c1c657..bb3667a35 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -19,35 +19,15 @@ const WILDCARDS: string[] = [":", "*", "{", "}"]; // REQUEST_RESPONSE_MAPPINGS dictionary maps your static responses to the specified API request path const REQUEST_RESPONSE_MAPPINGS: IResponses = { GET: { - // this is a basic path with no wildcards - "/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc": - RESPONSES.ALL_HOSTS, - // this basic path only matches with '1337' as the value for the team id query param - "/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id=1337": - RESPONSES.HOSTS_TEAM_1337, - // this wildcard path matches with any other value for the team id query param - "/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id={team_id}": - RESPONSES.HOSTS_TEAM_ID, - // this basic path only matches with '1337' as the value for the host id route param - "/hosts/1337": RESPONSES.HOST_1337, - // this wildcard path matches with any other value for the host id route param - "/hosts/*id": RESPONSES.HOST_ID, - // this wildcard path matches with any value for the host id route param - "/hosts/:id/device_mapping": RESPONSES.DEVICE_MAPPING, - // this wildcard path matches with any value for the host id route param - "hosts/{*}/macadmins": RESPONSES.MACADMINS, - // this is a basic path with no wildcards - "hosts/count": { - count: 1, - }, - // this wildcard path matches with any value for the team id route param - "hosts/count?team_id={*}": { - count: 1, - }, + config: RESPONSES.config1, // just first integration -- to throw error, rename config as configz }, // additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.) - POST: { - "/:id/refetch": {}, // this wildcard route returns empty JSON + PATCH: { + config: RESPONSES.configAdd2, // will add second integration to first one + }, + DELETE: { + // will remove second integration + config: RESPONSES.config1, }, } as IResponses; diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index c42fe49d0..e65dcd257 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -4,93 +4,185 @@ * Also please check the README for how to use the mock service :) */ -const HOST_ID = { - host: { - created_at: "2021-03-31T00:00:00Z", - updated_at: "2021-03-31T00:00:00Z", - software: [], - id: 1337, - detail_updated_at: "2021-03-31T00:00:00Z", - label_updated_at: "2021-03-31T00:00:00Z", - policy_updated_at: "2021-03-31T00:00:00Z", - last_enrolled_at: "2021-03-31T00:00:00Z", - seen_time: "2021-03-31T00:00:00ZZ", - refetch_requested: false, - hostname: "myf1337d3v1c3", - uuid: "13371337-0000-0000-1337-133713371337", - platform: "rhel", - osquery_version: "5.1.0", - os_version: "Ubuntu 20.4.0", - build: "", - platform_like: "deb", - code_name: "", - uptime: 13371337133713371337, - memory: 143593800000, - cpu_type: "1337", - cpu_subtype: "1337", - cpu_brand: "Intel(R) Core(TM) i3-37k CPU @ 13.37GHz", - cpu_physical_cores: 8, - cpu_logical_cores: 8, - hardware_vendor: "", - hardware_model: "", - hardware_version: "", - hardware_serial: "", - computer_name: "myf1337d3v1c3", - primary_ip: "133.7.133.7", - primary_mac: "13:37:13:37:13:37", - distributed_interval: 1337, - config_tls_refresh: 1337, - logger_tls_period: 1337, - team_id: null, - pack_stats: [], - team_name: null, - users: [ - { - uid: 1337, - username: "root", - type: "", - groupname: "root", - shell: "/bin/bash", - }, - ], - gigs_disk_space_available: 13.37, - percent_disk_space_available: 13.37, - issues: { - total_issues_count: 1337, - failing_policies_count: 1337, - }, - labels: [], - packs: [], - policies: [], - status: "online", - display_text: "myf1337d3v1c3", - }, +const mockJira0 = { + url: "https://example0.jira.com", + username: "adminUser", + password: "abc123", + project_key: "EXAMPLE", + enable_software_vulnerabilities: false, }; -const HOST_1337 = { - ...HOST_ID, - team_id: 1337, - team_name: "h4x0r", + +const mockJira1 = { + url: "https://example1.jira.com", + username: "adminUser", + password: "abc123", + project_key: "PROJECT", + enable_software_vulnerabilities: false, +}; + +const mockJira2 = { + url: "https://example2.jira.com", + username: "adminUser", + password: "abc123", + project_key: "KEY", + enable_software_vulnerabilities: true, +}; + +const mockIntegration1 = { + jira: [mockJira0, mockJira1], +}; + +const mockIntegration2 = { + jira: [mockJira2], +}; + +const mockIntegrationAdd2 = { + jira: [mockJira0, mockJira1, mockJira2], +}; + +const mockConfig = { + org_info: { + org_name: "s", + org_logo_url: "", + }, + server_settings: { + server_url: "https://localhost:8080", + live_query_disabled: false, + enable_analytics: true, + deferred_save_host: false, + }, + smtp_settings: { + enable_smtp: false, + configured: true, + sender_address: "", + server: "", + port: 0, + authentication_type: "authtype_none", + user_name: "", + password: "", + enable_ssl_tls: true, + authentication_method: "authmethod_plain", + domain: "", + verify_ssl_certs: true, + enable_start_tls: true, + }, + host_expiry_settings: { + host_expiry_enabled: true, + host_expiry_window: 9, + }, + host_settings: { + enable_host_users: true, + enable_software_inventory: true, + }, + agent_options: { + config: { + options: { + logger_plugin: "tls", + pack_delimiter: "/", + logger_tls_period: 100, + distributed_plugin: "tls", + disable_distributed: false, + logger_tls_endpoint: "/api/v1/osquery/log", + distributed_interval: 10, + distributed_tls_max_attempts: 3, + }, + decorators: { + load: [ + "SELECT uuid AS host_uuid FROM system_info;", + "SELECT hostname AS hostname FROM system_info;", + ], + }, + }, + overrides: {}, + }, + sso_settings: { + entity_id: "", + issuer_uri: "", + idp_image_url: "", + metadata: "", + metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php", + idp_name: "", + enable_sso: false, + enable_sso_idp_login: false, + }, + vulnerability_settings: { + databases_path: "", + }, + webhook_settings: { + host_status_webhook: { + enable_host_status_webhook: false, + destination_url: "", + host_percentage: 0, + days_count: 0, + }, + failing_policies_webhook: { + enable_failing_policies_webhook: false, + destination_url: "", + policy_ids: [], + host_batch_size: 0, + }, + vulnerabilities_webhook: { + enable_vulnerabilities_webhook: true, + destination_url: "www.example.com/", + host_batch_size: 0, + }, + interval: "24h0m0s", + }, + update_interval: { + osquery_detail: 10000000000, + osquery_policy: 3600000000000, + }, + vulnerabilities: { + databases_path: "/tmp/vulndbs", + periodicity: 3600000000000, + cpe_database_url: "", + cve_feed_prefix_url: "", + current_instance_checks: "auto", + disable_data_sync: false, + }, + license: { + tier: "premium", + organization: "development-only", + device_count: 100, + expiration: "2022-06-30T20:00:00-04:00", + note: "for development only", + }, + logging: { + debug: false, + json: false, + result: { + plugin: "filesystem", + config: { + status_log_file: + "/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_status", + result_log_file: + "/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_result", + enable_log_rotation: false, + enable_log_compression: false, + }, + }, + status: { + plugin: "filesystem", + config: { + status_log_file: + "/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_status", + result_log_file: + "/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_result", + enable_log_rotation: false, + enable_log_compression: false, + }, + }, + }, }; export default { - ALL_HOSTS: { - hosts: [HOST_ID.host], + config1: { ...mockConfig, integrations: mockIntegration1 }, + config2: { + ...mockConfig, + integrations: mockIntegration2, }, - HOSTS_TEAM_ID: { - hosts: [{ ...HOST_ID.host, team_id: 2, team_name: "n00bz" }], - }, - HOSTS_TEAM_1337: { - hosts: [HOST_1337.host], - }, - HOST_ID, - HOST_1337, - DEVICE_MAPPING: { - host_id: 1337, - device_mapping: null, - foo: "bar", - }, - MACADMINS: { - macadmins: null, - foo: "bar", + configAdd2: { + ...mockConfig, + integrations: mockIntegrationAdd2, }, }; diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts index 2e8c180dc..0f981f040 100644 --- a/frontend/utilities/constants.ts +++ b/frontend/utilities/constants.ts @@ -340,6 +340,13 @@ export const DEFAULT_CREATE_USER_ERRORS = { sso_enabled: null, }; +export const DEFAULT_CREATE_INTEGRATION_ERRORS = { + url: "", + username: "", + password: "", + projectKey: "", +}; + export const DEFAULT_CREATE_LABEL_ERRORS = { name: "", };