diff --git a/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md b/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md new file mode 100644 index 00000000000..016235f78df --- /dev/null +++ b/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add `Confirmation Dialog` in `AlertListTable.tsx`, add `message` prop in `AlertConfirmationDialog.tsx` ([#11981](https://github.com/linode/manager/pull/11981)) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts index 0163f19851c..328620c8cf5 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts @@ -85,6 +85,7 @@ describe('Alerts Listing Page - Error Handling', () => { }); ui.actionMenuItem.findByTitle(action).should('be.visible').click(); + ui.button.findByTitle(action).should('be.visible').click(); cy.wait(alias).then(({ response }) => { ui.toast.assertMessage(response?.body.errors[0].reason); }); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index 90d9498dd7b..8481c24768f 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -64,6 +64,17 @@ const mockAlerts = [ }), ]; +interface AlertActionOptions { + action: 'Disable' | 'Enable'; + alertName: string; + alias: string; +} + +interface AlertToggleOptions extends AlertActionOptions { + confirmationText: string; + successMessage: string; +} + /** * @description * This code validates the presence and correct text of the table headers @@ -305,12 +316,13 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { }; // Function to toggle an alert's status - const toggleAlertStatus = ( - alertName: string, - action: 'Disable' | 'Enable', - alias: string, - successMessage: string - ) => { + const toggleAlertStatus = ({ + action, + alertName, + alias, + confirmationText, + successMessage, + }: AlertToggleOptions) => { cy.findByText(alertName) .should('be.visible') .closest('tr') @@ -320,29 +332,48 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { .should('be.visible') .click(); }); - ui.actionMenuItem.findByTitle(action).should('be.visible').click(); - cy.wait(alias).then(({ response }) => { + // verify dialog title + ui.dialog + .findByTitle(`${action} ${alertName} Alert?`) + .should('be.visible') + .within(() => { + cy.findByText(confirmationText).should('be.visible'); + ui.button + .findByTitle(action) + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(alias).then(({}) => { ui.toast.assertMessage(successMessage); }); }; // Disable "Alert-1" - searchAlert('Alert-1'); - toggleAlertStatus( - 'Alert-1', - 'Disable', - '@getFirstAlertDefinitions', - UPDATE_ALERT_SUCCESS_MESSAGE - ); + const actions: Array = [ + { + action: 'Disable', + alertName: 'Alert-1', + alias: '@getFirstAlertDefinitions', + }, + { + action: 'Enable', + alertName: 'Alert-2', + alias: '@getSecondAlertDefinitions', + }, + ]; - // Enable "Alert-2" - searchAlert('Alert-2'); - toggleAlertStatus( - 'Alert-2', - 'Enable', - '@getSecondAlertDefinitions', - UPDATE_ALERT_SUCCESS_MESSAGE - ); + actions.forEach(({ action, alertName, alias }) => { + searchAlert(alertName); + toggleAlertStatus({ + action, + alertName, + alias, + confirmationText: `Are you sure you want to ${action.toLowerCase()} this alert definition?`, + successMessage: UPDATE_ALERT_SUCCESS_MESSAGE, + }); + }); }); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.test.tsx index a6c52bbd471..6a469f94974 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.test.tsx @@ -9,35 +9,39 @@ import { AlertConfirmationDialog } from './AlertConfirmationDialog'; const entityName = 'entity-1'; const alert = alertFactory.build({ service_type: 'dbaas' }); const confirmFunction = vi.fn(); +const messages = { + disableMessage: `Are you sure you want to disable the alert for ${entityName}?`, + disableTitle: `Disable ${alert.label} Alert?`, + enableMessage: `Are you sure you want to enable the alert for ${entityName}?`, + enableTitle: `Enable ${alert.label} Alert?`, +}; describe('Alert confirmation dialog', () => { it('should show confirmation dialog', () => { const { getByTestId, getByText } = renderWithTheme( ); expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); - expect(getByText(`Disable ${alert.label} Alert?`)).toBeVisible(); - expect( - getByText(`Are you sure you want to disable the alert for ${entityName}?`) - ).toBeInTheDocument(); + expect(getByText(messages.disableTitle)).toBeVisible(); + expect(getByText(messages.disableMessage)).toBeInTheDocument(); }); it('should click confirm button', async () => { const { getByText } = renderWithTheme( ); @@ -51,18 +55,16 @@ describe('Alert confirmation dialog', () => { const { getByTestId, getByText } = renderWithTheme( ); expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); - expect(getByText(`Enable ${alert.label} Alert?`)).toBeVisible(); - expect( - getByText(`Are you sure you want to enable the alert for ${entityName}?`) - ).toBeInTheDocument(); + expect(getByText(messages.enableTitle)).toBeVisible(); + expect(getByText(messages.enableMessage)).toBeInTheDocument(); }); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx index 9afecdfce86..5138633d40a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx @@ -11,11 +11,6 @@ interface AlertConfirmationDialogProps { */ alert: Alert; - /** - * Name of the selected entity - */ - entityName: string; - /** * Handler function for cancel button */ @@ -31,7 +26,7 @@ interface AlertConfirmationDialogProps { /** * Current state of the toggle button whether active or not */ - isActive: boolean; + isEnabled: boolean; /** * Loading state of the confirmation dialog @@ -42,26 +37,31 @@ interface AlertConfirmationDialogProps { * Current state of the confirmation dialoge whether open or not */ isOpen: boolean; + + /** + * Message to be displayed in the confirmation dialog + */ + message: string; } export const AlertConfirmationDialog = React.memo( (props: AlertConfirmationDialogProps) => { const { alert, - entityName, handleCancel, handleConfirm, - isActive, + isEnabled, isLoading = false, isOpen, + message, } = props; const actionsPanel = ( handleConfirm(alert, isActive), + onClick: () => handleConfirm(alert, isEnabled), }} secondaryButtonProps={{ disabled: isLoading, @@ -77,12 +77,9 @@ export const AlertConfirmationDialog = React.memo( data-testid="confirmation-dialog" onClose={handleCancel} open={isOpen} - title={`${isActive ? 'Disable' : 'Enable'} ${alert.label} Alert?`} + title={`${isEnabled ? 'Disable' : 'Enable'} ${alert.label} Alert?`} > - - Are you sure you want to {isActive ? 'disable' : 'enable'} the alert - for {entityName}? - + {message} ); } diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx index 8ebd5c96310..e8f30ca8953 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx @@ -86,7 +86,12 @@ describe('Alert List Table test', () => { it('should show success snackbar when enabling alert succeeds', async () => { const alert = alertFactory.build({ status: 'disabled', type: 'user' }); - const { getByLabelText, getByText } = renderWithTheme( + const { + getByLabelText, + getByRole, + getByTestId, + getByText, + } = renderWithTheme( { const actionMenu = getByLabelText(`Action menu for Alert ${alert.label}`); await userEvent.click(actionMenu); await userEvent.click(getByText('Enable')); // click the enable button to enable alert + + expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); + + await userEvent.click(getByRole('button', { name: 'Enable' })); + expect(getByText(UPDATE_ALERT_SUCCESS_MESSAGE)).toBeInTheDocument(); // validate whether snackbar is displayed properly }); it('should show success snackbar when disabling alert succeeds', async () => { const alert = alertFactory.build({ status: 'enabled', type: 'user' }); - const { getByLabelText, getByText } = renderWithTheme( + const { + getByLabelText, + getByRole, + getByTestId, + getByText, + } = renderWithTheme( { const actionMenu = getByLabelText(`Action menu for Alert ${alert.label}`); await userEvent.click(actionMenu); await userEvent.click(getByText('Disable')); // click the enable button to enable alert + + expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); + + await userEvent.click(getByRole('button', { name: 'Disable' })); + expect(getByText(UPDATE_ALERT_SUCCESS_MESSAGE)).toBeInTheDocument(); // validate whether snackbar is displayed properly }); @@ -126,7 +146,12 @@ describe('Alert List Table test', () => { }); const alert = alertFactory.build({ status: 'disabled', type: 'user' }); - const { getByLabelText, getByText } = renderWithTheme( + const { + getByLabelText, + getByRole, + getByTestId, + getByText, + } = renderWithTheme( { await userEvent.click(actionMenu); await userEvent.click(getByText('Enable')); + expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); + + await userEvent.click(getByRole('button', { name: 'Enable' })); + expect(getByText('Enabling alert failed')).toBeInTheDocument(); // validate whether snackbar is displayed properly if an error is encountered while enabling an alert }); @@ -150,7 +179,12 @@ describe('Alert List Table test', () => { }); const alert = alertFactory.build({ status: 'enabled', type: 'user' }); - const { getByLabelText, getByText } = renderWithTheme( + const { + getByLabelText, + getByRole, + getByTestId, + getByText, + } = renderWithTheme( { await userEvent.click(actionMenu); await userEvent.click(getByText('Disable')); + expect(getByTestId('confirmation-dialog')).toBeInTheDocument(); + + await userEvent.click(getByRole('button', { name: 'Disable' })); + expect(getByText('Disabling alert failed')).toBeInTheDocument(); // validate whether snackbar is displayed properly if an error is encountered while disabling an alert }); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx index 4523734aa4f..533d68d59c8 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx @@ -14,6 +14,7 @@ import { TableSortCell } from 'src/components/TableSortCell'; import { useEditAlertDefinition } from 'src/queries/cloudpulse/alerts'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { AlertConfirmationDialog } from '../AlertsLanding/AlertConfirmationDialog'; import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { AlertTableRow } from './AlertTableRow'; import { AlertListingTableLabelMap } from './constants'; @@ -52,6 +53,10 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => { const history = useHistory(); const { mutateAsync: editAlertDefinition } = useEditAlertDefinition(); // put call to update alert status + const [selectedAlert, setSelectedAlert] = React.useState({} as Alert); + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const [isUpdating, setIsUpdating] = React.useState(false); + const handleDetails = ({ id: _id, service_type: serviceType }: Alert) => { history.push(`${location.pathname}/detail/${serviceType}/${_id}`); }; @@ -60,11 +65,20 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => { history.push(`${location.pathname}/edit/${serviceType}/${id}`); }; - const handleEnableDisable = React.useCallback( - (alert: Alert) => { - const toggleStatus = alert.status === 'enabled' ? 'disabled' : 'enabled'; - const errorStatus = - toggleStatus === 'disabled' ? 'Disabling' : 'Enabling'; + const handleEnableDisable = (alert: Alert) => { + setSelectedAlert(alert); + setIsDialogOpen(true); + }; + + const handleCancel = React.useCallback(() => { + setIsDialogOpen(false); + }, []); + + const handleConfirm = React.useCallback( + (alert: Alert, currentStatus: boolean) => { + const toggleStatus = currentStatus ? 'disabled' : 'enabled'; + const errorStatus = currentStatus ? 'Disabling' : 'Enabling'; + setIsUpdating(true); editAlertDefinition({ alertId: alert.id, serviceType: alert.service_type, @@ -85,105 +99,125 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => { enqueueSnackbar(errorResponse[0].reason, { variant: 'error', }); + }) + .finally(() => { + setIsDialogOpen(false); + setIsUpdating(false); }); }, [editAlertDefinition] ); + const isEnabled = selectedAlert.status !== 'disabled'; + return ( - - {({ data: orderedData, handleOrderChange, order, orderBy }) => ( - - {({ - count, - data: paginatedAndOrderedAlerts, - handlePageChange, - handlePageSizeChange, - page, - pageSize, - }) => ( - <> - - - - - {AlertListingTableLabelMap.map((value) => ( - { - if (order) { - handleOrderChange(orderBy, order); - handlePageChange(1); - } + <> + + {({ data: orderedData, handleOrderChange, order, orderBy }) => ( + + {({ + count, + data: paginatedAndOrderedAlerts, + handlePageChange, + handlePageSizeChange, + page, + pageSize, + }) => ( + <> + +
+ + + {AlertListingTableLabelMap.map((value) => ( + { + if (order) { + handleOrderChange(orderBy, order); + handlePageChange(1); + } + }} + active={orderBy === value.label} + data-qa-header={value.label} + data-qa-sorting={value.label} + direction={order} + key={value.label} + label={value.label} + noWrap + > + {value.colName} + + ))} + + + + + + {paginatedAndOrderedAlerts?.map((alert) => ( + handleDetails(alert), + handleEdit: () => handleEdit(alert), + handleEnableDisable: () => + handleEnableDisable(alert), }} - active={orderBy === value.label} - data-qa-header={value.label} - data-qa-sorting={value.label} - direction={order} - key={value.label} - label={value.label} - noWrap - > - {value.colName} - + alert={alert} + key={alert.id} + services={services} + /> ))} - - - - - - {paginatedAndOrderedAlerts?.map((alert) => ( - handleDetails(alert), - handleEdit: () => handleEdit(alert), - handleEnableDisable: () => handleEnableDisable(alert), - }} - alert={alert} - key={alert.id} - services={services} - /> - ))} - -
-
- { - handlePageChange(page); - requestAnimationFrame(() => { - scrollToElement(); - }); - }} - handleSizeChange={(pageSize) => { - handlePageSizeChange(pageSize); - handlePageChange(1); - requestAnimationFrame(() => { - scrollToElement(); - }); - }} - count={count} - eventCategory="Alert Definitions Table" - page={page} - pageSize={pageSize} - sx={{ border: 0 }} - /> - - )} -
- )} -
+ + + + { + handlePageChange(page); + requestAnimationFrame(() => { + scrollToElement(); + }); + }} + handleSizeChange={(pageSize) => { + handlePageSizeChange(pageSize); + handlePageChange(1); + requestAnimationFrame(() => { + scrollToElement(); + }); + }} + count={count} + eventCategory="Alert Definitions Table" + page={page} + pageSize={pageSize} + sx={{ border: 0 }} + /> + + )} + + )} + + + ); }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx index fdc98763b18..aff0cdd8db3 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx @@ -120,6 +120,9 @@ export const AlertInformationActionTable = ( setIsDialogOpen(true); setSelectedAlert(alert); }; + + const isEnabled = selectedAlert.entity_ids?.includes(entityId) ?? false; + return ( <> @@ -191,11 +194,12 @@ export const AlertInformationActionTable = ( )}