-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [WD-14036] Duplicate instance (lxc copy)
Signed-off-by: Nkeiruka <[email protected]>
- Loading branch information
Showing
4 changed files
with
336 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { FC } from "react"; | ||
import { LxdInstance } from "types/instance"; | ||
import { Button } from "@canonical/react-components"; | ||
import usePortal from "react-useportal"; | ||
import DuplicateInstanceForm from "../forms/DuplicateInstanceForm"; | ||
|
||
interface Props { | ||
instance: LxdInstance; | ||
isLoading: boolean; | ||
} | ||
|
||
const DuplicateInstanceBtn: FC<Props> = ({ instance, isLoading }) => { | ||
const { openPortal, closePortal, isOpen, Portal } = usePortal(); | ||
|
||
return ( | ||
<> | ||
{isOpen && ( | ||
<Portal> | ||
<DuplicateInstanceForm close={closePortal} instance={instance} /> | ||
</Portal> | ||
)} | ||
<Button | ||
appearance="default" | ||
aria-label="Duplicate instance" | ||
className="u-no-margin--bottom" | ||
disabled={isLoading} | ||
onClick={openPortal} | ||
title="Duplicate instance" | ||
> | ||
<span>Duplicate</span> | ||
</Button> | ||
</> | ||
); | ||
}; | ||
|
||
export default DuplicateInstanceBtn; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
import { FC, useState } from "react"; | ||
import { LxdInstance } from "types/instance"; | ||
import { useFormik } from "formik"; | ||
import { useToastNotification } from "context/toastNotificationProvider"; | ||
import { | ||
ActionButton, | ||
Button, | ||
Form, | ||
Input, | ||
Modal, | ||
Select, | ||
} from "@canonical/react-components"; | ||
import * as Yup from "yup"; | ||
import { createInstance, fetchInstances } from "api/instances"; | ||
import { isClusteredServer } from "util/settings"; | ||
import { useSettings } from "context/useSettings"; | ||
import { useQuery, useQueryClient } from "@tanstack/react-query"; | ||
import { queryKeys } from "util/queryKeys"; | ||
import { fetchStoragePools } from "api/storage-pools"; | ||
import { fetchClusterMembers } from "api/cluster"; | ||
import { Link, useNavigate } from "react-router-dom"; | ||
import { instanceNameValidation, truncateInstanceName } from "util/instances"; | ||
import { fetchProjects } from "api/projects"; | ||
import { LxdDiskDevice } from "types/device"; | ||
import InstanceLink from "pages/instances/InstanceLink"; | ||
import { useEventQueue } from "context/eventQueue"; | ||
|
||
interface Props { | ||
instance: LxdInstance; | ||
close: () => void; | ||
} | ||
|
||
export interface LxdInstanceDuplicate { | ||
instanceName: string; | ||
targetProject: string; | ||
targetClusterMember?: string; | ||
targetStoragePool: string; | ||
allowInconsistent: boolean; | ||
instanceOnly: boolean; | ||
stateful: boolean; | ||
} | ||
|
||
const DuplicateInstanceForm: FC<Props> = ({ instance, close }) => { | ||
const toastNotify = useToastNotification(); | ||
const { data: settings } = useSettings(); | ||
const isClustered = isClusteredServer(settings); | ||
const controllerState = useState<AbortController | null>(null); | ||
const navigate = useNavigate(); | ||
const eventQueue = useEventQueue(); | ||
const queryClient = useQueryClient(); | ||
|
||
const { data: projects = [], isLoading: projectsLoading } = useQuery({ | ||
queryKey: [queryKeys.projects], | ||
queryFn: fetchProjects, | ||
}); | ||
|
||
const { data: storagePools = [], isLoading: storagePoolsLoading } = useQuery({ | ||
queryKey: [queryKeys.storage], | ||
queryFn: () => fetchStoragePools(instance.project), | ||
}); | ||
|
||
const { data: clusterMembers = [], isLoading: clusterMembersLoading } = | ||
useQuery({ | ||
queryKey: [queryKeys.cluster], | ||
queryFn: fetchClusterMembers, | ||
enabled: isClustered, | ||
}); | ||
|
||
const { data: instances = [] } = useQuery({ | ||
queryKey: [queryKeys.instances], | ||
queryFn: () => fetchInstances(instance.project), | ||
}); | ||
|
||
const notifySuccess = (instanceName: string, instanceProject: string) => { | ||
const message = ( | ||
<> | ||
Created Instance <strong>{instanceName}</strong>. | ||
</> | ||
); | ||
|
||
const actions = [ | ||
{ | ||
label: `${instanceName}`, | ||
onClick: () => | ||
navigate(`/ui/project/${instanceProject}/instance/${instanceName}`), | ||
}, | ||
]; | ||
|
||
toastNotify.success(message, actions); | ||
}; | ||
|
||
const getDuplicatedInstanceName = (instance: LxdInstance): string => { | ||
const instanceNames = instances.map((instance) => instance.name); | ||
const newInstanceName = truncateInstanceName(instance.name, "-duplicate"); | ||
|
||
if (instanceNames.includes(newInstanceName)) { | ||
let count = 1; | ||
while (instanceNames.includes(`${newInstanceName}-${count}`)) { | ||
count++; | ||
} | ||
return `${newInstanceName}-${count}`; | ||
} | ||
void queryClient.invalidateQueries({ | ||
predicate: (query) => { | ||
return query.queryKey[0] === queryKeys.instances; | ||
}, | ||
}); | ||
|
||
return newInstanceName; | ||
}; | ||
|
||
const formik = useFormik<LxdInstanceDuplicate>({ | ||
initialValues: { | ||
instanceName: getDuplicatedInstanceName(instance), | ||
targetProject: instance.project, | ||
allowInconsistent: false, | ||
instanceOnly: false, | ||
stateful: false, | ||
targetClusterMember: isClustered ? instance.location : "", | ||
targetStoragePool: | ||
(instance.devices["root"] as LxdDiskDevice)?.pool ?? | ||
storagePools[0]?.name, | ||
}, | ||
enableReinitialize: true, | ||
validationSchema: Yup.object().shape({ | ||
instanceName: instanceNameValidation( | ||
instance.project, | ||
controllerState, | ||
).required(), | ||
}), | ||
onSubmit: (values) => { | ||
createInstance( | ||
JSON.stringify({ | ||
description: instance.description, | ||
name: values.instanceName, | ||
architecture: instance.architecture, | ||
source: { | ||
allow_inconsistent: values.allowInconsistent, | ||
instance_only: values.instanceOnly, | ||
source: instance.name, | ||
type: "copy", | ||
project: instance.project, | ||
}, | ||
stateful: values.stateful, | ||
devices: { | ||
...instance.devices, | ||
root: { | ||
path: "/", | ||
type: "disk", | ||
pool: values.targetStoragePool, | ||
}, | ||
}, | ||
}), | ||
values.targetProject, | ||
values.targetClusterMember, | ||
) | ||
.then((operation) => { | ||
toastNotify.info(`Duplication of instance ${instance.name} started.`); | ||
close(); | ||
eventQueue.set( | ||
operation.metadata.id, | ||
() => notifySuccess(values.instanceName, values.targetProject), | ||
(msg) => | ||
toastNotify.failure( | ||
"Instance duplication failed.", | ||
new Error(msg), | ||
<InstanceLink instance={instance} />, | ||
), | ||
); | ||
}) | ||
.catch((e) => { | ||
toastNotify.failure( | ||
"Instance duplication failed.", | ||
e, | ||
<InstanceLink instance={instance} />, | ||
); | ||
close(); | ||
}); | ||
}, | ||
}); | ||
|
||
return ( | ||
<Modal | ||
close={close} | ||
className="duplicate-instances-modal" | ||
title="Duplicate Instance" | ||
buttonRow={ | ||
<> | ||
<Button | ||
appearance="base" | ||
className="u-no-margin--bottom" | ||
type="button" | ||
onClick={close} | ||
> | ||
Cancel | ||
</Button> | ||
<ActionButton | ||
appearance="positive" | ||
className="u-no-margin--bottom" | ||
loading={formik.isSubmitting} | ||
disabled={ | ||
!formik.isValid || | ||
storagePoolsLoading || | ||
clusterMembersLoading || | ||
projectsLoading | ||
} | ||
onClick={() => void formik.submitForm()} | ||
> | ||
Duplicate | ||
</ActionButton> | ||
</> | ||
} | ||
> | ||
<Form onSubmit={formik.handleSubmit}> | ||
<Input | ||
{...formik.getFieldProps("instanceName")} | ||
type="text" | ||
label="New instance name" | ||
error={ | ||
formik.touched.instanceName ? formik.errors.instanceName : null | ||
} | ||
/> | ||
{isClustered && ( | ||
<Select | ||
{...formik.getFieldProps("targetClusterMember")} | ||
id="targetClusterMember" | ||
label="Target cluster member" | ||
options={clusterMembers.map((clusterMember) => { | ||
return { | ||
label: clusterMember.server_name, | ||
value: clusterMember.server_name, | ||
}; | ||
})} | ||
/> | ||
)} | ||
|
||
<Select | ||
{...formik.getFieldProps("targetStoragePool")} | ||
id="storagePool" | ||
label="Storage pool" | ||
options={storagePools.map((storagePool) => { | ||
return { | ||
label: storagePool.name, | ||
value: storagePool.name, | ||
}; | ||
})} | ||
/> | ||
<Select | ||
{...formik.getFieldProps("targetProject")} | ||
id="project" | ||
label="Target project" | ||
options={projects.map((project) => { | ||
return { | ||
label: project.name, | ||
value: project.name, | ||
}; | ||
})} | ||
/> | ||
<Input | ||
{...formik.getFieldProps("allowInconsistent")} | ||
type="checkbox" | ||
label="Ignore copy errors for volatile files" | ||
error={ | ||
formik.touched.allowInconsistent | ||
? formik.errors.allowInconsistent | ||
: null | ||
} | ||
/> | ||
<Input | ||
{...formik.getFieldProps("instanceOnly")} | ||
type="checkbox" | ||
label="Copy without instance snapshots" | ||
error={ | ||
formik.touched.instanceOnly ? formik.errors.instanceOnly : null | ||
} | ||
/> | ||
<Input | ||
{...formik.getFieldProps("stateful")} | ||
type="checkbox" | ||
label="Copy stateful" | ||
error={formik.touched.stateful ? formik.errors.stateful : null} | ||
/> | ||
{/* hidden submit to enable enter key in inputs */} | ||
<Input type="submit" hidden value="Hidden input" /> | ||
</Form> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default DuplicateInstanceForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters