fleet/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx
jacobshandling 5d9026b7e5
UI - GitOps Mode: Core abstractions, first batch of applications (#26401)
## For #26229 – Part 1


![ezgif-6bbe6d60c12ed4](https://github.com/user-attachments/assets/37a04b64-abd7-4605-b4ac-9542836ff562)

- This PR contains the core abstractions, routes, API updates, and types
for GitOps mode in the UI. Since this work will touch essentially every
part of the Fleet UI, it is ripe for merge conflicts. To mitigate such
conflicts, I'll be merging this work in a number of iterative PRs. ~To
effectively gate any of this work from showing until it is all merged to
`main`, [this commit](feedbb2d4c) hides
the settings section that allows enabling/disabling this setting,
effectively feature flagging the entire thing. In the last of these
iterative PRs, that commit will be reverted to engage the entire
feature. For testing purposes, reviewers can `git revert
feedbb2d4c25ec2e304e1f18d409cee62f6752ed` locally~ The new settings
section for this feature is feature flagged until all PRs are merged -
to show the setting section while testing, run `ALLOW_GITOPS_MODE=true
NODE_ENV=development yarn run webpack --progress --watch` in place of
`make generate-dev`

- Changes file will be added and feature flag removed in the last PR

- [x] Settings page with routing, form, API integration (hidden until
last PR)
- [x] Activities
- [x] Navbar indicator
- Apply GOM conditional UI to:
    - [x] Manage enroll secret modal: .5
    -  Controls >
        - [x] Scripts:
        - Setup experience > 
            - [x] Install software > Select software modal
        - [x] OS Settings >
            - [x] Custom settings
            - [x] Disk encryption
        - [x] OS Updates
 
2/18/25, added to this PR:

   - [x] Controls > Setup experience > Run script
   - [x] Software >
        - [x] Manage automations modal
        - [x] Add software >
            - [x] App Store (VPP)
            - [x] Custom package
   - [x] Queries
        - [x] Manage
        - [x] Automations modal
        - [x] New
        - [x] Edit
   - [x] Policies
     - [x] Manage
     - [x] New
     - [x] Edit
     -  Manage automations
       - [x] Calendar events


- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-02-20 08:41:07 -08:00

558 lines
18 KiB
TypeScript

import React, { useState, useEffect, useContext } from "react";
import { useQuery } from "react-query";
import { Link } from "react-router";
import PATHS from "router/paths";
import { AppContext } from "context/app";
import {
IJiraIntegration,
IZendeskIntegration,
IIntegration,
IGlobalIntegrations,
IIntegrationType,
} from "interfaces/integration";
import {
IConfig,
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS,
} from "interfaces/config";
import configAPI from "services/entities/config";
import { SUPPORT_LINK } from "utilities/constants";
// @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 CustomLink from "components/CustomLink";
import validUrl from "components/forms/validators/valid_url";
import TooltipWrapper from "components/TooltipWrapper";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
import useDeepEffect from "hooks/useDeepEffect";
import { isEmpty, omit } from "lodash";
import PreviewPayloadModal from "../PreviewPayloadModal";
import PreviewTicketModal from "../PreviewTicketModal";
interface ISoftwareAutomations {
webhook_settings: {
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
};
integrations: {
jira: IJiraIntegration[];
zendesk: IZendeskIntegration[];
};
}
interface IManageSoftwareAutomationsModalProps {
onCancel: () => void;
onCreateWebhookSubmit: (formData: ISoftwareAutomations) => void;
togglePreviewPayloadModal: () => void;
togglePreviewTicketModal: () => void;
showPreviewPayloadModal: boolean;
showPreviewTicketModal: boolean;
softwareVulnerabilityAutomationEnabled?: boolean;
softwareVulnerabilityWebhookEnabled?: boolean;
currentDestinationUrl?: string;
recentVulnerabilityMaxAge?: number;
}
const validateWebhookURL = (url: string) => {
const errors: { [key: string]: string } = {};
if (!url) {
errors.url = "Please add a destination URL";
} else if (!validUrl({ url })) {
errors.url = `${url} is not a valid URL`;
} else {
delete errors.url;
}
return { valid: isEmpty(errors), errors };
};
const baseClass = "manage-software-automations-modal";
const ManageAutomationsModal = ({
onCancel: onReturnToApp,
onCreateWebhookSubmit,
togglePreviewPayloadModal,
togglePreviewTicketModal,
showPreviewPayloadModal,
showPreviewTicketModal,
softwareVulnerabilityAutomationEnabled,
softwareVulnerabilityWebhookEnabled,
currentDestinationUrl,
recentVulnerabilityMaxAge,
}: IManageSoftwareAutomationsModalProps): JSX.Element => {
const [destinationUrl, setDestinationUrl] = useState(
currentDestinationUrl || ""
);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [softwareAutomationsEnabled, setSoftwareAutomationsEnabled] = useState(
softwareVulnerabilityAutomationEnabled || false
);
const [integrationEnabled, setIntegrationEnabled] = useState(
!softwareVulnerabilityWebhookEnabled
);
const [jiraIntegrationsIndexed, setJiraIntegrationsIndexed] = useState<
IIntegration[]
>();
const [zendeskIntegrationsIndexed, setZendeskIntegrationsIndexed] = useState<
IIntegration[]
>();
const [allIntegrationsIndexed, setAllIntegrationsIndexed] = useState<
IIntegration[]
>();
const [
selectedIntegration,
setSelectedIntegration,
] = useState<IIntegration>();
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
useDeepEffect(() => {
setSoftwareAutomationsEnabled(
softwareVulnerabilityAutomationEnabled || false
);
}, [softwareVulnerabilityAutomationEnabled]);
useDeepEffect(() => {
if (destinationUrl) {
setErrors({});
}
}, [destinationUrl]);
const { data: integrations } = useQuery<IConfig, Error, IGlobalIntegrations>(
["integrations"],
() => configAPI.loadAll(),
{
select: (data: IConfig) => {
return data.integrations;
},
onSuccess: (data) => {
// Set jira and zendesk integrations
const addJiraIndexed = data.jira
? data.jira.map((integration, index) => {
return {
...integration,
originalIndex: index,
type: "jira" as IIntegrationType,
};
})
: [];
setJiraIntegrationsIndexed(addJiraIndexed);
const addZendeskIndexed = data.zendesk
? data.zendesk.map((integration, index) => {
return {
...integration,
originalIndex: index,
type: "zendesk" as IIntegrationType,
};
})
: [];
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);
};
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
const {
valid: validWebhookUrl,
errors: errorsWebhookUrl,
} = validateWebhookURL(destinationUrl);
if (!validWebhookUrl) {
setErrors((prevErrs) => ({ ...prevErrs, ...errorsWebhookUrl }));
} else {
setErrors((prevErrs) => omit(prevErrs, "url"));
}
// Original config keys for software automation (webhook_settings, integrations)
const configSoftwareAutomations: ISoftwareAutomations = {
webhook_settings: {
vulnerabilities_webhook: {
destination_url: validWebhookUrl
? destinationUrl
: currentDestinationUrl, // if new destination url is not valid, revert to current destination url
enable_vulnerabilities_webhook: softwareVulnerabilityWebhookEnabled,
},
},
integrations: {
jira: integrations?.jira || [],
zendesk: integrations?.zendesk || [],
},
};
const readyForSubmission = (): boolean => {
if (!softwareAutomationsEnabled) {
// 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) => {
return { ...integration, enable_software_vulnerabilities: false };
}
);
configSoftwareAutomations.integrations.jira = disableAllJira;
const disableAllZendesk = configSoftwareAutomations.integrations.zendesk.map(
(integration) => {
return {
...integration,
enable_software_vulnerabilities: false,
};
}
);
configSoftwareAutomations.integrations.zendesk = disableAllZendesk;
return true;
}
if (!integrationEnabled) {
if (!isEmpty(errorsWebhookUrl)) {
return 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) => {
return {
...integration,
enable_software_vulnerabilities: false,
};
}
);
configSoftwareAutomations.integrations.jira = disableAllJira;
const disableAllZendesk = configSoftwareAutomations.integrations.zendesk.map(
(integration) => {
return {
...integration,
enable_software_vulnerabilities: false,
};
}
);
configSoftwareAutomations.integrations.zendesk = disableAllZendesk;
return true;
}
// 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:
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;
return true;
};
if (!readyForSubmission()) {
return;
}
onCreateWebhookSubmit(configSoftwareAutomations);
onReturnToApp();
};
const createIntegrationDropdownOptions = () => {
const integrationOptions = allIntegrationsIndexed?.map((i) => {
return {
value: String(i.dropdownIndex),
label: `${i.url} - ${i.project_key || i.group_id}`,
};
});
return integrationOptions;
};
const onChangeSelectIntegration = (selectIntegrationIndex: string) => {
const integrationWithIndex:
| IIntegration
| undefined = allIntegrationsIndexed?.find(
(integ: IIntegration) =>
integ.dropdownIndex === parseInt(selectIntegrationIndex, 10)
);
setSelectedIntegration(integrationWithIndex);
};
const onRadioChange = (
enableIntegration: boolean
): ((evt: string) => void) => {
return () => {
setIntegrationEnabled(enableIntegration);
};
};
const renderTicket = () => {
return (
<>
<div className={`${baseClass}__software-automation-description`}>
A ticket will be created in your <b>Integration</b> if a detected
vulnerability (CVE) was published in the last{" "}
{recentVulnerabilityMaxAge ||
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS}{" "}
days.
</div>
{(jiraIntegrationsIndexed && jiraIntegrationsIndexed.length > 0) ||
(zendeskIntegrationsIndexed &&
zendeskIntegrationsIndexed.length > 0) ? (
<Dropdown
disabled={gomEnabled}
searchable
options={createIntegrationDropdownOptions()}
onChange={onChangeSelectIntegration}
placeholder="Select integration"
value={selectedIntegration?.dropdownIndex}
label="Integration"
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
helpText="For each new vulnerability detected, Fleet will create a ticket with a list of the affected hosts."
/>
) : (
<div className={`form-field ${baseClass}__no-integrations`}>
<div className="form-field__label">You have no integrations.</div>
<Link
to={PATHS.ADMIN_INTEGRATIONS}
className={`${baseClass}__add-integration-link`}
tabIndex={softwareAutomationsEnabled ? 0 : -1}
>
Add integration
</Link>
</div>
)}
{!!selectedIntegration && (
<Button
type="button"
variant="text-link"
onClick={togglePreviewTicketModal}
>
Preview ticket
</Button>
)}
</>
);
};
const renderWebhook = () => {
return (
<>
<div className={`${baseClass}__software-automation-description`}>
<p>
A request will be sent to your configured <b>Destination URL</b> if
a detected vulnerability (CVE) was published in the last{" "}
{recentVulnerabilityMaxAge || "30"} days.
</p>
</div>
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label="Destination URL"
type="text"
value={destinationUrl}
onChange={onURLChange}
error={errors.url}
helpText={
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
}
placeholder="https://server.com/example"
tooltip="Provide a URL to deliver a webhook request to."
disabled={!softwareAutomationsEnabled || gomEnabled}
/>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
disabled={!softwareAutomationsEnabled}
>
Preview payload
</Button>
</>
);
};
if (showPreviewTicketModal && selectedIntegration?.type) {
return (
<PreviewTicketModal
integrationType={selectedIntegration.type}
onCancel={togglePreviewTicketModal}
/>
);
}
if (showPreviewPayloadModal) {
return <PreviewPayloadModal onCancel={togglePreviewPayloadModal} />;
}
const renderSaveButton = () => {
const hasIntegrations = !(
((jiraIntegrationsIndexed && jiraIntegrationsIndexed.length === 0) ||
(zendeskIntegrationsIndexed &&
zendeskIntegrationsIndexed.length === 0)) &&
integrationEnabled &&
softwareAutomationsEnabled
);
const renderRawButton = (gomDisabled = false) => (
<TooltipWrapper
tipContent={
<>
Add an integration to create
<br /> tickets for vulnerability automations.
</>
}
disableTooltip={hasIntegrations || gomDisabled}
position="bottom"
underline={false}
showArrow
tipOffset={6}
>
<Button
type="submit"
variant="brand"
onClick={handleSaveAutomation}
disabled={
(softwareAutomationsEnabled &&
integrationEnabled &&
!selectedIntegration) ||
(softwareAutomationsEnabled &&
!integrationEnabled &&
destinationUrl === "") ||
gomDisabled
}
>
Save
</Button>
</TooltipWrapper>
);
return (
<GitOpsModeTooltipWrapper
renderChildren={renderRawButton}
tipOffset={6}
/>
);
};
return (
<Modal
onExit={onReturnToApp}
title="Manage automations"
className={baseClass}
width="large"
>
<div className={`${baseClass} form`}>
<Slider
disabled={gomEnabled}
value={softwareAutomationsEnabled}
onChange={() =>
setSoftwareAutomationsEnabled(!softwareAutomationsEnabled)
}
inactiveText="Vulnerability automations disabled"
activeText="Vulnerability automations enabled"
/>
<div
className={`form ${baseClass}__software-automations${
softwareAutomationsEnabled ? "" : "__disabled"
}`}
>
<div className="form-field">
<div className="form-field__label">Workflow</div>
<Radio
className={`${baseClass}__radio-input`}
label="Ticket"
id="ticket-radio-btn"
checked={integrationEnabled}
value="ticket"
name="workflow-type"
onChange={onRadioChange(true)}
disabled={!softwareAutomationsEnabled || gomEnabled}
/>
<Radio
className={`${baseClass}__radio-input`}
label="Webhook"
id="webhook-radio-btn"
checked={!integrationEnabled}
value="webhook"
name="workflow-type"
onChange={onRadioChange(false)}
disabled={!softwareAutomationsEnabled || gomEnabled}
/>
</div>
{integrationEnabled ? renderTicket() : renderWebhook()}
<p>
Vulnerability automations currently run for software
vulnerabilities. Interested in automations for OS vulnerabilities?{" "}
<CustomLink
url={SUPPORT_LINK}
text="Let us know"
newTab
disableKeyboardNavigation={!softwareAutomationsEnabled}
/>
</p>
</div>
<div className="modal-cta-wrap">
{renderSaveButton()}
<Button onClick={onReturnToApp} variant="inverse">
Cancel
</Button>
</div>
</div>
</Modal>
);
};
export default ManageAutomationsModal;