Skip to content

Commit

Permalink
fix(instance) add validation for missing root storage, missing networ…
Browse files Browse the repository at this point in the history
…k device name and missing storage volume path #485

Signed-off-by: David Edler <david.edler@canonical.com>
  • Loading branch information
edlerd committed Nov 24, 2023
1 parent af34763 commit 3f20b80
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 53 deletions.
27 changes: 18 additions & 9 deletions src/components/forms/DiskDeviceFormCustom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getConfigurationRowBase } from "components/ConfigurationRow";
import DetachDiskDeviceBtn from "pages/instances/actions/DetachDiskDeviceBtn";
import classnames from "classnames";
import { LxdStorageVolume } from "types/storage";
import { isDiskDeviceMountPointMissing } from "util/instanceValidation";

interface Props {
formik: SharedFormikTypes;
Expand Down Expand Up @@ -39,6 +40,9 @@ const DiskDeviceFormCustom: FC<Props> = ({
source: volume.name,
});
void formik.setFieldValue("devices", copy);

const name = `devices.${copy.length - 1}.path`;
setTimeout(() => document.getElementById(name)?.focus(), 100);
};

const changeVolume = (
Expand Down Expand Up @@ -96,7 +100,7 @@ const DiskDeviceFormCustom: FC<Props> = ({
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: (
<Label forId={`volume-${index}-pool`}>Pool / volume</Label>
<Label forId={`devices.${index}.pool`}>Pool / volume</Label>
),
inherited: (
<div className="custom-disk-volume-source">
Expand All @@ -119,7 +123,7 @@ const DiskDeviceFormCustom: FC<Props> = ({
project={project}
setValue={(volume) => changeVolume(volume, formVolume, index)}
buttonProps={{
id: `volume-${index}-pool`,
id: `devices.${index}.pool`,
appearance: "base",
className: "u-no-margin--bottom",
}}
Expand All @@ -134,11 +138,12 @@ const DiskDeviceFormCustom: FC<Props> = ({
);

if (formVolume.path !== undefined) {
const hasError = isDiskDeviceMountPointMissing(formik, index);
rows.push(
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: (
<Label forId={`volume-${index}-path`} required>
<Label forId={`devices.${index}.path`} required>
Mount point
</Label>
),
Expand All @@ -148,7 +153,8 @@ const DiskDeviceFormCustom: FC<Props> = ({
</div>
) : (
<Input
id={`volume-${index}-path`}
id={`devices.${index}.path`}
name={`devices.${index}.path`}
onBlur={formik.handleBlur}
onChange={(e) => {
void formik.setFieldValue(
Expand All @@ -159,7 +165,8 @@ const DiskDeviceFormCustom: FC<Props> = ({
value={formVolume.path}
type="text"
placeholder="Enter full path (e.g. /data)"
className="u-no-margin--bottom"
className={hasError ? undefined : "u-no-margin--bottom"}
error={hasError ? "Path is required" : undefined}
/>
),
override: "",
Expand All @@ -171,7 +178,7 @@ const DiskDeviceFormCustom: FC<Props> = ({
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: (
<Label forId={`volume-${index}-read-limit`}>Read limit</Label>
<Label forId={`devices.${index}.limits.read`}>Read limit</Label>
),
inherited: isReadOnly ? (
<div className="mono-font">
Expand All @@ -184,7 +191,8 @@ const DiskDeviceFormCustom: FC<Props> = ({
) : (
<div className="custom-disk-device-limits">
<Input
id={`volume-${index}-read-limit`}
id={`devices.${index}.limits.read`}
name={`devices.${index}.limits.read`}
onBlur={formik.handleBlur}
onChange={(e) => {
void formik.setFieldValue(
Expand All @@ -208,7 +216,7 @@ const DiskDeviceFormCustom: FC<Props> = ({
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: (
<Label forId={`volume-${index}-write-limit`}>Write limit</Label>
<Label forId={`devices.${index}.limits.write`}>Write limit</Label>
),
inherited: isReadOnly ? (
<div className="mono-font">
Expand All @@ -221,7 +229,8 @@ const DiskDeviceFormCustom: FC<Props> = ({
) : (
<div className="custom-disk-device-limits">
<Input
id={`volume-${index}-write-limit`}
id={`devices.${index}.limits.write`}
name={`devices.${index}.limits.write`}
onBlur={formik.handleBlur}
onChange={(e) => {
void formik.setFieldValue(
Expand Down
9 changes: 9 additions & 0 deletions src/components/forms/DiskDeviceFormRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import DiskSizeSelector from "components/forms/DiskSizeSelector";
import { LxdStoragePool } from "types/storage";
import { LxdProfile } from "types/profile";
import { removeDevice } from "util/formDevices";
import { hasNoRootDisk } from "util/instanceValidation";

interface Props {
formik: SharedFormikTypes;
Expand Down Expand Up @@ -141,6 +142,14 @@ const DiskDeviceFormRoot: FC<Props> = ({
}),
]}
/>
{hasNoRootDisk(formik.values, profiles) && (
<div className="is-error ">
<p className="p-form-validation__message">
<strong>Error:</strong> Missing root storage. Create an override, or
add a profile with root storage.
</p>
</div>
)}
</>
);
};
Expand Down
9 changes: 8 additions & 1 deletion src/components/forms/FormMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React, { FC } from "react";
import { slugify } from "util/slugify";
import { Button } from "@canonical/react-components";
import classnames from "classnames";

interface Props {
active: string;
setActive: (item: string) => void;
label: string;
disableReason?: string;
hasError?: boolean;
}

const FormMenuItem: FC<Props> = ({
active,
setActive,
label,
disableReason,
hasError,
}) => {
if (disableReason) {
return (
Expand All @@ -29,7 +32,11 @@ const FormMenuItem: FC<Props> = ({
);
}
return (
<li className="p-side-navigation__item">
<li
className={classnames("p-side-navigation__item", {
"has-error": hasError,
})}
>
<a
className="p-side-navigation__link"
onClick={() => setActive(label)}
Expand Down
50 changes: 27 additions & 23 deletions src/components/forms/NetworkDevicesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Button,
Icon,
Input,
Label,
Select,
Tooltip,
useNotify,
Expand All @@ -20,6 +19,7 @@ import { getConfigurationRowBase } from "components/ConfigurationRow";
import Loader from "components/Loader";
import { figureInheritedNetworks } from "util/instanceConfigInheritance";
import { CustomNetworkDevice } from "util/formDevices";
import { isNicDeviceNameMissing } from "util/instanceValidation";

interface Props {
formik: SharedFormikTypes;
Expand Down Expand Up @@ -69,6 +69,9 @@ const NetworkDevicesForm: FC<Props> = ({ formik, project }) => {
const copy = [...formik.values.devices];
copy.push({ type: "nic", name: "" });
void formik.setFieldValue("devices", copy);

const name = `devices.${copy.length - 1}.name`;
setTimeout(() => document.getElementById(name)?.focus(), 100);
};

const getNetworkOptions = () => {
Expand Down Expand Up @@ -134,27 +137,28 @@ or remove the originating item"

return getConfigurationRowBase({
configuration: (
<Label forId={`networkDevice${index}`}>
<b>
{isReadOnly || device.type === "custom-nic" ? (
device.name
) : (
<Input
label="Device name"
required
name={`devices.${index}.name`}
id={`networkName${index}`}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={
(formik.values.devices[index] as LxdNicDevice).name
}
type="text"
placeholder="Enter name"
/>
)}
</b>
</Label>
<>
{isReadOnly || device.type === "custom-nic" ? (
device.name
) : (
<Input
label="Device name"
required
name={`devices.${index}.name`}
id={`devices.${index}.name`}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={(formik.values.devices[index] as LxdNicDevice).name}
type="text"
placeholder="Enter name"
error={
isNicDeviceNameMissing(formik, index)
? "Device name is required"
: undefined
}
/>
)}
</>
),
inherited: "",
override:
Expand All @@ -173,7 +177,7 @@ or remove the originating item"
<Select
label="Network"
name={`devices.${index}.network`}
id={`networkDevice${index}`}
id={`devices.${index}.network`}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
value={
Expand Down
40 changes: 32 additions & 8 deletions src/pages/instances/CreateInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { useFormik } from "formik";
import * as Yup from "yup";
import { createInstance, startInstance } from "api/instances";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import SubmitButton from "components/SubmitButton";
import { LxdImageType, RemoteImage } from "types/image";
Expand Down Expand Up @@ -66,6 +66,12 @@ import { useEventQueue } from "context/eventQueue";
import { getInstanceName } from "util/operations";
import NotificationRow from "components/NotificationRow";
import BaseLayout from "components/BaseLayout";
import { fetchProfiles } from "api/profiles";
import {
hasDiskError,
hasNetworkError,
hasNoRootDisk,
} from "util/instanceValidation";

export type CreateInstanceFormValues = InstanceDetailsFormValues &
FormDeviceValues &
Expand Down Expand Up @@ -213,14 +219,25 @@ const CreateInstance: FC = () => {
}
};

const submit = (values: CreateInstanceFormValues, shouldStart = true) => {
const formUrl = location.pathname + location.search;
navigate(`/ui/project/${project}/instances`);
const { data: profiles = [] } = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});

const instancePayload = values.yaml
const submit = (values: CreateInstanceFormValues, shouldStart = true) => {
const instancePayload: Partial<LxdInstance> = values.yaml
? yamlToObject(values.yaml)
: getPayload(values);

if (hasNoRootDisk(values, profiles)) {
setConfigOpen(true);
setSection(DISK_DEVICES);
return;
}

const formUrl = location.pathname + location.search;
navigate(`/ui/project/${project}/instances`);

createInstance(JSON.stringify(instancePayload), project, values.target)
.then((operation) => {
const instanceName = getInstanceName(operation.metadata);
Expand All @@ -246,7 +263,7 @@ const CreateInstance: FC = () => {
initialValues: location.state?.retryFormValues ?? {
instanceType: "container",
profiles: ["default"],
devices: [{ type: "nic", name: "" }],
devices: [],
readOnly: false,
type: "instance",
},
Expand Down Expand Up @@ -318,6 +335,11 @@ const CreateInstance: FC = () => {
return dumpYaml(payload);
}

const diskError = hasDiskError(formik);
const networkError = hasNetworkError(formik);
const hasErrors =
!formik.isValid || !formik.values.image || diskError || networkError;

return (
<BaseLayout title="Create an instance" contentClassName="create-instance">
<Form onSubmit={formik.handleSubmit} stacked className="form">
Expand All @@ -327,6 +349,8 @@ const CreateInstance: FC = () => {
isConfigDisabled={!formik.values.image}
isConfigOpen={isConfigOpen}
toggleConfigOpen={toggleMenu}
hasDiskError={diskError || hasNoRootDisk(formik.values, profiles)}
hasNetworkError={networkError}
/>
<Row className="form-contents" key={section}>
<Col size={12}>
Expand Down Expand Up @@ -393,15 +417,15 @@ const CreateInstance: FC = () => {
</Button>
<SubmitButton
isSubmitting={formik.isSubmitting}
isDisabled={!formik.isValid || !formik.values.image}
isDisabled={hasErrors}
buttonLabel="Create"
appearance={isLocalIsoImage ? "positive" : "default"}
onClick={() => submit(formik.values, false)}
/>
{!isLocalIsoImage && (
<SubmitButton
isSubmitting={formik.isSubmitting}
isDisabled={!formik.isValid || !formik.values.image}
isDisabled={hasErrors}
buttonLabel="Create and start"
onClick={() => submit(formik.values)}
/>
Expand Down
9 changes: 8 additions & 1 deletion src/pages/instances/EditInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
} from "util/instanceEdit";
import { slugify } from "util/slugify";
import { useEventQueue } from "context/eventQueue";
import { hasDiskError, hasNetworkError } from "util/instanceValidation";

export type EditInstanceFormValues = InstanceEditDetailsFormValues &
FormDeviceValues &
Expand Down Expand Up @@ -161,6 +162,8 @@ const EditInstance: FC<Props> = ({ instance }) => {
isConfigDisabled={false}
isConfigOpen={isConfigOpen}
toggleConfigOpen={toggleMenu}
hasDiskError={hasDiskError(formik)}
hasNetworkError={hasNetworkError(formik)}
/>
<Row className="form-contents" key={activeSection}>
<Col size={12}>
Expand Down Expand Up @@ -239,7 +242,11 @@ const EditInstance: FC<Props> = ({ instance }) => {
</Button>
<SubmitButton
isSubmitting={formik.isSubmitting}
isDisabled={!formik.isValid}
isDisabled={
!formik.isValid ||
hasDiskError(formik) ||
hasNetworkError(formik)
}
buttonLabel="Save changes"
onClick={() => void formik.submitForm()}
/>
Expand Down
Loading

0 comments on commit 3f20b80

Please sign in to comment.