Skip to content

Commit

Permalink
feat(instance) migration of instance to other cluster members WD-4264
Browse files Browse the repository at this point in the history
  • Loading branch information
edlerd committed Jun 7, 2023
1 parent d930130 commit 088ecbe
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 2 deletions.
18 changes: 18 additions & 0 deletions src/api/instances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ export const renameInstance = (
});
};

export const migrateInstance = (
name: string,
project: string,
target: string
): Promise<LxdOperationResponse> => {
return new Promise((resolve, reject) => {
fetch(`/1.0/instances/${name}?project=${project}&target=${target}`, {
method: "POST",
body: JSON.stringify({
migration: true,
}),
})
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const startInstance = (instance: LxdInstance) => {
return putInstanceAction(instance.name, instance.project, "start");
};
Expand Down
6 changes: 5 additions & 1 deletion src/pages/instances/EditInstanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ const EditInstanceForm: FC<Props> = ({ instance }) => {
<Col size={12}>
{(activeSection === slugify(INSTANCE_DETAILS) ||
!activeSection) && (
<InstanceEditDetailsForm formik={formik} project={project} />
<InstanceEditDetailsForm
formik={formik}
project={project}
setInTabNotification={setInTabNotification}
/>
)}

{activeSection === slugify(STORAGE) && (
Expand Down
90 changes: 90 additions & 0 deletions src/pages/instances/MigrateInstanceForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { FC, KeyboardEvent } from "react";
import { Button, Form, Modal, Select } from "@canonical/react-components";
import * as Yup from "yup";
import { useFormik } from "formik";
import { LxdClusterMember } from "types/cluster";

interface Props {
close: () => void;
migrate: (target: string) => void;
instance: string;
location: string;
members: LxdClusterMember[];
}

const MigrateInstanceForm: FC<Props> = ({
close,
migrate,
instance,
location,
members,
}) => {
const memberNames = members.map((member) => member.server_name);

const MigrateSchema = Yup.object().shape({
target: Yup.string().min(1, "This field is required"),
});

const formik = useFormik({
initialValues: {
target: memberNames.find((member) => member !== location) ?? "",
},
validationSchema: MigrateSchema,
onSubmit: (values) => {
migrate(values.target);
},
});

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

return (
<Modal
close={close}
className="migrate-instance-modal"
title={`Migrate instance ${instance}`}
buttonRow={
<>
<Button
className="u-no-margin--bottom"
type="button"
aria-label="cancel migrate"
onClick={close}
>
Cancel
</Button>
<Button
className="u-no-margin--bottom"
appearance="positive"
onClick={formik.submitForm}
disabled={!formik.isValid}
>
Migrate
</Button>
</>
}
onKeyDown={handleEscKey}
>
<Form onSubmit={formik.handleSubmit}>
<Select
id="locationMember"
label="Move instance to cluster member"
onChange={(e) => void formik.setFieldValue("target", e.target.value)}
value={formik.values.target}
options={memberNames.map((member) => {
return {
label: member,
value: member,
disabled: member === location,
};
})}
/>
</Form>
</Modal>
);
};

export default MigrateInstanceForm;
108 changes: 108 additions & 0 deletions src/pages/instances/actions/MigrateInstanceBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { FC } from "react";
import { Button } from "@canonical/react-components";
import MigrateInstanceForm from "pages/instances/MigrateInstanceForm";
import usePortal from "react-useportal";
import { migrateInstance } from "api/instances";
import { failure, info, success } from "context/notify";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchClusterMembers } from "api/cluster";
import Loader from "components/Loader";
import { useEventQueue } from "context/eventQueue";
import { Notification } from "types/notification";
import ItemName from "components/ItemName";

interface Props {
instance: string;
location: string;
project: string;
setInTabNotification: (msg: Notification) => void;
onFinish: (newLocation: string) => void;
}

const MigrateInstanceBtn: FC<Props> = ({
instance,
location,
project,
setInTabNotification,
onFinish,
}) => {
const eventQueue = useEventQueue();
const { openPortal, closePortal, isOpen, Portal } = usePortal();
const queryClient = useQueryClient();

const { data: members = [], isLoading } = useQuery({
queryKey: [queryKeys.cluster, queryKeys.members],
queryFn: fetchClusterMembers,
});

if (isLoading) {
return <Loader />;
}

const handleSuccess = (newTarget: string) => {
setInTabNotification(
success(
<>
Migration finished for instance{" "}
<ItemName item={{ name: instance }} bold />
</>
)
);
onFinish(newTarget);
void queryClient.invalidateQueries({
queryKey: [queryKeys.instances, instance],
});
};

const notifyFailure = (e: unknown) => {
setInTabNotification(
failure(`Migration failed on instance ${instance}`, e)
);
};

const handleFailure = (msg: string) => {
notifyFailure(new Error(msg));
void queryClient.invalidateQueries({
queryKey: [queryKeys.instances, instance],
});
};

const handleMigrate = (target: string) => {
migrateInstance(instance, project, target)
.then((operation) => {
eventQueue.set(
operation.metadata.id,
() => handleSuccess(target),
handleFailure
);
setInTabNotification(info("Migration started"));
closePortal();
})
.catch((e) => {
notifyFailure(e);
closePortal();
});
};

return (
<>
{isOpen && (
<Portal>
<MigrateInstanceForm
close={closePortal}
migrate={handleMigrate}
instance={instance}
location={location}
members={members}
/>
</Portal>
)}
<Button className="instance-migrate" onClick={openPortal} type="button">
Migrate
</Button>
</>
);
};

export default MigrateInstanceBtn;
41 changes: 40 additions & 1 deletion src/pages/instances/forms/InstanceEditDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { Col, Input, Row, Textarea } from "@canonical/react-components";
import ProfileSelect from "pages/profiles/ProfileSelector";
import { FormikProps } from "formik/dist/types";
import { EditInstanceFormValues } from "pages/instances/EditInstanceForm";
import { useSettings } from "context/useSettings";
import MigrateInstanceBtn from "pages/instances/actions/MigrateInstanceBtn";
import { Notification } from "types/notification";

export interface InstanceEditDetailsFormValues {
name: string;
description?: string;
instanceType: string;
location: string;
profiles: string[];
type: string;
readOnly: boolean;
Expand All @@ -25,10 +29,17 @@ export const instanceEditDetailPayload = (values: EditInstanceFormValues) => {
interface Props {
formik: FormikProps<EditInstanceFormValues>;
project: string;
setInTabNotification: (msg: Notification) => void;
}

const InstanceEditDetailsForm: FC<Props> = ({ formik, project }) => {
const InstanceEditDetailsForm: FC<Props> = ({
formik,
project,
setInTabNotification,
}) => {
const isReadOnly = formik.values.readOnly;
const { data: settings } = useSettings();
const isClustered = settings?.environment?.server_clustered;

return (
<div className="details">
Expand Down Expand Up @@ -64,6 +75,34 @@ const InstanceEditDetailsForm: FC<Props> = ({ formik, project }) => {
/>
</Col>
</Row>
{isClustered && (
<Row>
<Col size={8}>
<Input
id="target"
name="target"
type="text"
label="Instance location"
value={formik.values.location}
required
disabled={true}
/>
</Col>
{!isReadOnly && (
<Col size={4}>
<MigrateInstanceBtn
instance={formik.values.name}
location={formik.values.location}
project={project}
setInTabNotification={setInTabNotification}
onFinish={(newLocation: string) =>
formik.setFieldValue("location", newLocation)
}
/>
</Col>
)}
</Row>
)}
<ProfileSelect
project={project}
selected={formik.values.profiles}
Expand Down
6 changes: 6 additions & 0 deletions src/sass/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
margin-top: $spv--x-large;
}

@include large {
.instance-migrate {
margin-top: 2.5rem;
}
}

.is-disabled {
cursor: not-allowed;
opacity: 0.33;
Expand Down
6 changes: 6 additions & 0 deletions src/sass/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,9 @@ $border-thin: 1px solid $color-mid-light !default;
width: 25.65rem;
}
}

@include large {
.migrate-instance-modal .p-modal__dialog {
min-width: 30rem;
}
}
1 change: 1 addition & 0 deletions src/util/instanceEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const getInstanceEditValues = (instance: LxdInstance) => {
return {
instanceType: instance.type,
profiles: instance.profiles,
location: instance.location,
readOnly: true,
type: "instance",
...getEditValues(instance),
Expand Down

0 comments on commit 088ecbe

Please sign in to comment.