Skip to content

Commit

Permalink
UI – Calendar events modal follow up (#17788)
Browse files Browse the repository at this point in the history
## 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>
  • Loading branch information
2 people authored and getvictor committed Mar 25, 2024
1 parent d99f7fd commit 89ab202
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 76 deletions.
8 changes: 5 additions & 3 deletions frontend/interfaces/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
6 changes: 4 additions & 2 deletions frontend/pages/SoftwarePage/SoftwarePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import {
IJiraIntegration,
IZendeskIntegration,
IIntegrations,
IZendeskJiraIntegrations,
} from "interfaces/integration";
import { ITeamConfig } from "interfaces/team";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,7 +17,7 @@ interface IAddIntegrationModalProps {
) => void;
serverErrors?: { base: string; email: string };
backendValidators: { [key: string]: string };
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
testingConnection: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -15,7 +15,7 @@ interface IEditIntegrationModalProps {
onCancel: () => void;
onSubmit: (jiraIntegrationSubmitData: IIntegration[]) => void;
backendValidators: { [key: string]: string };
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
integrationEditing?: IIntegrationTableData;
testingConnection: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
IIntegrationFormData,
IIntegrationTableData,
IIntegration,
IIntegrations,
IZendeskJiraIntegrations,
IIntegrationType,
} from "interfaces/integration";

Expand All @@ -26,7 +26,7 @@ interface IIntegrationFormProps {
integrationDestination: string
) => void;
integrationEditing?: IIntegrationTableData;
integrations: IIntegrations;
integrations: IZendeskJiraIntegrations;
integrationEditingUrl?: string;
integrationEditingUsername?: string;
integrationEditingEmail?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.host-actions-dropdown {
@include button-dropdown;
color: $core-fleet-black;
.Select-multi-value-wrapper {
width: 55px;
}
Expand Down
89 changes: 59 additions & 30 deletions frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 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
);
});

// 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,
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(
Expand Down Expand Up @@ -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>
Expand All @@ -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 />
Expand Down Expand Up @@ -920,7 +949,7 @@ const ManagePolicyPage = ({
availablePolicies={availablePoliciesForAutomation}
isUpdatingAutomations={isUpdatingAutomations}
onExit={toggleOtherWorkflowsModal}
handleSubmit={handleUpdateAutomations}
handleSubmit={handleUpdateOtherWorkflows}
/>
)}
{showAddPolicyModal && (
Expand Down
30 changes: 27 additions & 3 deletions frontend/pages/policies/ManagePoliciesPage/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
},
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
Loading

0 comments on commit 89ab202

Please sign in to comment.