Skip to content

Commit

Permalink
EVG-19954: Force save when changing providers (evergreen-ci#2028)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophstad authored Sep 18, 2023
1 parent 8081578 commit 34da89a
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 105 deletions.
49 changes: 49 additions & 0 deletions cypress/integration/distroSettings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,53 @@ describe("using the distro dropdown", () => {
cy.location("pathname").should("not.contain", "localhost");
cy.location("pathname").should("contain", "rhel71-power8-large");
});

describe("warning modal", () => {
it("warns when navigating away from distro settings with unsaved changes and allows returning to distro settings", () => {
cy.getInputByLabel("Notes").type("my note");
cy.dataCy("save-settings-button").should(
"not.have.attr",
"aria-disabled",
"true"
);
cy.dataCy("project-health-link").click();
cy.dataCy("navigation-warning-modal").should("be.visible");
cy.contains("button", "Cancel").click();
cy.dataCy("navigation-warning-modal").should("not.exist");
cy.location("pathname").should(
"eq",
"/distro/localhost/settings/general"
);
});

describe("modifying the distro provider", () => {
beforeEach(() => {
cy.visit("/distro/localhost/settings/provider");
});

it("warns when navigating to another distro settings tab after the provider has changed and allows save", () => {
cy.selectLGOption("Provider", "Docker");
cy.dataCy("save-settings-button").should(
"not.have.attr",
"aria-disabled",
"true"
);
cy.contains("a", "Task Settings").click();
cy.dataCy("save-modal").should("be.visible");
cy.dataCy("provider-warning-banner").should("be.visible");
});

it("shows the standard save warning modal when non-provider fields have changed", () => {
cy.getInputByLabel("User Data").type("test user data");
cy.dataCy("save-settings-button").should(
"not.have.attr",
"aria-disabled",
"true"
);
cy.dataCy("project-health-link").click();
cy.dataCy("navigation-warning-modal").should("be.visible");
cy.dataCy("provider-warning-banner").should("not.exist");
});
});
});
});
109 changes: 10 additions & 99 deletions src/pages/distroSettings/HeaderButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,20 @@
import { useState } from "react";
import { useMutation } from "@apollo/client";
import styled from "@emotion/styled";
import Button from "@leafygreen-ui/button";
import { Radio, RadioGroup } from "@leafygreen-ui/radio-group";
import { Body, BodyProps } from "@leafygreen-ui/typography";
import pluralize from "pluralize";
import { useDistroSettingsAnalytics } from "analytics";
import { ConfirmationModal } from "components/ConfirmationModal";
import { size } from "constants/tokens";
import { useToastContext } from "context/toast";
import {
DistroOnSaveOperation,
DistroQuery,
SaveDistroMutation,
SaveDistroMutationVariables,
} from "gql/generated/types";
import { SAVE_DISTRO } from "gql/mutations";
import { DistroQuery } from "gql/generated/types";
import { useDistroSettingsContext } from "./Context";
import { formToGqlMap } from "./tabs/transformers";
import { FormToGqlFunction, WritableDistroSettingsType } from "./tabs/types";
import { SaveModal } from "./SaveModal";
import { WritableDistroSettingsType } from "./tabs/types";

interface Props {
distro: DistroQuery["distro"];
tab: WritableDistroSettingsType;
}

export const HeaderButtons: React.FC<Props> = ({ distro, tab }) => {
const { sendEvent } = useDistroSettingsAnalytics();
const dispatchToast = useToastContext();

const { getTab, saveTab } = useDistroSettingsContext();
const { formData, hasChanges, hasError } = getTab(tab);
const { getTab } = useDistroSettingsContext();
const { hasChanges, hasError } = getTab(tab);

const [modalOpen, setModalOpen] = useState(false);
const [onSaveOperation, setOnSaveOperation] = useState(
DistroOnSaveOperation.None
);

const [saveDistro] = useMutation<
SaveDistroMutation,
SaveDistroMutationVariables
>(SAVE_DISTRO, {
onCompleted({ saveDistro: { hostCount } }) {
saveTab(tab);
dispatchToast.success(
`Updated distro${
onSaveOperation !== DistroOnSaveOperation.None
? ` and scheduled ${hostCount} ${pluralize(
"host",
hostCount
)} to update`
: ""
}.`
);
},
onError(err) {
dispatchToast.error(err.message);
},
refetchQueries: ["Distro"],
});

const handleSave = () => {
// Only perform the save operation is the tab is valid.
// eslint-disable-next-line no-prototype-builtins
if (formToGqlMap.hasOwnProperty(tab)) {
const formToGql: FormToGqlFunction<typeof tab> = formToGqlMap[tab];
const changes = formToGql(formData, distro);
saveDistro({
variables: {
distro: changes,
onSave: onSaveOperation,
},
});
setModalOpen(false);
sendEvent({ name: "Save distro", section: tab });
}
};

return (
<>
Expand All @@ -87,41 +26,13 @@ export const HeaderButtons: React.FC<Props> = ({ distro, tab }) => {
>
Save changes on page
</Button>
<ConfirmationModal
buttonText="Save"
data-cy="save-modal"
<SaveModal
distro={distro}
open={modalOpen}
onConfirm={() => setModalOpen(false)}
onCancel={() => setModalOpen(false)}
onConfirm={handleSave}
title="Save page"
>
<StyledBody>
Evergreen can perform one of the following actions on save:
</StyledBody>
<RadioGroup
onChange={(e) =>
setOnSaveOperation(e.target.value as DistroOnSaveOperation)
}
value={onSaveOperation}
>
<Radio value={DistroOnSaveOperation.None}>
Nothing, only new hosts will have updated distro settings applied
</Radio>
<Radio value={DistroOnSaveOperation.Decommission}>
Decommission hosts of this distro
</Radio>
<Radio value={DistroOnSaveOperation.RestartJasper}>
Restart Jasper service on running hosts of this distro
</Radio>
<Radio value={DistroOnSaveOperation.Reprovision}>
Reprovision running hosts of this distro
</Radio>
</RadioGroup>
</ConfirmationModal>
tab={tab}
/>
</>
);
};

const StyledBody = styled(Body)<BodyProps>`
margin-bottom: ${size.xs};
`;
130 changes: 130 additions & 0 deletions src/pages/distroSettings/SaveModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useState } from "react";
import { useMutation } from "@apollo/client";
import styled from "@emotion/styled";
import { Radio, RadioGroup } from "@leafygreen-ui/radio-group";
import { Body, BodyProps } from "@leafygreen-ui/typography";
import pluralize from "pluralize";
import { useDistroSettingsAnalytics } from "analytics";
import { ConfirmationModal } from "components/ConfirmationModal";
import { size } from "constants/tokens";
import { useToastContext } from "context/toast";
import {
DistroOnSaveOperation,
DistroQuery,
SaveDistroMutation,
SaveDistroMutationVariables,
} from "gql/generated/types";
import { SAVE_DISTRO } from "gql/mutations";
import { useDistroSettingsContext } from "./Context";
import { formToGqlMap } from "./tabs/transformers";
import { FormToGqlFunction, WritableDistroSettingsType } from "./tabs/types";

type SaveModalProps = {
banner?: React.ReactNode;
distro: DistroQuery["distro"];
onCancel?: () => void;
onConfirm?: () => void;
open: boolean;
tab: WritableDistroSettingsType;
};

export const SaveModal: React.FC<SaveModalProps> = ({
banner,
distro,
onCancel,
onConfirm,
open,
tab,
}) => {
const { sendEvent } = useDistroSettingsAnalytics();
const dispatchToast = useToastContext();

const { getTab, saveTab } = useDistroSettingsContext();
const { formData } = getTab(tab);
const [onSaveOperation, setOnSaveOperation] = useState(
DistroOnSaveOperation.None
);

const [saveDistro] = useMutation<
SaveDistroMutation,
SaveDistroMutationVariables
>(SAVE_DISTRO, {
onCompleted({ saveDistro: { hostCount } }) {
saveTab(tab);
dispatchToast.success(
`Updated distro${
onSaveOperation !== DistroOnSaveOperation.None
? ` and scheduled ${hostCount} ${pluralize(
"host",
hostCount
)} to update`
: ""
}.`
);
},
onError(err) {
dispatchToast.error(err.message);
},
refetchQueries: ["Distro"],
});

const handleSave = () => {
// Only perform the save operation if the tab is valid.
// eslint-disable-next-line no-prototype-builtins
if (formToGqlMap.hasOwnProperty(tab)) {
const formToGql: FormToGqlFunction<typeof tab> = formToGqlMap[tab];
const changes = formToGql(formData, distro);
saveDistro({
variables: {
distro: changes,
onSave: onSaveOperation,
},
});
sendEvent({ name: "Save distro", section: tab });
}
};

return (
<ConfirmationModal
buttonText="Save"
data-cy="save-modal"
open={open}
onCancel={() => {
onCancel?.();
}}
onConfirm={() => {
onConfirm?.();
handleSave();
}}
title="Save page"
>
{banner}
<StyledBody>
Evergreen can perform one of the following actions on save:
</StyledBody>
<RadioGroup
onChange={(e) =>
setOnSaveOperation(e.target.value as DistroOnSaveOperation)
}
value={onSaveOperation}
>
<Radio value={DistroOnSaveOperation.None}>
Nothing, only new hosts will have updated distro settings applied
</Radio>
<Radio value={DistroOnSaveOperation.Decommission}>
Decommission hosts of this distro
</Radio>
<Radio value={DistroOnSaveOperation.RestartJasper}>
Restart Jasper service on running hosts of this distro
</Radio>
<Radio value={DistroOnSaveOperation.Reprovision}>
Reprovision running hosts of this distro
</Radio>
</RadioGroup>
</ConfirmationModal>
);
};

const StyledBody = styled(Body)<BodyProps>`
margin-bottom: ${size.xs};
`;
1 change: 1 addition & 0 deletions src/pages/distroSettings/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const DistroSettingsTabs: React.FC<Props> = ({ distro }) => {
path={DistroSettingsTabRoutes.Provider}
element={
<ProviderTab
distro={distro}
distroData={tabData[DistroSettingsTabRoutes.Provider]}
/>
}
Expand Down
32 changes: 27 additions & 5 deletions src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
import { useMemo } from "react";
import { useDistroSettingsContext } from "../../Context";
import { BaseTab } from "../BaseTab";
import {
FormToGqlFunction,
WritableDistroSettingsTabs,
WritableDistroSettingsType,
} from "../types";
import { getFormSchema } from "./getFormSchema";
import { TabProps } from "./types";

export const ProviderTab: React.FC<TabProps> = ({ distroData }) => {
const initialFormState = distroData;
import { ProviderFormState, TabProps } from "./types";
import { UnsavedModal } from "./UnsavedModal";

export const ProviderTab: React.FC<TabProps> = ({ distro, distroData }) => {
const formSchema = useMemo(() => getFormSchema(), []);

const { getTab } = useDistroSettingsContext();

// @ts-expect-error - see TabState for details.
const {
formData,
initialData,
}: {
formData: ProviderFormState;
initialData: ReturnType<FormToGqlFunction<WritableDistroSettingsType>>;
} = getTab(WritableDistroSettingsTabs.Provider);

return (
<BaseTab formSchema={formSchema} initialFormState={initialFormState} />
<>
{/* Use conditional rendering instead of the shouldBlock prop so that modifying fields other than the provider triggers the standard navigation warning modal */}
{initialData?.provider !== formData?.provider?.providerName && (
<UnsavedModal distro={distro} shouldBlock />
)}
<BaseTab formSchema={formSchema} initialFormState={distroData} />
</>
);
};
Loading

0 comments on commit 34da89a

Please sign in to comment.