Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI – Calendar events modal follow up #17788

Merged
merged 8 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love it

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 });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

}}
>
{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
Loading