diff --git a/src/components/ScrollableForm.tsx b/src/components/ScrollableForm.tsx new file mode 100644 index 0000000000..94438961e7 --- /dev/null +++ b/src/components/ScrollableForm.tsx @@ -0,0 +1,41 @@ +import React, { DependencyList, FC, ReactNode, useEffect, useRef } from "react"; +import useEventListener from "@use-it/event-listener"; +import { getAbsoluteHeightBelow, getParentsBottomSpacing } from "util/helpers"; + +interface Props { + children: ReactNode; + dependencies: DependencyList; + belowId?: string; +} + +const ScrollableForm: FC = ({ + dependencies, + children, + belowId = "", +}) => { + const ref = useRef(null); + + const updateFormHeight = () => { + const form = ref.current?.children[0]; + if (!form) { + return; + } + const above = form.getBoundingClientRect().top + 1; + const below = getAbsoluteHeightBelow(belowId); + const parentsBottomSpacing = getParentsBottomSpacing(form as HTMLElement); + const offset = Math.ceil(above + below + parentsBottomSpacing); + const style = `height: calc(100vh - ${offset}px); min-height: calc(100vh - ${offset}px)`; + form.setAttribute("style", style); + }; + + useEventListener("resize", updateFormHeight); + useEffect(updateFormHeight, [...dependencies, ref]); + + return ( +
+
{children}
+
+ ); +}; + +export default ScrollableForm; diff --git a/src/components/ScrollableTable.tsx b/src/components/ScrollableTable.tsx index 2a702489f5..553476d7b2 100644 --- a/src/components/ScrollableTable.tsx +++ b/src/components/ScrollableTable.tsx @@ -1,6 +1,6 @@ import React, { DependencyList, FC, ReactNode, useEffect, useRef } from "react"; import useEventListener from "@use-it/event-listener"; -import { getParentsBottomSpacing } from "util/helpers"; +import { getAbsoluteHeightBelow, getParentsBottomSpacing } from "util/helpers"; interface Props { children: ReactNode; @@ -8,21 +8,13 @@ interface Props { belowId?: string; } -const ScrollableTable: FC = ({ dependencies, children, belowId }) => { +const ScrollableTable: FC = ({ + dependencies, + children, + belowId = "", +}) => { const ref = useRef(null); - const getAbsoluteHeightBelow = () => { - const element = belowId ? document.getElementById(belowId) : undefined; - if (!element) { - return 0; - } - const style = window.getComputedStyle(element); - const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom); - const padding = - parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); - return element.offsetHeight + margin + padding + 1; - }; - const updateTBodyHeight = () => { const table = ref.current?.children[0]; if (!table || table.children.length !== 2) { @@ -30,7 +22,7 @@ const ScrollableTable: FC = ({ dependencies, children, belowId }) => { } const tBody = table.children[1]; const above = tBody.getBoundingClientRect().top + 1; - const below = getAbsoluteHeightBelow(); + const below = getAbsoluteHeightBelow(belowId); const parentsBottomSpacing = getParentsBottomSpacing(table as HTMLElement); const offset = Math.ceil(above + below + parentsBottomSpacing); const style = `height: calc(100vh - ${offset}px); min-height: calc(100vh - ${offset}px)`; diff --git a/src/components/forms/DiskDeviceForm.tsx b/src/components/forms/DiskDeviceForm.tsx index 7d2a83e2e8..b5041f6297 100644 --- a/src/components/forms/DiskDeviceForm.tsx +++ b/src/components/forms/DiskDeviceForm.tsx @@ -11,6 +11,7 @@ import DiskDeviceFormRoot from "./DiskDeviceFormRoot"; import DiskDeviceFormInherited from "./DiskDeviceFormInherited"; import DiskDeviceFormCustom from "./DiskDeviceFormCustom"; import classnames from "classnames"; +import ScrollableForm from "components/ScrollableForm"; interface Props { formik: InstanceAndProfileFormikProps; @@ -58,23 +59,28 @@ const DiskDeviceForm: FC = ({ formik, project }) => { "disk-device-form--edit": !formik.values.readOnly, })} > - {/* hidden submit to enable enter key in inputs */} - - - - + + {/* hidden submit to enable enter key in inputs */} + + + + + ); }; diff --git a/src/pages/instances/forms/EditInstanceDetails.tsx b/src/pages/instances/forms/EditInstanceDetails.tsx index 75428860b8..3bae910d54 100644 --- a/src/pages/instances/forms/EditInstanceDetails.tsx +++ b/src/pages/instances/forms/EditInstanceDetails.tsx @@ -1,5 +1,5 @@ import React, { FC } from "react"; -import { Col, Input, Row } from "@canonical/react-components"; +import { Col, Input, Row, useNotify } from "@canonical/react-components"; import ProfileSelect from "pages/profiles/ProfileSelector"; import { FormikProps } from "formik/dist/types"; import { EditInstanceFormValues } from "pages/instances/EditInstance"; @@ -7,6 +7,7 @@ import { useSettings } from "context/useSettings"; import MigrateInstanceBtn from "pages/instances/actions/MigrateInstanceBtn"; import { isClusteredServer } from "util/settings"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; +import ScrollableForm from "components/ScrollableForm"; export const instanceEditDetailPayload = (values: EditInstanceFormValues) => { return { @@ -26,9 +27,10 @@ const EditInstanceDetails: FC = ({ formik, project }) => { const readOnly = formik.values.readOnly; const { data: settings } = useSettings(); const isClustered = isClusteredServer(settings); + const notify = useNotify(); return ( -
+ = ({ formik, project }) => { setSelected={(value) => void formik.setFieldValue("profiles", value)} readOnly={readOnly} /> -
+ ); }; diff --git a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx index 8fbd61b1f9..61963749bf 100644 --- a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx +++ b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx @@ -6,6 +6,7 @@ import { Input, Row, Select, + useNotify, } from "@canonical/react-components"; import ProfileSelect from "pages/profiles/ProfileSelector"; import SelectImageBtn from "pages/images/actions/SelectImageBtn"; @@ -17,6 +18,7 @@ import { LxdImageType, RemoteImage } from "types/image"; import InstanceLocationSelect from "pages/instances/forms/InstanceLocationSelect"; import UseCustomIsoBtn from "pages/images/actions/UseCustomIsoBtn"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; +import ScrollableForm from "components/ScrollableForm"; export interface InstanceDetailsFormValues { name?: string; @@ -66,6 +68,8 @@ const InstanceCreateDetailsForm: FC = ({ onSelectImage, project, }) => { + const notify = useNotify(); + function figureBaseImageName() { const image = formik.values.image; return image @@ -74,7 +78,7 @@ const InstanceCreateDetailsForm: FC = ({ } return ( -
+ = ({ : "" } /> -
+ ); }; diff --git a/src/pages/networks/forms/NetworkFormBridge.tsx b/src/pages/networks/forms/NetworkFormBridge.tsx index fd8a00be99..7f92a4f6ae 100644 --- a/src/pages/networks/forms/NetworkFormBridge.tsx +++ b/src/pages/networks/forms/NetworkFormBridge.tsx @@ -1,9 +1,9 @@ import React, { FC } from "react"; import { Input, Select } from "@canonical/react-components"; import { FormikProps } from "formik/dist/types"; -import ConfigurationTable from "components/ConfigurationTable"; import { getConfigurationRow } from "components/ConfigurationRow"; import { NetworkFormValues } from "pages/networks/forms/NetworkForm"; +import ScrollableConfigurationTable from "components/forms/ScrollableConfigurationTable"; interface Props { formik: FormikProps; @@ -11,7 +11,7 @@ interface Props { const NetworkFormBridge: FC = ({ formik }) => { return ( - ; @@ -11,7 +11,7 @@ interface Props { const NetworkFormDns: FC = ({ formik }) => { return ( - ; @@ -14,7 +14,7 @@ const NetworkFormIpv4: FC = ({ formik }) => { const hasDhcp = formik.values.ipv4_dhcp !== "false"; return ( - ; @@ -14,7 +14,7 @@ const NetworkFormIpv6: FC = ({ formik }) => { const hasDhcp = formik.values.ipv6_dhcp !== "false"; return ( - ; @@ -16,6 +23,7 @@ interface Props { } const NetworkFormMain: FC = ({ formik, project }) => { + const notify = useNotify(); const getFormProps = (id: "network" | "name" | "description") => { return { id: id, @@ -29,7 +37,7 @@ const NetworkFormMain: FC = ({ formik, project }) => { }; return ( - <> + @@ -145,7 +153,7 @@ const NetworkFormMain: FC = ({ formik, project }) => { : []), ]} /> - + ); }; diff --git a/src/pages/networks/forms/NetworkForwardForm.tsx b/src/pages/networks/forms/NetworkForwardForm.tsx index 65898dfe92..e838ec721b 100644 --- a/src/pages/networks/forms/NetworkForwardForm.tsx +++ b/src/pages/networks/forms/NetworkForwardForm.tsx @@ -22,6 +22,7 @@ import NotificationRow from "components/NotificationRow"; import NetworkForwardFormPorts, { NetworkForwardPortFormValues, } from "pages/networks/forms/NetworkForwardFormPorts"; +import ScrollableForm from "components/ScrollableForm"; export const toNetworkForward = ( values: NetworkForwardFormValues, @@ -110,72 +111,77 @@ const NetworkForwardForm: FC = ({
- {/* hidden submit to enable enter key in inputs */} - - - - - Name: {network?.name} -
- {network?.config["ipv4.address"] && ( + + {/* hidden submit to enable enter key in inputs */} + + + + + Name: {network?.name} +
+ {network?.config["ipv4.address"] && ( + <> + IPv4 subnet: {network?.config["ipv4.address"]} +
+ + )} + {network?.config["ipv6.address"] && ( + <>IPv6 subnet: {network?.config["ipv6.address"]} + )} +
+
+ + - IPv4 subnet: {network?.config["ipv4.address"]} + Fallback target for traffic that does not match a port + specified below.
+ Must be from the network {network?.name}. - )} - {network?.config["ipv6.address"] && ( - <>IPv6 subnet: {network?.config["ipv6.address"]} - )} -
-
- - - Fallback target for traffic that does not match a port specified - below. -
- Must be from the network {network?.name}. - - } - placeholder="Enter IP address" - stacked - /> - - {formik.values.ports.length > 0 && ( - - )} - + } + placeholder="Enter IP address" + stacked + /> + + {formik.values.ports.length > 0 && ( + + )} + +
diff --git a/src/pages/profiles/forms/ProfileDetailsForm.tsx b/src/pages/profiles/forms/ProfileDetailsForm.tsx index 948321e3cb..3587ff1a66 100644 --- a/src/pages/profiles/forms/ProfileDetailsForm.tsx +++ b/src/pages/profiles/forms/ProfileDetailsForm.tsx @@ -1,8 +1,9 @@ import React, { FC } from "react"; -import { Col, Input, Row } from "@canonical/react-components"; +import { Col, Input, Row, useNotify } from "@canonical/react-components"; import { FormikProps } from "formik/dist/types"; import { CreateProfileFormValues } from "pages/profiles/CreateProfile"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; +import ScrollableForm from "components/ScrollableForm"; export interface ProfileDetailsFormValues { name: string; @@ -26,9 +27,10 @@ interface Props { const ProfileDetailsForm: FC = ({ formik, isEdit }) => { const readOnly = formik.values.readOnly; const isDefaultProfile = formik.values.name === "default"; + const nofity = useNotify(); return ( -
+ = ({ formik, isEdit }) => { /> -
+ ); }; diff --git a/src/pages/projects/forms/ProjectDetailsForm.tsx b/src/pages/projects/forms/ProjectDetailsForm.tsx index 17062ad600..931c439527 100644 --- a/src/pages/projects/forms/ProjectDetailsForm.tsx +++ b/src/pages/projects/forms/ProjectDetailsForm.tsx @@ -7,12 +7,14 @@ import { Row, Select, Tooltip, + useNotify, } from "@canonical/react-components"; import { FormikProps } from "formik/dist/types"; import { getProjectKey } from "util/projectConfigFields"; import { isProjectEmpty } from "util/projects"; import { LxdProject } from "types/project"; import AutoExpandingTextArea from "components/AutoExpandingTextArea"; +import ScrollableForm from "components/ScrollableForm"; export interface ProjectDetailsFormValues { name: string; @@ -97,6 +99,7 @@ const ProjectDetailsForm: FC = ({ formik, project, isEdit }) => { return "default"; }; const [features, setFeatures] = useState(figureFeatures()); + const notify = useNotify(); const isDefaultProject = formik.values.name === "default"; const readOnly = formik.values.readOnly; @@ -106,7 +109,7 @@ const ProjectDetailsForm: FC = ({ formik, project, isEdit }) => { project?.config["features.networks.zones"] === "true"; return ( -
+ = ({ formik, project, isEdit }) => { /> -
+ ); }; diff --git a/src/pages/storage/forms/StoragePoolFormCeph.tsx b/src/pages/storage/forms/StoragePoolFormCeph.tsx index e84adad009..a9a166aa51 100644 --- a/src/pages/storage/forms/StoragePoolFormCeph.tsx +++ b/src/pages/storage/forms/StoragePoolFormCeph.tsx @@ -1,10 +1,10 @@ import { FormikProps } from "formik"; import React, { FC } from "react"; import { StoragePoolFormValues } from "./StoragePoolForm"; -import ConfigurationTable from "components/ConfigurationTable"; import { getConfigurationRow } from "components/ConfigurationRow"; import { Input, Select } from "@canonical/react-components"; import { optionTrueFalse } from "util/instanceOptions"; +import ScrollableConfigurationTable from "components/forms/ScrollableConfigurationTable"; interface Props { formik: FormikProps; @@ -12,7 +12,7 @@ interface Props { const StoragePoolFormCeph: FC = ({ formik }) => { return ( - ; @@ -21,6 +28,8 @@ interface Props { const StoragePoolFormMain: FC = ({ formik }) => { const { data: settings } = useSettings(); + const notify = useNotify(); + const getFormProps = (id: "name" | "description" | "size" | "source") => { return { id: id, @@ -38,7 +47,7 @@ const StoragePoolFormMain: FC = ({ formik }) => { const storageDriverOptions = getStorageDriverOptions(settings); return ( - <> + = ({ formik }) => { /> - + ); }; diff --git a/src/pages/storage/forms/StorageVolumeFormBlock.tsx b/src/pages/storage/forms/StorageVolumeFormBlock.tsx index c91aff3317..4cda52eabb 100644 --- a/src/pages/storage/forms/StorageVolumeFormBlock.tsx +++ b/src/pages/storage/forms/StorageVolumeFormBlock.tsx @@ -3,7 +3,7 @@ import { Input, Select } from "@canonical/react-components"; import { FormikProps } from "formik/dist/types"; import { StorageVolumeFormValues } from "pages/storage/forms/StorageVolumeForm"; import { getConfigurationRow } from "components/ConfigurationRow"; -import ConfigurationTable from "components/ConfigurationTable"; +import ScrollableConfigurationTable from "components/forms/ScrollableConfigurationTable"; interface Props { formik: FormikProps; @@ -11,7 +11,7 @@ interface Props { const StorageVolumeFormBlock: FC = ({ formik }) => { return ( - ; @@ -17,8 +25,10 @@ interface Props { } const StorageVolumeFormMain: FC = ({ formik, project }) => { + const notify = useNotify(); + return ( - <> + {formik.values.isCreating && ( @@ -120,7 +130,7 @@ const StorageVolumeFormMain: FC = ({ formik, project }) => { ]} /> )} - + ); }; diff --git a/src/pages/storage/forms/StorageVolumeFormSnapshots.tsx b/src/pages/storage/forms/StorageVolumeFormSnapshots.tsx index 8b52ce5f03..b5f8650c8c 100644 --- a/src/pages/storage/forms/StorageVolumeFormSnapshots.tsx +++ b/src/pages/storage/forms/StorageVolumeFormSnapshots.tsx @@ -1,7 +1,6 @@ import React, { FC } from "react"; import { Input, Notification } from "@canonical/react-components"; import { FormikProps } from "formik/dist/types"; -import ConfigurationTable from "components/ConfigurationTable"; import { getConfigurationRow } from "components/ConfigurationRow"; import { StorageVolumeFormValues } from "pages/storage/forms/StorageVolumeForm"; import SnapshotScheduleInput from "components/SnapshotScheduleInput"; @@ -9,6 +8,7 @@ import { useDocs } from "context/useDocs"; import { useProject } from "context/project"; import { isSnapshotsDisabled } from "util/snapshots"; import SnapshotDiabledWarningLink from "components/SnapshotDiabledWarningLink"; +import ScrollableConfigurationTable from "components/forms/ScrollableConfigurationTable"; interface Props { formik: FormikProps; @@ -29,7 +29,7 @@ const StorageVolumeFormSnapshots: FC = ({ formik }) => { )} - ; @@ -12,7 +12,7 @@ interface Props { const StorageVolumeFormZFS: FC = ({ formik }) => { return ( - div { + .form-contents > div, + .details { margin-left: 0; max-width: 67rem !important; } diff --git a/src/sass/_scrollable_table.scss b/src/sass/_scrollable_container.scss similarity index 79% rename from src/sass/_scrollable_table.scss rename to src/sass/_scrollable_container.scss index 1fa585a4cf..48ce78d205 100644 --- a/src/sass/_scrollable_table.scss +++ b/src/sass/_scrollable_container.scss @@ -33,3 +33,11 @@ width: 100%; } } + +.scrollable-form { + .details { + display: block; + overflow: hidden auto; + scrollbar-gutter: stable; + } +} diff --git a/src/sass/styles.scss b/src/sass/styles.scss index 06a541f4aa..06dcabaf19 100644 --- a/src/sass/styles.scss +++ b/src/sass/styles.scss @@ -82,7 +82,6 @@ $border-thin: 1px solid $color-mid-light !default; @import "progress_bar"; @import "project_select"; @import "rename_header"; -@import "scrollable_table"; @import "selectable_main_table"; @import "settings_page"; @import "snapshots"; @@ -91,6 +90,7 @@ $border-thin: 1px solid $color-mid-light !default; @import "storage_pool_form"; @import "storage_volume_form"; @import "upper_controls_bar"; +@import "scrollable_container"; .p-heading--4 { padding: 0; diff --git a/src/util/helpers.tsx b/src/util/helpers.tsx index a16a35d3e0..a1e97eb240 100644 --- a/src/util/helpers.tsx +++ b/src/util/helpers.tsx @@ -247,3 +247,15 @@ export const logout = () => export const capitalizeFirstLetter = (val: string) => val.charAt(0).toUpperCase() + val.slice(1); + +export const getAbsoluteHeightBelow = (belowId: string) => { + const element = belowId ? document.getElementById(belowId) : undefined; + if (!element) { + return 0; + } + const style = window.getComputedStyle(element); + const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom); + const padding = + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); + return element.offsetHeight + margin + padding + 1; +};