Skip to content

Commit

Permalink
feat: [WD-18264] CMS fields for storage pool source
Browse files Browse the repository at this point in the history
Signed-off-by: Nkeiruka <[email protected]>
  • Loading branch information
Kxiru committed Jan 28, 2025
1 parent cf5e5d2 commit b6d3346
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 54 deletions.
15 changes: 5 additions & 10 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import {
constructMemberError,
handleEtagResponse,
handleResponse,
} from "util/helpers";
import type {
LxdNetwork,
LXDNetworkOnClusterMember,
Expand Down Expand Up @@ -29,15 +33,6 @@ export const fetchNetworks = (
});
};

const constructMemberError = (
result: PromiseRejectedResult,
member: string,
) => {
const reason = result.reason as Error;
const message = `Error from cluster member ${member}: ${reason.message}`;
return new Error(message);
};

export const fetchNetworksFromClusterMembers = (
project: string,
clusterMembers: LxdClusterMember[],
Expand Down
68 changes: 57 additions & 11 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
constructMemberError,
handleEtagResponse,
handleResponse,
handleSettledResult,
} from "util/helpers";
import {
LxdStoragePool,
LXDStoragePoolOnClusterMember,
LxdStoragePoolResources,
LxdStorageVolume,
LxdStorageVolumeState,
Expand All @@ -14,6 +16,7 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdOperationResponse } from "types/operation";
import axios, { AxiosResponse } from "axios";
import type { LxdClusterMember } from "types/cluster";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export const fetchStoragePool = (
pool: string,
Expand Down Expand Up @@ -66,7 +69,7 @@ export const createPool = (
});
};

const getClusterAndMemberPools = (pool: Partial<LxdStoragePool>) => {
const getClusterAndMemberPoolPayload = (pool: Partial<LxdStoragePool>) => {
const memberSpecificConfigKeys = new Set([
"source",
"size",
Expand All @@ -85,27 +88,38 @@ const getClusterAndMemberPools = (pool: Partial<LxdStoragePool>) => {
}
}

const clusterPool = { ...pool, config: clusterConfig };
const memberPool = { ...pool, config: memberConfig };
const clusterPoolPayload = { ...pool, config: clusterConfig };
const memberPoolPayload = { ...pool, config: memberConfig };

return {
clusterPool,
memberPool,
clusterPoolPayload,
memberPoolPayload,
};
};

export const createClusteredPool = (
pool: LxdStoragePool,
clusterMembers: LxdClusterMember[],
sourcePerClusterMember?: ClusterSpecificValues,
): Promise<void> => {
const { memberPool, clusterPool } = getClusterAndMemberPools(pool);
const { memberPoolPayload, clusterPoolPayload } =
getClusterAndMemberPoolPayload(pool);
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((item) => createPool(memberPool, item.server_name)),
clusterMembers.map((item) => {
const clusteredMemberPool = {
...memberPoolPayload,
config: {
...memberPoolPayload.config,
source: sourcePerClusterMember?.[item.server_name],
},
};
return createPool(clusteredMemberPool, item.server_name);
}),
)
.then(handleSettledResult)
.then(() => {
return createPool(clusterPool);
return createPool(clusterPoolPayload);
})
.then(resolve)
.catch(reject);
Expand All @@ -132,15 +146,16 @@ export const updateClusteredPool = (
pool: Partial<LxdStoragePool>,
clusterMembers: LxdClusterMember[],
): Promise<void> => {
const { memberPool, clusterPool } = getClusterAndMemberPools(pool);
const { memberPoolPayload, clusterPoolPayload } =
getClusterAndMemberPoolPayload(pool);
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map(async (item) =>
updatePool(memberPool, item.server_name),
updatePool(memberPoolPayload, item.server_name),
),
)
.then(handleSettledResult)
.then(() => updatePool(clusterPool))
.then(() => updatePool(clusterPoolPayload))
.then(resolve)
.catch(reject);
});
Expand Down Expand Up @@ -174,6 +189,37 @@ export const deleteStoragePool = (pool: string): Promise<void> => {
});
};

export const fetchPoolFromClusterMembers = (
poolName: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDStoragePoolOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchStoragePool(poolName, member.server_name);
}),
)
.then((results) => {
const poolOnMembers: LXDStoragePoolOnClusterMember[] = [];
for (let i = 0; i < clusterMembers.length; i++) {
const memberName = clusterMembers[i].server_name;
const result = results[i];
if (result.status === "rejected") {
reject(constructMemberError(result, memberName));
}
if (result.status === "fulfilled") {
const promise = results[
i
] as PromiseFulfilledResult<LxdStoragePool>;
poolOnMembers.push({ ...promise.value, memberName: memberName });
}
}
resolve(poolOnMembers);
})
.catch(reject);
});
};

export const fetchStorageVolumes = (
pool: string,
project: string,
Expand Down
141 changes: 141 additions & 0 deletions src/components/forms/ClusterSpecificInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { FC, Fragment, useEffect, useState } from "react";
import { CheckboxInput, Input } from "@canonical/react-components";
import ResourceLink from "components/ResourceLink";
import FormEditButton from "components/FormEditButton";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

interface Props {
id: string;
isReadOnly: boolean;
onChange: (value: ClusterSpecificValues) => void;
toggleReadOnly: () => void;
memberNames: string[];
values?: ClusterSpecificValues;
canToggleSpecific?: boolean;
isDefaultSpecific?: boolean;
clusterMemberLinkTarget?: (member: string) => string;
disabled?: boolean;
helpText?: string;
}

const ClusterSpecificInput: FC<Props> = ({
values,
id,
isReadOnly,
memberNames,
onChange,
toggleReadOnly,
canToggleSpecific = true,
isDefaultSpecific = null,
clusterMemberLinkTarget = () => "/ui/cluster",
disabled = false,
helpText,
}) => {
const [isSpecific, setIsSpecific] = useState<boolean | null>(
isDefaultSpecific,
);
const firstValue = Object.values(values ?? {})[0];

useEffect(() => {
if (isSpecific === null && values) {
const newDefaultSpecific = !Object.values(values).some(
(item) => item !== Object.values(values)[0],
);
setIsSpecific(newDefaultSpecific);
}
}, [isSpecific, values]);

const setValueForAllMembers = (value: string) => {
const update: ClusterSpecificValues = {};
memberNames.forEach((member) => (update[member] = value));
onChange(update);
};

const setValueForMember = (value: string, member: string) => {
const update = {
...values,
[member]: value,
};
onChange(update);
};

return (
<div className="u-sv3">
{canToggleSpecific && !isReadOnly && (
<CheckboxInput
id={`${id}-same-for-all-toggle`}
label="Same for all cluster members"
checked={!isSpecific}
onChange={() => {
setIsSpecific((val) => !val);
}}
/>
)}
{isSpecific && (
<div className="cluster-specific-input">
{memberNames.map((item) => {
const activeValue = values?.[item];

return (
<Fragment key={item}>
<div className="cluster-specific-member">
<ResourceLink
type="cluster-member"
value={item}
to={clusterMemberLinkTarget(item)}
/>
</div>
<div className="cluster-specific-value">
{isReadOnly ? (
<>
{activeValue}
<FormEditButton toggleReadOnly={toggleReadOnly} />
</>
) : (
<Input
id={
memberNames.indexOf(item) === 0 ? id : `${id}-${item}`
}
type="text"
className="u-no-margin--bottom"
value={activeValue}
onChange={(e) => setValueForMember(e.target.value, item)}
disabled={disabled}
// help={
// disabled &&
// memberNames.indexOf(item) === memberNames.length - 1 &&
// helpText
// }
/>
)}
</div>
</Fragment>
);
})}
<div className="p-form-help-text">{helpText}</div>
</div>
)}
{!isSpecific && (
<div>
{isReadOnly ? (
<>
{firstValue}
<FormEditButton toggleReadOnly={toggleReadOnly} />
</>
) : (
<Input
id={id}
type="text"
value={firstValue}
onChange={(e) => setValueForAllMembers(e.target.value)}
disabled={disabled}
help={disabled && helpText}
/>
)}
</div>
)}
</div>
);
};

export default ClusterSpecificInput;
7 changes: 6 additions & 1 deletion src/pages/storage/CreateStoragePool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ const CreateStoragePool: FC = () => {

const mutation =
clusterMembers.length > 0
? () => createClusteredPool(storagePool, clusterMembers)
? () =>
createClusteredPool(
storagePool,
clusterMembers,
values.sourcePerClusterMember,
)
: () => createPool(storagePool);

mutation()
Expand Down
28 changes: 23 additions & 5 deletions src/pages/storage/EditStoragePool.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import { Button, useNotify } from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchPoolFromClusterMembers,
fetchStoragePool,
updateClusteredPool,
updatePool,
Expand Down Expand Up @@ -49,11 +50,24 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
const controllerState = useState<AbortController | null>(null);
const { data: clusterMembers = [] } = useClusterMembers();
const [version, setVersion] = useState(0);
const isClustered = clusterMembers.length > 0;

if (!project) {
return <>Missing project</>;
}

const { data: poolOnMembers = [], error } = useQuery({
queryKey: [queryKeys.storage, pool.name, queryKeys.cluster],
queryFn: () => fetchPoolFromClusterMembers(pool.name, clusterMembers),
enabled: isClustered,
});

useEffect(() => {
if (error) {
notify.failure("Loading storage pool from cluster members failed", error);
}
}, [error]);

const StoragePoolSchema = Yup.object().shape({
name: Yup.string()
.test(
Expand All @@ -67,7 +81,7 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
});

const formik = useFormik<StoragePoolFormValues>({
initialValues: toStoragePoolFormValues(pool),
initialValues: toStoragePoolFormValues(pool, poolOnMembers),
validationSchema: StoragePoolSchema,
enableReinitialize: true,
onSubmit: (values) => {
Expand Down Expand Up @@ -95,7 +109,9 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
);
const member = clusterMembers[0]?.server_name ?? undefined;
const updatedPool = await fetchStoragePool(values.name, member);
void formik.setValues(toStoragePoolFormValues(updatedPool));
void formik.setValues(
toStoragePoolFormValues(updatedPool, poolOnMembers),
);
})
.catch((e) => {
notify.failure("Storage pool update failed", e);
Expand Down Expand Up @@ -143,7 +159,9 @@ const EditStoragePool: FC<Props> = ({ pool }) => {
appearance="base"
onClick={() => {
setVersion((old) => old + 1);
void formik.setValues(toStoragePoolFormValues(pool));
void formik.setValues(
toStoragePoolFormValues(pool, poolOnMembers),
);
}}
>
Cancel
Expand Down
Loading

0 comments on commit b6d3346

Please sign in to comment.