diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx
index cf89aad0c2c..7cfa437fe4b 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx
+++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertConfirmationDialog.tsx
@@ -63,6 +63,7 @@ export const AlertConfirmationDialog = React.memo(
disabled: isLoading,
label: 'Cancel',
onClick: handleCancel,
+ buttonType: 'outlined',
}}
/>
);
diff --git a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.test.tsx
index 4de285d50fd..e1191701576 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.test.tsx
+++ b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.test.tsx
@@ -69,7 +69,9 @@ describe('Alert Listing Reusable Table for contextual view', () => {
});
it('Should show confirm dialog on save button click when changes are made', async () => {
- renderWithTheme();
+ renderWithTheme(
+
+ );
// First toggle an alert to make changes
const alert = alerts[0];
@@ -86,6 +88,24 @@ describe('Alert Listing Reusable Table for contextual view', () => {
expect(screen.getByTestId('confirmation-dialog')).toBeVisible();
});
+ it('Should hide confirm dialog on save button click when changes are made', async () => {
+ renderWithTheme();
+
+ // First toggle an alert to make changes
+ const alert = alerts[0];
+ const row = await screen.findByTestId(alert.id);
+ const toggle = await within(row).findByRole('checkbox');
+ await userEvent.click(toggle);
+
+ // Now the save button should be enabled
+ const saveButton = screen.getByTestId('save-alerts');
+ expect(saveButton).not.toBeDisabled();
+
+ // Click save and verify dialog appears
+ await userEvent.click(saveButton);
+ expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
+ });
+
it('Should have save button in disabled form when no changes are made', () => {
renderWithTheme();
diff --git a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx
index 48f58e8d6de..8c596005ebc 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx
+++ b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertInformationActionTable.tsx
@@ -68,6 +68,11 @@ export interface AlertInformationActionTableProps {
* Service type of the selected entity
*/
serviceType: string;
+
+ /**
+ * Flag to determine if confirmation dialog should be displayed
+ */
+ showConfirmationDialog?: boolean;
}
export interface TableColumnHeader {
@@ -108,11 +113,11 @@ export const AlertInformationActionTable = (
alerts,
columns,
entityId,
- entityName,
error,
orderByColumn,
serviceType,
onToggleAlert,
+ showConfirmationDialog,
} = props;
const alertsTableRef = React.useRef(null);
@@ -125,7 +130,7 @@ export const AlertInformationActionTable = (
const [isLoading, setIsLoading] = React.useState(false);
const isEditMode = !!entityId;
- const isCreateMode = !!onToggleAlert;
+ const isCreateMode = !isEditMode;
const { enabledAlerts, setEnabledAlerts, hasUnsavedChanges } =
useContextualAlertsState(alerts, entityId);
@@ -135,6 +140,13 @@ export const AlertInformationActionTable = (
entityId ?? ''
);
+ // To send initial state of alerts through toggle handler function
+ React.useEffect(() => {
+ if (onToggleAlert) {
+ onToggleAlert(enabledAlerts);
+ }
+ }, []);
+
const handleCancel = () => {
setIsDialogOpen(false);
};
@@ -274,11 +286,6 @@ export const AlertInformationActionTable = (
return null;
}
- // TODO: Remove this once we have a way to toggle ACCOUNT and REGION level alerts
- if (!isEditMode && alert.scope !== 'entity') {
- return null;
- }
-
const status = enabledAlerts[alert.type]?.includes(
alert.id
);
@@ -312,13 +319,14 @@ export const AlertInformationActionTable = (
buttonType="primary"
data-qa-buttons="true"
data-testid="save-alerts"
- disabled={!hasUnsavedChanges}
+ disabled={!hasUnsavedChanges || isLoading}
+ loading={isLoading}
onClick={() => {
- window.scrollTo({
- behavior: 'instant',
- top: 0,
- });
- setIsDialogOpen(true);
+ if (showConfirmationDialog) {
+ setIsDialogOpen(true);
+ } else {
+ handleConfirm(enabledAlerts);
+ }
}}
>
Save
@@ -337,13 +345,12 @@ export const AlertInformationActionTable = (
isOpen={isDialogOpen}
message={
<>
- Are you sure you want to save these settings for {entityName}? All
- legacy alert settings will be disabled and replaced by the new{' '}
- Alerts(Beta) settings.
+ Are you sure you want to save (Beta) Alerts? Legacy settings
+ will be disabled and replaced by (Beta) Alerts settings.
>
}
- primaryButtonLabel="Save"
- title="Save Alerts?"
+ primaryButtonLabel="Confirm"
+ title="Are you sure you want to save (Beta) Alerts? "
/>
>
);
diff --git a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.test.tsx
index 6799845c82d..2f8e251a68a 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.test.tsx
+++ b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.test.tsx
@@ -1,4 +1,4 @@
-import { waitFor } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -24,8 +24,18 @@ vi.mock('src/queries/cloudpulse/alerts', async () => {
const serviceType = 'linode';
const entityId = '123';
const entityName = 'test-instance';
+const region = 'us-ord';
+const onToggleAlert = vi.fn();
const alerts = [
- ...alertFactory.buildList(3, { service_type: serviceType }),
+ ...alertFactory.buildList(3, {
+ service_type: serviceType,
+ regions: ['us-ord'],
+ }),
+ alertFactory.build({
+ label: 'test-alert',
+ service_type: serviceType,
+ regions: ['us-ord'],
+ }),
...alertFactory.buildList(7, {
entity_ids: [entityId],
service_type: serviceType,
@@ -33,6 +43,7 @@ const alerts = [
...alertFactory.buildList(1, {
entity_ids: [entityId],
service_type: serviceType,
+ regions: ['us-ord'],
status: 'enabled',
type: 'system',
}),
@@ -48,6 +59,8 @@ const component = (
);
@@ -98,4 +111,24 @@ describe('Alert Resuable Component for contextual view', () => {
const alert = alerts[alerts.length - 1];
expect(getByText(alert.label)).toBeInTheDocument();
});
+
+ it('Should hide manage alerts button for undefined entityId', () => {
+ renderWithTheme();
+
+ const manageAlerts = screen.queryByTestId('manage-alerts');
+ expect(manageAlerts).not.toBeInTheDocument();
+ expect(screen.queryByText('Alerts')).not.toBeInTheDocument();
+ });
+
+ it('Should filter alerts based on region', async () => {
+ renderWithTheme(component);
+ await userEvent.click(screen.getByRole('button', { name: 'Open' }));
+ expect(screen.getByText('test-alert')).toBeVisible();
+ });
+
+ it('Should show header for edit mode', async () => {
+ renderWithTheme(component);
+ await userEvent.click(screen.getByText('Manage Alerts'));
+ expect(screen.getByText('Alerts')).toBeVisible();
+ });
});
diff --git a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.tsx b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.tsx
index 9840478ee92..483628c260d 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.tsx
+++ b/packages/manager/src/features/CloudPulse/Alerts/ContextualView/AlertReusableComponent.tsx
@@ -13,13 +13,11 @@ import React from 'react';
import { useHistory } from 'react-router-dom';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
+import { useFlags } from 'src/hooks/useFlags';
import { useAlertDefinitionByServiceTypeQuery } from 'src/queries/cloudpulse/alerts';
import { AlertContextualViewTableHeaderMap } from '../AlertsListing/constants';
-import {
- convertAlertsToTypeSet,
- filterAlertsByStatusAndType,
-} from '../Utils/utils';
+import { convertAlertsToTypeSet, filterAlerts } from '../Utils/utils';
import { AlertInformationActionTable } from './AlertInformationActionTable';
import type {
@@ -38,13 +36,22 @@ interface AlertReusableComponentProps {
*/
entityName?: string;
+ /**
+ * Whether the legacy alert is available for the entity
+ */
+ isLegacyAlertAvailable?: boolean;
+
/**
* Called when an alert is toggled on or off.
- * Only use in create flow.
* @param payload enabled alerts ids
*/
onToggleAlert?: (payload: CloudPulseAlertsPayload) => void;
+ /**
+ * Region ID for the selected entity
+ */
+ regionId?: string;
+
/**
* Service type of selected entity
*/
@@ -52,7 +59,14 @@ interface AlertReusableComponentProps {
}
export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
- const { entityId, entityName, onToggleAlert, serviceType } = props;
+ const {
+ entityId,
+ entityName,
+ onToggleAlert,
+ serviceType,
+ regionId,
+ isLegacyAlertAvailable,
+ } = props;
const {
data: alerts,
error,
@@ -64,12 +78,14 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
AlertDefinitionType | undefined
>();
- // Filter alerts based on status, search text & selected type
+ // Filter alerts based on status, search text, selected type, and region
const filteredAlerts = React.useMemo(
- () => filterAlertsByStatusAndType(alerts, searchText, selectedType),
- [alerts, searchText, selectedType]
+ () => filterAlerts({ alerts, searchText, selectedType, regionId }),
+ [alerts, regionId, searchText, selectedType]
);
+ const { aclpBetaServices } = useFlags();
+
const history = useHistory();
// Filter unique alert types from alerts list
@@ -80,21 +96,24 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
}
return (
-
+
-
-
- Alerts
-
+ {entityId && (
+
+
+ Alerts
+ {aclpBetaServices?.[serviceType]?.alerts && }
+
+
-
-
+ )}
{
onToggleAlert={onToggleAlert}
orderByColumn="Alert Name"
serviceType={serviceType}
+ showConfirmationDialog={isLegacyAlertAvailable}
/>
diff --git a/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts b/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts
index 8418fd4e21e..a80eff55fdd 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts
+++ b/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.test.ts
@@ -9,7 +9,7 @@ import {
convertAlertsToTypeSet,
convertSecondsToMinutes,
convertSecondsToOptions,
- filterAlertsByStatusAndType,
+ filterAlerts,
getSchemaWithEntityIdValidation,
getServiceTypeLabel,
handleMultipleError,
@@ -50,13 +50,32 @@ it('test convertSecondsToOptions method', () => {
expect(convertSecondsToOptions(900)).toEqual('15 min');
});
-it('test filterAlertsByStatusAndType method', () => {
- const alerts = alertFactory.buildList(12, { created_by: 'system' });
- expect(filterAlertsByStatusAndType(alerts, '', 'system')).toHaveLength(12);
- expect(filterAlertsByStatusAndType(alerts, '', 'user')).toHaveLength(0);
- expect(filterAlertsByStatusAndType(alerts, 'Alert-1', 'system')).toHaveLength(
- 4
- );
+it('test filterAlerts method', () => {
+ const alerts = [
+ ...alertFactory.buildList(12, { created_by: 'system' }),
+ alertFactory.build({
+ label: 'Alert-14',
+ scope: 'region',
+ regions: ['us-east'],
+ }),
+ ];
+ expect(
+ filterAlerts({ alerts, searchText: '', selectedType: 'system' })
+ ).toHaveLength(12);
+ expect(
+ filterAlerts({ alerts, searchText: '', selectedType: 'user' })
+ ).toHaveLength(0);
+ expect(
+ filterAlerts({ alerts, searchText: 'Alert-1', selectedType: 'system' })
+ ).toHaveLength(4);
+ expect(
+ filterAlerts({
+ alerts,
+ searchText: '',
+ selectedType: 'system',
+ regionId: 'us-east',
+ })
+ ).toHaveLength(13);
});
it('test convertAlertsToTypeSet method', () => {
diff --git a/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts b/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts
index b3a18689fa6..b86d15beb5d 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts
+++ b/packages/manager/src/features/CloudPulse/Alerts/Utils/utils.ts
@@ -127,6 +127,25 @@ interface SupportedRegionsProps {
serviceType: AlertServiceType | null;
}
+interface FilterAlertsProps {
+ /**
+ * The list of alerts to be filtered
+ */
+ alerts: Alert[] | undefined;
+ /**
+ * The region ID to filter the alerts
+ */
+ regionId?: string;
+ /**
+ * The search text to filter the alerts
+ */
+ searchText: string;
+ /**
+ * The selected alert type to filter the alerts
+ */
+ selectedType: AlertDefinitionType | undefined;
+}
+
/**
* @param serviceType Service type for which the label needs to be displayed
* @param serviceTypeList List of available service types in Cloud Pulse
@@ -229,19 +248,19 @@ export const getChipLabels = (
* @param alerts list of alerts to be filtered
* @param searchText text to be searched in alert name
* @param selectedType selecte alert type
- * @returns list of filtered alerts based on searchText & selectedType
+ * @param region region of the entity
+ * @returns list of filtered alerts based on searchText, selectedType, and region
*/
-export const filterAlertsByStatusAndType = (
- alerts: Alert[] | undefined,
- searchText: string,
- selectedType: string | undefined
-): Alert[] => {
+export const filterAlerts = (props: FilterAlertsProps): Alert[] => {
+ const { alerts, regionId, searchText, selectedType } = props;
return (
- alerts?.filter(({ label, status, type }) => {
+ alerts?.filter(({ label, status, type, scope, regions }) => {
return (
(status === 'enabled' || status === 'in progress') &&
(!selectedType || type === selectedType) &&
- (!searchText || label.toLowerCase().includes(searchText.toLowerCase()))
+ (!searchText ||
+ label.toLowerCase().includes(searchText.toLowerCase())) &&
+ (scope !== 'region' || (regionId && regions?.includes(regionId)))
);
}) ?? []
);
diff --git a/packages/manager/src/features/CloudPulse/Alerts/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/constants.ts
index 17630884a99..34aef1e600a 100644
--- a/packages/manager/src/features/CloudPulse/Alerts/constants.ts
+++ b/packages/manager/src/features/CloudPulse/Alerts/constants.ts
@@ -190,12 +190,12 @@ export const CREATE_ALERT_SUCCESS_MESSAGE =
export const UPDATE_ALERT_SUCCESS_MESSAGE =
'Alert successfully updated. It may take a few minutes for your changes to take effect.';
-export const ALERT_SCOPE_TOOLTIP_CONTEXTUAL =
- 'Indicates whether the alert applies to all Linodes in the account, Linodes in specific regions, or just this Linode (entity).';
-
export const ALERT_SCOPE_TOOLTIP_TEXT =
'The set of entities to which the alert applies: account-wide, specific regions, or individual entities.';
+export const ALERT_SCOPE_TOOLTIP_CONTEXTUAL =
+ 'Indicates whether the alert applies to all entities in the account, entities in specific regions, or just this entity.';
+
export type AlertFormMode = 'create' | 'edit' | 'view';
export const DELETE_ALERT_SUCCESS_MESSAGE = 'Alert successfully deleted.';
diff --git a/packages/manager/src/features/CloudPulse/Utils/utils.ts b/packages/manager/src/features/CloudPulse/Utils/utils.ts
index 2a46110e0ff..0979fa7a269 100644
--- a/packages/manager/src/features/CloudPulse/Utils/utils.ts
+++ b/packages/manager/src/features/CloudPulse/Utils/utils.ts
@@ -89,19 +89,20 @@ export const useContextualAlertsState = (
user: [],
};
- if (entityId) {
- alerts.forEach((alert) => {
- const isAccountOrRegion =
- alert.scope === 'region' || alert.scope === 'account';
- const shouldInclude = entityId
- ? isAccountOrRegion || alert.entity_ids.includes(entityId)
- : false;
-
- if (shouldInclude) {
- initialStates[alert.type]?.push(alert.id);
- }
- });
- }
+ 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
+ const shouldInclude = entityId
+ ? isAccountOrRegion || alert.entity_ids.includes(entityId)
+ : isAccountOrRegion;
+
+ if (shouldInclude) {
+ initialStates[alert.type]?.push(alert.id);
+ }
+ });
+
return initialStates;
},
[]
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeAlerts/LinodeAlerts.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeAlerts/LinodeAlerts.tsx
index 7edbfaa1a6a..fa7370800ec 100644
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeAlerts/LinodeAlerts.tsx
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeAlerts/LinodeAlerts.tsx
@@ -1,4 +1,5 @@
import { useLinodeQuery } from '@linode/queries';
+import { useIsLinodeAclpSubscribed } from '@linode/shared';
import { Box } from '@linode/ui';
import { useParams } from '@tanstack/react-router';
import * as React from 'react';
@@ -27,6 +28,7 @@ const LinodeAlerts = () => {
regionId: linode?.region,
type: 'alerts',
});
+ const isLinodeAclpSubscribed = useIsLinodeAclpSubscribed(id, 'beta');
return (
@@ -45,6 +47,7 @@ const LinodeAlerts = () => {
) : (
diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts
index 4ed70cfb712..52a3ff38fe8 100644
--- a/packages/manager/src/mocks/serverHandlers.ts
+++ b/packages/manager/src/mocks/serverHandlers.ts
@@ -2730,6 +2730,7 @@ export const handlers = [
service_type: serviceType === 'dbaas' ? 'dbaas' : 'linode',
type: 'user',
scope: 'region',
+ regions: ['us-east'],
}),
],
});