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', }); } };