2022-04-11 19:04:41 +00:00
|
|
|
import React, { useState, useContext, useCallback } from "react";
|
|
|
|
import { useQuery } from "react-query";
|
2022-05-11 02:33:30 +00:00
|
|
|
import memoize from "memoize-one";
|
2022-04-11 19:04:41 +00:00
|
|
|
|
|
|
|
import { NotificationContext } from "context/notification";
|
|
|
|
import { IConfig } from "interfaces/config";
|
|
|
|
import {
|
|
|
|
IJiraIntegration,
|
2022-05-11 02:33:30 +00:00
|
|
|
IZendeskIntegration,
|
|
|
|
IIntegration,
|
|
|
|
IIntegrationTableData,
|
|
|
|
IIntegrations,
|
2022-04-11 19:04:41 +00:00
|
|
|
} from "interfaces/integration";
|
|
|
|
import { IApiError } from "interfaces/errors";
|
|
|
|
|
|
|
|
import Button from "components/buttons/Button";
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
|
|
import configAPI from "services/entities/config";
|
|
|
|
|
|
|
|
import TableContainer from "components/TableContainer";
|
2022-05-03 20:57:08 +00:00
|
|
|
import TableDataError from "components/DataError";
|
2022-04-11 19:04:41 +00:00
|
|
|
import AddIntegrationModal from "./components/CreateIntegrationModal";
|
|
|
|
import DeleteIntegrationModal from "./components/DeleteIntegrationModal";
|
|
|
|
import EditIntegrationModal from "./components/EditIntegrationModal";
|
2022-06-02 23:03:03 +00:00
|
|
|
import ExternalURLIcon from "../../../../assets/images/icon-external-url-12x12@2x.png";
|
2022-04-11 19:04:41 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
generateTableHeaders,
|
2022-05-11 02:33:30 +00:00
|
|
|
combineDataSets,
|
2022-04-11 19:04:41 +00:00
|
|
|
} 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 =
|
2022-05-11 02:33:30 +00:00
|
|
|
"Invalid login credentials or URL. Please correct and try again.";
|
2022-04-11 19:04:41 +00:00
|
|
|
const UNKNOWN_ERROR =
|
2022-05-11 02:33:30 +00:00
|
|
|
"We experienced an error when attempting to connect. Please try again later.";
|
2022-04-11 19:04:41 +00:00
|
|
|
|
|
|
|
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,
|
2022-05-11 02:33:30 +00:00
|
|
|
] = useState<IIntegrationTableData>();
|
|
|
|
const [jiraIntegrations, setJiraIntegrations] = useState<
|
|
|
|
IJiraIntegration[]
|
|
|
|
>();
|
|
|
|
const [zendeskIntegrations, setZendeskIntegrations] = useState<
|
|
|
|
IZendeskIntegration[]
|
2022-04-11 19:04:41 +00:00
|
|
|
>();
|
|
|
|
const [backendValidators, setBackendValidators] = useState<{
|
|
|
|
[key: string]: string;
|
|
|
|
}>({});
|
|
|
|
const [testingConnection, setTestingConnection] = useState<boolean>(false);
|
|
|
|
|
|
|
|
const {
|
|
|
|
data: integrations,
|
|
|
|
isLoading: isLoadingIntegrations,
|
|
|
|
error: loadingIntegrationsError,
|
|
|
|
refetch: refetchIntegrations,
|
2022-05-11 02:33:30 +00:00
|
|
|
} = useQuery<IConfig, Error, IIntegrations>(
|
2022-04-11 19:04:41 +00:00
|
|
|
["integrations"],
|
|
|
|
() => configAPI.loadAll(),
|
|
|
|
{
|
|
|
|
select: (data: IConfig) => {
|
2022-05-11 02:33:30 +00:00
|
|
|
return data.integrations;
|
2022-04-11 19:04:41 +00:00
|
|
|
},
|
|
|
|
onSuccess: (data) => {
|
|
|
|
if (data) {
|
2022-05-11 02:33:30 +00:00
|
|
|
setJiraIntegrations(data.jira);
|
|
|
|
setZendeskIntegrations(data.zendesk);
|
2022-04-11 19:04:41 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-05-11 02:33:30 +00:00
|
|
|
const combineJiraAndZendesk = memoize(() => {
|
|
|
|
return combineDataSets(jiraIntegrations || [], zendeskIntegrations || []);
|
|
|
|
});
|
|
|
|
|
2022-04-11 19:04:41 +00:00
|
|
|
const toggleAddIntegrationModal = useCallback(() => {
|
|
|
|
setShowAddIntegrationModal(!showAddIntegrationModal);
|
|
|
|
setBackendValidators({});
|
|
|
|
}, [
|
|
|
|
showAddIntegrationModal,
|
|
|
|
setShowAddIntegrationModal,
|
|
|
|
setBackendValidators,
|
|
|
|
]);
|
|
|
|
|
|
|
|
const toggleDeleteIntegrationModal = useCallback(
|
2022-05-11 02:33:30 +00:00
|
|
|
(integration?: IIntegrationTableData) => {
|
2022-04-11 19:04:41 +00:00
|
|
|
setShowDeleteIntegrationModal(!showDeleteIntegrationModal);
|
|
|
|
integration
|
|
|
|
? setIntegrationEditing(integration)
|
|
|
|
: setIntegrationEditing(undefined);
|
|
|
|
},
|
|
|
|
[
|
|
|
|
showDeleteIntegrationModal,
|
|
|
|
setShowDeleteIntegrationModal,
|
|
|
|
setIntegrationEditing,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
const toggleEditIntegrationModal = useCallback(
|
2022-05-11 02:33:30 +00:00
|
|
|
(integration?: IIntegrationTableData) => {
|
2022-04-11 19:04:41 +00:00
|
|
|
setShowEditIntegrationModal(!showEditIntegrationModal);
|
|
|
|
setBackendValidators({});
|
|
|
|
integration
|
|
|
|
? setIntegrationEditing(integration)
|
|
|
|
: setIntegrationEditing(undefined);
|
|
|
|
},
|
|
|
|
[
|
|
|
|
showEditIntegrationModal,
|
|
|
|
setShowEditIntegrationModal,
|
|
|
|
setIntegrationEditing,
|
|
|
|
setBackendValidators,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
const onCreateSubmit = useCallback(
|
2022-05-11 02:33:30 +00:00
|
|
|
(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 };
|
|
|
|
};
|
|
|
|
|
2022-04-11 19:04:41 +00:00
|
|
|
setTestingConnection(true);
|
|
|
|
configAPI
|
2022-05-11 02:33:30 +00:00
|
|
|
.update({ integrations: destination() })
|
2022-04-11 19:04:41 +00:00
|
|
|
.then(() => {
|
|
|
|
renderFlash(
|
|
|
|
"success",
|
|
|
|
<>
|
|
|
|
Successfully added{" "}
|
|
|
|
<b>
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationSubmitData[integrationSubmitData.length - 1].url} -{" "}
|
|
|
|
{integrationSubmitData[integrationSubmitData.length - 1]
|
|
|
|
.project_key ||
|
|
|
|
integrationSubmitData[integrationSubmitData.length - 1]
|
|
|
|
.group_id}
|
2022-04-11 19:04:41 +00:00
|
|
|
</b>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
setBackendValidators({});
|
|
|
|
toggleAddIntegrationModal();
|
|
|
|
refetchIntegrations();
|
|
|
|
})
|
|
|
|
.catch((createError: { data: IApiError }) => {
|
|
|
|
if (createError.data.message.includes("Validation Failed")) {
|
|
|
|
renderFlash("error", VALIDATION_FAILED_ERROR);
|
2022-05-06 16:31:11 +00:00
|
|
|
} else if (createError.data.message.includes("Bad request")) {
|
|
|
|
if (
|
|
|
|
createError.data.errors[0].reason.includes(
|
|
|
|
"duplicate Jira integration for project key"
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
renderFlash(
|
|
|
|
"error",
|
|
|
|
<>
|
|
|
|
Could not add add{" "}
|
|
|
|
<b>
|
|
|
|
{
|
2022-05-11 02:33:30 +00:00
|
|
|
integrationSubmitData[integrationSubmitData.length - 1]
|
|
|
|
.url
|
2022-05-06 16:31:11 +00:00
|
|
|
}{" "}
|
|
|
|
-{" "}
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationSubmitData[integrationSubmitData.length - 1]
|
|
|
|
.project_key ||
|
|
|
|
integrationSubmitData[integrationSubmitData.length - 1]
|
|
|
|
.group_id}
|
2022-05-06 16:31:11 +00:00
|
|
|
</b>
|
|
|
|
. This integration already exists
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
renderFlash("error", BAD_REQUEST_ERROR);
|
|
|
|
}
|
|
|
|
} else if (createError.data.message.includes("Unknown Error")) {
|
2022-04-11 19:04:41 +00:00
|
|
|
renderFlash("error", UNKNOWN_ERROR);
|
|
|
|
} else {
|
|
|
|
renderFlash(
|
|
|
|
"error",
|
|
|
|
<>
|
|
|
|
Could not add{" "}
|
|
|
|
<b>
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationSubmitData[integrationSubmitData.length - 1].url}
|
2022-04-11 19:04:41 +00:00
|
|
|
</b>
|
|
|
|
. Please try again.
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
toggleAddIntegrationModal();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setTestingConnection(false);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[toggleAddIntegrationModal]
|
|
|
|
);
|
|
|
|
|
|
|
|
const onDeleteSubmit = useCallback(() => {
|
|
|
|
if (integrationEditing) {
|
2022-05-11 02:33:30 +00:00
|
|
|
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()
|
2022-04-11 19:04:41 +00:00
|
|
|
.then(() => {
|
|
|
|
renderFlash(
|
|
|
|
"success",
|
|
|
|
<>
|
2022-05-06 16:31:11 +00:00
|
|
|
Successfully deleted{" "}
|
|
|
|
<b>
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationEditing.url} -{" "}
|
|
|
|
{integrationEditing.projectKey ||
|
|
|
|
integrationEditing.groupId?.toString()}
|
2022-05-06 16:31:11 +00:00
|
|
|
</b>
|
2022-04-11 19:04:41 +00:00
|
|
|
</>
|
|
|
|
);
|
2022-05-05 18:04:34 +00:00
|
|
|
refetchIntegrations();
|
2022-04-11 19:04:41 +00:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
renderFlash(
|
|
|
|
"error",
|
|
|
|
<>
|
2022-05-06 16:31:11 +00:00
|
|
|
Could not delete{" "}
|
|
|
|
<b>
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationEditing.url} -{" "}
|
|
|
|
{integrationEditing.projectKey ||
|
|
|
|
integrationEditing.groupId?.toString()}
|
2022-05-06 16:31:11 +00:00
|
|
|
</b>
|
|
|
|
. Please try again.
|
2022-04-11 19:04:41 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
toggleDeleteIntegrationModal();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [integrationEditing, toggleDeleteIntegrationModal]);
|
|
|
|
|
|
|
|
const onEditSubmit = useCallback(
|
2022-05-11 02:33:30 +00:00
|
|
|
(integrationSubmitData: IIntegration[]) => {
|
2022-04-11 19:04:41 +00:00
|
|
|
if (integrationEditing) {
|
|
|
|
setTestingConnection(true);
|
2022-05-11 02:33:30 +00:00
|
|
|
|
|
|
|
const editIntegrationDestination = () => {
|
|
|
|
if (integrationEditing.type === "jira") {
|
|
|
|
return configAPI.update({
|
|
|
|
integrations: {
|
|
|
|
jira: integrationSubmitData,
|
|
|
|
zendesk: zendeskIntegrations,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return configAPI.update({
|
|
|
|
integrations: {
|
|
|
|
zendesk: integrationSubmitData,
|
|
|
|
jira: jiraIntegrations,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
editIntegrationDestination()
|
2022-04-11 19:04:41 +00:00
|
|
|
.then(() => {
|
|
|
|
renderFlash(
|
|
|
|
"success",
|
|
|
|
<>
|
|
|
|
Successfully edited{" "}
|
|
|
|
<b>
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationSubmitData[integrationEditing?.originalIndex].url}{" "}
|
|
|
|
-{" "}
|
|
|
|
{integrationSubmitData[integrationEditing?.originalIndex]
|
|
|
|
.project_key ||
|
|
|
|
integrationSubmitData[integrationEditing?.originalIndex]
|
|
|
|
.group_id}
|
2022-04-11 19:04:41 +00:00
|
|
|
</b>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
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",
|
|
|
|
<>
|
2022-05-06 16:31:11 +00:00
|
|
|
Could not edit{" "}
|
|
|
|
<b>
|
|
|
|
{integrationEditing?.url} -{" "}
|
2022-05-11 02:33:30 +00:00
|
|
|
{integrationEditing?.projectKey ||
|
|
|
|
integrationEditing?.groupId?.toString()}
|
2022-05-06 16:31:11 +00:00
|
|
|
</b>
|
|
|
|
. Please try again.
|
2022-04-11 19:04:41 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setTestingConnection(false);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[integrationEditing, toggleEditIntegrationModal]
|
|
|
|
);
|
|
|
|
|
|
|
|
const onActionSelection = (
|
|
|
|
action: string,
|
2022-05-11 02:33:30 +00:00
|
|
|
integration: IIntegrationTableData
|
2022-04-11 19:04:41 +00:00
|
|
|
): void => {
|
|
|
|
switch (action) {
|
|
|
|
case "edit":
|
|
|
|
toggleEditIntegrationModal(integration);
|
|
|
|
break;
|
|
|
|
case "delete":
|
|
|
|
toggleDeleteIntegrationModal(integration);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const NoIntegrationsComponent = () => {
|
|
|
|
return (
|
|
|
|
<div className={`${noIntegrationsClass}`}>
|
|
|
|
<div className={`${noIntegrationsClass}__inner`}>
|
|
|
|
<div className={`${noIntegrationsClass}__inner-text`}>
|
2022-08-09 15:45:42 +00:00
|
|
|
<h2>Set up integrations</h2>
|
2022-04-11 19:04:41 +00:00
|
|
|
<p>
|
|
|
|
Create tickets automatically when Fleet detects new
|
|
|
|
vulnerabilities.
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
Want to learn more?
|
|
|
|
<a
|
|
|
|
href="https://fleetdm.com/docs/using-fleet/automations"
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
>
|
|
|
|
Read about automations
|
2022-06-02 23:03:03 +00:00
|
|
|
<img alt="Open external link" src={ExternalURLIcon} />
|
2022-04-11 19:04:41 +00:00
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
<Button
|
|
|
|
variant="brand"
|
|
|
|
className={`${noIntegrationsClass}__create-button`}
|
|
|
|
onClick={toggleAddIntegrationModal}
|
|
|
|
>
|
|
|
|
Add integration
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const tableHeaders = generateTableHeaders(onActionSelection);
|
2022-05-11 02:33:30 +00:00
|
|
|
|
|
|
|
const tableData = combineJiraAndZendesk();
|
2022-04-11 19:04:41 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={`${baseClass}`}>
|
|
|
|
<p className={`${baseClass}__page-description`}>
|
|
|
|
Add or edit integrations to create tickets when Fleet detects new
|
|
|
|
vulnerabilities.
|
|
|
|
</p>
|
|
|
|
{loadingIntegrationsError ? (
|
|
|
|
<TableDataError />
|
|
|
|
) : (
|
|
|
|
<TableContainer
|
|
|
|
columns={tableHeaders}
|
|
|
|
data={tableData}
|
|
|
|
isLoading={isLoadingIntegrations}
|
|
|
|
defaultSortHeader={"name"}
|
|
|
|
defaultSortDirection={"asc"}
|
|
|
|
actionButtonText={"Add integration"}
|
2022-05-11 02:33:30 +00:00
|
|
|
hideActionButton={!tableData?.length}
|
2022-04-11 19:04:41 +00:00
|
|
|
actionButtonVariant={"brand"}
|
|
|
|
onActionButtonClick={toggleAddIntegrationModal}
|
|
|
|
resultsTitle={"integrations"}
|
|
|
|
emptyComponent={NoIntegrationsComponent}
|
|
|
|
showMarkAllPages={false}
|
|
|
|
isAllPagesSelected={false}
|
|
|
|
disablePagination
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{showAddIntegrationModal && (
|
|
|
|
<AddIntegrationModal
|
|
|
|
onCancel={toggleAddIntegrationModal}
|
|
|
|
onSubmit={onCreateSubmit}
|
|
|
|
backendValidators={backendValidators}
|
2022-05-11 02:33:30 +00:00
|
|
|
integrations={integrations || { jira: [], zendesk: [] }}
|
2022-04-11 19:04:41 +00:00
|
|
|
testingConnection={testingConnection}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{showDeleteIntegrationModal && (
|
|
|
|
<DeleteIntegrationModal
|
|
|
|
onCancel={toggleDeleteIntegrationModal}
|
|
|
|
onSubmit={onDeleteSubmit}
|
|
|
|
url={integrationEditing?.url || ""}
|
2022-05-11 02:33:30 +00:00
|
|
|
projectKey={
|
|
|
|
integrationEditing?.projectKey ||
|
|
|
|
integrationEditing?.groupId?.toString() ||
|
|
|
|
""
|
|
|
|
}
|
2022-04-11 19:04:41 +00:00
|
|
|
/>
|
|
|
|
)}
|
2022-05-11 02:33:30 +00:00
|
|
|
{showEditIntegrationModal && integrations && (
|
2022-04-11 19:04:41 +00:00
|
|
|
<EditIntegrationModal
|
|
|
|
onCancel={toggleEditIntegrationModal}
|
|
|
|
onSubmit={onEditSubmit}
|
|
|
|
backendValidators={backendValidators}
|
2022-05-11 02:33:30 +00:00
|
|
|
integrations={integrations}
|
2022-04-11 19:04:41 +00:00
|
|
|
integrationEditing={integrationEditing}
|
|
|
|
testingConnection={testingConnection}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default IntegrationsPage;
|