Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: [M3-8331] - useToastNotification async toasts #10841

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Refactor useToastNotification async toasts ([#10841](https://github.com/linode/manager/pull/10841))
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ const eventIntercept = (
* @param message - Expected failure message.
*/
const assertFailed = (label: string, id: string, message: string) => {
ui.toast.assertMessage(
`There was a problem uploading image ${label}: ${message}`
);
ui.toast.assertMessage(`Image ${label} could not be uploaded: ${message}`);

cy.get(`[data-qa-image-cell="${id}"]`).within(() => {
fbtVisible(label);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,6 @@ describe('create image (using mocks)', () => {
cy.wait('@getEvents');

// Verify a success toast shows
ui.toast.assertMessage('Image My Config successfully created.');
ui.toast.assertMessage('Image My Config has been created.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('clone linode', () => {

ui.toast.assertMessage(`Your Linode ${newLinodeLabel} is being created.`);
ui.toast.assertMessage(
`Linode ${linode.label} successfully cloned to ${newLinodeLabel}.`,
`Linode ${linode.label} has been cloned to ${newLinodeLabel}.`,
{ timeout: CLONE_TIMEOUT }
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ describe('Linode Config management', () => {

// Confirm toast message and that UI updates to reflect clone in progress.
ui.toast.assertMessage(
`Linode ${sourceLinode.label} successfully cloned to ${destLinode.label}.`
`Linode ${sourceLinode.label} has been cloned to ${destLinode.label}.`
);
cy.findByText(/CLONING \(\d+%\)/).should('be.visible');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ describe('linode storage tab', () => {
cy.wait('@deleteDisk').its('response.statusCode').should('eq', 200);
cy.findByText('Deleting', { exact: false }).should('be.visible');
ui.button.findByTitle('Add a Disk').should('be.enabled');
ui.toast.assertMessage(`Disk ${diskName} successfully deleted.`);
ui.toast.assertMessage(
`Disk ${diskName} on Linode ${linode.label} has been deleted.`
);
cy.findByLabelText('List of Disks').within(() => {
cy.contains(diskName).should('not.exist');
});
Expand Down Expand Up @@ -209,7 +211,9 @@ describe('linode storage tab', () => {
cy.wait('@resizeDisk').its('response.statusCode').should('eq', 200);
ui.toast.assertMessage('Disk queued for resizing.');
// cy.findByText('Resizing', { exact: false }).should('be.visible');
ui.toast.assertMessage(`Disk ${diskName} successfully resized.`);
ui.toast.assertMessage(
`Disk ${diskName} on Linode ${linode.label} has been resized.`
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ describe('resize linode', () => {
});

// Wait until the disk resize is done.
ui.toast.assertMessage(`Disk ${diskName} successfully resized.`);
ui.toast.assertMessage(
`Disk ${diskName} on Linode ${linode.label} has been resized.`
);

interceptLinodeResize(linode.id).as('linodeResize');
cy.visitWithLogin(`/linodes/${linode.id}?resize=true`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ describe('volume attach and detach flows', () => {

// Confirm that volume has been attached to Linode.
cy.wait('@attachVolume').its('response.statusCode').should('eq', 200);
ui.toast.assertMessage(`Volume ${volume.label} successfully attached.`);
ui.toast.assertMessage(
`Volume ${volume.label} has been attached to Linode ${linode.label}.`
);
cy.findByText(volume.label)
.should('be.visible')
.closest('tr')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('volume delete flow', () => {
// Confirm that volume is deleted.
cy.wait('@deleteVolume').its('response.statusCode').should('eq', 200);
cy.findByText(volume.label).should('not.exist');
ui.toast.assertMessage('Volume successfully deleted.');
ui.toast.assertMessage(`Volume ${volume.label} has been deleted.`);
}
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('volume upgrade/migration', () => {

cy.findByText('active').should('be.visible');

ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`);
ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
});

it('can upgrade an attached volume from the volumes landing page', () => {
Expand Down Expand Up @@ -178,7 +178,7 @@ describe('volume upgrade/migration', () => {

cy.findByText('active').should('be.visible');

ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`);
ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
});

it('can upgrade an attached volume from the linode details page', () => {
Expand Down Expand Up @@ -265,6 +265,6 @@ describe('volume upgrade/migration', () => {

cy.findByText('active').should('be.visible');

ui.toast.assertMessage(`Volume ${volume.label} successfully upgraded.`);
ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
});
});
140 changes: 140 additions & 0 deletions packages/manager/src/features/Events/asyncToasts.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { createToast } from './asyncToasts';

describe('createToast', () => {
it('should handle case with both failure and success options as true or empty objects', () => {
const trueOptions = { failure: true, success: true };
const emptyObjectOptions = { failure: {}, success: {} };

[trueOptions, emptyObjectOptions].forEach((options) => {
const result = createToast(options);
const expected = {
failure: {
message: expect.any(Function),
},
success: {
message: expect.any(Function),
},
};

expect(result).toEqual(expected);
});
});

it('should handle case with only failure option as true or empty object or with success as false', () => {
const scenarios = [
{ failure: true },
{ failure: {} },
{ failure: true, success: false },
{ failure: {}, success: false },
];

scenarios.forEach((options) => {
const result = createToast(options);
const expected = {
failure: {
message: expect.any(Function),
},
};

expect(result).toEqual(expected);
});
});

it('should handle case with only success option as true or empty object or with failure as false', () => {
const scenarios = [
{ success: true },
{ success: {} },
{ failure: false, success: true },
{ failure: false, success: {} },
];

scenarios.forEach((options) => {
const result = createToast(options);
const expected = {
success: {
message: expect.any(Function),
},
};

expect(result).toEqual(expected);
});
});

it('should return an empty object if both failure and success are false or not provided', () => {
const falseOptions = { failure: false, success: false };
const emptyOptions = {};
[falseOptions, emptyOptions].forEach((options) => {
const result = createToast(options);

expect(result).toEqual({});
});
});

it('should handle cases with specific values for only failure or success options', () => {
// Only the failure options with specific values
const failureOnlyOptions = {
failure: { persist: true },
};
const result1 = createToast(failureOnlyOptions);
const expected1 = {
failure: {
message: expect.any(Function),
persist: true,
},
};
expect(result1).toEqual(expected1);

// Only the success options with specific values
const successOnlyOptions = {
success: { invertVariant: true, persist: false },
};
const result2 = createToast(successOnlyOptions);
const expected2 = {
success: {
invertVariant: true,
message: expect.any(Function),
persist: false,
},
};
expect(result2).toEqual(expected2);
});

it('should handle case with both failure and success options with specific values', () => {
const options1 = {
failure: { invertVariant: true, persist: true },
success: { invertVariant: true, persist: false },
};
const result1 = createToast(options1);
const expected1 = {
failure: {
invertVariant: true,
message: expect.any(Function),
persist: true,
},
success: {
invertVariant: true,
message: expect.any(Function),
persist: false,
},
};
expect(result1).toEqual(expected1);

const options2 = {
failure: { persist: true },
success: { invertVariant: true },
};

const result2 = createToast(options2);
const expected2 = {
failure: {
message: expect.any(Function),
persist: true,
},
success: {
invertVariant: true,
message: expect.any(Function),
},
};
expect(result2).toEqual(expected2);
});
});
95 changes: 95 additions & 0 deletions packages/manager/src/features/Events/asyncToasts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getEventMessage } from './utils';

import type { Event, EventAction } from '@linode/api-v4';

interface ToastMessage {
/**
* If true, the toast will be displayed with an error variant for success messages \
* or a success variant for error messages.
*/
invertVariant?: boolean;
message: ((event: Event) => JSX.Element | null | string | undefined) | string;
persist?: boolean;
}

interface Toast {
failure?: ToastMessage;
success?: ToastMessage;
}

type Toasts = {
[key in EventAction]?: Toast;
};

interface ToastOption {
invertVariant?: boolean;
persist?: boolean;
}

interface ToastOptions {
failure?: ToastOption | boolean;
success?: ToastOption | boolean;
}

export const createToast = (options: ToastOptions) => {
const toastConfig: Toast = {};

const getToastMessage = (option: ToastOption | boolean): ToastMessage => {
const message: ToastMessage['message'] = (e) => getEventMessage(e);

if (typeof option === 'boolean') {
return { message };
}

return {
message,
...(option.invertVariant !== undefined && {
invertVariant: option.invertVariant,
}),
...(option.persist !== undefined && { persist: option.persist }),
};
};

if (options.failure) {
toastConfig.failure = getToastMessage(options.failure);
}

if (options.success) {
toastConfig.success = getToastMessage(options.success);
}

return toastConfig;
};

/**
* This constant defines toast notifications that will be displayed
* when our events polling system gets a new event.
*
* Use this feature to notify users of *asynchronous tasks* such as migrating a Linode.
*
* 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.
*/
export const toasts: Toasts = {
backups_restore: createToast({ failure: { persist: true } }),
disk_delete: createToast({ failure: false, success: true }),
disk_imagize: createToast({ failure: { persist: true }, success: true }),
disk_resize: createToast({ failure: { persist: true }, success: true }),
image_delete: createToast({ failure: true, success: true }),
image_upload: createToast({ failure: { persist: true }, success: true }),
linode_clone: createToast({ failure: true, success: true }),
linode_migrate: createToast({ failure: true, success: true }),
linode_migrate_datacenter: createToast({ failure: true, success: true }),
linode_resize: createToast({ failure: true, success: true }),
linode_snapshot: createToast({ failure: { persist: true } }),
longviewclient_create: createToast({ failure: true, success: true }),
tax_id_invalid: createToast({
failure: true,
success: { invertVariant: true, persist: true },
}),
volume_attach: createToast({ failure: true, success: true }),
volume_create: createToast({ failure: true, success: true }),
volume_delete: createToast({ failure: true, success: true }),
volume_detach: createToast({ failure: true, success: true }),
volume_migrate: createToast({ failure: true, success: true }),
};
5 changes: 3 additions & 2 deletions packages/manager/src/features/Events/factories/backup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export const backup: PartialEventMap<'backups'> = {
backups_restore: {
failed: (e) => (
<>
Backup could <strong>not</strong> be <strong>restored</strong> for
Backup could <strong>not</strong> be <strong>restored</strong> for{' '}
{e.entity!.label}.{' '}
<Link to="https://www.linode.com/docs/products/storage/backups/#limits-and-considerations">
Learn more about limits and considerations.
Learn more about limits and considerations
</Link>
.
</>
),
finished: (e) => (
Expand Down
Loading
Loading