Skip to content

Commit

Permalink
feat(ui): display vm host and architecure errors
Browse files Browse the repository at this point in the history
Display errors when adding a KVM host. Display an error if there are no architectures when composing
from an LXD host.

Fixes: canonical#3115. Fixes: canonical#3114.
  • Loading branch information
huwshimi committed Oct 7, 2021
1 parent ae1442e commit 23060a6
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 5 deletions.
Expand Up @@ -4,6 +4,7 @@ import { MemoryRouter } from "react-router-dom";
import configureStore from "redux-mock-store";

import AddLxd from "./AddLxd";
import CredentialsForm from "./CredentialsForm";

import { actions as podActions } from "app/store/pod";
import { PodType } from "app/store/pod/constants";
Expand Down Expand Up @@ -167,4 +168,22 @@ describe("AddLxd", () => {
)
);
});

it("can display submission errors", () => {
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<MemoryRouter
initialEntries={[{ pathname: "/kvm/add", key: "testKey" }]}
>
<AddLxd clearHeaderContent={jest.fn()} setKvmType={jest.fn()} />
</MemoryRouter>
</Provider>
);
wrapper.find(CredentialsForm).invoke("setSubmissionErrors")("Uh oh!");
wrapper.update();
expect(
wrapper.find("Notification[data-test='submission-error']").exists()
).toBe(true);
});
});
13 changes: 13 additions & 0 deletions ui/src/app/kvm/components/KVMHeaderForms/AddKVM/AddLxd/AddLxd.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";

import { Notification } from "@canonical/react-components";
import { useDispatch, useSelector } from "react-redux";

import type { SetKvmType } from "../AddKVM";
Expand Down Expand Up @@ -34,6 +35,7 @@ export const AddLxd = ({
const resourcePools = useSelector(resourcePoolSelectors.all);
const zones = useSelector(zoneSelectors.all);
const [step, setStep] = useState<AddLxdStepValues>("credentials");
const [submissionErrors, setSubmissionErrors] = useState<string | null>(null);
const [newPodValues, setNewPodValues] = useState<NewPodValues>({
certificate: "",
key: "",
Expand Down Expand Up @@ -65,13 +67,23 @@ export const AddLxd = ({
currentStep={step}
/>
<hr />
{submissionErrors ? (
<Notification
data-test="submission-error"
severity="negative"
title="Error:"
>
{submissionErrors}
</Notification>
) : null}
{step === AddLxdSteps.CREDENTIALS && (
<CredentialsForm
clearHeaderContent={clearHeaderContent}
newPodValues={newPodValues}
setKvmType={setKvmType}
setNewPodValues={setNewPodValues}
setStep={setStep}
setSubmissionErrors={setSubmissionErrors}
/>
)}
{step === AddLxdSteps.AUTHENTICATION && (
Expand All @@ -87,6 +99,7 @@ export const AddLxd = ({
clearHeaderContent={clearHeaderContent}
newPodValues={newPodValues}
setStep={setStep}
setSubmissionErrors={setSubmissionErrors}
/>
)}
</>
Expand Down
Expand Up @@ -76,6 +76,7 @@ describe("CredentialsForm", () => {
setNewPodValues={setNewPodValues}
setKvmType={jest.fn()}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -129,6 +130,7 @@ describe("CredentialsForm", () => {
setNewPodValues={setNewPodValues}
setKvmType={jest.fn()}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -196,6 +198,7 @@ describe("CredentialsForm", () => {
setNewPodValues={jest.fn()}
setKvmType={jest.fn()}
setStep={setStep}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -231,6 +234,7 @@ describe("CredentialsForm", () => {
setNewPodValues={jest.fn()}
setKvmType={jest.fn()}
setStep={setStep}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -264,6 +268,7 @@ describe("CredentialsForm", () => {
setNewPodValues={jest.fn()}
setKvmType={jest.fn()}
setStep={setStep}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -299,11 +304,47 @@ describe("CredentialsForm", () => {
setNewPodValues={jest.fn()}
setKvmType={jest.fn()}
setStep={setStep}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
);

expect(setStep).not.toHaveBeenCalled();
});

it("clears the submission errors when unmounting", () => {
const setSubmissionErrors = jest.fn();
state.pod.projects = {
"192.168.1.1": [podProjectFactory()],
};
state.pod.errors = "Failed to fetch projects.";
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<MemoryRouter
initialEntries={[{ pathname: "/kvm/add", key: "testKey" }]}
>
<CredentialsForm
clearHeaderContent={jest.fn()}
newPodValues={{
certificate: "certificate",
key: "key",
name: "my-favourite-kvm",
password: "",
pool: "0",
power_address: "192.168.1.1",
zone: "0",
}}
setNewPodValues={jest.fn()}
setKvmType={jest.fn()}
setStep={jest.fn()}
setSubmissionErrors={setSubmissionErrors}
/>
</MemoryRouter>
</Provider>
);
wrapper.unmount();
expect(setSubmissionErrors).toHaveBeenCalled();
});
});
Expand Up @@ -29,6 +29,7 @@ type Props = {
setKvmType: SetKvmType;
setNewPodValues: (values: NewPodValues) => void;
setStep: (step: AddLxdStepValues) => void;
setSubmissionErrors: (submissionErrors: string | null) => void;
};

export const CredentialsForm = ({
Expand All @@ -37,6 +38,7 @@ export const CredentialsForm = ({
setKvmType,
setNewPodValues,
setStep,
setSubmissionErrors,
}: Props): JSX.Element => {
const dispatch = useDispatch();
const projects = useSelector((state: RootState) =>
Expand Down Expand Up @@ -78,6 +80,13 @@ export const CredentialsForm = ({
}
}, [setStep, shouldGoToProjectStep]);

useEffect(
() => () => {
setSubmissionErrors(null);
},
[setSubmissionErrors]
);

const CredentialsFormSchema = Yup.object()
.shape({
certificate: shouldGenerateCert
Expand Down
@@ -1,3 +1,5 @@
import { useEffect } from "react";

import { Col, Row } from "@canonical/react-components";
import { useFormikContext } from "formik";
import { useSelector } from "react-redux";
Expand Down Expand Up @@ -27,7 +29,16 @@ export const CredentialsFormFields = ({
const lxdAddresses = useSelector(podSelectors.groupByLxdServer).map(
(group) => group.address
);
const { setFieldValue } = useFormikContext<CredentialsFormValues>();
const { setFieldValue, setFieldTouched } =
useFormikContext<CredentialsFormValues>();

useEffect(() => {
// The validation schema changes depending on the state of
// `shouldGenerateCert`. Here we touch the fields so that the new validation
// is applied to the fields that changed.
setFieldTouched("certificate");
setFieldTouched("key");
}, [shouldGenerateCert, setFieldTouched]);

return (
<Row>
Expand Down
Expand Up @@ -69,6 +69,7 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand All @@ -94,6 +95,7 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -125,6 +127,7 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand All @@ -149,6 +152,7 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -192,6 +196,7 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={jest.fn()}
setSubmissionErrors={jest.fn()}
/>
</MemoryRouter>
</Provider>
Expand Down Expand Up @@ -221,6 +226,7 @@ describe("SelectProjectForm", () => {

it("reverts back to credentials step if attempt to create pod results in error", () => {
const setStep = jest.fn();
const setSubmissionErrors = jest.fn();
state.pod.errors = "it didn't work";
const store = mockStore(state);
mount(
Expand All @@ -232,11 +238,13 @@ describe("SelectProjectForm", () => {
clearHeaderContent={jest.fn()}
newPodValues={newPodValues}
setStep={setStep}
setSubmissionErrors={setSubmissionErrors}
/>
</MemoryRouter>
</Provider>
);

expect(setStep).toHaveBeenCalledWith(AddLxdSteps.CREDENTIALS);
expect(setSubmissionErrors).toHaveBeenCalledWith("it didn't work");
});
});
Expand Up @@ -20,18 +20,20 @@ import { actions as podActions } from "app/store/pod";
import { PodType } from "app/store/pod/constants";
import podSelectors from "app/store/pod/selectors";
import type { RootState } from "app/store/root/types";
import { preparePayload } from "app/utils";
import { formatErrors, preparePayload } from "app/utils";

type Props = {
clearHeaderContent: ClearHeaderContent;
newPodValues: NewPodValues;
setStep: (step: AddLxdStepValues) => void;
setSubmissionErrors: (submissionErrors: string | null) => void;
};

export const SelectProjectForm = ({
clearHeaderContent,
newPodValues,
setStep,
setSubmissionErrors,
}: Props): JSX.Element => {
const dispatch = useDispatch();
const errors = useSelector(podSelectors.errors);
Expand Down Expand Up @@ -76,15 +78,17 @@ export const SelectProjectForm = ({
useEffect(() => {
if (!!errors) {
dispatch(podActions.clearProjects());
setSubmissionErrors(formatErrors(errors));
setStep(AddLxdSteps.CREDENTIALS);
}
}, [dispatch, errors, setStep]);
}, [dispatch, errors, setStep, setSubmissionErrors]);

useEffect(() => {
return () => {
dispatch(podActions.clearProjects());
dispatch(cleanup());
};
}, [dispatch]);
}, [dispatch, cleanup]);

return (
<FormikForm<SelectProjectFormValues>
Expand Down
Expand Up @@ -269,7 +269,7 @@ const ComposeForm = ({ clearHeaderContent }: Props): JSX.Element => {
};

const ComposeFormSchema = Yup.object().shape({
architecture: Yup.string(),
architecture: Yup.string().required("An architecture is required."),
cores: Yup.number()
.positive("Cores must be a positive number.")
.min(1, "Cores must be a positive number.")
Expand Down

0 comments on commit 23060a6

Please sign in to comment.