diff --git a/packages/manager/.changeset/pr-10654-tech-stories-1720468649871.md b/packages/manager/.changeset/pr-10654-tech-stories-1720468649871.md
new file mode 100644
index 00000000000..0df861ae755
--- /dev/null
+++ b/packages/manager/.changeset/pr-10654-tech-stories-1720468649871.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Tech Stories
+---
+
+Improve API flexibility for useToastNotification ([#10654](https://github.com/linode/manager/pull/10654))
diff --git a/packages/manager/src/hooks/useToastNotifications.tsx b/packages/manager/src/hooks/useToastNotifications.tsx
index fca04d10fd9..d62593921b7 100644
--- a/packages/manager/src/hooks/useToastNotifications.tsx
+++ b/packages/manager/src/hooks/useToastNotifications.tsx
@@ -1,4 +1,3 @@
-import { Event, EventAction } from '@linode/api-v4/lib/account/types';
import { useSnackbar } from 'notistack';
import * as React from 'react';
@@ -6,6 +5,8 @@ import { Link } from 'src/components/Link';
import { SupportLink } from 'src/components/SupportLink';
import { sendLinodeDiskEvent } from 'src/utilities/analytics/customEventAnalytics';
+import type { Event, EventAction } from '@linode/api-v4/lib/account/types';
+
export const getLabel = (event: Event) => event.entity?.label ?? '';
export const getSecondaryLabel = (event: Event) =>
event.secondary_entity?.label ?? '';
@@ -17,11 +18,19 @@ const formatLink = (text: string, link: string, handleClick?: () => void) => {
);
};
-interface Toast {
- failure?: ((event: Event) => string | undefined) | string;
+interface ToastMessage {
link?: JSX.Element;
- persistFailureMessage?: boolean;
- success?: ((event: Event) => string | undefined) | string;
+ message: ((event: Event) => string | undefined) | string;
+ persist?: boolean;
+}
+
+interface Toast {
+ failure?: ToastMessage;
+ /**
+ * If true, the toast will be displayed with an error variant.
+ */
+ invertVariant?: boolean;
+ success?: ToastMessage;
}
type Toasts = {
@@ -34,165 +43,220 @@ type Toasts = {
*
* Use this feature to notify users of *asynchronous tasks* such as migrating a Linode.
*
- * DO NOT use this feature to notifiy the user of tasks like changing the label of a Linode.
- * Toasts for that can be handeled at the time of making the PUT request.
+ * DO NOT use this feature to notify the user of tasks like changing the label of a Linode.
+ * Toasts for that can be handled at the time of making the PUT request.
*/
const toasts: Toasts = {
backups_restore: {
- failure: (e) => `Backup restoration failed for ${getLabel(e)}.`,
- link: formatLink(
- 'Learn more about limits and considerations.',
- 'https://www.linode.com/docs/products/storage/backups/#limits-and-considerations'
- ),
- persistFailureMessage: true,
+ failure: {
+ link: formatLink(
+ 'Learn more about limits and considerations.',
+ 'https://www.linode.com/docs/products/storage/backups/#limits-and-considerations'
+ ),
+ message: (e) => `Backup restoration failed for ${getLabel(e)}.`,
+ persist: true,
+ },
},
disk_delete: {
- failure: (e) =>
- `Unable to delete disk ${getSecondaryLabel(e)} ${
- getLabel(e) ? ` on ${getLabel(e)}` : ''
- }. Is it attached to a configuration profile that is in use?`,
- success: (e) => `Disk ${getSecondaryLabel(e)} successfully deleted.`,
+ failure: {
+ message: (e) =>
+ `Unable to delete disk ${getSecondaryLabel(e)} ${
+ getLabel(e) ? ` on ${getLabel(e)}` : ''
+ }. Is it attached to a configuration profile that is in use?`,
+ },
+ success: {
+ message: (e) => `Disk ${getSecondaryLabel(e)} successfully deleted.`,
+ },
},
disk_imagize: {
- failure: (e) =>
- `There was a problem creating Image ${getSecondaryLabel(e)}.`,
- link: formatLink(
- 'Learn more about image technical specifications.',
- 'https://www.linode.com/docs/products/tools/images/#technical-specifications'
- ),
- persistFailureMessage: true,
- success: (e) => `Image ${getSecondaryLabel(e)} successfully created.`,
+ failure: {
+ link: formatLink(
+ 'Learn more about image technical specifications.',
+ 'https://www.linode.com/docs/products/tools/images/#technical-specifications'
+ ),
+ message: (e) =>
+ `There was a problem creating Image ${getSecondaryLabel(e)}.`,
+ persist: true,
+ },
+
+ success: {
+ message: (e) => `Image ${getSecondaryLabel(e)} successfully created.`,
+ },
},
disk_resize: {
- failure: `Disk resize failed.`,
- link: formatLink(
- 'Learn more about resizing restrictions.',
- 'https://www.linode.com/docs/products/compute/compute-instances/guides/disks-and-storage/',
- () =>
- sendLinodeDiskEvent('Resize', 'Click:link', 'Disk resize failed toast')
- ),
- persistFailureMessage: true,
- success: (e) => `Disk ${getSecondaryLabel(e)} successfully resized.`,
+ failure: {
+ link: formatLink(
+ 'Learn more about resizing restrictions.',
+ 'https://www.linode.com/docs/products/compute/compute-instances/guides/disks-and-storage/',
+ () =>
+ sendLinodeDiskEvent(
+ 'Resize',
+ 'Click:link',
+ 'Disk resize failed toast'
+ )
+ ),
+ message: `Disk resize failed.`,
+ persist: true,
+ },
+ success: {
+ message: (e) => `Disk ${getSecondaryLabel(e)} successfully resized.`,
+ },
},
image_delete: {
- failure: (e) => `Error deleting Image ${getLabel(e)}.`,
- success: (e) => `Image ${getLabel(e)} successfully deleted.`,
+ failure: { message: (e) => `Error deleting Image ${getLabel(e)}.` },
+ success: { message: (e) => `Image ${getLabel(e)} successfully deleted.` },
},
image_upload: {
- failure(event) {
- const isDeletion = event.message === 'Upload canceled.';
+ failure: {
+ message: (e) => {
+ const isDeletion = e.message === 'Upload canceled.';
- if (isDeletion) {
- return undefined;
- }
+ if (isDeletion) {
+ return undefined;
+ }
- return `There was a problem uploading image ${getLabel(
- event
- )}: ${event.message?.replace(/(\d+)/g, '$1 MB')}`;
+ return `There was a problem uploading image ${getLabel(
+ e
+ )}: ${e.message?.replace(/(\d+)/g, '$1 MB')}`;
+ },
+ persist: true,
},
- persistFailureMessage: true,
- success: (e) => `Image ${getLabel(e)} is now available.`,
+ success: { message: (e) => `Image ${getLabel(e)} is now available.` },
},
linode_clone: {
- failure: (e) => `Error cloning Linode ${getLabel(e)}.`,
- success: (e) =>
- `Linode ${getLabel(e)} successfully cloned to ${getSecondaryLabel(e)}.`,
+ failure: { message: (e) => `Error cloning Linode ${getLabel(e)}.` },
+ success: {
+ message: (e) =>
+ `Linode ${getLabel(e)} successfully cloned to ${getSecondaryLabel(e)}.`,
+ },
},
linode_migrate: {
- failure: (e) => `Error migrating Linode ${getLabel(e)}.`,
- success: (e) => `Linode ${getLabel(e)} successfully migrated.`,
+ failure: { message: (e) => `Error migrating Linode ${getLabel(e)}.` },
+ success: { message: (e) => `Linode ${getLabel(e)} successfully migrated.` },
},
linode_migrate_datacenter: {
- failure: (e) => `Error migrating Linode ${getLabel(e)}.`,
- success: (e) => `Linode ${getLabel(e)} successfully migrated.`,
+ failure: { message: (e) => `Error migrating Linode ${getLabel(e)}.` },
+ success: { message: (e) => `Linode ${getLabel(e)} successfully migrated.` },
},
linode_resize: {
- failure: (e) => `Error resizing Linode ${getLabel(e)}.`,
- success: (e) => `Linode ${getLabel(e)} successfully resized.`,
+ failure: { message: (e) => `Error resizing Linode ${getLabel(e)}.` },
+ success: { message: (e) => `Linode ${getLabel(e)} successfully resized.` },
},
linode_snapshot: {
- failure: (e) => `Snapshot backup failed on Linode ${getLabel(e)}.`,
- link: formatLink(
- 'Learn more about limits and considerations.',
- 'https://www.linode.com/docs/products/storage/backups/#limits-and-considerations'
- ),
- persistFailureMessage: true,
+ failure: {
+ link: formatLink(
+ 'Learn more about limits and considerations.',
+ 'https://www.linode.com/docs/products/storage/backups/#limits-and-considerations'
+ ),
+ message: (e) => `Snapshot backup failed on Linode ${getLabel(e)}.`,
+ persist: true,
+ },
},
longviewclient_create: {
- failure: (e) => `Error creating Longview Client ${getLabel(e)}.`,
- success: (e) => `Longview Client ${getLabel(e)} successfully created.`,
+ failure: {
+ message: (e) => `Error creating Longview Client ${getLabel(e)}.`,
+ },
+ success: {
+ message: (e) => `Longview Client ${getLabel(e)} successfully created.`,
+ },
+ },
+ tax_id_invalid: {
+ failure: { message: 'Error validating Tax Identification Number.' },
+ invertVariant: true,
+ success: {
+ message: 'Tax Identification Number could not be verified.',
+ persist: true,
+ },
},
volume_attach: {
- failure: (e) => `Error attaching Volume ${getLabel(e)}.`,
- success: (e) => `Volume ${getLabel(e)} successfully attached.`,
+ failure: { message: (e) => `Error attaching Volume ${getLabel(e)}.` },
+ success: { message: (e) => `Volume ${getLabel(e)} successfully attached.` },
},
volume_create: {
- failure: (e) => `Error creating Volume ${getLabel(e)}.`,
- success: (e) => `Volume ${getLabel(e)} successfully created.`,
+ failure: { message: (e) => `Error creating Volume ${getLabel(e)}.` },
+ success: { message: (e) => `Volume ${getLabel(e)} successfully created.` },
},
volume_delete: {
- failure: 'Error deleting Volume.',
- success: 'Volume successfully deleted.',
+ failure: { message: 'Error deleting Volume.' },
+ success: { message: 'Volume successfully deleted.' },
},
volume_detach: {
- failure: (e) => `Error detaching Volume ${getLabel(e)}.`,
- success: (e) => `Volume ${getLabel(e)} successfully detached.`,
+ failure: { message: (e) => `Error detaching Volume ${getLabel(e)}.` },
+ success: { message: (e) => `Volume ${getLabel(e)} successfully detached.` },
},
volume_migrate: {
- failure: (e) => `Error upgrading Volume ${getLabel(e)}.`,
- success: (e) => `Volume ${getLabel(e)} successfully upgraded.`,
+ failure: { message: (e) => `Error upgrading Volume ${getLabel(e)}.` },
+ success: { message: (e) => `Volume ${getLabel(e)} successfully upgraded.` },
},
};
-export const useToastNotifications = () => {
+const getToastMessage = (
+ toastMessage: ((event: Event) => string | undefined) | string,
+ event: Event
+): string | undefined =>
+ typeof toastMessage === 'function' ? toastMessage(event) : toastMessage;
+
+const createFormattedMessage = (
+ message: string | undefined,
+ link: JSX.Element | undefined,
+ hasSupportLink: boolean
+) => (
+ <>
+ {message?.replace(/ contact Support/i, '') ?? message}
+ {hasSupportLink && (
+ <>
+
+ .
+ >
+ )}
+ {link && <> {link}>}
+ >
+);
+
+export const useToastNotifications = (): {
+ handleGlobalToast: (event: Event) => void;
+} => {
const { enqueueSnackbar } = useSnackbar();
- const handleGlobalToast = (event: Event) => {
+ const handleGlobalToast = (event: Event): void => {
const toastInfo = toasts[event.action];
-
if (!toastInfo) {
return;
}
- if (
- ['finished', 'notification'].includes(event.status) &&
- toastInfo.success
- ) {
- const successMessage =
- typeof toastInfo.success === 'function'
- ? toastInfo.success(event)
- : toastInfo.success;
-
- enqueueSnackbar(successMessage, {
- variant: 'success',
+ const isSuccessEvent = ['finished', 'notification'].includes(event.status);
+
+ if (isSuccessEvent && toastInfo.success) {
+ const { link, message, persist } = toastInfo.success;
+ const successMessage = getToastMessage(message, event);
+
+ const formattedSuccessMessage = createFormattedMessage(
+ successMessage,
+ link,
+ false
+ );
+
+ enqueueSnackbar(formattedSuccessMessage, {
+ persist: persist ?? false,
+ variant: toastInfo.invertVariant ? 'error' : 'success',
});
}
if (event.status === 'failed' && toastInfo.failure) {
- const failureMessage =
- typeof toastInfo.failure === 'function'
- ? toastInfo.failure(event)
- : toastInfo.failure;
-
+ const { link, message, persist } = toastInfo.failure;
+ const failureMessage = getToastMessage(message, event);
const hasSupportLink =
failureMessage?.includes('contact Support') ?? false;
- const formattedFailureMessage = (
- <>
- {failureMessage?.replace(/ contact Support/i, '') ?? failureMessage}
- {hasSupportLink ? (
- <>
-
- .
- >
- ) : null}
- {toastInfo.link ? <> {toastInfo.link}> : null}
- >
+ const formattedFailureMessage = createFormattedMessage(
+ failureMessage,
+ link,
+ hasSupportLink
);
enqueueSnackbar(formattedFailureMessage, {
- persist: toastInfo.persistFailureMessage,
- variant: 'error',
+ persist: persist ?? false,
+ variant: toastInfo.invertVariant ? 'success' : 'error',
});
}
};