mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
UI Zendesk integrations (#5356)
This commit is contained in:
parent
f893a9f8a9
commit
34a2d3e483
14 changed files with 626 additions and 270 deletions
BIN
assets/images/icon-zendesk-27x20@2x.png
Normal file
BIN
assets/images/icon-zendesk-27x20@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/images/icon-zendesk-32x24@2x.png
Normal file
BIN
assets/images/icon-zendesk-32x24@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 596 B |
1
changes/issue-5188-zendesk-ui-added
Normal file
1
changes/issue-5188-zendesk-ui-added
Normal file
|
|
@ -0,0 +1 @@
|
|||
* UI allows for integration with Zendesk as well as Jira to manage software vulnerabilities
|
||||
|
|
@ -4,30 +4,59 @@ export interface IJiraIntegration {
|
|||
api_token: string;
|
||||
project_key: string;
|
||||
enable_software_vulnerabilities?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface IJiraIntegrationIndexed extends IJiraIntegration {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface IJiraIntegrationFormData {
|
||||
export interface IZendeskIntegration {
|
||||
url: string;
|
||||
username: string;
|
||||
email: string;
|
||||
api_token: string;
|
||||
group_id: number;
|
||||
enable_software_vulnerabilities?: boolean;
|
||||
}
|
||||
|
||||
export interface IIntegration {
|
||||
url: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
api_token: string;
|
||||
project_key?: string;
|
||||
group_id?: number;
|
||||
enable_software_vulnerabilities?: boolean;
|
||||
originalIndex?: number;
|
||||
type?: string;
|
||||
tableIndex?: number;
|
||||
dropdownIndex?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IIntegrationFormData {
|
||||
url: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
apiToken: string;
|
||||
projectKey: string;
|
||||
projectKey?: string;
|
||||
groupId?: number;
|
||||
enableSoftwareVulnerabilities?: boolean;
|
||||
}
|
||||
|
||||
export interface IJiraIntegrationFormErrors {
|
||||
export interface IIntegrationTableData extends IIntegrationFormData {
|
||||
originalIndex: number;
|
||||
type: string;
|
||||
tableIndex?: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IIntegrationFormErrors {
|
||||
url?: string | null;
|
||||
email?: string | null;
|
||||
username?: string | null;
|
||||
apiToken?: string | null;
|
||||
groupId?: number | null;
|
||||
projectKey?: string | null;
|
||||
enableSoftwareVulnerabilities?: boolean;
|
||||
}
|
||||
|
||||
export interface IIntegrations {
|
||||
zendesk: IZendeskIntegration[];
|
||||
jira: IJiraIntegration[];
|
||||
}
|
||||
|
||||
export type IIntegration = IJiraIntegration;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import React, { useState, useContext, useCallback } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IJiraIntegrationIndexed,
|
||||
IJiraIntegrationFormErrors,
|
||||
IZendeskIntegration,
|
||||
IIntegration,
|
||||
IIntegrationTableData,
|
||||
IIntegrations,
|
||||
} 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";
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ import EditIntegrationModal from "./components/EditIntegrationModal";
|
|||
|
||||
import {
|
||||
generateTableHeaders,
|
||||
generateDataSet,
|
||||
combineDataSets,
|
||||
} from "./IntegrationsTableConfig";
|
||||
|
||||
const baseClass = "integrations-management";
|
||||
|
|
@ -34,9 +36,9 @@ 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.";
|
||||
"Invalid login credentials or URL. Please correct and try again.";
|
||||
const UNKNOWN_ERROR =
|
||||
"We experienced an error when attempting to connect to Jira. Please try again later.";
|
||||
"We experienced an error when attempting to connect. Please try again later.";
|
||||
|
||||
const IntegrationsPage = (): JSX.Element => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
|
@ -51,17 +53,16 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
const [
|
||||
integrationEditing,
|
||||
setIntegrationEditing,
|
||||
] = useState<IJiraIntegrationIndexed>();
|
||||
const [integrationsIndexed, setIntegrationsIndexed] = useState<
|
||||
IJiraIntegrationIndexed[]
|
||||
] = useState<IIntegrationTableData>();
|
||||
const [jiraIntegrations, setJiraIntegrations] = useState<
|
||||
IJiraIntegration[]
|
||||
>();
|
||||
const [zendeskIntegrations, setZendeskIntegrations] = useState<
|
||||
IZendeskIntegration[]
|
||||
>();
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const [
|
||||
createIntegrationError,
|
||||
setCreateIntegrationError,
|
||||
] = useState<IJiraIntegrationFormErrors>(DEFAULT_CREATE_INTEGRATION_ERRORS);
|
||||
const [testingConnection, setTestingConnection] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
|
|
@ -69,26 +70,26 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
isLoading: isLoadingIntegrations,
|
||||
error: loadingIntegrationsError,
|
||||
refetch: refetchIntegrations,
|
||||
} = useQuery<IConfig, Error, IJiraIntegration[]>(
|
||||
} = useQuery<IConfig, Error, IIntegrations>(
|
||||
["integrations"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
select: (data: IConfig) => {
|
||||
return data.integrations.jira;
|
||||
return data.integrations;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
const addIndex = data.map((integration, index) => {
|
||||
return { ...integration, index };
|
||||
});
|
||||
setIntegrationsIndexed(addIndex);
|
||||
} else {
|
||||
setIntegrationsIndexed([]);
|
||||
setJiraIntegrations(data.jira);
|
||||
setZendeskIntegrations(data.zendesk);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const combineJiraAndZendesk = memoize(() => {
|
||||
return combineDataSets(jiraIntegrations || [], zendeskIntegrations || []);
|
||||
});
|
||||
|
||||
const toggleAddIntegrationModal = useCallback(() => {
|
||||
setShowAddIntegrationModal(!showAddIntegrationModal);
|
||||
setBackendValidators({});
|
||||
|
|
@ -99,7 +100,7 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
]);
|
||||
|
||||
const toggleDeleteIntegrationModal = useCallback(
|
||||
(integration?: IJiraIntegrationIndexed) => {
|
||||
(integration?: IIntegrationTableData) => {
|
||||
setShowDeleteIntegrationModal(!showDeleteIntegrationModal);
|
||||
integration
|
||||
? setIntegrationEditing(integration)
|
||||
|
|
@ -113,7 +114,7 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
);
|
||||
|
||||
const toggleEditIntegrationModal = useCallback(
|
||||
(integration?: IJiraIntegrationIndexed) => {
|
||||
(integration?: IIntegrationTableData) => {
|
||||
setShowEditIntegrationModal(!showEditIntegrationModal);
|
||||
setBackendValidators({});
|
||||
integration
|
||||
|
|
@ -129,27 +130,29 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
);
|
||||
|
||||
const onCreateSubmit = useCallback(
|
||||
(jiraIntegrationSubmitData: IJiraIntegration[]) => {
|
||||
(integrationSubmitData: IIntegration[], integrationDestination: string) => {
|
||||
// Updates either integrations.jira or integrations.zendesk
|
||||
const destination = () => {
|
||||
if (integrationDestination === "jira") {
|
||||
return { jira: integrationSubmitData, zendesk: zendeskIntegrations };
|
||||
}
|
||||
return { zendesk: integrationSubmitData, jira: jiraIntegrations };
|
||||
};
|
||||
|
||||
setTestingConnection(true);
|
||||
configAPI
|
||||
.update({ integrations: { jira: jiraIntegrationSubmitData } })
|
||||
.update({ integrations: destination() })
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully added{" "}
|
||||
<b>
|
||||
{
|
||||
jiraIntegrationSubmitData[
|
||||
jiraIntegrationSubmitData.length - 1
|
||||
].url
|
||||
}{" "}
|
||||
-{" "}
|
||||
{
|
||||
jiraIntegrationSubmitData[
|
||||
jiraIntegrationSubmitData.length - 1
|
||||
].project_key
|
||||
}
|
||||
{integrationSubmitData[integrationSubmitData.length - 1].url} -{" "}
|
||||
{integrationSubmitData[integrationSubmitData.length - 1]
|
||||
.project_key ||
|
||||
integrationSubmitData[integrationSubmitData.length - 1]
|
||||
.group_id}
|
||||
</b>
|
||||
</>
|
||||
);
|
||||
|
|
@ -172,16 +175,14 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
Could not add add{" "}
|
||||
<b>
|
||||
{
|
||||
jiraIntegrationSubmitData[
|
||||
jiraIntegrationSubmitData.length - 1
|
||||
].url
|
||||
integrationSubmitData[integrationSubmitData.length - 1]
|
||||
.url
|
||||
}{" "}
|
||||
-{" "}
|
||||
{
|
||||
jiraIntegrationSubmitData[
|
||||
jiraIntegrationSubmitData.length - 1
|
||||
].project_key
|
||||
}
|
||||
{integrationSubmitData[integrationSubmitData.length - 1]
|
||||
.project_key ||
|
||||
integrationSubmitData[integrationSubmitData.length - 1]
|
||||
.group_id}
|
||||
</b>
|
||||
. This integration already exists
|
||||
</>
|
||||
|
|
@ -197,11 +198,7 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
<>
|
||||
Could not add{" "}
|
||||
<b>
|
||||
{
|
||||
jiraIntegrationSubmitData[
|
||||
jiraIntegrationSubmitData.length - 1
|
||||
].url
|
||||
}
|
||||
{integrationSubmitData[integrationSubmitData.length - 1].url}
|
||||
</b>
|
||||
. Please try again.
|
||||
</>
|
||||
|
|
@ -218,16 +215,35 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
|
||||
const onDeleteSubmit = useCallback(() => {
|
||||
if (integrationEditing) {
|
||||
integrations?.splice(integrationEditing.index, 1);
|
||||
configAPI
|
||||
.update({ integrations: { jira: integrations } })
|
||||
const deleteIntegrationDestination = () => {
|
||||
if (integrationEditing.type === "jira") {
|
||||
integrations?.jira.splice(integrationEditing.originalIndex, 1);
|
||||
return configAPI.update({
|
||||
integrations: {
|
||||
jira: integrations?.jira,
|
||||
zendesk: zendeskIntegrations,
|
||||
},
|
||||
});
|
||||
}
|
||||
integrations?.zendesk.splice(integrationEditing.originalIndex, 1);
|
||||
return configAPI.update({
|
||||
integrations: {
|
||||
zendesk: integrations?.zendesk,
|
||||
jira: jiraIntegrations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
deleteIntegrationDestination()
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully deleted{" "}
|
||||
<b>
|
||||
{integrationEditing.url} - {integrationEditing.project_key}
|
||||
{integrationEditing.url} -{" "}
|
||||
{integrationEditing.projectKey ||
|
||||
integrationEditing.groupId?.toString()}
|
||||
</b>
|
||||
</>
|
||||
);
|
||||
|
|
@ -239,7 +255,9 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
<>
|
||||
Could not delete{" "}
|
||||
<b>
|
||||
{integrationEditing.url} - {integrationEditing.project_key}
|
||||
{integrationEditing.url} -{" "}
|
||||
{integrationEditing.projectKey ||
|
||||
integrationEditing.groupId?.toString()}
|
||||
</b>
|
||||
. Please try again.
|
||||
</>
|
||||
|
|
@ -252,22 +270,40 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
}, [integrationEditing, toggleDeleteIntegrationModal]);
|
||||
|
||||
const onEditSubmit = useCallback(
|
||||
(jiraIntegrationSubmitData: IJiraIntegration[]) => {
|
||||
(integrationSubmitData: IIntegration[]) => {
|
||||
if (integrationEditing) {
|
||||
setTestingConnection(true);
|
||||
configAPI
|
||||
.update({ integrations: { jira: jiraIntegrationSubmitData } })
|
||||
|
||||
const editIntegrationDestination = () => {
|
||||
if (integrationEditing.type === "jira") {
|
||||
return configAPI.update({
|
||||
integrations: {
|
||||
jira: integrationSubmitData,
|
||||
zendesk: zendeskIntegrations,
|
||||
},
|
||||
});
|
||||
}
|
||||
return configAPI.update({
|
||||
integrations: {
|
||||
zendesk: integrationSubmitData,
|
||||
jira: jiraIntegrations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
editIntegrationDestination()
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully edited{" "}
|
||||
<b>
|
||||
{jiraIntegrationSubmitData[integrationEditing?.index].url} -{" "}
|
||||
{
|
||||
jiraIntegrationSubmitData[integrationEditing?.index]
|
||||
.project_key
|
||||
}
|
||||
{integrationSubmitData[integrationEditing?.originalIndex].url}{" "}
|
||||
-{" "}
|
||||
{integrationSubmitData[integrationEditing?.originalIndex]
|
||||
.project_key ||
|
||||
integrationSubmitData[integrationEditing?.originalIndex]
|
||||
.group_id}
|
||||
</b>
|
||||
</>
|
||||
);
|
||||
|
|
@ -292,7 +328,8 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
Could not edit{" "}
|
||||
<b>
|
||||
{integrationEditing?.url} -{" "}
|
||||
{integrationEditing?.project_key}
|
||||
{integrationEditing?.projectKey ||
|
||||
integrationEditing?.groupId?.toString()}
|
||||
</b>
|
||||
. Please try again.
|
||||
</>
|
||||
|
|
@ -309,7 +346,7 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
|
||||
const onActionSelection = (
|
||||
action: string,
|
||||
integration: IJiraIntegrationIndexed
|
||||
integration: IIntegrationTableData
|
||||
): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
|
|
@ -357,9 +394,8 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
};
|
||||
|
||||
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||
const tableData = integrationsIndexed
|
||||
? generateDataSet(integrationsIndexed)
|
||||
: [];
|
||||
|
||||
const tableData = combineJiraAndZendesk();
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
|
|
@ -377,8 +413,8 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
actionButtonText={"Add integration"}
|
||||
hideActionButton={!tableData?.length}
|
||||
actionButtonVariant={"brand"}
|
||||
hideActionButton={!integrations || integrations.length === 0}
|
||||
onActionButtonClick={toggleAddIntegrationModal}
|
||||
resultsTitle={"integrations"}
|
||||
emptyComponent={NoIntegrationsComponent}
|
||||
|
|
@ -392,7 +428,7 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
onCancel={toggleAddIntegrationModal}
|
||||
onSubmit={onCreateSubmit}
|
||||
backendValidators={backendValidators}
|
||||
integrations={integrations || []}
|
||||
integrations={integrations || { jira: [], zendesk: [] }}
|
||||
testingConnection={testingConnection}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -401,15 +437,19 @@ const IntegrationsPage = (): JSX.Element => {
|
|||
onCancel={toggleDeleteIntegrationModal}
|
||||
onSubmit={onDeleteSubmit}
|
||||
url={integrationEditing?.url || ""}
|
||||
projectKey={integrationEditing?.project_key || ""}
|
||||
projectKey={
|
||||
integrationEditing?.projectKey ||
|
||||
integrationEditing?.groupId?.toString() ||
|
||||
""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showEditIntegrationModal && (
|
||||
{showEditIntegrationModal && integrations && (
|
||||
<EditIntegrationModal
|
||||
onCancel={toggleEditIntegrationModal}
|
||||
onSubmit={onEditSubmit}
|
||||
backendValidators={backendValidators}
|
||||
integrations={integrations || []}
|
||||
integrations={integrations}
|
||||
integrationEditing={integrationEditing}
|
||||
testingConnection={testingConnection}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
|
|||
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IJiraIntegrationIndexed,
|
||||
IZendeskIntegration,
|
||||
IIntegrationTableData as IIntegrationCompleteData,
|
||||
} from "interfaces/integration";
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
|
||||
import JiraIcon from "../../../../assets/images/icon-jira-24x24@2x.png";
|
||||
import ZendeskIcon from "../../../../assets/images/icon-zendesk-32x24@2x.png";
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
|
|
@ -20,7 +22,7 @@ interface IHeaderProps {
|
|||
|
||||
interface IRowProps {
|
||||
row: {
|
||||
original: IJiraIntegrationIndexed;
|
||||
original: IIntegrationTableData;
|
||||
};
|
||||
}
|
||||
interface ICellProps extends IRowProps {
|
||||
|
|
@ -47,7 +49,7 @@ interface IDataColumn {
|
|||
sortType?: string;
|
||||
}
|
||||
|
||||
export interface IIntegrationTableData extends IJiraIntegration {
|
||||
export interface IIntegrationTableData extends IIntegrationCompleteData {
|
||||
actions: IDropdownOption[];
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -57,7 +59,7 @@ export interface IIntegrationTableData extends IJiraIntegration {
|
|||
const generateTableHeaders = (
|
||||
actionSelectHandler: (
|
||||
value: string,
|
||||
integration: IJiraIntegrationIndexed
|
||||
integration: IIntegrationTableData
|
||||
) => void
|
||||
): IDataColumn[] => {
|
||||
return [
|
||||
|
|
@ -66,8 +68,20 @@ const generateTableHeaders = (
|
|||
Header: "",
|
||||
disableSortBy: true,
|
||||
sortType: "caseInsensitive",
|
||||
accessor: "logo",
|
||||
Cell: () => <img src={JiraIcon} alt="jira-icon" />,
|
||||
accessor: "type",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
return (
|
||||
<div className={"logo-cell"}>
|
||||
<img
|
||||
src={cellProps.cell.value === "jira" ? JiraIcon : ZendeskIcon}
|
||||
alt="integration-icon"
|
||||
className={
|
||||
cellProps.cell.value === "jira" ? "jira-icon" : "zendesk-icon"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
|
|
@ -108,28 +122,55 @@ const generateActionDropdownOptions = (): IDropdownOption[] => {
|
|||
];
|
||||
};
|
||||
|
||||
const enhanceIntegrationData = (
|
||||
integrations: IJiraIntegrationIndexed[]
|
||||
const enhanceJiraData = (
|
||||
jiraIntegrations: IJiraIntegration[]
|
||||
): IIntegrationTableData[] => {
|
||||
return Object.values(integrations).map((integration) => {
|
||||
return jiraIntegrations.map((integration, index) => {
|
||||
return {
|
||||
url: integration.url,
|
||||
username: integration.username,
|
||||
api_token: integration.api_token,
|
||||
project_key: integration.project_key,
|
||||
actions: generateActionDropdownOptions(),
|
||||
enable_software_vulnerabilities:
|
||||
apiToken: integration.api_token,
|
||||
projectKey: integration.project_key,
|
||||
enableSoftwareVulnerabilities:
|
||||
integration.enable_software_vulnerabilities,
|
||||
name: `${integration.url} - ${integration.project_key}`,
|
||||
index: integration.index,
|
||||
actions: generateActionDropdownOptions(),
|
||||
originalIndex: index,
|
||||
type: "jira",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const generateDataSet = (
|
||||
integrations: IJiraIntegrationIndexed[]
|
||||
const enhanceZendeskData = (
|
||||
zendeskIntegrations: IZendeskIntegration[]
|
||||
): IIntegrationTableData[] => {
|
||||
return [...enhanceIntegrationData(integrations)];
|
||||
return zendeskIntegrations.map((integration, index) => {
|
||||
return {
|
||||
url: integration.url,
|
||||
email: integration.email,
|
||||
apiToken: integration.api_token,
|
||||
groupId: integration.group_id,
|
||||
enableSoftwareVulnerabilities:
|
||||
integration.enable_software_vulnerabilities,
|
||||
name: `${integration.url} - ${integration.group_id}`,
|
||||
actions: generateActionDropdownOptions(),
|
||||
originalIndex: index,
|
||||
type: "zendesk",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export { generateTableHeaders, generateDataSet };
|
||||
const combineDataSets = (
|
||||
jiraIntegrations: IJiraIntegration[],
|
||||
zendeskIntegrations: IZendeskIntegration[]
|
||||
): IIntegrationTableData[] => {
|
||||
const combine = [
|
||||
...enhanceJiraData(jiraIntegrations),
|
||||
...enhanceZendeskData(zendeskIntegrations),
|
||||
];
|
||||
return combine.map((integration, index) => {
|
||||
return { ...integration, tableIndex: index };
|
||||
});
|
||||
};
|
||||
|
||||
export { generateTableHeaders, combineDataSets };
|
||||
|
|
|
|||
|
|
@ -93,12 +93,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.logo__header {
|
||||
width: 24px;
|
||||
.type__header {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.logo__cell {
|
||||
img {
|
||||
.type__cell {
|
||||
.logo-cell {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.zendesk-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.jira-icon {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,33 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import FleetIcon from "components/icons/FleetIcon";
|
||||
import Spinner from "components/Spinner";
|
||||
import { IJiraIntegration } from "interfaces/integration";
|
||||
import { IIntegration, IIntegrations } from "interfaces/integration";
|
||||
import IntegrationForm from "../IntegrationForm";
|
||||
|
||||
const baseClass = "create-integration-modal";
|
||||
|
||||
interface ICreateIntegrationModalProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||
onSubmit: (
|
||||
integrationSubmitData: IIntegration[],
|
||||
integrationDestination: string
|
||||
) => void;
|
||||
serverErrors?: { base: string; email: string };
|
||||
backendValidators: { [key: string]: string };
|
||||
integrations: IJiraIntegration[];
|
||||
integrations: IIntegrations;
|
||||
testingConnection: boolean;
|
||||
}
|
||||
|
||||
const destinationOptions = [
|
||||
{ label: "Jira", value: "jira" },
|
||||
{ label: "Zendesk", value: "zendesk" },
|
||||
];
|
||||
|
||||
const CreateIntegrationModal = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
|
|
@ -29,6 +38,11 @@ const CreateIntegrationModal = ({
|
|||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
const [destination, setDestination] = useState<string>("jira");
|
||||
|
||||
const onDestinationChange = (value: string) => {
|
||||
setDestination(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
|
|
@ -38,28 +52,35 @@ const CreateIntegrationModal = ({
|
|||
<Modal title={"Add integration"} onExit={onCancel} className={baseClass}>
|
||||
{testingConnection ? (
|
||||
<div className={`${baseClass}__testing-connection`}>
|
||||
<b>Testing connection to Jira</b>
|
||||
<b>Testing connection</b>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p className={`${baseClass}__info-header`}>
|
||||
Fleet supports Jira as a ticket destination.
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/issues/new?assignees=&labels=idea&template=feature-request.md&title="
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Suggest a new destination
|
||||
<FleetIcon name="external-link" />
|
||||
</a>
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div className={`${baseClass}__info-header`}>
|
||||
<Dropdown
|
||||
label="Ticket destination"
|
||||
name="destination"
|
||||
onChange={onDestinationChange}
|
||||
value={destination}
|
||||
options={destinationOptions}
|
||||
classname={`${baseClass}__destination-dropdown`}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/issues/new?assignees=&labels=idea&template=feature-request.md&title="
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Suggest a new destination
|
||||
<FleetIcon name="external-link" />
|
||||
</a>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
integrations={integrations}
|
||||
destination={destination}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
margin-top: $pad-small;
|
||||
|
||||
img {
|
||||
width: 12px;
|
||||
|
|
@ -12,12 +13,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__sandbox-info {
|
||||
margin: $pad-medium 0;
|
||||
}
|
||||
|
||||
&__info-header {
|
||||
margin: 0;
|
||||
margin-bottom: $pad-xlarge;
|
||||
}
|
||||
|
||||
&__btn-wrap {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import React, { useState, useEffect } from "react";
|
|||
import Modal from "components/Modal";
|
||||
import Spinner from "components/Spinner";
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IJiraIntegrationIndexed,
|
||||
IIntegration,
|
||||
IIntegrations,
|
||||
IIntegrationTableData,
|
||||
} from "interfaces/integration";
|
||||
import IntegrationForm from "../IntegrationForm";
|
||||
|
||||
|
|
@ -12,10 +13,10 @@ const baseClass = "edit-team-modal";
|
|||
|
||||
interface IEditIntegrationModalProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||
onSubmit: (jiraIntegrationSubmitData: IIntegration[]) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
integrations: IJiraIntegration[];
|
||||
integrationEditing?: IJiraIntegrationIndexed;
|
||||
integrations: IIntegrations;
|
||||
integrationEditing?: IIntegrationTableData;
|
||||
testingConnection: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -39,16 +40,33 @@ const EditIntegrationModal = ({
|
|||
<Modal title={"Edit integration"} onExit={onCancel} className={baseClass}>
|
||||
{testingConnection ? (
|
||||
<div className={`${baseClass}__testing-connection`}>
|
||||
<b>Testing connection to Jira</b>
|
||||
<b>Testing connection</b>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<IntegrationForm
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
integrations={integrations}
|
||||
integrationEditing={integrationEditing}
|
||||
/>
|
||||
<>
|
||||
<p>
|
||||
<b>Ticket destination:</b>
|
||||
<br />
|
||||
{integrationEditing?.type === "jira" ? "Jira" : "Zendesk"}
|
||||
</p>
|
||||
<IntegrationForm
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
integrations={integrations}
|
||||
integrationEditing={integrationEditing}
|
||||
integrationEditingUrl={integrationEditing?.url || ""}
|
||||
integrationEditingUsername={integrationEditing?.username || ""}
|
||||
integrationEditingEmail={integrationEditing?.email || ""}
|
||||
integrationEditingApiToken={integrationEditing?.apiToken || ""}
|
||||
integrationEditingProjectKey={integrationEditing?.projectKey || ""}
|
||||
integrationEditingGroupId={integrationEditing?.groupId || 0}
|
||||
integrationEnableSoftwareVulnerabilities={
|
||||
integrationEditing?.enableSoftwareVulnerabilities || false
|
||||
}
|
||||
integrationEditingType={integrationEditing?.type}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FormEvent, useState } from "react";
|
||||
import React, { FormEvent, useState, useEffect } from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IJiraIntegrationFormData,
|
||||
IJiraIntegrationIndexed,
|
||||
IIntegrationFormData,
|
||||
IIntegrationTableData,
|
||||
IIntegration,
|
||||
IIntegrations,
|
||||
} from "interfaces/integration";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
@ -15,9 +16,21 @@ const baseClass = "integration-form";
|
|||
|
||||
interface IIntegrationFormProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||
integrationEditing?: IJiraIntegrationIndexed;
|
||||
integrations: IJiraIntegration[];
|
||||
onSubmit: (
|
||||
untegrationSubmitData: IIntegration[],
|
||||
integrationDestination: string
|
||||
) => void;
|
||||
integrationEditing?: IIntegrationTableData;
|
||||
integrations: IIntegrations;
|
||||
integrationEditingUrl?: string;
|
||||
integrationEditingUsername?: string;
|
||||
integrationEditingEmail?: string;
|
||||
integrationEditingApiToken?: string;
|
||||
integrationEditingProjectKey?: string;
|
||||
integrationEditingGroupId?: number;
|
||||
integrationEnableSoftwareVulnerabilities?: boolean;
|
||||
integrationEditingType?: string;
|
||||
destination?: string;
|
||||
}
|
||||
|
||||
interface IFormField {
|
||||
|
|
@ -30,18 +43,37 @@ const IntegrationForm = ({
|
|||
onSubmit,
|
||||
integrationEditing,
|
||||
integrations,
|
||||
integrationEditingUrl,
|
||||
integrationEditingUsername,
|
||||
integrationEditingEmail,
|
||||
integrationEditingApiToken,
|
||||
integrationEditingProjectKey,
|
||||
integrationEditingGroupId,
|
||||
integrationEnableSoftwareVulnerabilities,
|
||||
integrationEditingType,
|
||||
destination,
|
||||
}: IIntegrationFormProps): JSX.Element => {
|
||||
const [formData, setFormData] = useState<IJiraIntegrationFormData>({
|
||||
url: integrationEditing?.url || "",
|
||||
username: integrationEditing?.username || "",
|
||||
apiToken: integrationEditing?.api_token || "",
|
||||
projectKey: integrationEditing?.project_key || "",
|
||||
const { jira: jiraIntegrations, zendesk: zendeskIntegrations } = integrations;
|
||||
const [formData, setFormData] = useState<IIntegrationFormData>({
|
||||
url: integrationEditingUrl || "",
|
||||
username: integrationEditingUsername || "",
|
||||
email: integrationEditingEmail || "",
|
||||
apiToken: integrationEditingApiToken || "",
|
||||
projectKey: integrationEditingProjectKey || "",
|
||||
groupId: integrationEditingGroupId || 0,
|
||||
enableSoftwareVulnerabilities:
|
||||
integrationEditing?.enable_software_vulnerabilities || false,
|
||||
integrationEnableSoftwareVulnerabilities || false,
|
||||
});
|
||||
const [integrationDestination, setIntegrationDestination] = useState<string>(
|
||||
integrationEditingType || destination || "jira"
|
||||
);
|
||||
const [urlError, setUrlError] = useState<string | null>(null);
|
||||
|
||||
const { url, username, apiToken, projectKey } = formData;
|
||||
useEffect(() => {
|
||||
setIntegrationDestination(destination || integrationEditingType || "jira");
|
||||
}, [destination, integrationEditingType]);
|
||||
|
||||
const { url, username, email, apiToken, projectKey, groupId } = formData;
|
||||
|
||||
const onInputChange = ({ name, value }: IFormField) => {
|
||||
setFormData({ ...formData, [name]: value });
|
||||
|
|
@ -57,38 +89,72 @@ const IntegrationForm = ({
|
|||
setUrlError(error);
|
||||
};
|
||||
|
||||
// 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;
|
||||
// IntegrationForm component can be used to create a new integration or edit an existing integration so submitData will be assembled accordingly
|
||||
const createSubmitData = (): IIntegration[] => {
|
||||
let jiraIntegrationSubmitData = jiraIntegrations || [];
|
||||
let zendeskIntegrationSubmitData = zendeskIntegrations || [];
|
||||
|
||||
if (integrationEditing) {
|
||||
// Edit existing integration using array replacement
|
||||
jiraIntegrationSubmitData.splice(integrationEditing.index, 1, {
|
||||
// Editing through UI is temporarily deprecated in 4.14
|
||||
if (integrationDestination === "jira") {
|
||||
if (
|
||||
integrationEditing &&
|
||||
(integrationEditing.originalIndex ||
|
||||
integrationEditing.originalIndex === 0) &&
|
||||
integrationEditing.username
|
||||
) {
|
||||
// Edit existing jira integration using array replacement
|
||||
jiraIntegrationSubmitData.splice(integrationEditing.originalIndex, 1, {
|
||||
url,
|
||||
username: username || "",
|
||||
api_token: apiToken,
|
||||
project_key: projectKey || "",
|
||||
});
|
||||
} else {
|
||||
// Create new jira integration at end of array
|
||||
jiraIntegrationSubmitData = [
|
||||
...jiraIntegrationSubmitData,
|
||||
{
|
||||
url,
|
||||
username: username || "",
|
||||
api_token: apiToken,
|
||||
project_key: projectKey || "",
|
||||
},
|
||||
];
|
||||
}
|
||||
return jiraIntegrationSubmitData;
|
||||
}
|
||||
if (
|
||||
integrationEditing &&
|
||||
(integrationEditing.originalIndex ||
|
||||
integrationEditing.originalIndex === 0) &&
|
||||
integrationEditing.email
|
||||
) {
|
||||
// Edit existing zendesk integration using array replacement
|
||||
zendeskIntegrationSubmitData.splice(integrationEditing.originalIndex, 1, {
|
||||
url,
|
||||
username,
|
||||
email: email || "",
|
||||
api_token: apiToken,
|
||||
project_key: projectKey,
|
||||
group_id: groupId || 0,
|
||||
});
|
||||
} else {
|
||||
// Create new integration at end of array
|
||||
jiraIntegrationSubmitData = [
|
||||
...jiraIntegrationSubmitData,
|
||||
// Create new zendesk integration at end of array
|
||||
zendeskIntegrationSubmitData = [
|
||||
...zendeskIntegrationSubmitData,
|
||||
{
|
||||
url,
|
||||
username,
|
||||
email: email || "",
|
||||
api_token: apiToken,
|
||||
project_key: projectKey,
|
||||
group_id: parseInt(groupId as any, 10) || 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return jiraIntegrationSubmitData;
|
||||
return zendeskIntegrationSubmitData;
|
||||
};
|
||||
|
||||
const onFormSubmit = (evt: FormEvent): void => {
|
||||
evt.preventDefault();
|
||||
|
||||
return onSubmit(createSubmitData());
|
||||
return onSubmit(createSubmitData(), integrationDestination);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -101,62 +167,94 @@ const IntegrationForm = ({
|
|||
autofocus
|
||||
name="url"
|
||||
onChange={onInputChange}
|
||||
label="Jira site URL"
|
||||
placeholder="https://jira.example.com"
|
||||
label="URL"
|
||||
placeholder={
|
||||
integrationDestination === "jira"
|
||||
? "https://example.atlassian.net"
|
||||
: "https://example.zendesk.com"
|
||||
}
|
||||
parseTarget
|
||||
value={url}
|
||||
error={urlError}
|
||||
onBlur={validateForm}
|
||||
/>
|
||||
<InputField
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
label="Jira username"
|
||||
placeholder="name@example.com"
|
||||
parseTarget
|
||||
value={username}
|
||||
tooltip={
|
||||
"\
|
||||
This user must have “Create issues” for the project <br/> \
|
||||
in which the issues are created. \
|
||||
"
|
||||
}
|
||||
/>
|
||||
{integrationDestination === "jira" ? (
|
||||
<InputField
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
label="Username"
|
||||
placeholder="name@example.com"
|
||||
parseTarget
|
||||
value={username}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
name="email"
|
||||
onChange={onInputChange}
|
||||
label="Email"
|
||||
placeholder="name@example.com"
|
||||
parseTarget
|
||||
value={email}
|
||||
/>
|
||||
)}
|
||||
<InputField
|
||||
name="apiToken"
|
||||
onChange={onInputChange}
|
||||
label="Jira API token"
|
||||
label="API token"
|
||||
parseTarget
|
||||
value={apiToken}
|
||||
/>
|
||||
<InputField
|
||||
name="projectKey"
|
||||
onChange={onInputChange}
|
||||
label="Jira project key"
|
||||
placeholder="JRAEXAMPLE"
|
||||
parseTarget
|
||||
value={projectKey}
|
||||
tooltip={
|
||||
"\
|
||||
{integrationDestination === "jira" ? (
|
||||
<InputField
|
||||
name="projectKey"
|
||||
onChange={onInputChange}
|
||||
label="Project key"
|
||||
placeholder="JRAEXAMPLE"
|
||||
parseTarget
|
||||
value={projectKey}
|
||||
tooltip={
|
||||
"\
|
||||
To find the Jira project key, head to your project in <br /> \
|
||||
Jira. Your project key is in URL. For example, in <br /> \
|
||||
“jira.example.com/projects/JRAEXAMPLE,” <br /> \
|
||||
“JRAEXAMPLE” is your project key. \
|
||||
"
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
name="groupId"
|
||||
onChange={onInputChange}
|
||||
label="Group ID"
|
||||
placeholder="28134038"
|
||||
type="number"
|
||||
parseTarget
|
||||
value={groupId === 0 ? null : groupId}
|
||||
tooltip={
|
||||
"\
|
||||
To find the Zendesk group ID, select <b>Admin > <br /> \
|
||||
People > Groups</b>. Find the group and select it. <br /> \
|
||||
The group ID will appear in the search field. \
|
||||
"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div
|
||||
data-tip
|
||||
data-for="create-integration-button"
|
||||
data-tip-disable={
|
||||
!(
|
||||
formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.username === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.projectKey === ""
|
||||
)
|
||||
!(integrationDestination === "jira"
|
||||
? formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.username === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.projectKey === ""
|
||||
: formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.email === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.groupId === 0)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
|
|
@ -164,11 +262,17 @@ const IntegrationForm = ({
|
|||
type="submit"
|
||||
variant="brand"
|
||||
disabled={
|
||||
formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.username === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.projectKey === ""
|
||||
integrationDestination === "jira"
|
||||
? formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.username === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.projectKey === ""
|
||||
: formData.url === "" ||
|
||||
formData.url.slice(0, 8) !== "https://" ||
|
||||
formData.email === "" ||
|
||||
formData.apiToken === "" ||
|
||||
formData.groupId === 0
|
||||
}
|
||||
>
|
||||
Save
|
||||
|
|
@ -187,7 +291,7 @@ const IntegrationForm = ({
|
|||
className={`tooltip`}
|
||||
style={{ width: "152px", textAlign: "center" }}
|
||||
>
|
||||
Complete all fields to save the integration
|
||||
Complete all fields to save the integration.
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ 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";
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IZendeskIntegration,
|
||||
IIntegration,
|
||||
} from "interfaces/integration";
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
|
||||
import configAPI from "services/entities/config";
|
||||
import softwareAPI, {
|
||||
ISoftwareResponse,
|
||||
|
|
@ -50,6 +54,7 @@ interface ISoftwareAutomations {
|
|||
};
|
||||
integrations: {
|
||||
jira: IJiraIntegration[];
|
||||
zendesk: IZendeskIntegration[];
|
||||
};
|
||||
}
|
||||
interface IHeaderButtonsState extends ITeamsDropdownState {
|
||||
|
|
@ -108,14 +113,24 @@ const ManageSoftwarePage = ({
|
|||
let jiraIntegrationEnabled = false;
|
||||
if (data.integrations.jira) {
|
||||
jiraIntegrationEnabled = data?.integrations.jira.some(
|
||||
(integration: any) => {
|
||||
(integration: IIntegration) => {
|
||||
return integration.enable_software_vulnerabilities;
|
||||
}
|
||||
);
|
||||
}
|
||||
let zendeskIntegrationEnabled = false;
|
||||
if (data.integrations.zendesk) {
|
||||
zendeskIntegrationEnabled = data?.integrations.zendesk.some(
|
||||
(integration: IIntegration) => {
|
||||
return integration.enable_software_vulnerabilities;
|
||||
}
|
||||
);
|
||||
}
|
||||
setIsVulnerabilityAutomationsEnabled(
|
||||
data?.webhook_settings?.vulnerabilities_webhook
|
||||
.enable_vulnerabilities_webhook || jiraIntegrationEnabled
|
||||
.enable_vulnerabilities_webhook ||
|
||||
jiraIntegrationEnabled ||
|
||||
zendeskIntegrationEnabled
|
||||
);
|
||||
// Convert from nanosecond to nearest day
|
||||
setRecentVulnerabilityMaxAge(
|
||||
|
|
@ -260,6 +275,7 @@ const ManageSoftwarePage = ({
|
|||
"success",
|
||||
"Successfully updated vulnerability automations."
|
||||
);
|
||||
refetchSoftwareVulnerabilitiesWebhook();
|
||||
});
|
||||
} catch {
|
||||
renderFlash(
|
||||
|
|
@ -268,7 +284,6 @@ const ManageSoftwarePage = ({
|
|||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
refetchSoftwareVulnerabilitiesWebhook();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { Link } from "react-router";
|
||||
|
|
@ -6,7 +6,9 @@ import PATHS from "router/paths";
|
|||
|
||||
import {
|
||||
IJiraIntegration,
|
||||
IJiraIntegrationIndexed,
|
||||
IZendeskIntegration,
|
||||
IIntegration,
|
||||
IIntegrations,
|
||||
} from "interfaces/integration";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import configAPI from "services/entities/config";
|
||||
|
|
@ -33,6 +35,7 @@ interface ISoftwareAutomations {
|
|||
};
|
||||
integrations: {
|
||||
jira: IJiraIntegration[];
|
||||
zendesk: IZendeskIntegration[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -78,16 +81,23 @@ const ManageAutomationsModal = ({
|
|||
softwareAutomationsEnabled,
|
||||
setSoftwareAutomationsEnabled,
|
||||
] = useState<boolean>(softwareVulnerabilityAutomationEnabled || false);
|
||||
const [jiraEnabled, setJiraEnabled] = useState<boolean>(
|
||||
const [integrationEnabled, setIntegrationEnabled] = useState<boolean>(
|
||||
!softwareVulnerabilityWebhookEnabled
|
||||
);
|
||||
const [integrationsIndexed, setIntegrationsIndexed] = useState<
|
||||
IJiraIntegrationIndexed[]
|
||||
const [jiraIntegrationsIndexed, setJiraIntegrationsIndexed] = useState<
|
||||
IIntegration[]
|
||||
>();
|
||||
const [zendeskIntegrationsIndexed, setZendeskIntegrationsIndexed] = useState<
|
||||
IIntegration[]
|
||||
>();
|
||||
const [allIntegrationsIndexed, setAllIntegrationsIndexed] = useState<
|
||||
IIntegration[]
|
||||
>();
|
||||
const [
|
||||
selectedIntegration,
|
||||
setSelectedIntegration,
|
||||
] = useState<IJiraIntegration>();
|
||||
] = useState<IIntegration>();
|
||||
|
||||
useDeepEffect(() => {
|
||||
setSoftwareAutomationsEnabled(
|
||||
softwareVulnerabilityAutomationEnabled || false
|
||||
|
|
@ -100,30 +110,63 @@ const ManageAutomationsModal = ({
|
|||
}
|
||||
}, [destinationUrl]);
|
||||
|
||||
const { data: integrations } = useQuery<IConfig, Error, IJiraIntegration[]>(
|
||||
const { data: integrations } = useQuery<IConfig, Error, IIntegrations>(
|
||||
["integrations"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
select: (data: IConfig) => {
|
||||
return data.integrations.jira;
|
||||
return data.integrations;
|
||||
},
|
||||
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);
|
||||
}
|
||||
// Set jira and zendesk integrations
|
||||
const addJiraIndexed = data.jira
|
||||
? data.jira.map((integration, index) => {
|
||||
return { ...integration, originalIndex: index, type: "jira" };
|
||||
})
|
||||
: [];
|
||||
setJiraIntegrationsIndexed(addJiraIndexed);
|
||||
const addZendeskIndexed = data.zendesk
|
||||
? data.zendesk.map((integration, index) => {
|
||||
return {
|
||||
...integration,
|
||||
originalIndex: index,
|
||||
type: "zendesk",
|
||||
};
|
||||
})
|
||||
: [];
|
||||
setZendeskIntegrationsIndexed(addZendeskIndexed);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (jiraIntegrationsIndexed && zendeskIntegrationsIndexed) {
|
||||
const combineDataSets = jiraIntegrationsIndexed.concat(
|
||||
zendeskIntegrationsIndexed
|
||||
);
|
||||
setAllIntegrationsIndexed(
|
||||
combineDataSets?.map((integration, index) => {
|
||||
return { ...integration, dropdownIndex: index };
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [
|
||||
jiraIntegrationsIndexed,
|
||||
zendeskIntegrationsIndexed,
|
||||
setAllIntegrationsIndexed,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (allIntegrationsIndexed) {
|
||||
const currentSelectedIntegration = allIntegrationsIndexed.find(
|
||||
(integration) => {
|
||||
return integration.enable_software_vulnerabilities === true;
|
||||
}
|
||||
);
|
||||
setSelectedIntegration(currentSelectedIntegration);
|
||||
}
|
||||
}, [allIntegrationsIndexed]);
|
||||
|
||||
const onURLChange = (value: string) => {
|
||||
setDestinationUrl(value);
|
||||
};
|
||||
|
|
@ -148,13 +191,16 @@ const ManageAutomationsModal = ({
|
|||
},
|
||||
},
|
||||
integrations: {
|
||||
jira: integrations || [],
|
||||
jira: integrations?.jira || [],
|
||||
zendesk: integrations?.zendesk || [],
|
||||
},
|
||||
};
|
||||
|
||||
const updateSoftwareAutomation = () => {
|
||||
if (!softwareAutomationsEnabled) {
|
||||
// set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false
|
||||
// set enable_vulnerabilities_webhook
|
||||
// jira.enable_software_vulnerabilities
|
||||
// and zendesk.enable_software_vulnerabilities to false
|
||||
configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = false;
|
||||
const disableAllJira = configSoftwareAutomations.integrations.jira.map(
|
||||
(integration) => {
|
||||
|
|
@ -162,13 +208,24 @@ const ManageAutomationsModal = ({
|
|||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.jira = disableAllJira;
|
||||
const disableAllZendesk = configSoftwareAutomations.integrations.zendesk.map(
|
||||
(integration) => {
|
||||
return {
|
||||
...integration,
|
||||
enable_software_vulnerabilities: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.zendesk = disableAllZendesk;
|
||||
return;
|
||||
}
|
||||
if (!jiraEnabled) {
|
||||
if (!integrationEnabled) {
|
||||
if (!validUrl) {
|
||||
return;
|
||||
}
|
||||
// set enable_vulnerabilities_webhook to true and all jira.enable_software_vulnerabilities to false
|
||||
// set enable_vulnerabilities_webhook to true
|
||||
// all jira.enable_software_vulnerabilities to false
|
||||
// all zendesk.enable_software_vulnerabilities to false
|
||||
configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = true;
|
||||
const disableAllJira = configSoftwareAutomations.integrations.jira.map(
|
||||
(integration) => {
|
||||
|
|
@ -179,21 +236,46 @@ const ManageAutomationsModal = ({
|
|||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.jira = disableAllJira;
|
||||
const disableAllZendesk = configSoftwareAutomations.integrations.zendesk.map(
|
||||
(integration) => {
|
||||
return {
|
||||
...integration,
|
||||
enable_software_vulnerabilities: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.zendesk = disableAllZendesk;
|
||||
return;
|
||||
}
|
||||
// set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false
|
||||
// except the one jira integration selected
|
||||
// set enable_vulnerabilities_webhook to false
|
||||
// all jira.enable_software_vulnerabilities to false
|
||||
// all zendesk.enable_software_vulnerabilities to false
|
||||
// except the one 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,
|
||||
selectedIntegration?.type === "jira"
|
||||
? index === selectedIntegration?.originalIndex
|
||||
: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.jira = enableSelectedJiraIntegrationOnly;
|
||||
const enableSelectedZendeskIntegrationOnly = configSoftwareAutomations.integrations.zendesk.map(
|
||||
(integration, index) => {
|
||||
return {
|
||||
...integration,
|
||||
enable_software_vulnerabilities:
|
||||
selectedIntegration?.type === "zendesk"
|
||||
? index === selectedIntegration?.originalIndex
|
||||
: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
configSoftwareAutomations.integrations.zendesk = enableSelectedZendeskIntegrationOnly;
|
||||
};
|
||||
|
||||
updateSoftwareAutomation();
|
||||
|
|
@ -202,10 +284,10 @@ const ManageAutomationsModal = ({
|
|||
};
|
||||
|
||||
const createIntegrationDropdownOptions = () => {
|
||||
const integrationOptions = integrationsIndexed?.map((i) => {
|
||||
const integrationOptions = allIntegrationsIndexed?.map((i) => {
|
||||
return {
|
||||
value: String(i.index),
|
||||
label: `${i.url} - ${i.project_key}`,
|
||||
value: String(i.dropdownIndex),
|
||||
label: `${i.url} - ${i.project_key || i.group_id}`,
|
||||
};
|
||||
});
|
||||
return integrationOptions;
|
||||
|
|
@ -213,17 +295,19 @@ const ManageAutomationsModal = ({
|
|||
|
||||
const onChangeSelectIntegration = (selectIntegrationIndex: string) => {
|
||||
const integrationWithIndex:
|
||||
| IJiraIntegrationIndexed
|
||||
| undefined = integrationsIndexed?.find(
|
||||
(integ: IJiraIntegrationIndexed) =>
|
||||
integ.index === parseInt(selectIntegrationIndex, 10)
|
||||
| IIntegration
|
||||
| undefined = allIntegrationsIndexed?.find(
|
||||
(integ: IIntegration) =>
|
||||
integ.dropdownIndex === parseInt(selectIntegrationIndex, 10)
|
||||
);
|
||||
setSelectedIntegration(integrationWithIndex);
|
||||
};
|
||||
|
||||
const onRadioChange = (jira: boolean): ((evt: string) => void) => {
|
||||
const onRadioChange = (
|
||||
enableIntegration: boolean
|
||||
): ((evt: string) => void) => {
|
||||
return () => {
|
||||
setJiraEnabled(jira);
|
||||
setIntegrationEnabled(enableIntegration);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -237,13 +321,15 @@ const ManageAutomationsModal = ({
|
|||
{recentVulnerabilityMaxAge || "30"} days.
|
||||
</p>
|
||||
</div>
|
||||
{integrationsIndexed && integrationsIndexed.length > 0 ? (
|
||||
{(jiraIntegrationsIndexed && jiraIntegrationsIndexed.length > 0) ||
|
||||
(zendeskIntegrationsIndexed &&
|
||||
zendeskIntegrationsIndexed.length > 0) ? (
|
||||
<Dropdown
|
||||
searchable
|
||||
options={createIntegrationDropdownOptions()}
|
||||
onChange={onChangeSelectIntegration}
|
||||
placeholder={"Select Jira integration"}
|
||||
value={selectedIntegration?.index}
|
||||
placeholder={"Select integration"}
|
||||
value={selectedIntegration?.dropdownIndex}
|
||||
label={"Integration"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
hint={
|
||||
|
|
@ -333,7 +419,7 @@ const ManageAutomationsModal = ({
|
|||
className={`${baseClass}__radio-input`}
|
||||
label={"Ticket"}
|
||||
id={"ticket-radio-btn"}
|
||||
checked={jiraEnabled}
|
||||
checked={integrationEnabled}
|
||||
value={"ticket"}
|
||||
name={"ticket"}
|
||||
onChange={onRadioChange(true)}
|
||||
|
|
@ -342,13 +428,13 @@ const ManageAutomationsModal = ({
|
|||
className={`${baseClass}__radio-input`}
|
||||
label={"Webhook"}
|
||||
id={"webhook-radio-btn"}
|
||||
checked={!jiraEnabled}
|
||||
checked={!integrationEnabled}
|
||||
value={"webhook"}
|
||||
name={"webhook"}
|
||||
onChange={onRadioChange(false)}
|
||||
/>
|
||||
</div>
|
||||
{jiraEnabled ? renderTicket() : renderWebhook()}
|
||||
{integrationEnabled ? renderTicket() : renderWebhook()}
|
||||
</div>
|
||||
{!softwareAutomationsEnabled && (
|
||||
<div className={`${baseClass}__overlay`} />
|
||||
|
|
@ -367,9 +453,11 @@ const ManageAutomationsModal = ({
|
|||
data-for="save-automation-button"
|
||||
data-tip-disable={
|
||||
!(
|
||||
integrationsIndexed &&
|
||||
integrationsIndexed.length === 0 &&
|
||||
jiraEnabled &&
|
||||
((jiraIntegrationsIndexed &&
|
||||
jiraIntegrationsIndexed.length === 0) ||
|
||||
(zendeskIntegrationsIndexed &&
|
||||
zendeskIntegrationsIndexed.length === 0)) &&
|
||||
integrationEnabled &&
|
||||
softwareAutomationsEnabled
|
||||
)
|
||||
}
|
||||
|
|
@ -381,10 +469,10 @@ const ManageAutomationsModal = ({
|
|||
onClick={handleSaveAutomation}
|
||||
disabled={
|
||||
(softwareAutomationsEnabled &&
|
||||
jiraEnabled &&
|
||||
integrationEnabled &&
|
||||
!selectedIntegration) ||
|
||||
(softwareAutomationsEnabled &&
|
||||
!jiraEnabled &&
|
||||
!integrationEnabled &&
|
||||
destinationUrl === "")
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -340,13 +340,6 @@ 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: "",
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue