= ({ group }) => {
.then(() => {
const verb = group ? "saved" : "created";
navigate(`/ui/cluster/group/${values.name}`);
- toastNotify.success(`Cluster group ${values.name} ${verb}.`);
+ toastNotify.success(
+ <>
+ Cluster group{" "}
+ {" "}
+ {verb}.
+ >,
+ );
})
.catch((e: Error) => {
formik.setSubmitting(false);
diff --git a/src/pages/cluster/ClusterList.tsx b/src/pages/cluster/ClusterList.tsx
index 769412ddfd..298860a2f7 100644
--- a/src/pages/cluster/ClusterList.tsx
+++ b/src/pages/cluster/ClusterList.tsx
@@ -116,7 +116,7 @@ const ClusterList: FC = () => {
filteredMembers.length < 1 && (
}
+ image={}
title="Cluster group empty"
>
Add cluster members to this group.
@@ -136,7 +136,7 @@ const ClusterList: FC = () => {
{!isClustered && (
}
+ image={}
title="This server is not clustered"
>
diff --git a/src/pages/cluster/actions/DeleteClusterGroupBtn.tsx b/src/pages/cluster/actions/DeleteClusterGroupBtn.tsx
index 157aaebbe4..4e1474786c 100644
--- a/src/pages/cluster/actions/DeleteClusterGroupBtn.tsx
+++ b/src/pages/cluster/actions/DeleteClusterGroupBtn.tsx
@@ -6,6 +6,7 @@ import { queryKeys } from "util/queryKeys";
import { useQueryClient } from "@tanstack/react-query";
import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
group: string;
@@ -23,7 +24,17 @@ const DeleteClusterGroupBtn: FC = ({ group }) => {
deleteClusterGroup(group)
.then(() => {
navigate(`/ui/cluster`);
- toastNotify.success(`Cluster group ${group} deleted.`);
+ toastNotify.success(
+ <>
+ Cluster group{" "}
+ {" "}
+ deleted.
+ >,
+ );
})
.catch((e) => {
setLoading(false);
diff --git a/src/pages/cluster/actions/EvacuateClusterMemberBtn.tsx b/src/pages/cluster/actions/EvacuateClusterMemberBtn.tsx
index 71b3b8fbee..15b55cdcfd 100644
--- a/src/pages/cluster/actions/EvacuateClusterMemberBtn.tsx
+++ b/src/pages/cluster/actions/EvacuateClusterMemberBtn.tsx
@@ -6,6 +6,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { LxdClusterMember } from "types/cluster";
import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
member: LxdClusterMember;
@@ -22,7 +23,15 @@ const EvacuateClusterMemberBtn: FC = ({ member }) => {
postClusterMemberState(member, "evacuate")
.then(() => {
toastNotify.success(
- `Cluster member ${member.server_name} evacuation started.`,
+ <>
+ Cluster member{" "}
+ {" "}
+ evacuation started.
+ >,
);
})
.catch((e) => notify.failure("Cluster member evacuation failed", e))
diff --git a/src/pages/cluster/actions/RestoreClusterMemberBtn.tsx b/src/pages/cluster/actions/RestoreClusterMemberBtn.tsx
index 24b06c40ec..cc68010adc 100644
--- a/src/pages/cluster/actions/RestoreClusterMemberBtn.tsx
+++ b/src/pages/cluster/actions/RestoreClusterMemberBtn.tsx
@@ -6,6 +6,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { LxdClusterMember } from "types/cluster";
import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
member: LxdClusterMember;
@@ -22,7 +23,15 @@ const RestoreClusterMemberBtn: FC = ({ member }) => {
postClusterMemberState(member, "restore")
.then(() => {
toastNotify.success(
- `Cluster member ${member.server_name} restore started.`,
+ <>
+ Cluster member{" "}
+ {" "}
+ restore started.
+ >,
);
})
.catch((e) => notify.failure("Cluster member restore failed", e))
diff --git a/src/pages/images/ImageList.tsx b/src/pages/images/ImageList.tsx
index 8fa575694a..ac5e4d2ec0 100644
--- a/src/pages/images/ImageList.tsx
+++ b/src/pages/images/ImageList.tsx
@@ -241,7 +241,7 @@ const ImageList: FC = () => {
{images.length === 0 && (
}
+ image={}
title="No images found in this project"
>
diff --git a/src/pages/images/actions/DeleteImageBtn.tsx b/src/pages/images/actions/DeleteImageBtn.tsx
index e557b1ddee..a0e9162f57 100644
--- a/src/pages/images/actions/DeleteImageBtn.tsx
+++ b/src/pages/images/actions/DeleteImageBtn.tsx
@@ -6,6 +6,7 @@ import { queryKeys } from "util/queryKeys";
import { ConfirmationButton, Icon } from "@canonical/react-components";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
image: LxdImage;
@@ -22,6 +23,7 @@ const DeleteImageBtn: FC = ({ image, project }) => {
const handleDelete = () => {
setLoading(true);
+ const imageLabel = ;
void deleteImage(image, project)
.then((operation) =>
eventQueue.set(
@@ -33,18 +35,23 @@ const DeleteImageBtn: FC = ({ image, project }) => {
void queryClient.invalidateQueries({
queryKey: [queryKeys.projects, project],
});
- toastNotify.success(`Image ${description} deleted.`);
+ toastNotify.success(<>Image {imageLabel} deleted.>);
},
(msg) =>
toastNotify.failure(
`Image ${description} deletion failed`,
new Error(msg),
+ imageLabel,
),
() => setLoading(false),
),
)
.catch((e) => {
- toastNotify.failure(`Image ${description} deletion failed`, e);
+ toastNotify.failure(
+ `Image ${description} deletion failed`,
+ e,
+ imageLabel,
+ );
setLoading(false);
});
};
diff --git a/src/pages/images/actions/DownloadImageBtn.tsx b/src/pages/images/actions/DownloadImageBtn.tsx
index e3cdd15693..cffec01cb0 100644
--- a/src/pages/images/actions/DownloadImageBtn.tsx
+++ b/src/pages/images/actions/DownloadImageBtn.tsx
@@ -2,6 +2,7 @@ import { FC, useState } from "react";
import { LxdImage } from "types/image";
import { ActionButton, Icon } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
image: LxdImage;
@@ -17,6 +18,13 @@ const DownloadImageBtn: FC = ({ image, project }) => {
const handleExport = () => {
setLoading(true);
+ const imageLink = (
+
+ );
try {
const a = document.createElement("a");
@@ -26,10 +34,17 @@ const DownloadImageBtn: FC = ({ image, project }) => {
window.URL.revokeObjectURL(url);
toastNotify.success(
- `Image ${description} download started. Please check your downloads folder.`,
+ <>
+ Image {imageLink} download started. Please check your downloads
+ folder.
+ >,
);
} catch (e) {
- toastNotify.failure(`Image ${description} was unable to download.`, e);
+ toastNotify.failure(
+ `Image ${description} was unable to download.`,
+ e,
+ imageLink,
+ );
} finally {
setLoading(false);
}
diff --git a/src/pages/images/actions/UploadCustomIsoBtn.tsx b/src/pages/images/actions/UploadCustomIsoBtn.tsx
index 933d8e3868..076e7ff6ca 100644
--- a/src/pages/images/actions/UploadCustomIsoBtn.tsx
+++ b/src/pages/images/actions/UploadCustomIsoBtn.tsx
@@ -5,12 +5,14 @@ import usePortal from "react-useportal";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
className?: string;
+ project: string;
}
-const UploadCustomIsoBtn: FC = ({ className }) => {
+const UploadCustomIsoBtn: FC = ({ className, project }) => {
const toastNotify = useToastNotification();
const { openPortal, closePortal, isOpen, Portal } = usePortal();
const queryClient = useQueryClient();
@@ -20,7 +22,13 @@ const UploadCustomIsoBtn: FC = ({ className }) => {
const handleFinish = (name: string) => {
toastNotify.success(
<>
- Image {name} uploaded successfully
+ Custom ISO{" "}
+ {" "}
+ uploaded successfully.
>,
);
void queryClient.invalidateQueries({ queryKey: [queryKeys.isoVolumes] });
diff --git a/src/pages/instances/CreateInstance.tsx b/src/pages/instances/CreateInstance.tsx
index 9cbedcc3fb..6b41138336 100644
--- a/src/pages/instances/CreateInstance.tsx
+++ b/src/pages/instances/CreateInstance.tsx
@@ -17,7 +17,7 @@ import { LxdImageType, RemoteImage } from "types/image";
import { isContainerOnlyImage, isVmOnlyImage, LOCAL_ISO } from "util/images";
import { dump as dumpYaml } from "js-yaml";
import { yamlToObject } from "util/yaml";
-import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
import { LxdInstance } from "types/instance";
import { Location } from "history";
import InstanceCreateDetailsForm, {
@@ -89,6 +89,9 @@ import OtherDeviceForm from "components/forms/OtherDeviceForm";
import YamlSwitch from "components/forms/YamlSwitch";
import YamlNotification from "components/forms/YamlNotification";
import ProxyDeviceForm from "components/forms/ProxyDeviceForm";
+import ResourceLabel from "components/ResourceLabel";
+import InstanceLinkChip from "./InstanceLinkChip";
+import { InstanceIconType } from "components/ResourceIcon";
export type CreateInstanceFormValues = InstanceDetailsFormValues &
FormDeviceValues &
@@ -146,8 +149,16 @@ const CreateInstance: FC = () => {
});
};
- const notifyCreationStarted = (instanceName: string) => {
- toastNotify.info(<>Creation for instance {instanceName} started.>);
+ const notifyCreationStarted = (
+ instanceName: string,
+ instanceType: InstanceIconType,
+ ) => {
+ toastNotify.info(
+ <>
+ Creation for instance{" "}
+ started.
+ >,
+ );
};
const notifyCreatedNowStarting = (instanceLink: ReactNode) => {
@@ -198,11 +209,15 @@ const CreateInstance: FC = () => {
clearCache();
};
- const notifyCreationAndStarting = (instanceName: string) => {
+ const notifyCreationAndStarting = (
+ instanceName: string,
+ instanceType: InstanceIconType,
+ ) => {
toastNotify.info(
<>
- Instance {instanceName} creation has begun. The instance will
- automatically start upon completion.
+ Instance {" "}
+ creation has begun. The instance will automatically start upon
+ completion.
>,
);
};
@@ -211,11 +226,12 @@ const CreateInstance: FC = () => {
instanceName: string,
shouldStart: boolean,
isIsoImage: boolean,
+ instanceType: InstanceIconType,
) => {
const instanceLink = (
-
- {instanceName}
-
+
);
// only send a second request to start the instance if the lxd version does not support the instance_create_start api extension
@@ -289,15 +305,21 @@ const CreateInstance: FC = () => {
}
if (shouldStart && hasInstanceCreateStart) {
- notifyCreationAndStarting(instanceName);
+ notifyCreationAndStarting(instanceName, values.instanceType);
} else {
- notifyCreationStarted(instanceName);
+ notifyCreationStarted(instanceName, values.instanceType);
}
const isIsoImage = values.image?.server === LOCAL_ISO;
eventQueue.set(
operation.metadata.id,
- () => creationCompletedHandler(instanceName, shouldStart, isIsoImage),
+ () =>
+ creationCompletedHandler(
+ instanceName,
+ shouldStart,
+ isIsoImage,
+ values.instanceType,
+ ),
(msg) =>
notifyCreationFailed(
new Error(msg),
diff --git a/src/pages/instances/EditInstance.tsx b/src/pages/instances/EditInstance.tsx
index b30b9a574a..c9945ae9f2 100644
--- a/src/pages/instances/EditInstance.tsx
+++ b/src/pages/instances/EditInstance.tsx
@@ -52,7 +52,6 @@ import { useEventQueue } from "context/eventQueue";
import { hasDiskError, hasNetworkError } from "util/instanceValidation";
import FormFooterLayout from "components/forms/FormFooterLayout";
import { useToastNotification } from "context/toastNotificationProvider";
-import InstanceLink from "pages/instances/InstanceLink";
import { useDocs } from "context/useDocs";
import MigrationForm, {
MigrationFormValues,
@@ -63,6 +62,7 @@ import YamlSwitch from "components/forms/YamlSwitch";
import YamlNotification from "components/forms/YamlNotification";
import ProxyDeviceForm from "components/forms/ProxyDeviceForm";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import InstanceLinkChip from "./InstanceLinkChip";
export interface InstanceEditDetailsFormValues {
name: string;
@@ -123,7 +123,7 @@ const EditInstance: FC = ({ instance }) => {
// ensure the etag is set (it is missing on the yaml)
instancePayload.etag = instance.etag;
- const instanceLink = ;
+ const instanceLink = ;
void updateInstance(instancePayload, project)
.then((operation) => {
diff --git a/src/pages/instances/InstanceConsole.tsx b/src/pages/instances/InstanceConsole.tsx
index 0e7840710b..98a246b229 100644
--- a/src/pages/instances/InstanceConsole.tsx
+++ b/src/pages/instances/InstanceConsole.tsx
@@ -112,7 +112,7 @@ const InstanceConsole: FC = ({ instance }) => {
{isGraphic && !isRunning && (
}
+ image={}
title="Instance stopped"
>
Start the instance to access the graphic console.
diff --git a/src/pages/instances/InstanceDetailHeader.tsx b/src/pages/instances/InstanceDetailHeader.tsx
index 30a9f458a2..2f5dd65611 100644
--- a/src/pages/instances/InstanceDetailHeader.tsx
+++ b/src/pages/instances/InstanceDetailHeader.tsx
@@ -9,13 +9,12 @@ import * as Yup from "yup";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
import {
- instanceLinkFromName,
instanceLinkFromOperation,
instanceNameValidation,
} from "util/instances";
import { getInstanceName } from "util/operations";
-import InstanceLink from "pages/instances/InstanceLink";
import InstanceDetailActions from "./InstanceDetailActions";
+import InstanceLinkChip from "./InstanceLinkChip";
interface Props {
name: string;
@@ -55,10 +54,15 @@ const InstanceDetailHeader: FC = ({
}
void renameInstance(name, values.name, project)
.then((operation) => {
- const instanceLink = instanceLinkFromName({
- instanceName: values.name,
- project,
- });
+ const instanceLink = (
+
+ );
eventQueue.set(
operation.metadata.id,
() => {
@@ -76,7 +80,11 @@ const InstanceDetailHeader: FC = ({
toastNotify.failure(
"Renaming instance failed.",
new Error(msg),
- instanceLinkFromOperation({ operation, project }),
+ instanceLinkFromOperation({
+ operation,
+ project,
+ instanceType: instance?.type || "instance",
+ }),
),
() => formik.setSubmitting(false),
);
@@ -86,7 +94,7 @@ const InstanceDetailHeader: FC = ({
toastNotify.failure(
`Renaming instance failed.`,
e,
- instance ? : undefined,
+ instance && ,
);
});
},
diff --git a/src/pages/instances/InstanceLinkChip.tsx b/src/pages/instances/InstanceLinkChip.tsx
new file mode 100644
index 0000000000..8543de244b
--- /dev/null
+++ b/src/pages/instances/InstanceLinkChip.tsx
@@ -0,0 +1,23 @@
+import { FC } from "react";
+import { LxdInstance } from "types/instance";
+import ResourceLink from "components/ResourceLink";
+import { InstanceIconType } from "components/ResourceIcon";
+
+interface Props {
+ instance: Partial> & {
+ name: string;
+ type: InstanceIconType;
+ };
+}
+
+const InstanceLinkChip: FC = ({ instance }) => {
+ return (
+
+ );
+};
+
+export default InstanceLinkChip;
diff --git a/src/pages/instances/InstanceList.tsx b/src/pages/instances/InstanceList.tsx
index 2d7d64f0eb..b911be9493 100644
--- a/src/pages/instances/InstanceList.tsx
+++ b/src/pages/instances/InstanceList.tsx
@@ -627,7 +627,7 @@ const InstanceList: FC = () => {
{!hasInstances && (
}
+ image={}
title="No instances found"
>
diff --git a/src/pages/instances/InstanceSnapshotLinkChip.tsx b/src/pages/instances/InstanceSnapshotLinkChip.tsx
new file mode 100644
index 0000000000..c2bbfb55cc
--- /dev/null
+++ b/src/pages/instances/InstanceSnapshotLinkChip.tsx
@@ -0,0 +1,21 @@
+import { FC } from "react";
+import ResourceLink from "components/ResourceLink";
+import { PartialWithRequired } from "types/partial";
+import { LxdInstance } from "types/instance";
+
+interface Props {
+ name: string;
+ instance: PartialWithRequired;
+}
+
+const InstanceSnapshotLinkChip: FC = ({ name, instance }) => {
+ return (
+
+ );
+};
+
+export default InstanceSnapshotLinkChip;
diff --git a/src/pages/instances/InstanceSnapshots.tsx b/src/pages/instances/InstanceSnapshots.tsx
index 2ecc2b9d4d..99fdc50303 100644
--- a/src/pages/instances/InstanceSnapshots.tsx
+++ b/src/pages/instances/InstanceSnapshots.tsx
@@ -291,7 +291,7 @@ const InstanceSnapshots = (props: Props) => {
) : (
}
+ image={}
title="No snapshots found"
>
diff --git a/src/pages/instances/InstanceTerminal.tsx b/src/pages/instances/InstanceTerminal.tsx
index acc4d7c90d..645c947e3d 100644
--- a/src/pages/instances/InstanceTerminal.tsx
+++ b/src/pages/instances/InstanceTerminal.tsx
@@ -219,7 +219,7 @@ const InstanceTerminal: FC = ({ instance }) => {
{!isRunning && (
}
+ image={}
title="Instance stopped"
>
Start the instance to access the terminal.
diff --git a/src/pages/instances/MigrateInstanceModal.tsx b/src/pages/instances/MigrateInstanceModal.tsx
index 74cc504b38..e8c042fd0e 100644
--- a/src/pages/instances/MigrateInstanceModal.tsx
+++ b/src/pages/instances/MigrateInstanceModal.tsx
@@ -85,12 +85,12 @@ const MigrateInstanceModal: FC = ({ close, instance }) => {
{isClustered && !type && (
setType("cluster member")}
/>
setType("root storage pool")}
/>
diff --git a/src/pages/instances/actions/AttachIsoBtn.tsx b/src/pages/instances/actions/AttachIsoBtn.tsx
index f9076aaa64..97afcc3912 100644
--- a/src/pages/instances/actions/AttachIsoBtn.tsx
+++ b/src/pages/instances/actions/AttachIsoBtn.tsx
@@ -14,6 +14,8 @@ import { remoteImageToIsoDevice } from "util/formDevices";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
import { instanceLinkFromOperation } from "util/instances";
+import ResourceLink from "components/ResourceLink";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -41,19 +43,21 @@ const AttachIsoBtn: FC = ({ instance }) => {
instance,
values,
) as LxdInstance;
+ const instanceLink = ;
void updateInstance(instanceMinusIso, project ?? "")
.then((operation) => {
- const instanceLink = instanceLinkFromOperation({
- operation,
- project,
- });
eventQueue.set(
operation.metadata.id,
() =>
toastNotify.success(
<>
- ISO {attachedIso?.source ?? ""} detached from{" "}
- {instanceLink}
+ ISO{" "}
+ {" "}
+ detached from {instanceLink}
>,
),
(msg) =>
@@ -72,7 +76,7 @@ const AttachIsoBtn: FC = ({ instance }) => {
})
.catch((e) => {
setLoading(false);
- toastNotify.failure("Detaching ISO failed.", e);
+ toastNotify.failure("Detaching ISO failed.", e, instanceLink);
});
};
@@ -88,13 +92,20 @@ const AttachIsoBtn: FC = ({ instance }) => {
const instanceLink = instanceLinkFromOperation({
operation,
project,
+ instanceType: instance.type,
});
eventQueue.set(
operation.metadata.id,
() =>
toastNotify.success(
<>
- ISO {image.aliases} attached to {instanceLink}
+ ISO{" "}
+ {" "}
+ attached to {instanceLink}
>,
),
(msg) =>
diff --git a/src/pages/instances/actions/DeleteInstanceBtn.tsx b/src/pages/instances/actions/DeleteInstanceBtn.tsx
index 27e4a43b04..f848cc02fc 100644
--- a/src/pages/instances/actions/DeleteInstanceBtn.tsx
+++ b/src/pages/instances/actions/DeleteInstanceBtn.tsx
@@ -10,8 +10,9 @@ import { useEventQueue } from "context/eventQueue";
import { queryKeys } from "util/queryKeys";
import { useQueryClient } from "@tanstack/react-query";
import { useToastNotification } from "context/toastNotificationProvider";
-import InstanceLink from "pages/instances/InstanceLink";
import { useInstanceLoading } from "context/instanceLoading";
+import ResourceLabel from "components/ResourceLabel";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -29,6 +30,8 @@ const DeleteInstanceBtn: FC = ({ instance, classname, onClose }) => {
const handleDelete = () => {
setLoading(true);
+ const instanceLink = ;
+
void deleteInstance(instance)
.then((operation) => {
eventQueue.set(
@@ -38,23 +41,29 @@ const DeleteInstanceBtn: FC = ({ instance, classname, onClose }) => {
queryKey: [queryKeys.projects, instance.project],
});
navigate(`/ui/project/${instance.project}/instances`);
- toastNotify.success(`Instance ${instance.name} deleted.`);
+ toastNotify.success(
+ <>
+ Instance{" "}
+ {" "}
+ deleted.
+ >,
+ );
},
(msg) =>
toastNotify.failure(
"Instance deletion failed",
new Error(msg),
- ,
+ instanceLink,
),
() => setLoading(false),
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance deletion failed",
- e,
- ,
- );
+ toastNotify.failure("Instance deletion failed", e, instanceLink);
setLoading(false);
});
};
diff --git a/src/pages/instances/actions/ExportInstanceBtn.tsx b/src/pages/instances/actions/ExportInstanceBtn.tsx
index 458d648b2a..6cd0668022 100644
--- a/src/pages/instances/actions/ExportInstanceBtn.tsx
+++ b/src/pages/instances/actions/ExportInstanceBtn.tsx
@@ -5,7 +5,7 @@ import classNames from "classnames";
import { createInstanceBackup } from "api/instances";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
-import { Link } from "react-router-dom";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -17,11 +17,7 @@ const ExportInstanceBtn: FC = ({ instance, classname, onClose }) => {
const eventQueue = useEventQueue();
const toastNotify = useToastNotification();
- const instanceLink = (
-
- {instance.name}
-
- );
+ const instanceLink = ;
const startDownload = (backupName: string) => {
const url = `/1.0/instances/${instance.name}/backups/${backupName}/export?project=${instance.project}`;
@@ -72,11 +68,16 @@ const ExportInstanceBtn: FC = ({ instance, classname, onClose }) => {
toastNotify.failure(
`Could not download instance ${instance.name}`,
new Error(msg),
+ instanceLink,
),
);
})
.catch((e) =>
- toastNotify.failure(`Could not download instance ${instance.name}`, e),
+ toastNotify.failure(
+ `Could not download instance ${instance.name}`,
+ e,
+ instanceLink,
+ ),
)
.finally(() => {
onClose?.();
diff --git a/src/pages/instances/actions/FreezeInstanceBtn.tsx b/src/pages/instances/actions/FreezeInstanceBtn.tsx
index 1b88e90432..979ba535f0 100644
--- a/src/pages/instances/actions/FreezeInstanceBtn.tsx
+++ b/src/pages/instances/actions/FreezeInstanceBtn.tsx
@@ -4,11 +4,11 @@ import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { freezeInstance } from "api/instances";
import { useInstanceLoading } from "context/instanceLoading";
-import InstanceLink from "pages/instances/InstanceLink";
-import ItemName from "components/ItemName";
import { ConfirmationButton, Icon } from "@canonical/react-components";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
+import ItemName from "components/ItemName";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -30,6 +30,8 @@ const FreezeInstanceBtn: FC = ({ instance }) => {
instanceLoading.getType(instance) === "Freezing" ||
instance.status === "Freezing";
+ const instanceLink = ;
+
const handleFreeze = () => {
instanceLoading.setLoading(instance, "Freezing");
void freezeInstance(instance)
@@ -37,18 +39,14 @@ const FreezeInstanceBtn: FC = ({ instance }) => {
eventQueue.set(
operation.metadata.id,
() => {
- toastNotify.success(
- <>
- Instance frozen.
- >,
- );
+ toastNotify.success(<>Instance {instanceLink} frozen.>);
clearCache();
},
(msg) => {
toastNotify.failure(
"Instance freeze failed",
new Error(msg),
- ,
+ instanceLink,
);
// Delay clearing the cache, because the instance is reported as FROZEN
// when a freeze operation failed, only shortly after it goes back to RUNNING
@@ -61,11 +59,7 @@ const FreezeInstanceBtn: FC = ({ instance }) => {
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance freeze failed",
- e,
- ,
- );
+ toastNotify.failure("Instance freeze failed", e, instanceLink);
instanceLoading.setFinish(instance);
});
};
diff --git a/src/pages/instances/actions/RestartInstanceBtn.tsx b/src/pages/instances/actions/RestartInstanceBtn.tsx
index 164ad07eff..ca7bb3d740 100644
--- a/src/pages/instances/actions/RestartInstanceBtn.tsx
+++ b/src/pages/instances/actions/RestartInstanceBtn.tsx
@@ -4,12 +4,12 @@ import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { restartInstance } from "api/instances";
import { useInstanceLoading } from "context/instanceLoading";
-import InstanceLink from "pages/instances/InstanceLink";
import ConfirmationForce from "components/ConfirmationForce";
-import ItemName from "components/ItemName";
import { ConfirmationButton, Icon } from "@canonical/react-components";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
+import ItemName from "components/ItemName";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -25,23 +25,20 @@ const RestartInstanceBtn: FC = ({ instance }) => {
instanceLoading.getType(instance) === "Restarting" ||
instance.status === "Restarting";
+ const instanceLink = ;
+
const handleRestart = () => {
instanceLoading.setLoading(instance, "Restarting");
void restartInstance(instance, isForce)
.then((operation) => {
eventQueue.set(
operation.metadata.id,
- () =>
- toastNotify.success(
- <>
- Instance restarted.
- >,
- ),
+ () => toastNotify.success(<>Instance {instanceLink} restarted.>),
(msg) =>
toastNotify.failure(
"Instance restart failed",
new Error(msg),
- ,
+ instanceLink,
),
() => {
instanceLoading.setFinish(instance);
@@ -52,11 +49,7 @@ const RestartInstanceBtn: FC = ({ instance }) => {
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance restart failed",
- e,
- ,
- );
+ toastNotify.failure("Instance restart failed", e, instanceLink);
instanceLoading.setFinish(instance);
});
};
diff --git a/src/pages/instances/actions/StopInstanceBtn.tsx b/src/pages/instances/actions/StopInstanceBtn.tsx
index 587e98a2b3..06083654bb 100644
--- a/src/pages/instances/actions/StopInstanceBtn.tsx
+++ b/src/pages/instances/actions/StopInstanceBtn.tsx
@@ -4,12 +4,12 @@ import { useQueryClient } from "@tanstack/react-query";
import { stopInstance } from "api/instances";
import { queryKeys } from "util/queryKeys";
import { useInstanceLoading } from "context/instanceLoading";
-import InstanceLink from "pages/instances/InstanceLink";
import ConfirmationForce from "components/ConfirmationForce";
-import ItemName from "components/ItemName";
import { ConfirmationButton, Icon } from "@canonical/react-components";
import { useEventQueue } from "context/eventQueue";
import { useToastNotification } from "context/toastNotificationProvider";
+import ItemName from "components/ItemName";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -32,6 +32,8 @@ const StopInstanceBtn: FC = ({ instance }) => {
instanceLoading.getType(instance) === "Stopping" ||
instance.status === "Stopping";
+ const instanceLink = ;
+
const handleStop = () => {
instanceLoading.setLoading(instance, "Stopping");
void stopInstance(instance, isForce)
@@ -39,18 +41,14 @@ const StopInstanceBtn: FC = ({ instance }) => {
eventQueue.set(
operation.metadata.id,
() => {
- toastNotify.success(
- <>
- Instance stopped.
- >,
- );
+ toastNotify.success(<>Instance {instanceLink} stopped.>);
clearCache();
},
(msg) => {
toastNotify.failure(
"Instance stop failed",
new Error(msg),
- ,
+ instanceLink,
);
// Delay clearing the cache, because the instance is reported as STOPPED
// when a stop operation failed, only shortly after it goes back to RUNNING
@@ -63,11 +61,7 @@ const StopInstanceBtn: FC = ({ instance }) => {
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance stop failed",
- e,
- ,
- );
+ toastNotify.failure("Instance stop failed", e, instanceLink);
instanceLoading.setFinish(instance);
});
};
diff --git a/src/pages/instances/actions/snapshots/InstanceSnapshotActions.tsx b/src/pages/instances/actions/snapshots/InstanceSnapshotActions.tsx
index aea05682f3..6b2e606db1 100644
--- a/src/pages/instances/actions/snapshots/InstanceSnapshotActions.tsx
+++ b/src/pages/instances/actions/snapshots/InstanceSnapshotActions.tsx
@@ -14,6 +14,8 @@ import { useEventQueue } from "context/eventQueue";
import InstanceEditSnapshotBtn from "./InstanceEditSnapshotBtn";
import CreateImageFromInstanceSnapshotBtn from "pages/instances/actions/snapshots/CreateImageFromInstanceSnapshotBtn";
import CreateInstanceFromSnapshotBtn from "./CreateInstanceFromSnapshotBtn";
+import ResourceLabel from "components/ResourceLabel";
+import InstanceSnapshotLinkChip from "pages/instances/InstanceSnapshotLinkChip";
interface Props {
instance: LxdInstance;
@@ -43,7 +45,9 @@ const InstanceSnapshotActions: FC = ({
() =>
onSuccess(
<>
- Snapshot deleted.
+ Snapshot{" "}
+ {" "}
+ deleted.
>,
),
(msg) => onFailure("Snapshot deletion failed", new Error(msg)),
@@ -70,7 +74,12 @@ const InstanceSnapshotActions: FC = ({
() =>
onSuccess(
<>
- Snapshot restored.
+ Snapshot{" "}
+ {" "}
+ restored.
>,
),
(msg) => onFailure("Snapshot restore failed", new Error(msg)),
diff --git a/src/pages/instances/forms/CreateImageFromInstanceForm.tsx b/src/pages/instances/forms/CreateImageFromInstanceForm.tsx
index 04d2e3f7da..3c33e9b60b 100644
--- a/src/pages/instances/forms/CreateImageFromInstanceForm.tsx
+++ b/src/pages/instances/forms/CreateImageFromInstanceForm.tsx
@@ -15,6 +15,7 @@ import * as Yup from "yup";
import { Link } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
+import InstanceLinkChip from "../InstanceLinkChip";
interface Props {
instance: LxdInstance;
@@ -25,6 +26,7 @@ const CreateImageFromInstanceForm: FC = ({ instance, close }) => {
const eventQueue = useEventQueue();
const toastNotify = useToastNotification();
const queryClient = useQueryClient();
+ const instanceLink = ;
const notifySuccess = () => {
const created = (
@@ -32,7 +34,7 @@ const CreateImageFromInstanceForm: FC = ({ instance, close }) => {
);
toastNotify.success(
<>
- Image {created} from instance {instance.name}.
+ Image {created} from instance {instanceLink}.
>,
);
};
@@ -71,11 +73,7 @@ const CreateImageFromInstanceForm: FC = ({ instance, close }) => {
createImage(getInstanceToImageBody(instance, values.isPublic), instance)
.then((operation) => {
- toastNotify.info(
- <>
- Creation of image from instance {instance.name} started.
- >,
- );
+ toastNotify.info(<>Creation of image from {instanceLink} started.>);
close();
eventQueue.set(
operation.metadata.id,
@@ -100,6 +98,7 @@ const CreateImageFromInstanceForm: FC = ({ instance, close }) => {
toastNotify.failure(
`Image creation from instance "${instance.name}" failed.`,
new Error(msg),
+ instanceLink,
);
},
);
@@ -108,6 +107,7 @@ const CreateImageFromInstanceForm: FC = ({ instance, close }) => {
toastNotify.failure(
`Image creation from instance "${instance.name}" failed.`,
e,
+ instanceLink,
);
});
},
diff --git a/src/pages/instances/forms/CreateImageFromInstanceSnapshotForm.tsx b/src/pages/instances/forms/CreateImageFromInstanceSnapshotForm.tsx
index 28d9df6850..94f51afe10 100644
--- a/src/pages/instances/forms/CreateImageFromInstanceSnapshotForm.tsx
+++ b/src/pages/instances/forms/CreateImageFromInstanceSnapshotForm.tsx
@@ -15,6 +15,7 @@ import * as Yup from "yup";
import { Link } from "react-router-dom";
import { queryKeys } from "util/queryKeys";
import { useQueryClient } from "@tanstack/react-query";
+import InstanceSnapshotLinkChip from "../InstanceSnapshotLinkChip";
interface Props {
instance: LxdInstance;
@@ -30,6 +31,9 @@ const CreateImageFromInstanceSnapshotForm: FC = ({
const eventQueue = useEventQueue();
const toastNotify = useToastNotification();
const queryClient = useQueryClient();
+ const snapshotLink = (
+
+ );
const notifySuccess = () => {
const created = (
@@ -37,7 +41,7 @@ const CreateImageFromInstanceSnapshotForm: FC = ({
);
toastNotify.success(
<>
- Image {created} from snapshot {snapshot.name}.
+ Image {created} from snapshot {snapshotLink}.
>,
);
};
@@ -80,11 +84,7 @@ const CreateImageFromInstanceSnapshotForm: FC = ({
instance,
)
.then((operation) => {
- toastNotify.info(
- <>
- Creation of image from snapshot {snapshot.name} started.
- >,
- );
+ toastNotify.info(<>Creation of image from {snapshotLink} started.>);
close();
eventQueue.set(
operation.metadata.id,
@@ -109,6 +109,7 @@ const CreateImageFromInstanceSnapshotForm: FC = ({
toastNotify.failure(
`Image creation from snapshot "${snapshot.name}" failed.`,
new Error(msg),
+ snapshotLink,
);
},
);
@@ -117,6 +118,7 @@ const CreateImageFromInstanceSnapshotForm: FC = ({
toastNotify.failure(
`Image creation from snapshot "${snapshot.name}" failed.`,
e,
+ snapshotLink,
);
});
},
diff --git a/src/pages/instances/forms/CreateInstanceFromSnapshotForm.tsx b/src/pages/instances/forms/CreateInstanceFromSnapshotForm.tsx
index a91a1070ce..47d8e8b5ae 100644
--- a/src/pages/instances/forms/CreateInstanceFromSnapshotForm.tsx
+++ b/src/pages/instances/forms/CreateInstanceFromSnapshotForm.tsx
@@ -17,13 +17,14 @@ import { useSettings } from "context/useSettings";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchStoragePools } from "api/storage-pools";
-import { Link } 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";
import ClusterMemberSelector from "pages/cluster/ClusterMemberSelector";
+import ResourceLabel from "components/ResourceLabel";
+import InstanceLinkChip from "../InstanceLinkChip";
+import { InstanceIconType } from "components/ResourceIcon";
interface Props {
instance: LxdInstance;
@@ -104,9 +105,13 @@ const CreateInstanceFromSnapshotForm: FC = ({
queryFn: () => fetchInstances(instance.project),
});
- const notifySuccess = (name: string, project: string) => {
+ const notifySuccess = (
+ name: string,
+ project: string,
+ type: InstanceIconType,
+ ) => {
const instanceLink = (
- {name}
+
);
const message = <>Created instance {instanceLink}.>;
@@ -167,7 +172,9 @@ const CreateInstanceFromSnapshotForm: FC = ({
instance.name,
).required(),
}),
+
onSubmit: (values) => {
+ const instanceLink = ;
createInstance(
JSON.stringify(instanceFromSnapshotPayload(values, instance, snapshot)),
values.targetProject,
@@ -175,25 +182,34 @@ const CreateInstanceFromSnapshotForm: FC = ({
)
.then((operation) => {
toastNotify.info(
- `Instance creation started for ${values.instanceName}.`,
+ <>
+ Instance creation started for{" "}
+
+ .
+ >,
);
eventQueue.set(
operation.metadata.id,
- () => notifySuccess(values.instanceName, values.targetProject),
+ () =>
+ notifySuccess(
+ values.instanceName,
+ values.targetProject,
+ instance.type,
+ ),
(msg) =>
toastNotify.failure(
"Instance creation failed.",
new Error(msg),
- ,
+ instanceLink,
),
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance creation failed.",
- e,
- ,
- );
+ toastNotify.failure("Instance creation failed.", e, instanceLink);
})
.finally(() => {
close();
diff --git a/src/pages/instances/forms/CreateInstanceSnapshotForm.tsx b/src/pages/instances/forms/CreateInstanceSnapshotForm.tsx
index 3026fd7638..e4b7aab767 100644
--- a/src/pages/instances/forms/CreateInstanceSnapshotForm.tsx
+++ b/src/pages/instances/forms/CreateInstanceSnapshotForm.tsx
@@ -19,9 +19,11 @@ import { SnapshotFormValues, getExpiresAt } from "util/snapshots";
import { UNDEFINED_DATE, stringToIsoTime } from "util/helpers";
import { createInstanceSnapshot } from "api/instance-snapshots";
import { queryKeys } from "util/queryKeys";
-import ItemName from "components/ItemName";
import { TOOLTIP_OVER_MODAL_ZINDEX } from "util/zIndex";
import { useToastNotification } from "context/toastNotificationProvider";
+import InstanceLinkChip from "../InstanceLinkChip";
+import { getInstanceSnapshotName } from "util/operations";
+import InstanceSnapshotLinkChip from "../InstanceSnapshotLinkChip";
interface Props {
close: () => void;
@@ -57,6 +59,7 @@ const CreateInstanceSnapshotForm: FC = ({
getExpiresAt(values.expirationDate, values.expirationTime),
)
: UNDEFINED_DATE;
+ const instanceLink = ;
void createInstanceSnapshot(
instance,
values.name,
@@ -72,8 +75,12 @@ const CreateInstanceSnapshotForm: FC = ({
});
onSuccess(
<>
- Snapshot created for instance{" "}
- {instance.name}.
+ Snapshot{" "}
+ {" "}
+ created for instance {instanceLink}.
>,
);
resetForm();
@@ -83,6 +90,7 @@ const CreateInstanceSnapshotForm: FC = ({
toastNotify.failure(
`Snapshot creation failed for instance ${instance.name}`,
new Error(msg),
+ instanceLink,
);
formik.setSubmitting(false);
close();
@@ -90,7 +98,7 @@ const CreateInstanceSnapshotForm: FC = ({
),
)
.catch((error: Error) => {
- notify.failure("Snapshot creation failed", error);
+ notify.failure("Snapshot creation failed", error, instanceLink);
formik.setSubmitting(false);
close();
});
diff --git a/src/pages/instances/forms/DuplicateInstanceForm.tsx b/src/pages/instances/forms/DuplicateInstanceForm.tsx
index add7a49f8b..b0190d92f8 100644
--- a/src/pages/instances/forms/DuplicateInstanceForm.tsx
+++ b/src/pages/instances/forms/DuplicateInstanceForm.tsx
@@ -21,10 +21,12 @@ import { 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";
import ClusterMemberSelector from "pages/cluster/ClusterMemberSelector";
import { getUniqueResourceName } from "util/helpers";
+import ResourceLink from "components/ResourceLink";
+import InstanceLinkChip from "../InstanceLinkChip";
+import { InstanceIconType } from "components/ResourceIcon";
interface Props {
instance: LxdInstance;
@@ -64,20 +66,23 @@ const DuplicateInstanceForm: FC = ({ instance, close }) => {
queryFn: () => fetchInstances(instance.project),
});
- const notifySuccess = (instanceName: string, instanceProject: string) => {
+ const notifySuccess = (
+ name: string,
+ project: string,
+ type: InstanceIconType,
+ ) => {
+ const instanceUrl = `/ui/project/${project}/instance/${name}`;
const message = (
<>
- Created instance {instanceName}.
+ Created instance{" "}
+ .
>
);
const actions = [
{
label: "Configure",
- onClick: () =>
- navigate(
- `/ui/project/${instanceProject}/instance/${instanceName}/configuration`,
- ),
+ onClick: () => navigate(`${instanceUrl}/configuration`),
},
];
@@ -109,6 +114,7 @@ const DuplicateInstanceForm: FC = ({ instance, close }) => {
).required(),
}),
onSubmit: (values) => {
+ const instanceLink = ;
createInstance(
JSON.stringify({
description: instance.description,
@@ -135,24 +141,27 @@ const DuplicateInstanceForm: FC = ({ instance, close }) => {
values.targetClusterMember,
)
.then((operation) => {
- toastNotify.info(`Duplication of instance ${instance.name} started.`);
+ toastNotify.info(
+ <>Duplication of instance {instanceLink} started.>,
+ );
eventQueue.set(
operation.metadata.id,
- () => notifySuccess(values.instanceName, values.targetProject),
+ () =>
+ notifySuccess(
+ values.instanceName,
+ values.targetProject,
+ instance.type,
+ ),
(msg) =>
toastNotify.failure(
"Instance duplication failed.",
new Error(msg),
- ,
+ instanceLink,
),
);
})
.catch((e) => {
- toastNotify.failure(
- "Instance duplication failed.",
- e,
- ,
- );
+ toastNotify.failure("Instance duplication failed.", e, instanceLink);
})
.finally(() => {
close();
diff --git a/src/pages/instances/forms/EditInstanceSnapshotForm.tsx b/src/pages/instances/forms/EditInstanceSnapshotForm.tsx
index 8149f6725f..6441cd6154 100644
--- a/src/pages/instances/forms/EditInstanceSnapshotForm.tsx
+++ b/src/pages/instances/forms/EditInstanceSnapshotForm.tsx
@@ -6,7 +6,6 @@ import {
renameInstanceSnapshot,
updateInstanceSnapshot,
} from "api/instance-snapshots";
-import ItemName from "components/ItemName";
import { useEventQueue } from "context/eventQueue";
import { useFormik } from "formik";
import {
@@ -18,6 +17,8 @@ import { getInstanceSnapshotSchema } from "util/instanceSnapshots";
import { queryKeys } from "util/queryKeys";
import { SnapshotFormValues, getExpiresAt } from "util/snapshots";
import { useToastNotification } from "context/toastNotificationProvider";
+import InstanceLinkChip from "../InstanceLinkChip";
+import InstanceSnapshotLinkChip from "../InstanceSnapshotLinkChip";
interface Props {
instance: LxdInstance;
@@ -43,8 +44,8 @@ const EditInstanceSnapshotForm: FC = ({
});
onSuccess(
<>
- Snapshot saved for instance{" "}
- {instance.name}.
+ Snapshot {" "}
+ saved for instance .
>,
);
close();
@@ -56,6 +57,7 @@ const EditInstanceSnapshotForm: FC = ({
name: newName,
} as LxdInstanceSnapshot)
: snapshot;
+ const instanceLink = ;
void updateInstanceSnapshot(instance, targetSnapshot, expiresAt)
.then((operation) =>
eventQueue.set(
@@ -65,18 +67,26 @@ const EditInstanceSnapshotForm: FC = ({
toastNotify.failure(
`Snapshot update failed for instance ${instance.name}`,
new Error(msg),
+ instanceLink,
);
formik.setSubmitting(false);
},
),
)
.catch((e) => {
- toastNotify.failure("Snapshot update failed", e);
+ toastNotify.failure(
+ `Snapshot update failed for instance ${instance.name}`,
+ e,
+ instanceLink,
+ );
formik.setSubmitting(false);
});
};
const rename = (newName: string, expiresAt?: string) => {
+ const snapshotLink = (
+
+ );
void renameInstanceSnapshot(instance, snapshot, newName)
.then((operation) =>
eventQueue.set(
@@ -90,15 +100,20 @@ const EditInstanceSnapshotForm: FC = ({
},
(msg) => {
toastNotify.failure(
- `Snapshot rename failed for instance ${instance.name}`,
+ `Snapshot rename failed for ${snapshot.name}`,
new Error(msg),
+ snapshotLink,
);
formik.setSubmitting(false);
},
),
)
.catch((e) => {
- toastNotify.failure("Snapshot rename failed", e);
+ toastNotify.failure(
+ `Snapshot rename failed for ${snapshot.name}`,
+ e,
+ snapshotLink,
+ );
formik.setSubmitting(false);
});
};
diff --git a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx
index 8801772092..5e3f008a53 100644
--- a/src/pages/instances/forms/InstanceCreateDetailsForm.tsx
+++ b/src/pages/instances/forms/InstanceCreateDetailsForm.tsx
@@ -25,12 +25,13 @@ import AutoExpandingTextArea from "components/AutoExpandingTextArea";
import ScrollableForm from "components/ScrollableForm";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import UploadInstanceFileBtn from "../actions/UploadInstanceFileBtn";
+import { InstanceIconType } from "components/ResourceIcon";
export interface InstanceDetailsFormValues {
name?: string;
description?: string;
image?: RemoteImage;
- instanceType: string;
+ instanceType: InstanceIconType;
profiles: string[];
target?: string;
entityType: "instance";
diff --git a/src/pages/instances/forms/UploadExternalFormatFileForm.tsx b/src/pages/instances/forms/UploadExternalFormatFileForm.tsx
index c5fc26883a..99665d0b7e 100644
--- a/src/pages/instances/forms/UploadExternalFormatFileForm.tsx
+++ b/src/pages/instances/forms/UploadExternalFormatFileForm.tsx
@@ -21,7 +21,6 @@ import { createInstance } from "api/instances";
import { useFormik } from "formik";
import * as Yup from "yup";
import { useSettings } from "context/useSettings";
-import InstanceLink from "../InstanceLink";
import {
UploadExternalFormatFileFormValues,
uploadExternalFormatFilePayload,
@@ -35,6 +34,8 @@ import InstanceFileTypeSelector, {
InstanceFileType,
} from "./InstanceFileTypeSelector";
import ClusterMemberSelector from "pages/cluster/ClusterMemberSelector";
+import ResourceLink from "components/ResourceLink";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
close: () => void;
@@ -80,7 +81,7 @@ const UploadExternalFormatFileForm: FC = ({
toastNotify.info(
<>
Upload completed. Now creating instance{" "}
- {instanceName}.
+ .
>,
);
navigate(`/ui/project/${project?.name}/instances`);
@@ -100,23 +101,18 @@ const UploadExternalFormatFileForm: FC = ({
};
const handleSuccess = (instanceName: string) => {
+ const instanceUrl = `/ui/project/${project?.name}/instance/${instanceName}`;
const message = (
<>
Created instance{" "}
-
- .
+ .
>
);
const actions = [
{
label: "Configure",
- onClick: () =>
- navigate(
- `/ui/project/${project?.name}/instance/${instanceName}/configuration`,
- ),
+ onClick: () => navigate(`${instanceUrl}/configuration`),
},
];
diff --git a/src/pages/instances/forms/UploadInstanceBackupFileForm.tsx b/src/pages/instances/forms/UploadInstanceBackupFileForm.tsx
index 4cfdd588b3..ea20eb5777 100644
--- a/src/pages/instances/forms/UploadInstanceBackupFileForm.tsx
+++ b/src/pages/instances/forms/UploadInstanceBackupFileForm.tsx
@@ -25,6 +25,8 @@ import { useSupportedFeatures } from "context/useSupportedFeatures";
import InstanceFileTypeSelector, {
InstanceFileType,
} from "./InstanceFileTypeSelector";
+import ResourceLink from "components/ResourceLink";
+import ResourceLabel from "components/ResourceLabel";
export interface UploadInstanceBackupFileFormValues {
instanceFile: File | null;
@@ -60,19 +62,18 @@ const UploadInstanceBackupFileForm: FC = ({
const { hasInstanceImportConversion } = useSupportedFeatures();
const handleSuccess = (instanceName: string) => {
+ const instanceUrl = `/ui/project/${project?.name}/instance/${instanceName}`;
const message = (
<>
- Created instance {instanceName}.
+ Created instance{" "}
+ .
>
);
const actions = [
{
label: "Configure",
- onClick: () =>
- navigate(
- `/ui/project/${project?.name}/instance/${instanceName}/configuration`,
- ),
+ onClick: () => navigate(`${instanceUrl}/configuration`),
},
];
@@ -107,7 +108,7 @@ const UploadInstanceBackupFileForm: FC = ({
toastNotify.info(
<>
Upload completed. Now creating instance{" "}
- {values.name}.
+ .
>,
);
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
index c283cd5965..b64ad73b05 100644
--- a/src/pages/login/Login.tsx
+++ b/src/pages/login/Login.tsx
@@ -23,7 +23,7 @@ const Login: FC = () => {
return (
-
+
Login
{hasOidc && (
<>
diff --git a/src/pages/networks/CreateNetwork.tsx b/src/pages/networks/CreateNetwork.tsx
index a965c93fc2..0c077a2d73 100644
--- a/src/pages/networks/CreateNetwork.tsx
+++ b/src/pages/networks/CreateNetwork.tsx
@@ -24,6 +24,7 @@ import { slugify } from "util/slugify";
import FormFooterLayout from "components/forms/FormFooterLayout";
import { useToastNotification } from "context/toastNotificationProvider";
import YamlSwitch from "components/forms/YamlSwitch";
+import ResourceLink from "components/ResourceLink";
const CreateNetwork: FC = () => {
const navigate = useNavigate();
@@ -91,7 +92,17 @@ const CreateNetwork: FC = () => {
queryKey: [queryKeys.projects, project, queryKeys.networks],
});
navigate(`/ui/project/${project}/networks`);
- toastNotify.success(`Network ${values.name} created.`);
+ toastNotify.success(
+ <>
+ Network{" "}
+
{" "}
+ created.
+ >,
+ );
})
.catch((e) => {
formik.setSubmitting(false);
diff --git a/src/pages/networks/EditNetwork.tsx b/src/pages/networks/EditNetwork.tsx
index a1a3fc823c..bcbe5e66e7 100644
--- a/src/pages/networks/EditNetwork.tsx
+++ b/src/pages/networks/EditNetwork.tsx
@@ -24,6 +24,7 @@ import FormFooterLayout from "components/forms/FormFooterLayout";
import { useToastNotification } from "context/toastNotificationProvider";
import YamlSwitch from "components/forms/YamlSwitch";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import ResourceLink from "components/ResourceLink";
interface Props {
network: LxdNetwork;
@@ -89,7 +90,17 @@ const EditNetwork: FC
= ({ network, project }) => {
network.name,
],
});
- toastNotify.success(`Network ${network.name} updated.`);
+ toastNotify.success(
+ <>
+ Network{""}
+ {" "}
+ updated.
+ >,
+ );
})
.catch((e) => {
notify.failure("Network update failed", e);
diff --git a/src/pages/networks/NetworkDetailHeader.tsx b/src/pages/networks/NetworkDetailHeader.tsx
index 28e8cbbbff..50b3c6d591 100644
--- a/src/pages/networks/NetworkDetailHeader.tsx
+++ b/src/pages/networks/NetworkDetailHeader.tsx
@@ -9,6 +9,7 @@ import { renameNetwork } from "api/networks";
import DeleteNetworkBtn from "pages/networks/actions/DeleteNetworkBtn";
import { useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
name: string;
@@ -48,8 +49,14 @@ const NetworkDetailHeader: FC = ({ name, network, project }) => {
}
renameNetwork(name, values.name, project)
.then(() => {
- navigate(`/ui/project/${project}/network/${values.name}`);
- toastNotify.success(`Network ${name} renamed to ${values.name}.`);
+ const url = `/ui/project/${project}/network/${values.name}`;
+ navigate(url);
+ toastNotify.success(
+ <>
+ Network {name} renamed to{" "}
+ .
+ >,
+ );
void formik.setFieldValue("isRenaming", false);
})
.catch((e) => {
diff --git a/src/pages/networks/NetworkForwards.tsx b/src/pages/networks/NetworkForwards.tsx
index 2e92b8e69f..9fef4044e8 100644
--- a/src/pages/networks/NetworkForwards.tsx
+++ b/src/pages/networks/NetworkForwards.tsx
@@ -152,7 +152,7 @@ const NetworkForwards: FC = ({ network, project }) => {
{!isLoading && !hasNetworkForwards && (
}
+ image={}
title="No network forwards found"
>
There are no network forwards in this project.
diff --git a/src/pages/networks/NetworkList.tsx b/src/pages/networks/NetworkList.tsx
index a813408b28..faa93477e0 100644
--- a/src/pages/networks/NetworkList.tsx
+++ b/src/pages/networks/NetworkList.tsx
@@ -173,7 +173,7 @@ const NetworkList: FC = () => {
{!isLoading && !hasNetworks && (
}
+ image={}
title="No networks found"
>
There are no networks in this project.
diff --git a/src/pages/networks/actions/DeleteNetworkBtn.tsx b/src/pages/networks/actions/DeleteNetworkBtn.tsx
index 1519149e03..51b6a98a55 100644
--- a/src/pages/networks/actions/DeleteNetworkBtn.tsx
+++ b/src/pages/networks/actions/DeleteNetworkBtn.tsx
@@ -7,6 +7,7 @@ import { queryKeys } from "util/queryKeys";
import { useQueryClient } from "@tanstack/react-query";
import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
network: LxdNetwork;
@@ -31,7 +32,12 @@ const DeleteNetworkBtn: FC = ({ network, project }) => {
query.queryKey[2] === queryKeys.networks,
});
navigate(`/ui/project/${project}/networks`);
- toastNotify.success(`Network ${network.name} deleted.`);
+ toastNotify.success(
+ <>
+ Network {" "}
+ deleted.
+ >,
+ );
})
.catch((e) => {
setLoading(false);
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index 9764516b67..aac2fa728c 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -6,6 +6,7 @@ import {
} from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
import { deleteGroup, deleteGroups } from "api/auth-groups";
+import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { ChangeEvent, FC, useState } from "react";
import { LxdGroup } from "types/permissions";
@@ -44,9 +45,14 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
? deleteGroup(groups[0].name)
: deleteGroups(groups.map((group) => group.name));
- const successMessage = hasSingleGroup
- ? `Group ${groups[0].name} deleted.`
- : `${groups.length} groups deleted.`;
+ const successMessage = hasSingleGroup ? (
+ <>
+ Group {" "}
+ deleted.
+ >
+ ) : (
+ `${groups.length} groups deleted.`
+ );
mutationPromise
.then(() => {
diff --git a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
index 5150b4adc5..b067ef051e 100644
--- a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
+++ b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
@@ -1,6 +1,7 @@
import { ConfirmationModal, useNotify } from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
import { deleteIdpGroup, deleteIdpGroups } from "api/auth-idp-groups";
+import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { FC, useState } from "react";
import { IdpGroup } from "types/permissions";
@@ -25,9 +26,15 @@ const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
? deleteIdpGroup(idpGroups[0].name)
: deleteIdpGroups(idpGroups.map((group) => group.name));
- const successMessage = hasOneGroup
- ? `IDP group ${idpGroups[0].name} deleted.`
- : `${idpGroups.length} IDP groups deleted.`;
+ const successMessage = hasOneGroup ? (
+ <>
+ IDP group{" "}
+ {" "}
+ deleted.
+ >
+ ) : (
+ `${idpGroups.length} IDP groups deleted.`
+ );
mutationPromise
.then(() => {
diff --git a/src/pages/permissions/panels/CreateGroupPanel.tsx b/src/pages/permissions/panels/CreateGroupPanel.tsx
index 00d8c9af77..83d08d10ce 100644
--- a/src/pages/permissions/panels/CreateGroupPanel.tsx
+++ b/src/pages/permissions/panels/CreateGroupPanel.tsx
@@ -21,6 +21,7 @@ import EditGroupPermissionsForm, {
FormPermission,
} from "pages/permissions/panels/EditGroupPermissionsForm";
import GroupHeaderTitle from "pages/permissions/panels/GroupHeaderTitle";
+import ResourceLink from "components/ResourceLink";
export type GroupSubForm = "identity" | "permission" | null;
@@ -46,7 +47,17 @@ const CreateGroupPanel: FC = () => {
});
const handleSuccess = (groupName: string) => {
- toastNotify.success(`Group ${groupName} created.`);
+ toastNotify.success(
+ <>
+ Group{" "}
+ {" "}
+ created.
+ >,
+ );
closePanel();
};
diff --git a/src/pages/permissions/panels/CreateIdpGroupPanel.tsx b/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
index 4d1d38ed35..df0bebad6a 100644
--- a/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
+++ b/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
@@ -15,6 +15,7 @@ import IdpGroupForm, { IdpGroupFormValues } from "../forms/IdpGroupForm";
import GroupSelection from "./GroupSelection";
import useEditHistory from "util/useEditHistory";
import GroupSelectionActions from "../actions/GroupSelectionActions";
+import ResourceLink from "components/ResourceLink";
type GroupEditHistory = {
groupsAdded: Set;
@@ -76,7 +77,17 @@ const CreateIdpGroupPanel: FC = () => {
formik.setSubmitting(true);
createIdpGroup(newGroup)
.then(() => {
- toastNotify.success(`IDP group ${values.name} created.`);
+ toastNotify.success(
+ <>
+ IDP group{" "}
+ {" "}
+ created.
+ >,
+ );
void queryClient.invalidateQueries({
queryKey: [queryKeys.idpGroups],
});
diff --git a/src/pages/permissions/panels/EditGroupPanel.tsx b/src/pages/permissions/panels/EditGroupPanel.tsx
index 3025b4694f..15d6aeb46e 100644
--- a/src/pages/permissions/panels/EditGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupPanel.tsx
@@ -39,6 +39,7 @@ import { pluralize } from "util/instanceBulkActions";
import GroupHeaderTitle from "pages/permissions/panels/GroupHeaderTitle";
import { GroupSubForm } from "pages/permissions/panels/CreateGroupPanel";
import { fetchImageList } from "api/images";
+import ResourceLink from "components/ResourceLink";
interface Props {
group: LxdGroup;
@@ -177,7 +178,17 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
mutationPromise
.then(() => {
closePanel();
- toastNotify.success(`Group ${values.name} updated.`);
+ toastNotify.success(
+ <>
+ Group{" "}
+ {" "}
+ updated.
+ >,
+ );
})
.catch((e) => {
notify.failure("Group update failed", e);
diff --git a/src/pages/permissions/panels/EditIdpGroupPanel.tsx b/src/pages/permissions/panels/EditIdpGroupPanel.tsx
index 069f6cc2bd..5bacfe10cf 100644
--- a/src/pages/permissions/panels/EditIdpGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditIdpGroupPanel.tsx
@@ -16,6 +16,7 @@ import useEditHistory from "util/useEditHistory";
import IdpGroupForm, { IdpGroupFormValues } from "../forms/IdpGroupForm";
import GroupSelection from "./GroupSelection";
import GroupSelectionActions from "../actions/GroupSelectionActions";
+import ResourceLink from "components/ResourceLink";
type GroupEditHistory = {
groupsAdded: Set;
@@ -152,7 +153,17 @@ const EditIdpGroupPanel: FC = ({ idpGroup, onClose }) => {
formik.setSubmitting(true);
mutationPromise
.then(() => {
- toastNotify.success(`IDP group ${values.name} updated.`);
+ toastNotify.success(
+ <>
+ IDP group{" "}
+ {" "}
+ updated.
+ >,
+ );
void queryClient.invalidateQueries({
queryKey: [queryKeys.idpGroups],
});
diff --git a/src/pages/permissions/panels/GroupIdentitiesPanelConfirmModal.tsx b/src/pages/permissions/panels/GroupIdentitiesPanelConfirmModal.tsx
index a2c3608b17..59a07bdbf1 100644
--- a/src/pages/permissions/panels/GroupIdentitiesPanelConfirmModal.tsx
+++ b/src/pages/permissions/panels/GroupIdentitiesPanelConfirmModal.tsx
@@ -13,6 +13,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useToastNotification } from "context/toastNotificationProvider";
import { updateIdentities } from "api/auth-identities";
import { queryKeys } from "util/queryKeys";
+import ResourceLink from "components/ResourceLink";
interface Props {
onConfirm: () => void;
@@ -81,9 +82,18 @@ const GroupIdentitiesPanelConfirmModal: FC = ({
const modifiedGroupNames = Object.keys(groupIdentitiesChangeSummary);
const successMessage =
- modifiedGroupNames.length > 1
- ? `Updated identities for ${modifiedGroupNames.length} groups`
- : `Updated identities for ${modifiedGroupNames[0]}`;
+ modifiedGroupNames.length > 1 ? (
+ `Updated identities for ${modifiedGroupNames.length} groups`
+ ) : (
+ <>
+ Updated identities for{" "}
+
+ >
+ );
toastNotify.success(successMessage);
panelParams.clear();
diff --git a/src/pages/permissions/panels/IdentityGroupsPanelConfirmModal.tsx b/src/pages/permissions/panels/IdentityGroupsPanelConfirmModal.tsx
index 6a7c2c3828..f6ea3565f8 100644
--- a/src/pages/permissions/panels/IdentityGroupsPanelConfirmModal.tsx
+++ b/src/pages/permissions/panels/IdentityGroupsPanelConfirmModal.tsx
@@ -12,6 +12,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { useToastNotification } from "context/toastNotificationProvider";
import usePanelParams from "util/usePanelParams";
+import ResourceLink from "components/ResourceLink";
interface Props {
onConfirm: () => void;
@@ -71,9 +72,18 @@ const IdentityGroupsPanelConfirmModal: FC = ({
const modifiedGroupNames = Object.keys(identityGroupsChangeSummary);
const successMessage =
- modifiedGroupNames.length > 1
- ? `Updated groups for ${modifiedGroupNames.length} identities`
- : `Updated groups for ${modifiedGroupNames[0]}`;
+ modifiedGroupNames.length > 1 ? (
+ `Updated groups for ${modifiedGroupNames.length} identities`
+ ) : (
+ <>
+ Updated groups for{" "}
+
+ >
+ );
toastNotify.success(successMessage);
panelParams.clear();
diff --git a/src/pages/profiles/CreateProfile.tsx b/src/pages/profiles/CreateProfile.tsx
index ffb1e5ef72..0116df63a4 100644
--- a/src/pages/profiles/CreateProfile.tsx
+++ b/src/pages/profiles/CreateProfile.tsx
@@ -71,6 +71,7 @@ import YamlSwitch from "components/forms/YamlSwitch";
import YamlNotification from "components/forms/YamlNotification";
import ProxyDeviceForm from "components/forms/ProxyDeviceForm";
import { PROXY_DEVICES } from "pages/instances/forms/InstanceFormMenu";
+import ResourceLink from "components/ResourceLink";
export type CreateProfileFormValues = ProfileDetailsFormValues &
FormDeviceValues &
@@ -125,7 +126,17 @@ const CreateProfile: FC = () => {
createProfile(JSON.stringify(profilePayload), project)
.then(() => {
navigate(`/ui/project/${project}/profiles`);
- toastNotify.success(`Profile ${values.name} created.`);
+ toastNotify.success(
+ <>
+ Profile{" "}
+ {" "}
+ created.
+ >,
+ );
})
.catch((e: Error) => {
formik.setSubmitting(false);
diff --git a/src/pages/profiles/EditProfile.tsx b/src/pages/profiles/EditProfile.tsx
index 5b96b27c9e..b8851e9b99 100644
--- a/src/pages/profiles/EditProfile.tsx
+++ b/src/pages/profiles/EditProfile.tsx
@@ -67,6 +67,7 @@ import YamlNotification from "components/forms/YamlNotification";
import { PROXY_DEVICES } from "pages/instances/forms/InstanceFormMenu";
import ProxyDeviceForm from "components/forms/ProxyDeviceForm";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import ResourceLink from "components/ResourceLink";
export type EditProfileFormValues = ProfileDetailsFormValues &
FormDeviceValues &
@@ -124,7 +125,17 @@ const EditProfile: FC = ({ profile, featuresProfiles }) => {
updateProfile(profilePayload, project)
.then(() => {
- toastNotify.success(`Profile ${profile.name} updated.`);
+ toastNotify.success(
+ <>
+ Profile{" "}
+ {" "}
+ updated.
+ >,
+ );
void formik.setValues(getProfileEditValues(profilePayload));
})
.catch((e: Error) => {
diff --git a/src/pages/profiles/ProfileDetailHeader.tsx b/src/pages/profiles/ProfileDetailHeader.tsx
index b64d72620d..b47d5efa8d 100644
--- a/src/pages/profiles/ProfileDetailHeader.tsx
+++ b/src/pages/profiles/ProfileDetailHeader.tsx
@@ -9,6 +9,7 @@ import * as Yup from "yup";
import { checkDuplicateName } from "util/helpers";
import { useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
name: string;
@@ -55,7 +56,17 @@ const ProfileDetailHeader: FC = ({
renameProfile(name, values.name, project)
.then(() => {
navigate(`/ui/project/${project}/profile/${values.name}`);
- toastNotify.success(`Profile ${name} renamed to ${values.name}.`);
+ toastNotify.success(
+ <>
+ Profile {name} renamed to{" "}
+
+ .
+ >,
+ );
void formik.setFieldValue("isRenaming", false);
})
.catch((e) => {
diff --git a/src/pages/profiles/actions/DeleteProfileBtn.tsx b/src/pages/profiles/actions/DeleteProfileBtn.tsx
index 2e589b4df5..2fc429cacb 100644
--- a/src/pages/profiles/actions/DeleteProfileBtn.tsx
+++ b/src/pages/profiles/actions/DeleteProfileBtn.tsx
@@ -13,6 +13,7 @@ import classnames from "classnames";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
profile: LxdProfile;
@@ -40,7 +41,12 @@ const DeleteProfileBtn: FC = ({
queryKey: [queryKeys.projects, project],
});
navigate(`/ui/project/${project}/profiles`);
- toastNotify.success(`Profile ${profile.name} deleted.`);
+ toastNotify.success(
+ <>
+ Profile {" "}
+ deleted.
+ >,
+ );
})
.catch((e) => {
setLoading(false);
diff --git a/src/pages/projects/CreateProject.tsx b/src/pages/projects/CreateProject.tsx
index cbc39d6053..8f1140f081 100644
--- a/src/pages/projects/CreateProject.tsx
+++ b/src/pages/projects/CreateProject.tsx
@@ -41,6 +41,7 @@ import FormFooterLayout from "components/forms/FormFooterLayout";
import { slugify } from "util/slugify";
import { useToastNotification } from "context/toastNotificationProvider";
import { useSupportedFeatures } from "context/useSupportedFeatures";
+import ResourceLink from "components/ResourceLink";
export type ProjectFormValues = ProjectDetailsFormValues &
ProjectResourceLimitsFormValues &
@@ -111,7 +112,17 @@ const CreateProject: FC = () => {
)
.then(() => {
navigate(`/ui/project/${values.name}/instances`);
- toastNotify.success(`Project ${values.name} created.`);
+ toastNotify.success(
+ <>
+ Project{" "}
+ {" "}
+ created.
+ >,
+ );
})
.catch((e: Error) => {
formik.setSubmitting(false);
diff --git a/src/pages/projects/EditProject.tsx b/src/pages/projects/EditProject.tsx
index 418117a779..db57e3d200 100644
--- a/src/pages/projects/EditProject.tsx
+++ b/src/pages/projects/EditProject.tsx
@@ -22,6 +22,7 @@ import { slugify } from "util/slugify";
import { useToastNotification } from "context/toastNotificationProvider";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import ResourceLink from "components/ResourceLink";
interface Props {
project: LxdProject;
@@ -68,7 +69,17 @@ const EditProject: FC = ({ project }) => {
updateProject(projectPayload)
.then(() => {
- toastNotify.success(`Project ${project.name} updated.`);
+ toastNotify.success(
+ <>
+ Project{" "}
+ {" "}
+ updated.
+ >,
+ );
void formik.setFieldValue("readOnly", true);
})
.catch((e: Error) => {
diff --git a/src/pages/projects/ProjectConfigurationHeader.tsx b/src/pages/projects/ProjectConfigurationHeader.tsx
index 8e68fcb431..2483b23f43 100644
--- a/src/pages/projects/ProjectConfigurationHeader.tsx
+++ b/src/pages/projects/ProjectConfigurationHeader.tsx
@@ -11,6 +11,7 @@ import HelpLink from "components/HelpLink";
import { useEventQueue } from "context/eventQueue";
import { useDocs } from "context/useDocs";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
project: LxdProject;
@@ -47,14 +48,25 @@ const ProjectConfigurationHeader: FC = ({ project }) => {
formik.setSubmitting(false);
return;
}
+ const oldProjectLink = (
+
+ );
void renameProject(project.name, values.name)
.then((operation) =>
eventQueue.set(
operation.metadata.id,
() => {
- navigate(`/ui/project/${values.name}/configuration`);
+ const url = `/ui/project/${values.name}/configuration`;
+ navigate(url);
toastNotify.success(
- `Project ${project.name} renamed to ${values.name}.`,
+ <>
+ Project {project.name} renamed to{" "}
+ .
+ >,
);
void formik.setFieldValue("isRenaming", false);
},
@@ -62,13 +74,18 @@ const ProjectConfigurationHeader: FC = ({ project }) => {
toastNotify.failure(
`Renaming project ${project.name} failed`,
new Error(msg),
+ oldProjectLink,
),
() => formik.setSubmitting(false),
),
)
.catch((e) => {
formik.setSubmitting(false);
- toastNotify.failure(`Renaming project ${project.name} failed`, e);
+ toastNotify.failure(
+ `Renaming project ${project.name} failed`,
+ e,
+ oldProjectLink,
+ );
});
},
});
diff --git a/src/pages/projects/actions/DeleteProjectBtn.tsx b/src/pages/projects/actions/DeleteProjectBtn.tsx
index 55cf3194dd..777a198d38 100644
--- a/src/pages/projects/actions/DeleteProjectBtn.tsx
+++ b/src/pages/projects/actions/DeleteProjectBtn.tsx
@@ -17,6 +17,7 @@ import classnames from "classnames";
import { useToastNotification } from "context/toastNotificationProvider";
import { filterUsedByType } from "util/usedBy";
import { ResourceType } from "util/resourceDetails";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
project: LxdProject;
@@ -99,7 +100,12 @@ const DeleteProjectBtn: FC = ({ project }) => {
deleteProject(project)
.then(() => {
navigate(`/ui/project/default/instances`);
- toastNotify.success(`Project ${project.name} deleted.`);
+ toastNotify.success(
+ <>
+ Project {" "}
+ deleted.
+ >,
+ );
})
.catch((e) => {
setLoading(false);
diff --git a/src/pages/storage/CreateStoragePool.tsx b/src/pages/storage/CreateStoragePool.tsx
index af2c31e55b..34f203586a 100644
--- a/src/pages/storage/CreateStoragePool.tsx
+++ b/src/pages/storage/CreateStoragePool.tsx
@@ -25,6 +25,7 @@ import { useToastNotification } from "context/toastNotificationProvider";
import { yamlToObject } from "util/yaml";
import { LxdStoragePool } from "types/storage";
import YamlSwitch from "components/forms/YamlSwitch";
+import ResourceLink from "components/ResourceLink";
const CreateStoragePool: FC = () => {
const navigate = useNavigate();
@@ -74,7 +75,17 @@ const CreateStoragePool: FC = () => {
queryKey: [queryKeys.storage],
});
navigate(`/ui/project/${project}/storage/pools`);
- toastNotify.success(`Storage pool ${storagePool.name} created.`);
+ toastNotify.success(
+ <>
+ Storage pool{" "}
+ {" "}
+ created.
+ >,
+ );
})
.catch((e) => {
formik.setSubmitting(false);
diff --git a/src/pages/storage/CustomIsoList.tsx b/src/pages/storage/CustomIsoList.tsx
index 3cf33d8116..8f76106981 100644
--- a/src/pages/storage/CustomIsoList.tsx
+++ b/src/pages/storage/CustomIsoList.tsx
@@ -26,6 +26,7 @@ import CustomLayout from "components/CustomLayout";
import PageHeader from "components/PageHeader";
import HelpLink from "components/HelpLink";
import NotificationRow from "components/NotificationRow";
+import ResourceLabel from "components/ResourceLabel";
const CustomIsoList: FC = () => {
const docBaseLink = useDocs();
@@ -79,7 +80,13 @@ const CustomIsoList: FC = () => {
volume={image.volume}
project={project}
onFinish={() =>
- toastNotify.success(`Custom iso ${image.aliases} deleted.`)
+ toastNotify.success(
+ <>
+ Custom iso{" "}
+ {" "}
+ deleted.
+ >,
+ )
}
/>,
]}
@@ -153,7 +160,7 @@ const CustomIsoList: FC = () => {
const content = !hasImages ? (
}
+ image={}
title="No custom ISOs found in this project"
>
Custom ISOs will appear here
@@ -167,7 +174,7 @@ const CustomIsoList: FC = () => {
-
+
) : (
@@ -230,7 +237,10 @@ const CustomIsoList: FC = () => {
{hasImages && (
-
+
)}
diff --git a/src/pages/storage/EditStoragePool.tsx b/src/pages/storage/EditStoragePool.tsx
index 47331e3cb3..f28ca14935 100644
--- a/src/pages/storage/EditStoragePool.tsx
+++ b/src/pages/storage/EditStoragePool.tsx
@@ -30,6 +30,7 @@ import { useSettings } from "context/useSettings";
import { getSupportedStorageDrivers } from "util/storageOptions";
import YamlSwitch from "components/forms/YamlSwitch";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import ResourceLink from "components/ResourceLink";
interface Props {
pool: LxdStoragePool;
@@ -81,7 +82,17 @@ const EditStoragePool: FC
= ({ pool }) => {
mutation()
.then(async () => {
- toastNotify.success(`Storage pool ${savedPool.name} updated.`);
+ toastNotify.success(
+ <>
+ Storage pool{" "}
+ {" "}
+ updated.
+ >,
+ );
const member = clusterMembers[0]?.server_name ?? undefined;
const updatedPool = await fetchStoragePool(values.name, member);
void formik.setValues(toStoragePoolFormValues(updatedPool));
diff --git a/src/pages/storage/MigrateVolumeBtn.tsx b/src/pages/storage/MigrateVolumeBtn.tsx
index 25aeed2d59..8f0ad137fc 100644
--- a/src/pages/storage/MigrateVolumeBtn.tsx
+++ b/src/pages/storage/MigrateVolumeBtn.tsx
@@ -4,12 +4,13 @@ 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 { useNavigate } from "react-router-dom";
+import ResourceLabel from "components/ResourceLabel";
+import ResourceLink from "components/ResourceLink";
interface Props {
storageVolume: LxdStorageVolume;
@@ -35,16 +36,29 @@ const MigrateVolumeBtn: FC = ({
newTarget: string,
storageVolume: LxdStorageVolume,
) => {
+ 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}`;
+
+ const volume = (
+
+ );
+ const pool = (
+
+ );
toastNotify.success(
<>
- Volume {" "}
- successfully migrated to pool{" "}
-
+ Volume {volume} successfully migrated to pool {pool}
>,
);
- 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 (window.location.pathname.startsWith(oldVolumeUrl)) {
navigate(newVolumeUrl);
}
@@ -52,13 +66,18 @@ const MigrateVolumeBtn: FC = ({
const notifyFailure = (
e: unknown,
- storageVolume: string,
+ volumeName: string,
targetPool: string,
) => {
setVolumeLoading(false);
toastNotify.failure(
- `Migration failed for volume ${storageVolume} to pool ${targetPool}`,
+ `Migration failed for volume ${volumeName} to pool ${targetPool}`,
e,
+ ,
);
};
@@ -87,8 +106,20 @@ const MigrateVolumeBtn: FC = ({
(err) => handleFailure(err, storageVolume.name, targetPool),
handleFinish,
);
+ const volume = (
+
+ );
+ const pool = (
+
+ );
toastNotify.info(
- `Migration started for volume ${storageVolume.name} to pool ${targetPool}`,
+ <>
+ Migration started for volume {volume} to pool {pool}
+ >,
);
void queryClient.invalidateQueries({
queryKey: [queryKeys.storage, storageVolume.name, project],
diff --git a/src/pages/storage/StoragePoolHeader.tsx b/src/pages/storage/StoragePoolHeader.tsx
index fdb7e8b8dc..af1857f804 100644
--- a/src/pages/storage/StoragePoolHeader.tsx
+++ b/src/pages/storage/StoragePoolHeader.tsx
@@ -9,6 +9,7 @@ import DeleteStoragePoolBtn from "pages/storage/actions/DeleteStoragePoolBtn";
import { testDuplicateStoragePoolName } from "util/storagePool";
import { useNotify } from "@canonical/react-components";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
name: string;
@@ -42,9 +43,13 @@ const StoragePoolHeader: FC = ({ name, pool, project }) => {
}
renameStoragePool(name, values.name)
.then(() => {
- navigate(`/ui/project/${project}/storage/pool/${values.name}`);
+ const url = `/ui/project/${project}/storage/pool/${values.name}`;
+ navigate(url);
toastNotify.success(
- `Storage pool ${name} renamed to ${values.name}.`,
+ <>
+ Storage pool {name} renamed to{" "}
+ .
+ >,
);
void formik.setFieldValue("isRenaming", false);
})
diff --git a/src/pages/storage/StoragePools.tsx b/src/pages/storage/StoragePools.tsx
index a5c6973e82..5482950b89 100644
--- a/src/pages/storage/StoragePools.tsx
+++ b/src/pages/storage/StoragePools.tsx
@@ -181,7 +181,7 @@ const StoragePools: FC = () => {
) : (
}
+ image={}
title="No pools found in this project"
>
Storage pools will appear here.
diff --git a/src/pages/storage/StorageVolumeHeader.tsx b/src/pages/storage/StorageVolumeHeader.tsx
index 872a36df61..18abb48613 100644
--- a/src/pages/storage/StorageVolumeHeader.tsx
+++ b/src/pages/storage/StorageVolumeHeader.tsx
@@ -12,6 +12,8 @@ import { useToastNotification } from "context/toastNotificationProvider";
import MigrateVolumeBtn from "./MigrateVolumeBtn";
import DuplicateVolumeBtn from "./actions/DuplicateVolumeBtn";
import { useSupportedFeatures } from "context/useSupportedFeatures";
+import ResourceLink from "components/ResourceLink";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
volume: LxdStorageVolume;
@@ -53,11 +55,13 @@ const StorageVolumeHeader: FC = ({ volume, project }) => {
}
renameStorageVolume(project, volume, values.name)
.then(() => {
- navigate(
- `/ui/project/${project}/storage/pool/${volume.pool}/volumes/${volume.type}/${values.name}`,
- );
+ const url = `/ui/project/${project}/storage/pool/${volume.pool}/volumes/${volume.type}/${values.name}`;
+ navigate(url);
toastNotify.success(
- `Storage volume ${volume.name} renamed to ${values.name}.`,
+ <>
+ Storage volume {volume.name} renamed to{" "}
+ .
+ >,
);
void formik.setFieldValue("isRenaming", false);
})
@@ -99,7 +103,13 @@ const StorageVolumeHeader: FC = ({ volume, project }) => {
hasIcon={true}
onFinish={() => {
navigate(`/ui/project/${project}/storage/volumes`);
- toastNotify.success(`Storage volume ${volume.name} deleted.`);
+ toastNotify.success(
+ <>
+ Storage volume{" "}
+ {" "}
+ deleted.
+ >,
+ );
}}
classname={classname}
/>
diff --git a/src/pages/storage/StorageVolumeSnapshots.tsx b/src/pages/storage/StorageVolumeSnapshots.tsx
index 52da78e61d..6c64f9ea13 100644
--- a/src/pages/storage/StorageVolumeSnapshots.tsx
+++ b/src/pages/storage/StorageVolumeSnapshots.tsx
@@ -289,7 +289,7 @@ const StorageVolumeSnapshots: FC = ({ volume }) => {
) : (
}
+ image={}
title="No snapshots found"
>
diff --git a/src/pages/storage/StorageVolumes.tsx b/src/pages/storage/StorageVolumes.tsx
index 33bf5d2e7a..7898e2a3d2 100644
--- a/src/pages/storage/StorageVolumes.tsx
+++ b/src/pages/storage/StorageVolumes.tsx
@@ -368,7 +368,7 @@ const StorageVolumes: FC = () => {
const content = !hasVolumes ? (
}
+ image={}
title="No volumes found in this project"
>
Storage volumes will appear here
diff --git a/src/pages/storage/UploadCustomIso.tsx b/src/pages/storage/UploadCustomIso.tsx
index a7a04aa52d..bad7bfd5d2 100644
--- a/src/pages/storage/UploadCustomIso.tsx
+++ b/src/pages/storage/UploadCustomIso.tsx
@@ -70,7 +70,8 @@ const UploadCustomIso: FC = ({ onCancel, onFinish }) => {
eventQueue.set(
operation.metadata.id,
() => onFinish(name, pool),
- (msg) => toastNotify.failure("Image import failed", new Error(msg)),
+ (msg) =>
+ toastNotify.failure("Custom ISO upload failed", new Error(msg)),
() => {
setLoading(false);
setUploadState(null);
@@ -87,7 +88,7 @@ const UploadCustomIso: FC = ({ onCancel, onFinish }) => {
)
.catch((e: AxiosError>) => {
const error = new Error(e.response?.data.error);
- toastNotify.failure("Image import failed", error);
+ toastNotify.failure("Custom ISO upload failed", error);
setLoading(false);
setUploadState(null);
});
diff --git a/src/pages/storage/VolumeSnapshotLinkChip.tsx b/src/pages/storage/VolumeSnapshotLinkChip.tsx
new file mode 100644
index 0000000000..2acd03a0db
--- /dev/null
+++ b/src/pages/storage/VolumeSnapshotLinkChip.tsx
@@ -0,0 +1,21 @@
+import { FC } from "react";
+import ResourceLink from "components/ResourceLink";
+import { PartialWithRequired } from "types/partial";
+import { LxdStorageVolume } from "types/storage";
+
+interface Props {
+ name: string;
+ volume: PartialWithRequired;
+}
+
+const VolumeSnapshotLinkChip: FC = ({ name, volume }) => {
+ return (
+
+ );
+};
+
+export default VolumeSnapshotLinkChip;
diff --git a/src/pages/storage/actions/CustomStorageVolumeActions.tsx b/src/pages/storage/actions/CustomStorageVolumeActions.tsx
index 992489d83f..0d16292a98 100644
--- a/src/pages/storage/actions/CustomStorageVolumeActions.tsx
+++ b/src/pages/storage/actions/CustomStorageVolumeActions.tsx
@@ -7,6 +7,7 @@ import VolumeAddSnapshotBtn from "./snapshots/VolumeAddSnapshotBtn";
import { useToastNotification } from "context/toastNotificationProvider";
import { isSnapshotsDisabled } from "util/snapshots";
import { useProject } from "context/project";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
volume: LxdStorageVolume;
@@ -33,7 +34,13 @@ const CustomStorageVolumeActions: FC = ({ volume, className }) => {
volume={volume}
project={project?.name ?? ""}
onFinish={() => {
- toastNotify.success(`Storage volume ${volume.name} deleted.`);
+ toastNotify.success(
+ <>
+ Storage volume{" "}
+ {" "}
+ deleted.
+ >,
+ );
}}
/>,
]}
diff --git a/src/pages/storage/actions/DeleteStoragePoolBtn.tsx b/src/pages/storage/actions/DeleteStoragePoolBtn.tsx
index dab5eb4e58..6552715928 100644
--- a/src/pages/storage/actions/DeleteStoragePoolBtn.tsx
+++ b/src/pages/storage/actions/DeleteStoragePoolBtn.tsx
@@ -13,6 +13,7 @@ import { useNavigate } from "react-router-dom";
import { LxdStoragePool } from "types/storage";
import { queryKeys } from "util/queryKeys";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLabel from "components/ResourceLabel";
interface Props {
pool: LxdStoragePool;
@@ -40,7 +41,12 @@ const DeleteStoragePoolBtn: FC = ({
queryKey: [queryKeys.storage],
});
navigate(`/ui/project/${project}/storage/pools`);
- toastNotify.success(`Storage pool ${pool.name} deleted.`);
+ toastNotify.success(
+ <>
+ Storage pool {" "}
+ deleted.
+ >,
+ );
})
.catch((e) => {
setLoading(false);
diff --git a/src/pages/storage/actions/snapshots/VolumeConfigureSnapshotModal.tsx b/src/pages/storage/actions/snapshots/VolumeConfigureSnapshotModal.tsx
index e14e69901b..75a906b515 100644
--- a/src/pages/storage/actions/snapshots/VolumeConfigureSnapshotModal.tsx
+++ b/src/pages/storage/actions/snapshots/VolumeConfigureSnapshotModal.tsx
@@ -17,6 +17,7 @@ import { getStorageVolumeEditValues } from "util/storageVolumeEdit";
import { updateStorageVolume } from "api/storage-pools";
import StorageVolumeFormSnapshots from "pages/storage/forms/StorageVolumeFormSnapshots";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
interface Props {
volume: LxdStorageVolume;
@@ -38,7 +39,15 @@ const VolumeConfigureSnapshotModal: FC = ({ volume, close }) => {
})
.then(() => {
toastNotify.success(
- `Snapshot configuration updated for volume ${volume.name}.`,
+ <>
+ Snapshot configuration updated for volume{" "}
+
+ .
+ >,
);
void queryClient.invalidateQueries({
queryKey: [queryKeys.storage],
diff --git a/src/pages/storage/actions/snapshots/VolumeSnapshotActions.tsx b/src/pages/storage/actions/snapshots/VolumeSnapshotActions.tsx
index bd84f7941b..fe7125ed96 100644
--- a/src/pages/storage/actions/snapshots/VolumeSnapshotActions.tsx
+++ b/src/pages/storage/actions/snapshots/VolumeSnapshotActions.tsx
@@ -17,6 +17,8 @@ import ItemName from "components/ItemName";
import { useEventQueue } from "context/eventQueue";
import VolumeEditSnapshotBtn from "./VolumeEditSnapshotBtn";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLabel from "components/ResourceLabel";
+import VolumeSnapshotLinkChip from "pages/storage/VolumeSnapshotLinkChip";
interface Props {
volume: LxdStorageVolume;
@@ -33,15 +35,26 @@ const VolumeSnapshotActions: FC = ({ volume, snapshot }) => {
const handleDelete = () => {
setDeleting(true);
+ const snapshotLink = (
+
+ );
void deleteVolumeSnapshot(volume, snapshot)
.then((operation) =>
eventQueue.set(
operation.metadata.id,
- () => toastNotify.success(`Snapshot ${snapshot.name} deleted`),
+ () =>
+ toastNotify.success(
+ <>
+ Snapshot{" "}
+ {" "}
+ deleted.
+ >,
+ ),
(msg) =>
toastNotify.failure(
`Snapshot ${snapshot.name} deletion failed`,
new Error(msg),
+ snapshotLink,
),
() => {
setDeleting(false);
@@ -54,7 +67,7 @@ const VolumeSnapshotActions: FC = ({ volume, snapshot }) => {
),
)
.catch((e) => {
- notify.failure("Snapshot deletion failed", e);
+ notify.failure("Snapshot deletion failed", e, snapshotLink);
setDeleting(false);
});
};
@@ -63,7 +76,13 @@ const VolumeSnapshotActions: FC = ({ volume, snapshot }) => {
setRestoring(true);
void restoreVolumeSnapshot(volume, snapshot)
.then(() => {
- toastNotify.success(`Snapshot ${snapshot.name} restored`);
+ toastNotify.success(
+ <>
+ Snapshot{" "}
+ {" "}
+ restored.
+ >,
+ );
})
.catch((error: Error) => {
notify.failure("Snapshot restore failed", error);
diff --git a/src/pages/storage/forms/CreateStorageVolume.tsx b/src/pages/storage/forms/CreateStorageVolume.tsx
index fa97d38d66..8579e44ac3 100644
--- a/src/pages/storage/forms/CreateStorageVolume.tsx
+++ b/src/pages/storage/forms/CreateStorageVolume.tsx
@@ -19,6 +19,7 @@ import { slugify } from "util/slugify";
import { POOL } from "../StorageVolumesFilter";
import FormFooterLayout from "components/forms/FormFooterLayout";
import { useToastNotification } from "context/toastNotificationProvider";
+import ResourceLink from "components/ResourceLink";
const CreateStorageVolume: FC = () => {
const navigate = useNavigate();
@@ -72,7 +73,17 @@ const CreateStorageVolume: FC = () => {
predicate: (query) => query.queryKey[0] === queryKeys.volumes,
});
navigate(`/ui/project/${project}/storage/volumes`);
- toastNotify.success(`Storage volume ${values.name} created.`);
+ toastNotify.success(
+ <>
+ Storage volume{" "}
+ {" "}
+ created.
+ >,
+ );
})
.catch((e) => {
formik.setSubmitting(false);
diff --git a/src/pages/storage/forms/CreateVolumeSnapshotForm.tsx b/src/pages/storage/forms/CreateVolumeSnapshotForm.tsx
index b4514bdec3..95cc78a5cf 100644
--- a/src/pages/storage/forms/CreateVolumeSnapshotForm.tsx
+++ b/src/pages/storage/forms/CreateVolumeSnapshotForm.tsx
@@ -12,6 +12,8 @@ import { queryKeys } from "util/queryKeys";
import { SnapshotFormValues, getExpiresAt } from "util/snapshots";
import { getVolumeSnapshotSchema } from "util/storageVolumeSnapshots";
import { useToastNotification } from "context/toastNotificationProvider";
+import { getVolumeSnapshotName } from "util/operations";
+import VolumeSnapshotLinkChip from "../VolumeSnapshotLinkChip";
interface Props {
close: () => void;
@@ -55,7 +57,16 @@ const CreateVolumeSnapshotForm: FC = ({ close, volume }) => {
query.queryKey[0] === queryKeys.volumes ||
query.queryKey[0] === queryKeys.storage,
});
- toastNotify.success(`Snapshot ${values.name} created.`);
+ toastNotify.success(
+ <>
+ Snapshot{" "}
+ {" "}
+ created.
+ >,
+ );
close();
resetForm();
},
diff --git a/src/pages/storage/forms/DuplicateVolumeForm.tsx b/src/pages/storage/forms/DuplicateVolumeForm.tsx
index 43608c78e4..56e4ab1800 100644
--- a/src/pages/storage/forms/DuplicateVolumeForm.tsx
+++ b/src/pages/storage/forms/DuplicateVolumeForm.tsx
@@ -13,15 +13,15 @@ import * as Yup from "yup";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { duplicateStorageVolume } from "api/storage-pools";
-import { Link, useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { fetchProjects } from "api/projects";
import { useEventQueue } from "context/eventQueue";
import { LxdStorageVolume } from "types/storage";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { loadCustomVolumes } from "context/loadCustomVolumes";
-import ItemName from "components/ItemName";
import StoragePoolSelector from "../StoragePoolSelector";
import { checkDuplicateName, getUniqueResourceName } from "util/helpers";
+import ResourceLink from "components/ResourceLink";
interface Props {
volume: LxdStorageVolume;
@@ -53,17 +53,18 @@ const DuplicateVolumeForm: FC = ({ volume, close }) => {
});
const notifySuccess = (volumeName: string, project: string, pool: string) => {
+ const volumeUrl = `/ui/project/${project}/storage/pool/${pool}/volumes/custom/${volumeName}`;
const message = (
<>
- Created volume {volumeName}.
+ Created volume{" "}
+ .
>
);
- const redirectUrl = `/ui/project/${project}/storage/pool/${pool}/volumes/custom/${volumeName}/configuration`;
const actions = [
{
label: "Configure",
- onClick: () => navigate(redirectUrl),
+ onClick: () => navigate(`${volumeUrl}/configuration`),
},
];
@@ -87,7 +88,7 @@ const DuplicateVolumeForm: FC = ({ volume, close }) => {
const { name, project, pool } = values;
const notFound = await checkDuplicateName(
name,
- project || "",
+ project || "default",
controllerState,
`storage-pools/${pool}/volumes/custom`,
);
@@ -133,12 +134,11 @@ const DuplicateVolumeForm: FC = ({ volume, close }) => {
};
const existingVolumeLink = (
- e.stopPropagation()}
- >
-
-
+ />
);
const targetProject =
@@ -146,7 +146,9 @@ const DuplicateVolumeForm: FC = ({ volume, close }) => {
duplicateStorageVolume(payload, values.pool, targetProject)
.then((operation) => {
- toastNotify.info(`Duplication of volume ${volume.name} started.`);
+ toastNotify.info(
+ <>Duplication of volume {existingVolumeLink} started.>,
+ );
eventQueue.set(
operation.metadata.id,
() => notifySuccess(values.name, values.project, values.pool),
diff --git a/src/pages/storage/forms/EditStorageVolume.tsx b/src/pages/storage/forms/EditStorageVolume.tsx
index dd4b83ed34..02fc405df9 100644
--- a/src/pages/storage/forms/EditStorageVolume.tsx
+++ b/src/pages/storage/forms/EditStorageVolume.tsx
@@ -17,6 +17,7 @@ import { slugify } from "util/slugify";
import FormFooterLayout from "components/forms/FormFooterLayout";
import { useToastNotification } from "context/toastNotificationProvider";
import FormSubmitBtn from "components/forms/FormSubmitBtn";
+import ResourceLink from "components/ResourceLink";
interface Props {
volume: LxdStorageVolume;
@@ -62,7 +63,17 @@ const EditStorageVolume: FC = ({ volume }) => {
saveVolume.name,
],
});
- toastNotify.success(`Storage volume ${saveVolume.name} updated.`);
+ toastNotify.success(
+ <>
+ Storage volume{" "}
+ {" "}
+ updated.
+ >,
+ );
})
.catch((e) => {
notify.failure("Storage volume update failed", e);
diff --git a/src/pages/storage/forms/EditVolumeSnapshotForm.tsx b/src/pages/storage/forms/EditVolumeSnapshotForm.tsx
index ab00833a6b..37f53933d7 100644
--- a/src/pages/storage/forms/EditVolumeSnapshotForm.tsx
+++ b/src/pages/storage/forms/EditVolumeSnapshotForm.tsx
@@ -15,6 +15,7 @@ import { queryKeys } from "util/queryKeys";
import { SnapshotFormValues, getExpiresAt } from "util/snapshots";
import { getVolumeSnapshotSchema } from "util/storageVolumeSnapshots";
import { useToastNotification } from "context/toastNotificationProvider";
+import VolumeSnapshotLinkChip from "../VolumeSnapshotLinkChip";
interface Props {
volume: LxdStorageVolume;
@@ -35,7 +36,11 @@ const EditVolumeSnapshotForm: FC = ({ volume, snapshot, close }) => {
query.queryKey[0] === queryKeys.volumes ||
query.queryKey[0] === queryKeys.storage,
});
- toastNotify.success(`Snapshot ${name} saved.`);
+ toastNotify.success(
+ <>
+ Snapshot saved.
+ >,
+ );
formik.setSubmitting(false);
close();
};
@@ -52,6 +57,9 @@ const EditVolumeSnapshotForm: FC = ({ volume, snapshot, close }) => {
};
const rename = (newName: string): Promise => {
+ const snapshotLink = (
+
+ );
return new Promise((resolve) => {
void renameVolumeSnapshot({
volume,
@@ -66,13 +74,14 @@ const EditVolumeSnapshotForm: FC = ({ volume, snapshot, close }) => {
toastNotify.failure(
`Snapshot ${snapshot.name} rename failed`,
new Error(msg),
+ snapshotLink,
);
formik.setSubmitting(false);
},
),
)
.catch((e) => {
- notify.failure("Snapshot rename failed", e);
+ notify.failure("Snapshot rename failed", e, snapshotLink);
formik.setSubmitting(false);
});
});
diff --git a/src/types/instance.d.ts b/src/types/instance.d.ts
index 3d68b262be..806cc22583 100644
--- a/src/types/instance.d.ts
+++ b/src/types/instance.d.ts
@@ -102,6 +102,6 @@ export interface LxdInstance {
state?: LxdInstanceState;
stateful: boolean;
status: LxdInstanceStatus;
- type: string;
+ type: "container" | "virtual-machine";
etag?: string;
}
diff --git a/src/types/operation.d.ts b/src/types/operation.d.ts
index 7172fa5ce7..630f7b2378 100644
--- a/src/types/operation.d.ts
+++ b/src/types/operation.d.ts
@@ -16,6 +16,7 @@ export interface LxdOperation {
resources?: {
instances?: string[];
instances_snapshots?: string[];
+ storage_volume_snapshots?: string[];
};
status: LxdOperationStatus;
status_code: string;
diff --git a/src/types/partial.d.ts b/src/types/partial.d.ts
new file mode 100644
index 0000000000..04292e9913
--- /dev/null
+++ b/src/types/partial.d.ts
@@ -0,0 +1 @@
+export type PartialWithRequired = Partial & Pick;
diff --git a/src/util/instanceMigration.tsx b/src/util/instanceMigration.tsx
index 9796f7bf3c..c810ed5d6c 100644
--- a/src/util/instanceMigration.tsx
+++ b/src/util/instanceMigration.tsx
@@ -3,11 +3,12 @@ import { useEventQueue } from "context/eventQueue";
import { useInstanceLoading } from "context/instanceLoading";
import { useToastNotification } from "context/toastNotificationProvider";
import { queryKeys } from "./queryKeys";
-import ItemName from "components/ItemName";
import { migrateInstance } from "api/instances";
import { LxdInstance } from "types/instance";
import { ReactNode } from "react";
import { capitalizeFirstLetter } from "./helpers";
+import ResourceLink from "components/ResourceLink";
+import InstanceLinkChip from "pages/instances/InstanceLinkChip";
export type MigrationType = "cluster member" | "root storage pool" | "";
@@ -29,14 +30,18 @@ export const useInstanceMigration = ({
const eventQueue = useEventQueue();
const queryClient = useQueryClient();
- const handleSuccess = (newTarget: string, instanceName: string) => {
+ const handleSuccess = (newTarget: string) => {
let successMessage: ReactNode = "";
if (type === "cluster member") {
successMessage = (
<>
- Instance successfully
+ Instance successfully
migrated to cluster member{" "}
-
+
>
);
}
@@ -44,9 +49,13 @@ export const useInstanceMigration = ({
if (type === "root storage pool") {
successMessage = (
<>
- Instance root storage
+ Instance root storage
successfully migrated to pool{" "}
-
+
>
);
}
@@ -54,22 +63,26 @@ export const useInstanceMigration = ({
toastNotify.success(successMessage);
};
- const notifyFailure = (e: unknown, instanceName: string) => {
+ const notifyFailure = (e: unknown) => {
let failureMessage = "";
if (type === "cluster member") {
- failureMessage = `Cluster member migration failed for instance ${instanceName}`;
+ failureMessage = `Cluster member migration failed for instance ${instance.name}`;
}
if (type === "root storage pool") {
- failureMessage = `Root storage migration failed for instance ${instanceName}`;
+ failureMessage = `Root storage migration failed for instance ${instance.name}`;
}
instanceLoading.setFinish(instance);
- toastNotify.failure(failureMessage, e);
+ toastNotify.failure(
+ failureMessage,
+ e,
+ ,
+ );
};
- const handleFailure = (msg: string, instanceName: string) => {
- notifyFailure(new Error(msg), instanceName);
+ const handleFailure = (msg: string) => {
+ notifyFailure(new Error(msg));
};
const handleFinish = () => {
@@ -87,12 +100,15 @@ export const useInstanceMigration = ({
.then((operation) => {
eventQueue.set(
operation.metadata.id,
- () => handleSuccess(target, instance.name),
- (err) => handleFailure(err, instance.name),
+ () => handleSuccess(target),
+ (err) => handleFailure(err),
handleFinish,
);
toastNotify.info(
- `${capitalizeFirstLetter(type)} migration started for instance ${instance.name}`,
+ <>
+ {capitalizeFirstLetter(type)} migration started for{" "}
+ .
+ >,
);
void queryClient.invalidateQueries({
queryKey: [queryKeys.instances, instance.name, instance.project],
@@ -100,7 +116,7 @@ export const useInstanceMigration = ({
onSuccess();
})
.catch((e) => {
- notifyFailure(e, instance.name);
+ notifyFailure(e);
});
};
diff --git a/src/util/instanceStart.tsx b/src/util/instanceStart.tsx
index 12d15e2559..4ab810d867 100644
--- a/src/util/instanceStart.tsx
+++ b/src/util/instanceStart.tsx
@@ -1,12 +1,11 @@
import { useQueryClient } from "@tanstack/react-query";
import { unfreezeInstance, startInstance } from "api/instances";
import { useInstanceLoading } from "context/instanceLoading";
-import InstanceLink from "pages/instances/InstanceLink";
import { LxdInstance } from "types/instance";
import { queryKeys } from "./queryKeys";
import { useEventQueue } from "context/eventQueue";
-import ItemName from "components/ItemName";
import { useToastNotification } from "context/toastNotificationProvider";
+import InstanceLinkChip from "pages/instances/InstanceLinkChip";
export const useInstanceStart = (instance: LxdInstance) => {
const eventQueue = useEventQueue();
@@ -34,25 +33,21 @@ export const useInstanceStart = (instance: LxdInstance) => {
instanceLoading.setLoading(instance, "Starting");
const mutation =
instance.status === "Frozen" ? unfreezeInstance : startInstance;
+
+ const instanceLink = ;
void mutation(instance)
.then((operation) => {
eventQueue.set(
operation.metadata.id,
() => {
- toastNotify.success(
- <>
- Instance started.
- >,
- );
+ toastNotify.success(<>Instance {instanceLink} started.>);
clearCache();
},
(msg) => {
toastNotify.failure(
"Instance start failed",
new Error(msg),
- <>
- Instance :
- >,
+ instanceLink,
);
// Delay clearing the cache, because the instance is reported as RUNNING
// when a start operation failed, only shortly after it goes back to STOPPED
@@ -65,7 +60,7 @@ export const useInstanceStart = (instance: LxdInstance) => {
);
})
.catch((e) => {
- toastNotify.failure("Instance start failed", e);
+ toastNotify.failure("Instance start failed", e, instanceLink);
instanceLoading.setFinish(instance);
});
};
diff --git a/src/util/instances.tsx b/src/util/instances.tsx
index 59600fdea2..0df9deb309 100644
--- a/src/util/instances.tsx
+++ b/src/util/instances.tsx
@@ -1,6 +1,5 @@
import { LxdOperationResponse } from "types/operation";
import { getInstanceName } from "./operations";
-import InstanceLink from "pages/instances/InstanceLink";
import { ReactNode } from "react";
import {
AbortControllerState,
@@ -8,27 +7,28 @@ import {
getFileExtension,
} from "./helpers";
import * as Yup from "yup";
-
-export const instanceLinkFromName = (args: {
- instanceName: string;
- project?: string;
-}): ReactNode => {
- const { project, instanceName } = args;
- return (
-
- );
-};
+import InstanceLinkChip from "pages/instances/InstanceLinkChip";
+import { InstanceIconType } from "components/ResourceIcon";
export const instanceLinkFromOperation = (args: {
operation?: LxdOperationResponse;
project?: string;
+ instanceType: InstanceIconType;
}): ReactNode | undefined => {
- const { operation, project } = args;
+ const { operation, project, instanceType } = args;
const linkText = getInstanceName(operation?.metadata);
if (!linkText) {
return;
}
- return ;
+ return (
+
+ );
};
export const instanceNameValidation = (
diff --git a/src/util/operations.spec.ts b/src/util/operations.spec.ts
index 2c87a04089..c00de4dab3 100644
--- a/src/util/operations.spec.ts
+++ b/src/util/operations.spec.ts
@@ -1,11 +1,27 @@
-import { getInstanceName, getProjectName } from "./operations";
+import {
+ getInstanceName,
+ getInstanceSnapshotName,
+ getProjectName,
+ getVolumeSnapshotName,
+} from "./operations";
import { LxdOperation } from "types/operation";
const craftOperation = (...url: string[]) => {
const instances: string[] = [];
const instances_snapshots: string[] = [];
+ const storage_volume_snapshots: string[] = [];
for (const u of url) {
const segments = u.split("/");
+ if (u.includes("snapshots") && u.includes("storage-pools")) {
+ storage_volume_snapshots.push(u);
+ continue;
+ }
+
+ if (u.includes("snapshots") && u.includes("instances")) {
+ instances_snapshots.push(u);
+ continue;
+ }
+
if (segments.length > 4) {
instances_snapshots.push(u);
} else {
@@ -17,6 +33,7 @@ const craftOperation = (...url: string[]) => {
resources: {
instances,
instances_snapshots,
+ storage_volume_snapshots,
},
} as LxdOperation;
};
@@ -74,3 +91,43 @@ describe("getProjectName", () => {
expect(name).toBe("barProject");
});
});
+
+describe("getInstanceSnapshotName", () => {
+ it("identifies snapshot name from an instance snapshot operation", () => {
+ const operation = craftOperation(
+ "/1.0/instances/test-instance/snapshots/test-snapshot",
+ );
+ const name = getInstanceSnapshotName(operation);
+
+ expect(name).toBe("test-snapshot");
+ });
+
+ it("identifies snapshot name from an instance snapshot operation in a custom project", () => {
+ const operation = craftOperation(
+ "/1.0/instances/test-instance/snapshots/test-snapshot?project=project",
+ );
+ const name = getInstanceSnapshotName(operation);
+
+ expect(name).toBe("test-snapshot");
+ });
+});
+
+describe("getVolumeSnapshotName", () => {
+ it("identifies snapshot name from a volume snapshot operation", () => {
+ const operation = craftOperation(
+ "/1.0/storage-pools/test-pool/volumes/custom/test-volume/snapshots/test-snapshot",
+ );
+ const name = getVolumeSnapshotName(operation);
+
+ expect(name).toBe("test-snapshot");
+ });
+
+ it("identifies snapshot name from a volume snapshot operation in a custom project", () => {
+ const operation = craftOperation(
+ "/1.0/storage-pools/test-pool/volumes/custom/test-volume/snapshots/test-snapshot?project=project",
+ );
+ const name = getVolumeSnapshotName(operation);
+
+ expect(name).toBe("test-snapshot");
+ });
+});
diff --git a/src/util/operations.tsx b/src/util/operations.tsx
index b88004b50c..c8f166404f 100644
--- a/src/util/operations.tsx
+++ b/src/util/operations.tsx
@@ -13,6 +13,28 @@ export const getInstanceName = (operation?: LxdOperation): string => {
);
};
+export const getInstanceSnapshotName = (operation?: LxdOperation): string => {
+ // /1.0/instances//snapshots/
+ const instanceSnapshots = operation?.resources?.instances_snapshots ?? [];
+ if (instanceSnapshots.length) {
+ return instanceSnapshots[0].split("/")[5].split("?")[0];
+ }
+
+ return "";
+};
+
+export const getVolumeSnapshotName = (operation?: LxdOperation): string => {
+ // /1.0/storage-pools//volumes/custom//snapshots/
+ const storageVolumeSnapshots =
+ operation?.resources?.storage_volume_snapshots ?? [];
+
+ if (storageVolumeSnapshots.length) {
+ return storageVolumeSnapshots[0].split("/")[8].split("?")[0];
+ }
+
+ return "";
+};
+
export const getProjectName = (operation: LxdOperation): string => {
// the url can be
// /1.0/instances/?project=
diff --git a/tests/iso-volumes.spec.ts b/tests/iso-volumes.spec.ts
index 655e039da6..6c7161b1fb 100644
--- a/tests/iso-volumes.spec.ts
+++ b/tests/iso-volumes.spec.ts
@@ -26,7 +26,7 @@ test("upload and delete custom iso", async ({ page, lxdVersion }) => {
await page.getByLabel("Alias").fill(isoName);
await page.getByRole("button", { name: "Upload", exact: true }).click();
- await assertTextVisible(page, `Image ${isoName} uploaded successfully`);
+ await assertTextVisible(page, `Custom ISO ${isoName} uploaded successfully`);
await page.getByPlaceholder("Search for custom ISOs").fill(isoName);
await page.getByRole("button", { name: "Create instance" }).click();
diff --git a/tests/storage.spec.ts b/tests/storage.spec.ts
index 0450177b9d..04c04461c0 100644
--- a/tests/storage.spec.ts
+++ b/tests/storage.spec.ts
@@ -69,7 +69,7 @@ test("storage volume migrate", async ({ page }) => {
await createPool(page, pool2);
await migrateVolume(page, volume, pool2);
- await expect(page.getByRole("link", { name: pool2 })).toBeVisible();
+ await expect(page.getByRole("cell", { name: pool2 })).toBeVisible();
//Migrate back to default so that the Pool can be deleted
await migrateVolume(page, volume, "default");
@@ -139,7 +139,8 @@ test("storage pool with driver zfs", async ({ page }) => {
const pool = randomPoolName();
await createPool(page, pool, "ZFS");
- await expect(page.getByRole("link", { name: pool })).toBeVisible();
+ const poolRow = page.getByRole("row").filter({ hasText: pool });
+ await expect(poolRow.getByRole("link", { name: pool })).toBeVisible();
await deletePool(page, pool);