Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

CloudPulse-Alerts: Exclude account/region alerts in api payload while updating alerts for a linode and fix state reset issue on save ([#13301](https://github.com/linode/manager/pull/13301))
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import type {
TableColumnHeader,
} from './AlertInformationActionTable';

const mockUpdateAlerts = vi.fn();

vi.mock('src/queries/cloudpulse/useAlertsMutation', async () => {
const actual = await vi.importActual(
'src/queries/cloudpulse/useAlertsMutation'
);
return {
...actual,
useAlertsMutation: () => mockUpdateAlerts,
};
});

const serviceType = 'linode';
const entityId = '123';
const entityName = 'test-instance';
Expand All @@ -22,6 +34,27 @@ const alerts = [
service_type: serviceType,
status: 'enabled',
}),
alertFactory.build({
id: 9,
entity_ids: [],
service_type: serviceType,
type: 'user',
scope: 'entity',
}),
alertFactory.build({
id: 10,
entity_ids: [],
service_type: serviceType,
type: 'user',
scope: 'account',
}),
alertFactory.build({
id: 11,
entity_ids: [],
service_type: serviceType,
type: 'user',
scope: 'region',
}),
];
const columns: TableColumnHeader[] = [
{ columnName: 'Alert Name', label: 'label' },
Expand All @@ -39,6 +72,11 @@ const props: AlertInformationActionTableProps = {
};

describe('Alert Listing Reusable Table for contextual view', () => {
beforeEach(() => {
mockUpdateAlerts.mockClear();
mockUpdateAlerts.mockResolvedValue({});
});

it('Should render alert table', async () => {
renderWithTheme(<AlertInformationActionTable {...props} />);

Expand Down Expand Up @@ -112,4 +150,24 @@ describe('Alert Listing Reusable Table for contextual view', () => {
const saveButton = screen.getByTestId('save-alerts');
expect(saveButton).toBeDisabled();
});

it('Should send correct payload to the API when save button is clicked in edit mode', async () => {
renderWithTheme(<AlertInformationActionTable {...props} alerts={alerts} />);

// Toggle entity-level user alert with ID 2 to enable it
const userAlertRow = await screen.findByTestId('9');
await userEvent.click(await within(userAlertRow).findByRole('checkbox'));

const saveButton = screen.getByTestId('save-alerts');
expect(saveButton).not.toBeDisabled();
await userEvent.click(saveButton);

// Verify that account and region level alerts are not included in the payload
expect(mockUpdateAlerts).toHaveBeenCalledWith({
alerts: {
system_alerts: [1, 2, 3, 4, 5, 6, 7],
user_alerts: [9],
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -149,23 +149,16 @@
const payloadAlertType = (alert: Alert) =>
alert.type === 'system' ? 'system_alerts' : 'user_alerts';

const {
enabledAlerts,
setEnabledAlerts,
hasUnsavedChanges,
initialState,
resetToInitialState,
} = useContextualAlertsState(alerts, entityId);
const { enabledAlerts, setEnabledAlerts, hasUnsavedChanges, initialState } =
useContextualAlertsState(alerts, entityId);

const isAccountOrRegionAlert = (alert: Alert) =>
alert.scope === 'region' || alert.scope === 'account';

// Mutation to update alerts as per service type
const updateAlerts = useAlertsMutation(serviceType, entityId ?? '');

React.useEffect(() => {
// To send initial state of alerts through toggle handler function (For Create Flow)
if (!isEditMode && onToggleAlert) {
onToggleAlert(enabledAlerts);
}

return () => {
// Cleanup on unmount (For Edit flow)
if (isEditMode && onToggleAlert) {
Expand Down Expand Up @@ -195,8 +188,7 @@
enqueueSnackbar('Your settings for alerts have been saved.', {
variant: 'success',
});
// Reset the state to sync with the updated alerts from API
resetToInitialState();
onToggleAlert?.({}, false);
invalidateAclpAlerts(queryClient, serviceType, entityId, payload);
})
.catch(() => {
Expand All @@ -209,7 +201,7 @@
setIsDialogOpen(false);
});
},
[updateAlerts, enqueueSnackbar, resetToInitialState]
[updateAlerts, enqueueSnackbar, onToggleAlert]

Check warning on line 204 in packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 React Hook React.useCallback has missing dependencies: 'entityId', 'queryClient', and 'serviceType'. Either include them or remove the dependency array. Raw Output: {"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook React.useCallback has missing dependencies: 'entityId', 'queryClient', and 'serviceType'. Either include them or remove the dependency array.","line":204,"column":5,"nodeType":"ArrayExpression","endLine":204,"endColumn":51,"suggestions":[{"desc":"Update the dependencies array to be: [updateAlerts, serviceType, enqueueSnackbar, onToggleAlert, queryClient, entityId]","fix":{"range":[5706,5752],"text":"[updateAlerts, serviceType, enqueueSnackbar, onToggleAlert, queryClient, entityId]"}}]}
);

const handleToggleAlert = React.useCallback(
Expand Down Expand Up @@ -325,9 +317,12 @@
if (!(isEditMode || isCreateMode)) {
return null;
}
const status = enabledAlerts[
payloadAlertType(alert)
]?.includes(alert.id);
// If alert is account or region level, enable it by default and if it is entity type alert, check if it is enabled for that entity
const status =
isAccountOrRegionAlert(alert) ||
enabledAlerts[payloadAlertType(alert)]?.includes(
alert.id
);

return (
<AlertInformationActionRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe('useContextualAlertsState', () => {
});
});

it('should include alerts that match entityId or account/region level alerts in initial states', () => {
it('should include alerts that match entityId in initial states', () => {
const entityId = '123';
const alerts = [
alertFactory.build({
Expand All @@ -275,21 +275,13 @@ describe('useContextualAlertsState', () => {
entity_ids: [entityId],
scope: 'entity',
}),
alertFactory.build({
id: 3,
label: 'alert3',
type: 'system',
entity_ids: ['456'],
scope: 'region',
}),
];

const { result } = renderHook(() =>
useContextualAlertsState(alerts, entityId)
);

expect(result.current.initialState.system_alerts).toContain(1);
expect(result.current.initialState.system_alerts).toContain(3);
expect(result.current.initialState.user_alerts).toContain(2);
});

Expand Down
9 changes: 3 additions & 6 deletions packages/manager/src/features/CloudPulse/Utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,10 @@ export const useContextualAlertsState = (
};

alerts.forEach((alert) => {
const isAccountOrRegion =
alert.scope === 'region' || alert.scope === 'account';

// include alerts which has either account or region level scope or entityId is present in the alert's entity_ids
// include alerts for which entityId is present in the alert's entity_ids
const shouldInclude = entityId
? isAccountOrRegion || alert.entity_ids.includes(entityId)
: isAccountOrRegion;
? alert.entity_ids.includes(entityId)
: false;

if (shouldInclude) {
const payloadAlertType =
Expand Down