From a10aac29c609b4c930a7474bfdb2a67c45943d1b Mon Sep 17 00:00:00 2001
From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com>
Date: Fri, 22 Mar 2024 11:54:32 -0700
Subject: [PATCH] =?UTF-8?q?UI=20=E2=80=93=20Calendar=20events=20modal=20fo?=
=?UTF-8?q?llow=20up=20(#17788)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Follow-up work to #17717
**Finalize disabled options and tooltips:**
**Only update policies and settings when there's a diff:**

**Reorganize onChange handlers, types**
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling
---
frontend/interfaces/integration.ts | 8 +-
frontend/pages/SoftwarePage/SoftwarePage.tsx | 6 +-
.../AddIntegrationModal.tsx | 4 +-
.../EditIntegrationModal.tsx | 4 +-
.../IntegrationForm/IntegrationForm.tsx | 4 +-
.../HostActionsDropdown/_styles.scss | 1 +
.../ManagePoliciesPage/ManagePoliciesPage.tsx | 89 ++++++++++++-------
.../policies/ManagePoliciesPage/_styles.scss | 30 ++++++-
.../CalendarEventsModal.tsx | 54 ++++++-----
.../OtherWorkflowsModal.tsx | 7 +-
frontend/styles/var/mixins.scss | 1 -
11 files changed, 132 insertions(+), 76 deletions(-)
diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts
index aea79f99ee..f6302a67b8 100644
--- a/frontend/interfaces/integration.ts
+++ b/frontend/interfaces/integration.ts
@@ -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`, 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;
}
diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx
index 0d513c3481..20ccb325e4 100644
--- a/frontend/pages/SoftwarePage/SoftwarePage.tsx
+++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx
@@ -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)
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx
index 0dc4dc630a..ef3a693220 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx
@@ -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;
}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EditIntegrationModal/EditIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EditIntegrationModal/EditIntegrationModal.tsx
index 83d99a14fb..e5219f2708 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EditIntegrationModal/EditIntegrationModal.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/EditIntegrationModal/EditIntegrationModal.tsx
@@ -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;
}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx
index 0cce5bb5a1..1d4bad9950 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx
@@ -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;
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss
index 06bd48a653..04d394a3b5 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss
@@ -1,5 +1,6 @@
.host-actions-dropdown {
@include button-dropdown;
+ color: $core-fleet-black;
.Select-multi-value-wrapper {
width: 55px;
}
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index 80a832026b..a49d276128 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -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;
- // 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[] = [];
+ 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 = (
- Calendar events
-
+
+ Calendar events
+
+
Available in Fleet Premium
@@ -771,13 +798,15 @@ const ManagePolicyPage = ({
const tipId = uniqueId();
calEventsLabel = (
- Calendar events
+
+ Calendar events
+
Select a team to manage
@@ -920,7 +949,7 @@ const ManagePolicyPage = ({
availablePolicies={availablePoliciesForAutomation}
isUpdatingAutomations={isUpdatingAutomations}
onExit={toggleOtherWorkflowsModal}
- handleSubmit={handleUpdateAutomations}
+ handleSubmit={handleUpdateOtherWorkflows}
/>
)}
{showAddPolicyModal && (
diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
index ed99ad0137..3c88a2db1c 100644
--- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss
+++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
@@ -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;
+ }
+ }
+ }
}
}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/CalendarEventsModal/CalendarEventsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/CalendarEventsModal/CalendarEventsModal.tsx
index eba5abb4e0..93847411eb 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/CalendarEventsModal/CalendarEventsModal.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/CalendarEventsModal/CalendarEventsModal.tsx
@@ -55,10 +55,6 @@ const CalendarEventsModal = ({
const [formData, setFormData] = useState({
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 = ({
{
- onInputChange({ name: "enabled", value: !formData.enabled });
+ onFeatureEnabledOrUrlChange({
+ name: "enabled",
+ value: !formData.enabled,
+ });
}}
inactiveText="Disabled"
activeText="Enabled"
@@ -251,7 +247,7 @@ const CalendarEventsModal = ({
{
+const findEnabledIntegration = ({
+ jira,
+ zendesk,
+}: IZendeskJiraIntegrations) => {
return (
jira?.find((j) => j.enable_failing_policies) ||
zendesk?.find((z) => z.enable_failing_policies)
diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss
index 3751ffadf6..fab5ee9ec2 100644
--- a/frontend/styles/var/mixins.scss
+++ b/frontend/styles/var/mixins.scss
@@ -275,7 +275,6 @@ $max-width: 2560px;
}
.Select-placeholder {
- color: $core-fleet-black;
font-size: 14px;
line-height: normal;
padding-left: 0;