UI – Calendar events modal follow up (#17788)

## Follow-up work to #17717 

**Finalize disabled options and tooltips:**
<img width="697" alt="Screenshot 2024-03-21 at 5 14 40 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/ea5d880f-75f6-48ef-85cc-b807812c9a50">
<img width="697" alt="Screenshot 2024-03-21 at 5 15 13 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/bdd33118-933e-4676-9e1e-680ebcddbc7a">

**Only update policies and settings when there's a diff:**

![1(1)](https://github.com/fleetdm/fleet/assets/61553566/183d1834-3c54-4fef-a208-dfbb0354e507)

**Reorganize onChange handlers, types**

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

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2024-03-22 11:54:32 -07:00 committed by Victor Lyuboslavsky
parent fbb271caee
commit a10aac29c6
No known key found for this signature in database
11 changed files with 132 additions and 76 deletions

View File

@ -75,15 +75,17 @@ interface ITeamCalendarSettings {
// separated it can be present without the other 2 without nullifying them.
// TODO: Update these types to reflect this.
export interface IIntegrations {
export interface IZendeskJiraIntegrations {
zendesk: IZendeskIntegration[];
jira: IJiraIntegration[];
}
export interface IGlobalIntegrations extends IIntegrations {
// reality is that IZendeskJiraIntegrations are optional should be something like `extends
// Partial<IZendeskJiraIntegrations>`, but that leads to a mess of types to resolve.
export interface IGlobalIntegrations extends IZendeskJiraIntegrations {
google_calendar?: IGlobalCalendarIntegration[] | null;
}
export interface ITeamIntegrations extends IIntegrations {
export interface ITeamIntegrations extends IZendeskJiraIntegrations {
google_calendar?: ITeamCalendarSettings | null;
}

View File

@ -11,7 +11,7 @@ import {
import {
IJiraIntegration,
IZendeskIntegration,
IIntegrations,
IZendeskJiraIntegrations,
} from "interfaces/integration";
import { ITeamConfig } from "interfaces/team";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
@ -186,7 +186,9 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const vulnWebhookSettings =
softwareConfig?.webhook_settings?.vulnerabilities_webhook;
const isVulnWebhookEnabled = !!vulnWebhookSettings?.enable_vulnerabilities_webhook;
const isVulnIntegrationEnabled = (integrations?: IIntegrations) => {
const isVulnIntegrationEnabled = (
integrations?: IZendeskJiraIntegrations
) => {
return (
!!integrations?.jira?.some((j) => j.enable_software_vulnerabilities) ||
!!integrations?.zendesk?.some((z) => z.enable_software_vulnerabilities)

View File

@ -4,7 +4,7 @@ import Modal from "components/Modal";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import CustomLink from "components/CustomLink";
import { IIntegration, IIntegrations } from "interfaces/integration";
import { IIntegration, IZendeskJiraIntegrations } from "interfaces/integration";
import IntegrationForm from "../IntegrationForm";
const baseClass = "add-integration-modal";
@ -17,7 +17,7 @@ interface IAddIntegrationModalProps {
) => void;
serverErrors?: { base: string; email: string };
backendValidators: { [key: string]: string };
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
testingConnection: boolean;
}

View File

@ -4,7 +4,7 @@ import Modal from "components/Modal";
import Spinner from "components/Spinner";
import {
IIntegration,
IIntegrations,
IZendeskJiraIntegrations,
IIntegrationTableData,
} from "interfaces/integration";
import IntegrationForm from "../IntegrationForm";
@ -15,7 +15,7 @@ interface IEditIntegrationModalProps {
onCancel: () => void;
onSubmit: (jiraIntegrationSubmitData: IIntegration[]) => void;
backendValidators: { [key: string]: string };
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
integrationEditing?: IIntegrationTableData;
testingConnection: boolean;
}

View File

@ -5,7 +5,7 @@ import {
IIntegrationFormData,
IIntegrationTableData,
IIntegration,
IIntegrations,
IZendeskJiraIntegrations,
IIntegrationType,
} from "interfaces/integration";
@ -26,7 +26,7 @@ interface IIntegrationFormProps {
integrationDestination: string
) => void;
integrationEditing?: IIntegrationTableData;
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
integrationEditingUrl?: string;
integrationEditingUsername?: string;
integrationEditingEmail?: string;

View File

@ -1,5 +1,6 @@
.host-actions-dropdown {
@include button-dropdown;
color: $core-fleet-black;
.Select-multi-value-wrapper {
width: 55px;
}

View File

@ -14,7 +14,7 @@ import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import useTeamIdParam from "hooks/useTeamIdParam";
import { IConfig, IWebhookSettings } from "interfaces/config";
import { IIntegrations } from "interfaces/integration";
import { IZendeskJiraIntegrations } from "interfaces/integration";
import {
IPolicyStats,
ILoadAllPoliciesResponse,
@ -519,10 +519,9 @@ const ManagePolicyPage = ({
router?.replace(locationPath);
};
const handleUpdateAutomations = async (requestBody: {
const handleUpdateOtherWorkflows = async (requestBody: {
webhook_settings: Pick<IWebhookSettings, "failing_policies_webhook">;
// TODO - update below type to specify team integration
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
}) => {
setIsUpdatingAutomations(true);
try {
@ -549,32 +548,52 @@ const ManagePolicyPage = ({
setUpdatingPolicyEnabledCalendarEvents(true);
try {
// update enabled and URL in config
const configResponse = teamsAPI.update(
{
integrations: {
google_calendar: {
enable_calendar_events: formData.enabled,
webhook_url: formData.url,
// update team config if either field has been changed
const responses: Promise<any>[] = [];
if (
formData.enabled !==
teamConfig?.integrations.google_calendar?.enable_calendar_events ||
formData.url !== teamConfig?.integrations.google_calendar?.webhook_url
) {
responses.push(
teamsAPI.update(
{
integrations: {
google_calendar: {
enable_calendar_events: formData.enabled,
webhook_url: formData.url,
},
// These fields will never actually be changed here. See comment above
// IGlobalIntegrations definition.
zendesk: teamConfig?.integrations.zendesk || [],
jira: teamConfig?.integrations.jira || [],
},
},
// TODO - can omit these?
zendesk: teamConfig?.integrations.zendesk || [],
jira: teamConfig?.integrations.jira || [],
},
},
teamIdForApi
);
teamIdForApi
)
);
}
// update policies calendar events enabled
// TODO - only update changed policies
const policyResponses = formData.policies.map((formPolicy) =>
teamPoliciesAPI.update(formPolicy.id, {
calendar_events_enabled: formPolicy.isChecked,
team_id: teamIdForApi,
// update changed policies calendar events enabled
const changedPolicies = formData.policies.filter((formPolicy) => {
const prevPolicyState = teamPolicies?.find(
(policy) => policy.id === formPolicy.id
);
return (
formPolicy.isChecked !== prevPolicyState?.calendar_events_enabled
);
});
responses.concat(
changedPolicies.map((changedPolicy) => {
return teamPoliciesAPI.update(changedPolicy.id, {
calendar_events_enabled: changedPolicy.isChecked,
team_id: teamIdForApi,
});
})
);
await Promise.all([configResponse, ...policyResponses]);
await Promise.all(responses);
renderFlash("success", "Successfully updated policy automations.");
} catch {
renderFlash(
@ -761,8 +780,16 @@ const ManagePolicyPage = ({
const tipId = uniqueId();
calEventsLabel = (
<span>
<div data-tooltip-id={tipId}>Calendar events</div>
<ReactTooltip5 id={tipId} place="left">
<div className="label-text" data-tooltip-id={tipId}>
Calendar events
</div>
<ReactTooltip5
id={tipId}
place="left"
positionStrategy="fixed"
offset={24}
disableStyleInjection
>
Available in Fleet Premium
</ReactTooltip5>
</span>
@ -771,13 +798,15 @@ const ManagePolicyPage = ({
const tipId = uniqueId();
calEventsLabel = (
<span>
<div data-tooltip-id={tipId}>Calendar events</div>
<div className="label-text" data-tooltip-id={tipId}>
Calendar events
</div>
<ReactTooltip5
id={tipId}
place="left"
positionStrategy="fixed"
offset={24}
disableStyleInjection
offset={5}
>
Select a team to manage
<br />
@ -920,7 +949,7 @@ const ManagePolicyPage = ({
availablePolicies={availablePoliciesForAutomation}
isUpdatingAutomations={isUpdatingAutomations}
onExit={toggleOtherWorkflowsModal}
handleSubmit={handleUpdateAutomations}
handleSubmit={handleUpdateOtherWorkflows}
/>
)}
{showAddPolicyModal && (

View File

@ -21,19 +21,43 @@
.Select > .Select-menu-outer {
left: -186px;
width: 360px;
.dropdown__help-text {
color: $ui-fleet-black-50;
}
.is-disabled * {
color: $ui-fleet-black-25;
.label-text {
font-style: normal;
// increase height to allow for broader tooltip activation area
position: absolute;
height: 34px;
width: 100%;
}
.dropdown__help-text {
// compensate for absolute label-text height
margin-top: 20px;
}
.react-tooltip {
@include tooltip-text;
font-style: normal;
text-align: center;
}
}
}
.Select-control {
margin-top: 0;
gap: 6px;
}
.Select-placeholder {
font-weight: $bold;
.Select-placeholder {
color: $core-vibrant-blue;
font-weight: $bold;
}
.dropdown__custom-arrow .dropdown__icon {
svg {
path {
stroke: $core-vibrant-blue-over;
}
}
}
}
}

View File

@ -55,10 +55,6 @@ const CalendarEventsModal = ({
const [formData, setFormData] = useState<ICalendarEventsFormData>({
enabled,
url,
// TODO - stay udpdated on state of backend approach to syncing policies in the policies table
// and in the new calendar table
// id may change if policy was deleted
// name could change if policy was renamed
policies: policies.map((policy) => ({
name: policy.name,
id: policy.id,
@ -87,29 +83,26 @@ const CalendarEventsModal = ({
return errors;
};
// TODO - separate change handlers for checkboxes:
// const onPolicyUpdate = ...
// const onTextFieldUpdate = ...
const onInputChange = useCallback(
(newVal: { name: FormNames; value: string | number | boolean }) => {
// two onChange handlers to handle different levels of nesting in the form data
const onFeatureEnabledOrUrlChange = useCallback(
(newVal: { name: "enabled" | "url"; value: string | boolean }) => {
const { name, value } = newVal;
let newFormData: ICalendarEventsFormData;
// for the first two fields, set the new value directly
if (["enabled", "url"].includes(name)) {
newFormData = { ...formData, [name]: value };
} else if (typeof value === "boolean") {
// otherwise, set the value for a nested policy
const newFormPolicies = formData.policies.map((formPolicy) => {
if (formPolicy.name === name) {
return { ...formPolicy, isChecked: value };
}
return formPolicy;
});
newFormData = { ...formData, policies: newFormPolicies };
} else {
throw TypeError("Unexpected value type for policy checkbox");
}
const newFormData = { ...formData, [name]: value };
setFormData(newFormData);
setFormErrors(validateCalendarEventsFormData(newFormData));
},
[formData]
);
const onPolicyEnabledChange = useCallback(
(newVal: { name: FormNames; value: boolean }) => {
const { name, value } = newVal;
const newFormPolicies = formData.policies.map((formPolicy) => {
if (formPolicy.name === name) {
return { ...formPolicy, isChecked: value };
}
return formPolicy;
});
const newFormData = { ...formData, policies: newFormPolicies };
setFormData(newFormData);
setFormErrors(validateCalendarEventsFormData(newFormData));
},
@ -157,7 +150,7 @@ const CalendarEventsModal = ({
name={name}
// can't use parseTarget as value needs to be set to !currentValue
onChange={() => {
onInputChange({ name, value: !isChecked });
onPolicyEnabledChange({ name, value: !isChecked });
}}
>
{name}
@ -232,7 +225,10 @@ const CalendarEventsModal = ({
<Slider
value={formData.enabled}
onChange={() => {
onInputChange({ name: "enabled", value: !formData.enabled });
onFeatureEnabledOrUrlChange({
name: "enabled",
value: !formData.enabled,
});
}}
inactiveText="Disabled"
activeText="Enabled"
@ -251,7 +247,7 @@ const CalendarEventsModal = ({
<InputField
placeholder="https://server.com/example"
label="Resolution webhook URL"
onChange={onInputChange}
onChange={onFeatureEnabledOrUrlChange}
name="url"
value={formData.url}
parseTarget

View File

@ -6,7 +6,7 @@ import { IAutomationsConfig, IWebhookSettings } from "interfaces/config";
import {
IGlobalIntegrations,
IIntegration,
IIntegrations,
IZendeskJiraIntegrations,
ITeamIntegrations,
} from "interfaces/integration";
import { IPolicy } from "interfaces/policy";
@ -47,7 +47,10 @@ interface ICheckedPolicy {
isChecked: boolean;
}
const findEnabledIntegration = ({ jira, zendesk }: IIntegrations) => {
const findEnabledIntegration = ({
jira,
zendesk,
}: IZendeskJiraIntegrations) => {
return (
jira?.find((j) => j.enable_failing_policies) ||
zendesk?.find((z) => z.enable_failing_policies)

View File

@ -275,7 +275,6 @@ $max-width: 2560px;
}
.Select-placeholder {
color: $core-fleet-black;
font-size: 14px;
line-height: normal;
padding-left: 0;