diff --git a/src/components/ConfigurationRow.tsx b/src/components/ConfigurationRow.tsx index bd17a036a7..5b44a73d98 100644 --- a/src/components/ConfigurationRow.tsx +++ b/src/components/ConfigurationRow.tsx @@ -14,6 +14,7 @@ import { NetworkFormValues } from "pages/networks/forms/NetworkForm"; import { ProjectFormValues } from "pages/projects/CreateProject"; import { getConfigRowMetadata } from "util/configInheritance"; import { StoragePoolFormValues } from "pages/storage/forms/StoragePoolForm"; +import { ensureEditMode } from "util/instanceEdit"; export type ConfigurationRowFormikValues = | InstanceAndProfileFormValues @@ -22,7 +23,7 @@ export type ConfigurationRowFormikValues = | ProjectFormValues | StoragePoolFormValues; -type ConfigurationRowFormikProps = +export type ConfigurationRowFormikProps = | InstanceAndProfileFormikProps | FormikProps | FormikProps @@ -60,12 +61,20 @@ export const getConfigurationRow = ({ const overrideValue = readOnlyRenderer(value === "" ? "-" : value); const metadata = getConfigRowMetadata(formik.values, name); + const enableOverride = () => { + void formik.setFieldValue(name, defaultValue); + }; + + const focusOverride = () => { + setTimeout(() => document.getElementById(name)?.focus(), 100); + }; + const toggleDefault = () => { if (isOverridden) { void formik.setFieldValue(name, undefined); } else { - void formik.setFieldValue(name, defaultValue); - setTimeout(() => document.getElementById(name)?.focus(), 100); + enableOverride(); + focusOverride(); } }; @@ -132,7 +141,29 @@ export const getConfigurationRow = ({ const renderOverride = (): ReactNode => { if (formik.values.readOnly) { - return overrideValue; + return ( + <> + {overrideValue} + {!disabled && ( + + )} + + ); } if (isOverridden) { return getForm(); diff --git a/src/components/forms/DiskDeviceFormCustom.tsx b/src/components/forms/DiskDeviceFormCustom.tsx index 8ff95fd39d..1dcdfde01b 100644 --- a/src/components/forms/DiskDeviceFormCustom.tsx +++ b/src/components/forms/DiskDeviceFormCustom.tsx @@ -1,5 +1,5 @@ -import { FC } from "react"; -import { Icon, Input, Label } from "@canonical/react-components"; +import React, { FC } from "react"; +import { Button, Icon, Input, Label } from "@canonical/react-components"; import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues"; import { EditInstanceFormValues } from "pages/instances/EditInstance"; import { InheritedVolume } from "util/configInheritance"; @@ -13,6 +13,7 @@ import DetachDiskDeviceBtn from "pages/instances/actions/DetachDiskDeviceBtn"; import classnames from "classnames"; import { LxdStorageVolume } from "types/storage"; import { isDiskDeviceMountPointMissing } from "util/instanceValidation"; +import { ensureEditMode } from "util/instanceEdit"; interface Props { formik: InstanceAndProfileFormikProps; @@ -30,6 +31,10 @@ const DiskDeviceFormCustom: FC = ({ .filter((device) => device.name !== "root" && device.type === "disk") .map((device) => device as FormDiskDevice); + const focusField = (name: string) => { + setTimeout(() => document.getElementById(name)?.focus(), 100); + }; + const addVolume = (volume: LxdStorageVolume) => { const copy = [...formik.values.devices]; copy.push({ @@ -42,7 +47,7 @@ const DiskDeviceFormCustom: FC = ({ void formik.setFieldValue("devices", copy); const name = `devices.${copy.length - 1}.path`; - setTimeout(() => document.getElementById(name)?.focus(), 100); + focusField(name); }; const changeVolume = ( @@ -71,6 +76,22 @@ const DiskDeviceFormCustom: FC = ({ return candidate; }; + const editButton = (fieldName: string) => ( + + ); + const rows: MainTableRow[] = []; customVolumes.map((formVolume) => { @@ -83,15 +104,20 @@ const DiskDeviceFormCustom: FC = ({ - void formik.setFieldValue(`devices.${index}.name`, name) - } + setName={(name) => { + ensureEditMode(formik); + void formik.setFieldValue(`devices.${index}.name`, name); + }} /> ), inherited: "", - override: !readOnly && ( - removeDevice(index, formik)} /> + override: ( + { + ensureEditMode(formik); + removeDevice(index, formik); + }} + /> ), }), ); @@ -105,33 +131,29 @@ const DiskDeviceFormCustom: FC = ({ inherited: (
{formVolume.pool} / {formVolume.source}
- {!readOnly && ( - changeVolume(volume, formVolume, index)} - buttonProps={{ - id: `devices.${index}.pool`, - appearance: "base", - className: "u-no-margin--bottom", - "aria-label": `Select storage volume`, - }} - > - - - )} + { + ensureEditMode(formik); + changeVolume(volume, formVolume, index); + }} + buttonProps={{ + id: `devices.${index}.pool`, + appearance: "base", + className: "u-no-margin--bottom", + title: "Select storage volume", + dense: true, + }} + > + +
), override: "", @@ -149,8 +171,11 @@ const DiskDeviceFormCustom: FC = ({ ), inherited: readOnly ? ( -
- {formVolume.path} +
+
+ {formVolume.path} +
+ {editButton(`devices.${index}.path`)}
) : ( = ({ ), inherited: readOnly ? ( -
- - {formVolume.limits?.read - ? `${formVolume.limits.read} IOPS` - : "none"} - +
+
+ + {formVolume.limits?.read + ? `${formVolume.limits.read} IOPS` + : "none"} + +
+ {editButton(`devices.${index}.limits.read`)}
) : (
@@ -220,12 +248,15 @@ const DiskDeviceFormCustom: FC = ({ ), inherited: readOnly ? ( -
- - {formVolume.limits?.write - ? `${formVolume.limits.write} IOPS` - : "none"} - +
+
+ + {formVolume.limits?.write + ? `${formVolume.limits.write} IOPS` + : "none"} + +
+ {editButton(`devices.${index}.limits.write`)}
) : (
@@ -262,12 +293,16 @@ const DiskDeviceFormCustom: FC = ({ )} - {!readOnly && ( - - - Attach disk device - - )} + { + ensureEditMode(formik); + addVolume(volume); + }} + > + + Attach disk device +
); }; diff --git a/src/components/forms/DiskDeviceFormInherited.tsx b/src/components/forms/DiskDeviceFormInherited.tsx index 617a297617..d38907be4a 100644 --- a/src/components/forms/DiskDeviceFormInherited.tsx +++ b/src/components/forms/DiskDeviceFormInherited.tsx @@ -10,6 +10,7 @@ import { MainTableRow } from "@canonical/react-components/dist/components/MainTa import classnames from "classnames"; import { removeDevice } from "util/formDevices"; import DetachDiskDeviceBtn from "pages/instances/actions/DetachDiskDeviceBtn"; +import { ensureEditMode } from "util/instanceEdit"; interface Props { formik: InstanceAndProfileFormikProps; @@ -53,23 +54,27 @@ const DiskDeviceFormInherited: FC = ({ formik, inheritedVolumes }) => {
), inherited: "", - override: readOnly ? ( - isNoneDevice ? ( - <>Detached - ) : null - ) : isNoneDevice ? ( + override: isNoneDevice ? ( ) : ( - addNoneDevice(item.key)} /> + { + ensureEditMode(formik); + addNoneDevice(item.key); + }} + /> ), }), ); diff --git a/src/components/forms/DiskDeviceFormRoot.tsx b/src/components/forms/DiskDeviceFormRoot.tsx index 806e620a03..2fac0154a1 100644 --- a/src/components/forms/DiskDeviceFormRoot.tsx +++ b/src/components/forms/DiskDeviceFormRoot.tsx @@ -13,6 +13,7 @@ import { LxdStoragePool } from "types/storage"; import { LxdProfile } from "types/profile"; import { removeDevice } from "util/formDevices"; import { hasNoRootDisk } from "util/instanceValidation"; +import { ensureEditMode } from "util/instanceEdit"; interface Props { formik: InstanceAndProfileFormikProps; @@ -64,33 +65,37 @@ const DiskDeviceFormRoot: FC = ({ className: "override-with-form", configuration: Root storage, inherited: "", - override: - !readOnly && - (hasRootStorage ? ( -
- -
- ) : ( + override: hasRootStorage ? ( +
- )), +
+ ) : ( + + ), }), getDiskDeviceRow({ diff --git a/src/components/forms/NetworkDevicesForm.tsx b/src/components/forms/NetworkDevicesForm.tsx index fd8e7817c0..2be22f532a 100644 --- a/src/components/forms/NetworkDevicesForm.tsx +++ b/src/components/forms/NetworkDevicesForm.tsx @@ -20,6 +20,7 @@ import Loader from "components/Loader"; import { getInheritedNetworks } from "util/configInheritance"; import { CustomNetworkDevice } from "util/formDevices"; import { isNicDeviceNameMissing } from "util/instanceValidation"; +import { ensureEditMode } from "util/instanceEdit"; interface Props { formik: InstanceAndProfileFormikProps; @@ -59,6 +60,11 @@ const NetworkDevicesForm: FC = ({ formik, project }) => { return ; } + const focusNetwork = (id: number) => { + const name = `devices.${id}.name`; + setTimeout(() => document.getElementById(name)?.focus(), 100); + }; + const removeNetwork = (index: number) => { const copy = [...formik.values.devices]; copy.splice(index, 1); @@ -70,8 +76,7 @@ const NetworkDevicesForm: FC = ({ formik, project }) => { copy.push({ type: "nic", name: "", network: networks[0]?.name ?? "" }); void formik.setFieldValue("devices", copy); - const name = `devices.${copy.length - 1}.name`; - setTimeout(() => document.getElementById(name)?.focus(), 100); + focusNetwork(copy.length - 1); }; const getNetworkOptions = () => { @@ -171,33 +176,58 @@ or remove the originating item" {" "} - ) : readOnly ? ( - (formik.values.devices[index] as LxdNicDevice).network ) : (
- + )}
+ {readOnly && ( + + )}
@@ -205,18 +235,23 @@ or remove the originating item" }); }), - readOnly - ? {} - : getConfigurationRowBase({ - configuration: "", - inherited: "", - override: ( - - ), - }), + getConfigurationRowBase({ + configuration: "", + inherited: "", + override: ( + + ), + }), ].filter((row) => Object.values(row).length > 0)} emptyStateMsg="No networks defined" /> diff --git a/src/components/forms/RenameDiskDeviceInput.tsx b/src/components/forms/RenameDiskDeviceInput.tsx index 86c804d4bc..a2d5288190 100644 --- a/src/components/forms/RenameDiskDeviceInput.tsx +++ b/src/components/forms/RenameDiskDeviceInput.tsx @@ -5,25 +5,10 @@ interface Props { name: string; index: number; setName: (val: string) => void; - readOnly: boolean; } -const RenameDiskDeviceInput: FC = ({ - name, - index, - setName, - readOnly, -}) => { +const RenameDiskDeviceInput: FC = ({ name, index, setName }) => { const [isEditing, setEditing] = useState(false); - - if (readOnly) { - return ( -
- {name} -
- ); - } - return (
{isEditing ? ( diff --git a/src/pages/instances/CreateInstance.tsx b/src/pages/instances/CreateInstance.tsx index 84c4cb585b..62fee6ea52 100644 --- a/src/pages/instances/CreateInstance.tsx +++ b/src/pages/instances/CreateInstance.tsx @@ -450,7 +450,6 @@ const CreateInstance: FC = () => { {section === YAML_CONFIGURATION && ( void formik.setFieldValue("yaml", yaml)} > diff --git a/src/pages/instances/EditInstance.tsx b/src/pages/instances/EditInstance.tsx index 5278449988..fc92709c11 100644 --- a/src/pages/instances/EditInstance.tsx +++ b/src/pages/instances/EditInstance.tsx @@ -46,6 +46,7 @@ import { updateMaxHeight } from "util/updateMaxHeight"; import DiskDeviceForm from "components/forms/DiskDeviceForm"; import NetworkDevicesForm from "components/forms/NetworkDevicesForm"; import { + ensureEditMode, getInstanceEditValues, getInstancePayload, InstanceEditSchema, @@ -95,6 +96,7 @@ const EditInstance: FC = ({ instance }) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const [isConfigOpen, setConfigOpen] = useState(true); + const [version, setVersion] = useState(0); if (!project) { return <>Missing project; @@ -230,10 +232,12 @@ const EditInstance: FC = ({ instance }) => { {section === slugify(YAML_CONFIGURATION) && ( void formik.setFieldValue("yaml", yaml)} - readOnly={readOnly} + setYaml={(yaml) => { + ensureEditMode(formik); + void formik.setFieldValue("yaml", yaml); + }} > This is the YAML representation of the instance. @@ -252,20 +256,14 @@ const EditInstance: FC = ({ instance }) => { - {readOnly ? ( - - ) : ( + {readOnly ? null : ( <> diff --git a/src/pages/instances/forms/EditInstanceDetails.tsx b/src/pages/instances/forms/EditInstanceDetails.tsx index 44b09f2ae8..847a431bdc 100644 --- a/src/pages/instances/forms/EditInstanceDetails.tsx +++ b/src/pages/instances/forms/EditInstanceDetails.tsx @@ -1,12 +1,13 @@ import { FC } from "react"; import { Col, Input, Row } from "@canonical/react-components"; -import ProfileSelect from "pages/profiles/ProfileSelector"; +import ProfileSelector from "pages/profiles/ProfileSelector"; import { FormikProps } from "formik/dist/types"; import { EditInstanceFormValues } from "pages/instances/EditInstance"; import { useSettings } from "context/useSettings"; import { isClusteredServer } from "util/settings"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; import ScrollableForm from "components/ScrollableForm"; +import { ensureEditMode } from "util/instanceEdit"; export const instanceEditDetailPayload = (values: EditInstanceFormValues) => { return { @@ -23,7 +24,6 @@ interface Props { } const EditInstanceDetails: FC = ({ formik, project }) => { - const readOnly = formik.values.readOnly; const { data: settings } = useSettings(); const isClustered = isClusteredServer(settings); @@ -51,10 +51,12 @@ const EditInstanceDetails: FC = ({ formik, project }) => { label="Description" placeholder="Enter description" onBlur={formik.handleBlur} - onChange={formik.handleChange} + onChange={(e) => { + ensureEditMode(formik); + formik.handleChange(e); + }} value={formik.values.description} dynamicHeight - disabled={readOnly} /> @@ -73,11 +75,13 @@ const EditInstanceDetails: FC = ({ formik, project }) => { )} - void formik.setFieldValue("profiles", value)} - readOnly={readOnly} + setSelected={(value) => { + ensureEditMode(formik); + void formik.setFieldValue("profiles", value); + }} /> ); diff --git a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx index 54f3cacd9e..5de3744844 100644 --- a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx +++ b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx @@ -7,7 +7,7 @@ import { Row, Select, } from "@canonical/react-components"; -import ProfileSelect from "pages/profiles/ProfileSelector"; +import ProfileSelector from "pages/profiles/ProfileSelector"; import SelectImageBtn from "pages/images/actions/SelectImageBtn"; import { isContainerOnlyImage, @@ -170,7 +170,7 @@ const InstanceCreateDetailsForm: FC = ({ - void formik.setFieldValue("profiles", value)} diff --git a/src/pages/networks/EditNetwork.tsx b/src/pages/networks/EditNetwork.tsx index 515148b3a5..738bb7ca87 100644 --- a/src/pages/networks/EditNetwork.tsx +++ b/src/pages/networks/EditNetwork.tsx @@ -38,6 +38,7 @@ const EditNetwork: FC = ({ network, project }) => { const { section } = useParams<{ section?: string }>(); const queryClient = useQueryClient(); const controllerState = useState(null); + const [version, setVersion] = useState(0); if (!network?.managed) { return ( @@ -118,20 +119,17 @@ const EditNetwork: FC = ({ network, project }) => { project={project} section={section ?? slugify(MAIN_CONFIGURATION)} setSection={setSection} + version={version} /> - {readOnly ? ( - - ) : ( + {readOnly ? null : ( <> diff --git a/src/pages/networks/forms/NetworkForm.tsx b/src/pages/networks/forms/NetworkForm.tsx index c5858fd54f..58c54e592d 100644 --- a/src/pages/networks/forms/NetworkForm.tsx +++ b/src/pages/networks/forms/NetworkForm.tsx @@ -37,6 +37,7 @@ import { useDocs } from "context/useDocs"; import YamlConfirmation from "components/forms/YamlConfirmation"; import { getHandledNetworkConfigKeys, getNetworkKey } from "util/networks"; import NetworkFormOvn from "pages/networks/forms/NetworkFormOvn"; +import { ensureEditMode } from "util/instanceEdit"; export interface NetworkFormValues { readOnly: boolean; @@ -159,6 +160,7 @@ interface Props { project: string; section: string; setSection: (section: string) => void; + version?: number; } const NetworkForm: FC = ({ @@ -167,6 +169,7 @@ const NetworkForm: FC = ({ project, section, setSection, + version = 0, }) => { const [confirmModal, setConfirmModal] = useState(null); const docBaseLink = useDocs(); @@ -219,10 +222,12 @@ const NetworkForm: FC = ({ {section === slugify(OVN) && } {section === slugify(YAML_CONFIGURATION) && ( void formik.setFieldValue("yaml", yaml)} - readOnly={formik.values.readOnly} + setYaml={(yaml) => { + ensureEditMode(formik); + void formik.setFieldValue("yaml", yaml); + }} > This is the YAML representation of the network. diff --git a/src/pages/networks/forms/NetworkFormMain.tsx b/src/pages/networks/forms/NetworkFormMain.tsx index 091c70811f..fbe6091534 100644 --- a/src/pages/networks/forms/NetworkFormMain.tsx +++ b/src/pages/networks/forms/NetworkFormMain.tsx @@ -11,6 +11,7 @@ import { optionTrueFalse } from "util/instanceOptions"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; import ScrollableForm from "components/ScrollableForm"; import NetworkParentSelector from "pages/networks/forms/NetworkParentSelector"; +import { ensureEditMode } from "util/instanceEdit"; interface Props { formik: FormikProps; @@ -23,7 +24,10 @@ const NetworkFormMain: FC = ({ formik, project }) => { id: id, name: id, onBlur: formik.handleBlur, - onChange: formik.handleChange, + onChange: (e: unknown) => { + ensureEditMode(formik); + formik.handleChange(e); + }, value: formik.values[id] ?? "", error: formik.touched[id] ? (formik.errors[id] as ReactNode) : null, placeholder: `Enter ${id.replaceAll("_", " ")}`, @@ -50,22 +54,14 @@ const NetworkFormMain: FC = ({ formik, project }) => { {formik.values.networkType === "ovn" && ( - + )} {formik.values.networkType === "physical" && formik.values.isCreating && ( - + )} diff --git a/src/pages/networks/forms/NetworkParentSelector.tsx b/src/pages/networks/forms/NetworkParentSelector.tsx index 34de20fe98..ae999d2e39 100644 --- a/src/pages/networks/forms/NetworkParentSelector.tsx +++ b/src/pages/networks/forms/NetworkParentSelector.tsx @@ -7,11 +7,10 @@ import { fetchNetworks } from "api/networks"; import { useParams } from "react-router-dom"; interface Props { - isDisabled: boolean; props?: Record; } -const NetworkParentSelector: FC = ({ isDisabled, props }) => { +const NetworkParentSelector: FC = ({ props }) => { const { project } = useParams<{ project: string }>(); if (!project) { @@ -44,7 +43,6 @@ const NetworkParentSelector: FC = ({ isDisabled, props }) => { { {section === YAML_CONFIGURATION && ( void formik.setFieldValue("yaml", yaml)} > diff --git a/src/pages/profiles/EditProfile.tsx b/src/pages/profiles/EditProfile.tsx index 2327d6b78a..626a5c90c0 100644 --- a/src/pages/profiles/EditProfile.tsx +++ b/src/pages/profiles/EditProfile.tsx @@ -49,7 +49,7 @@ import NetworkDevicesForm from "components/forms/NetworkDevicesForm"; import ProfileDetailsForm, { ProfileDetailsFormValues, } from "pages/profiles/forms/ProfileDetailsForm"; -import { getProfileEditValues } from "util/instanceEdit"; +import { ensureEditMode, getProfileEditValues } from "util/instanceEdit"; import { slugify } from "util/slugify"; import { hasDiskError, hasNetworkError } from "util/instanceValidation"; import FormFooterLayout from "components/forms/FormFooterLayout"; @@ -85,6 +85,7 @@ const EditProfile: FC = ({ profile, featuresProfiles }) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const [isConfigOpen, setConfigOpen] = useState(true); + const [version, setVersion] = useState(0); if (!project) { return <>Missing project; @@ -210,10 +211,12 @@ const EditProfile: FC = ({ profile, featuresProfiles }) => { {section === slugify(YAML_CONFIGURATION) && ( void formik.setFieldValue("yaml", yaml)} - readOnly={readOnly} + setYaml={(yaml) => { + ensureEditMode(formik); + void formik.setFieldValue("yaml", yaml); + }} > This is the YAML representation of the profile. @@ -232,19 +235,14 @@ const EditProfile: FC = ({ profile, featuresProfiles }) => { - {readOnly ? ( - - ) : ( + {readOnly ? null : ( <> diff --git a/src/pages/profiles/forms/ProfileDetailsForm.tsx b/src/pages/profiles/forms/ProfileDetailsForm.tsx index 23ba5377e5..634eff3ff5 100644 --- a/src/pages/profiles/forms/ProfileDetailsForm.tsx +++ b/src/pages/profiles/forms/ProfileDetailsForm.tsx @@ -4,6 +4,7 @@ import { FormikProps } from "formik/dist/types"; import { CreateProfileFormValues } from "pages/profiles/CreateProfile"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; import ScrollableForm from "components/ScrollableForm"; +import { ensureEditMode } from "util/instanceEdit"; export interface ProfileDetailsFormValues { name: string; @@ -25,7 +26,6 @@ interface Props { } const ProfileDetailsForm: FC = ({ formik, isEdit }) => { - const readOnly = formik.values.readOnly; const isDefaultProfile = formik.values.name === "default"; return ( @@ -56,9 +56,13 @@ const ProfileDetailsForm: FC = ({ formik, isEdit }) => { label="Description" placeholder="Enter description" onBlur={formik.handleBlur} - onChange={formik.handleChange} + onChange={(e) => { + if (isEdit) { + ensureEditMode(formik); + } + formik.handleChange(e); + }} value={formik.values.description} - disabled={readOnly} dynamicHeight /> diff --git a/src/pages/projects/EditProject.tsx b/src/pages/projects/EditProject.tsx index 3955dc2499..b073ca0871 100644 --- a/src/pages/projects/EditProject.tsx +++ b/src/pages/projects/EditProject.tsx @@ -102,16 +102,7 @@ const EditProject: FC = ({ project }) => { /> {!isRestricted && ( - {formik.values.readOnly ? ( - <> - - - ) : ( + {formik.values.readOnly ? null : ( <> - ) : ( + {formik.values.readOnly ? null : ( <>