diff --git a/src/api/storage-pools.tsx b/src/api/storage-pools.tsx index 17a962811f..18d24a083a 100644 --- a/src/api/storage-pools.tsx +++ b/src/api/storage-pools.tsx @@ -101,6 +101,7 @@ export const createClusteredPool = ( pool: LxdStoragePool, clusterMembers: LxdClusterMember[], sourcePerClusterMember?: ClusterSpecificValues, + sizePerClusterMember?: ClusterSpecificValues, ): Promise => { const { memberPoolPayload, clusterPoolPayload } = getClusterAndMemberPoolPayload(pool); @@ -112,6 +113,7 @@ export const createClusteredPool = ( config: { ...memberPoolPayload.config, source: sourcePerClusterMember?.[item.server_name], + size: sizePerClusterMember?.[item.server_name], }, }; return createPool(clusteredMemberPool, item.server_name); @@ -145,14 +147,22 @@ export const updatePool = ( export const updateClusteredPool = ( pool: Partial, clusterMembers: LxdClusterMember[], + sizePerClusterMember?: ClusterSpecificValues, ): Promise => { const { memberPoolPayload, clusterPoolPayload } = getClusterAndMemberPoolPayload(pool); return new Promise((resolve, reject) => { Promise.allSettled( - clusterMembers.map(async (item) => - updatePool(memberPoolPayload, item.server_name), - ), + clusterMembers.map(async (item) => { + const clusteredMemberPool = { + ...memberPoolPayload, + config: { + ...memberPoolPayload.config, + size: sizePerClusterMember?.[item.server_name], + }, + }; + return updatePool(clusteredMemberPool, item.server_name); + }), ) .then(handleSettledResult) .then(() => updatePool(clusterPoolPayload)) diff --git a/src/components/forms/ClusteredDiskSizeSelector.tsx b/src/components/forms/ClusteredDiskSizeSelector.tsx new file mode 100644 index 0000000000..16e064298e --- /dev/null +++ b/src/components/forms/ClusteredDiskSizeSelector.tsx @@ -0,0 +1,148 @@ +import { FC, Fragment, useEffect, useState } from "react"; +import { CheckboxInput, Label } from "@canonical/react-components"; +import ResourceLink from "components/ResourceLink"; +import FormEditButton from "components/FormEditButton"; +import { ClusterSpecificValues } from "components/ClusterSpecificSelect"; +import { useClusterMembers } from "context/useClusterMembers"; +import DiskSizeSelector from "./DiskSizeSelector"; + +interface Props { + canToggleSpecific?: boolean; + id: string; + toggleReadOnly: () => void; + setMemoryLimit: (value: ClusterSpecificValues) => void; + sizeValues?: ClusterSpecificValues; + isReadOnly?: boolean; + isDefaultSpecific?: boolean; + clusterMemberLinkTarget?: (member: string) => string; + disabled?: boolean; + helpText?: string; +} + +const ClusteredDiskSizeSelector: FC = ({ + id, + canToggleSpecific = true, + toggleReadOnly, + setMemoryLimit, + sizeValues, + isReadOnly = false, + isDefaultSpecific = null, + clusterMemberLinkTarget = () => "/ui/cluster", + disabled = false, + helpText, +}) => { + const { data: clusterMembers = [] } = useClusterMembers(); + const memberNames = clusterMembers.map((member) => member.server_name); + const [isSpecific, setIsSpecific] = useState( + isDefaultSpecific, + ); + const firstValue = Object.values(sizeValues ?? {})[0]; + + useEffect(() => { + const rawValues = Object.values(sizeValues ?? {}); + if (isSpecific === null && rawValues.length > 0) { + const newDefaultSpecific = rawValues.some( + (item) => item !== rawValues[0], + ); + setIsSpecific(newDefaultSpecific); + } + }, [isSpecific, sizeValues]); + + const setValueForAllMembers = (value: string) => { + const update: ClusterSpecificValues = {}; + memberNames.forEach((member) => (update[member] = value)); + setMemoryLimit(update); + }; + + const setValueForMember = (value: string, member: string) => { + const update = { + ...sizeValues, + [member]: value, + }; + setMemoryLimit(update); + }; + + return ( +
+ + {canToggleSpecific && !isReadOnly && ( + { + setIsSpecific((val) => !val); + }} + /> + )} + {isSpecific && ( +
+ {memberNames.map((item) => { + const activeValue = sizeValues?.[item] ?? ""; + + return ( + +
+ +
+ +
+ {isReadOnly ? ( + <> + {activeValue} + + + ) : ( + + setValueForMember(value ?? "", item) + } + disabled={disabled} + classname="u-no-margin--bottom" + /> + )} +
+
+ ); + })} + {helpText && ( +
+ {helpText} +
+ )} +
+ )} + + {!isSpecific && ( +
+ {isReadOnly ? ( + <> + {firstValue} + + + ) : ( + <> + setValueForAllMembers(value ?? "")} + disabled={disabled} + /> + {helpText &&
{helpText}
} + + )} +
+ )} +
+ ); +}; + +export default ClusteredDiskSizeSelector; diff --git a/src/components/forms/DiskSizeSelector.tsx b/src/components/forms/DiskSizeSelector.tsx index 4bba6ba574..69b8752ab0 100644 --- a/src/components/forms/DiskSizeSelector.tsx +++ b/src/components/forms/DiskSizeSelector.tsx @@ -4,19 +4,23 @@ import { BYTES_UNITS } from "types/limits"; import { parseMemoryLimit } from "util/limits"; interface Props { + id?: string; label?: string; value?: string; help?: string; setMemoryLimit: (val?: string) => void; disabled?: boolean; + classname?: string; } const DiskSizeSelector: FC = ({ + id = "limits_disk", label, value, help, setMemoryLimit, disabled, + classname, }) => { const limit = parseMemoryLimit(value) ?? { value: 1, @@ -39,7 +43,7 @@ const DiskSizeSelector: FC = ({ )}
= ({ onChange={(e) => setMemoryLimit(e.target.value + limit.unit)} value={value?.match(/^\d/) ? limit.value : ""} disabled={disabled} + className={classname} /> = ({ formik }) => { label="Source" /> ) : ( - diff --git a/src/util/storagePoolForm.tsx b/src/util/storagePoolForm.tsx index 72e9a6ed5d..32fc3e3d6b 100644 --- a/src/util/storagePoolForm.tsx +++ b/src/util/storagePoolForm.tsx @@ -10,20 +10,15 @@ export const toStoragePoolFormValues = ( poolOnMembers?: LXDStoragePoolOnClusterMember[], ): StoragePoolFormValues => { const sourcePerClusterMember: ClusterSpecificValues = {}; - poolOnMembers?.forEach( - (item) => - (sourcePerClusterMember[item.memberName] = item.config?.source ?? ""), - ); + const sizePerClusterMember: ClusterSpecificValues = {}; + + poolOnMembers?.forEach((item) => { + sizePerClusterMember[item.memberName] = item.config?.size ?? ""; + sourcePerClusterMember[item.memberName] = item.config?.source ?? ""; + }); return { - readOnly: true, - isCreating: false, - name: pool.name, - description: pool.description, - driver: pool.driver, - source: pool.config?.source || "", - size: pool.config?.size || "GiB", - entityType: "storagePool", + barePool: pool, ceph_cluster_name: pool.config?.["ceph.cluster_name"], ceph_osd_pg_num: pool.config?.["ceph.osd.pg_num"], ceph_rbd_clone_copy: pool.config?.["ceph.rbd.clone_copy"], @@ -35,6 +30,12 @@ export const toStoragePoolFormValues = ( cephfs_osd_pg_num: pool.config?.["cephfs.osd_pg_num"], cephfs_path: pool.config?.["cephfs.path"], cephfs_user_name: pool.config?.["cephfs.user.name"], + description: pool.description, + driver: pool.driver, + entityType: "storagePool", + isCreating: false, + name: pool.name, + sizePerClusterMember, powerflex_clone_copy: pool.config?.["powerflex.clone_copy"], powerflex_domain: pool.config?.["powerflex.domain"], powerflex_gateway: pool.config?.["powerflex.gateway"], @@ -48,11 +49,13 @@ export const toStoragePoolFormValues = ( pure_gateway: pool.config?.["pure.gateway"], pure_gateway_verify: pool.config?.["pure.gateway.verify"], pure_mode: pool.config?.["pure.mode"], + readOnly: true, + size: pool.config?.size || "GiB", + source: pool.config?.source || "", + sourcePerClusterMember, zfs_clone_copy: pool.config?.["zfs.clone_copy"], zfs_export: pool.config?.["zfs.export"], zfs_pool_name: pool.config?.["zfs.pool_name"], - sourcePerClusterMember, - barePool: pool, }; };