diff --git a/frontend/interfaces/integration.ts b/frontend/interfaces/integration.ts index aea79f99e..f6302a67b 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 0d513c348..20ccb325e 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 0dc4dc630..ef3a69322 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 83d99a14f..e5219f270 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 0cce5bb5a..1d4bad995 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 06bd48a65..04d394a3b 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 80a832026..a49d27612 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 ed99ad013..3c88a2db1 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 eba5abb4e..93847411e 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 3751ffadf..fab5ee9ec 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;