Skip to content

Commit

Permalink
feat: [WD-14769] Move custom storage volume to aother storage pool.
Browse files Browse the repository at this point in the history
Signed-off-by: Nkeiruka <[email protected]>
  • Loading branch information
Kxiru committed Sep 24, 2024
1 parent 9a7a2c4 commit c86a1ad
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 28 deletions.
18 changes: 18 additions & 0 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,21 @@ export const deleteStorageVolume = (
.catch(reject);
});
};

export const migrateStorageVolume = (
volume: Partial<LxdStorageVolume>,
targetPool: string,
): Promise<LxdOperationResponse> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${volume.pool}/volumes/custom/${volume.name}`, {
method: "POST",
body: JSON.stringify({
name: volume.name,
pool: targetPool,
}),
})
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};
137 changes: 137 additions & 0 deletions src/pages/storage/MigrateVolumeBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { FC, useState } from "react";
import { ActionButton, Icon } from "@canonical/react-components";
import usePortal from "react-useportal";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { useEventQueue } from "context/eventQueue";
import ItemName from "components/ItemName";
import { useToastNotification } from "context/toastNotificationProvider";
import { LxdStorageVolume } from "types/storage";
import MigrateVolumeModal from "./MigrateVolumeModal";
import { migrateStorageVolume } from "api/storage-pools";
import { useLocation, useNavigate } from "react-router-dom";

interface Props {
storageVolume: LxdStorageVolume;
project: string;
classname?: string;
onClose?: () => void;
}

const MigrateVolumeBtn: FC<Props> = ({
storageVolume,
project,
classname,
onClose,
}) => {
const eventQueue = useEventQueue();
const toastNotify = useToastNotification();
const { openPortal, closePortal, isOpen, Portal } = usePortal();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [isVolumeLoading, setVolumeLoading] = useState(false);
const location = useLocation();

const handleSuccess = (
newTarget: string,
storageVolume: LxdStorageVolume,
) => {
toastNotify.success(
<>
Volume <ItemName item={{ name: storageVolume.name }} bold />{" "}
successfully migrated to pool{" "}
<ItemName item={{ name: newTarget }} bold />
</>,
);

const oldVolumeUrl = `/ui/project/${storageVolume.project}/storage/pool/${storageVolume.pool}/volumes/${storageVolume.type}/${storageVolume.name}`;
const newVolumeUrl = `/ui/project/${storageVolume.project}/storage/pool/${newTarget}/volumes/${storageVolume.type}/${storageVolume.name}`;
if (location.pathname.startsWith(oldVolumeUrl)) {
navigate(newVolumeUrl);
}
};

const notifyFailure = (
e: unknown,
storageVolume: string,
targetPool: string,
) => {
setVolumeLoading(false);
toastNotify.failure(
`Migration failed for volume ${storageVolume} to pool ${targetPool}`,
e,
);
};

const handleFailure = (
msg: string,
storageVolume: string,
targetPool: string,
) => {
notifyFailure(new Error(msg), storageVolume, targetPool);
};

const handleFinish = () => {
void queryClient.invalidateQueries({
queryKey: [queryKeys.storage, storageVolume.name],
});
setVolumeLoading(false);
};

const handleMigrate = (targetPool: string) => {
setVolumeLoading(true);
migrateStorageVolume(storageVolume, targetPool)
.then((operation) => {
eventQueue.set(
operation.metadata.id,
() => handleSuccess(targetPool, storageVolume),
(err) => handleFailure(err, storageVolume.name, targetPool),
handleFinish,
);
toastNotify.info(
`Migration started for volume ${storageVolume.name} to pool ${targetPool}`,
);
void queryClient.invalidateQueries({
queryKey: [queryKeys.storage, storageVolume.name, project],
});
})
.catch((e) => {
notifyFailure(e, storageVolume.name, targetPool);
})
.finally(() => {
handleClose();
});
};

const handleClose = () => {
closePortal();
onClose?.();
};

return (
<>
{isOpen && (
<Portal>
<MigrateVolumeModal
close={handleClose}
migrate={handleMigrate}
storageVolume={storageVolume}
/>
</Portal>
)}
<ActionButton
onClick={openPortal}
type="button"
className={classname}
loading={isVolumeLoading}
disabled={isVolumeLoading}
title="Migrate volume"
>
<Icon name="machines" />
<span>Migrate</span>
</ActionButton>
</>
);
};

export default MigrateVolumeBtn;
192 changes: 192 additions & 0 deletions src/pages/storage/MigrateVolumeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { FC, KeyboardEvent, useState } from "react";
import {
ActionButton,
Button,
MainTable,
Modal,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import ScrollableTable from "components/ScrollableTable";
import { LxdStorageVolume } from "types/storage";
import { fetchStoragePools } from "api/storage-pools";
import StoragePoolSize from "./StoragePoolSize";

interface Props {
close: () => void;
migrate: (target: string) => void;
storageVolume: LxdStorageVolume;
}

const MigrateVolumeModal: FC<Props> = ({ close, migrate, storageVolume }) => {
const { data: pools = [] } = useQuery({
queryKey: [queryKeys.storage],
queryFn: () => fetchStoragePools(storageVolume.project),
});

const [selectedPool, setSelectedPool] = useState("");

const handleEscKey = (e: KeyboardEvent<HTMLElement>) => {
if (e.key === "Escape") {
close();
}
};

const handleMigrate = () => {
migrate(selectedPool);
close();
};

const handleCancel = () => {
if (selectedPool) {
setSelectedPool("");
return;
}

close();
};

const headers = [
{ content: "Name", sortKey: "name" },
{ content: "Driver", sortKey: "driver" },
{ content: "Status", sortKey: "status" },
{ content: "Size", sortKey: "size" },
{ "aria-label": "Actions", className: "actions" },
];

const rows = pools.map((pool) => {
const disableReason =
pool.name === storageVolume.pool
? "Volume is located on this cluster member"
: "";

const selectPool = () => {
if (disableReason) {
return;
}

setSelectedPool(pool.name);
};

return {
className: "u-row",
columns: [
{
content: (
<div className="u-truncate migrate-instance-name" title={pool.name}>
{pool.name}
</div>
),
role: "cell",
"aria-label": "Name",
onClick: selectPool,
},
{
content: pool.driver,
role: "cell",
"aria-label": "Driver",
onClick: selectPool,
},
{
content: pool.status,
role: "cell",
"aria-label": "Status",
onClick: selectPool,
},
{
content: <StoragePoolSize pool={pool} />,
role: "cell",
"aria-label": "Size",
onClick: selectPool,
},
{
content: (
<Button
onClick={selectPool}
dense
title={disableReason}
disabled={Boolean(disableReason)}
>
Select
</Button>
),
role: "cell",
"aria-label": "Actions",
className: "u-align--right",
onClick: selectPool,
},
],
sortData: {
name: pool.name.toLowerCase(),
driver: pool.driver.toLowerCase(),
status: pool.status?.toLowerCase(),
size: pool.config.size?.toLowerCase(),
},
};
});

const modalTitle = selectedPool
? "Confirm migration"
: `Choose storage pool for volume ${storageVolume.name}`;

const summary = (
<div className="migrate-instance-summary">
<p>
This will migrate volume <strong>{storageVolume.name}</strong> to
storage pool <b>{selectedPool}</b>.
</p>
</div>
);

return (
<Modal
close={close}
className="migrate-instance-modal"
title={modalTitle}
buttonRow={
<div id="migrate-instance-actions">
<Button
className="u-no-margin--bottom"
type="button"
aria-label="cancel migrate"
appearance="base"
onClick={handleCancel}
>
Cancel
</Button>
<ActionButton
appearance="positive"
className="u-no-margin--bottom"
onClick={() => handleMigrate()}
disabled={!selectedPool}
>
Migrate
</ActionButton>
</div>
}
onKeyDown={handleEscKey}
>
{selectedPool ? (
summary
) : (
<div className="migrate-instance-table u-selectable-table-rows">
<ScrollableTable
dependencies={[]}
tableId="migrate-instance-table"
belowIds={["status-bar", "migrate-instance-actions"]}
>
<MainTable
id="migrate-instance-table"
headers={headers}
rows={rows}
sortable
className="u-table-layout--auto"
/>
</ScrollableTable>
</div>
)}
</Modal>
);
};

export default MigrateVolumeModal;
6 changes: 6 additions & 0 deletions src/pages/storage/StoragePoolSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface Props {
setValue: (value: string) => void;
selectProps?: SelectProps;
hidePoolsWithUnsupportedDrivers?: boolean;
disabled?: boolean;
help?: string;
}

const StoragePoolSelector: FC<Props> = ({
Expand All @@ -22,6 +24,8 @@ const StoragePoolSelector: FC<Props> = ({
setValue,
selectProps,
hidePoolsWithUnsupportedDrivers = false,
disabled,
help,
}) => {
const notify = useNotify();
const { data: settings } = useSettings();
Expand Down Expand Up @@ -82,6 +86,8 @@ const StoragePoolSelector: FC<Props> = ({
onChange={(e) => setValue(e.target.value)}
value={value}
{...selectProps}
disabled={disabled}
help={help}
/>
);
};
Expand Down
Loading

0 comments on commit c86a1ad

Please sign in to comment.