mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
fbb271caee
commit
a10aac29c6
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
.host-actions-dropdown {
|
||||
@include button-dropdown;
|
||||
color: $core-fleet-black;
|
||||
.Select-multi-value-wrapper {
|
||||
width: 55px;
|
||||
}
|
||||
|
@ -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(
|
||||
// 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,
|
||||
},
|
||||
// TODO - can omit these?
|
||||
// These fields will never actually be changed here. See comment above
|
||||
// IGlobalIntegrations definition.
|
||||
zendesk: teamConfig?.integrations.zendesk || [],
|
||||
jira: teamConfig?.integrations.jira || [],
|
||||
},
|
||||
},
|
||||
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,
|
||||
// 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 && (
|
||||
|
@ -21,20 +21,44 @@
|
||||
.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 {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
}
|
||||
.dropdown__custom-arrow .dropdown__icon {
|
||||
svg {
|
||||
path {
|
||||
stroke: $core-vibrant-blue-over;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
|
@ -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;
|
||||
const newFormData = { ...formData, [name]: value };
|
||||
setFormData(newFormData);
|
||||
setFormErrors(validateCalendarEventsFormData(newFormData));
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
const onPolicyEnabledChange = useCallback(
|
||||
(newVal: { name: FormNames; value: 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, 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
|
||||
|
@ -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)
|
||||
|
@ -275,7 +275,6 @@ $max-width: 2560px;
|
||||
}
|
||||
|
||||
.Select-placeholder {
|
||||
color: $core-fleet-black;
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
padding-left: 0;
|
||||
|
Loading…
Reference in New Issue
Block a user