Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
67b2377
change: [DI-26188] - Hide manage alerts button for undefined entity id
nikhagra-akamai Jul 16, 2025
f21f367
change: [DI-26188] - Updated scope tooltip message
nikhagra-akamai Jul 16, 2025
cdb62a8
change: [DI-26188] - Added isLegacyAvailable prop to reusable component
nikhagra-akamai Jul 16, 2025
193269a
change: [DI-26188] - Added regionId props to filter region scope alert
nikhagra-akamai Jul 16, 2025
450ed8c
change: [DI-26188] - Added loading state to save button
nikhagra-akamai Jul 16, 2025
e165d89
test: [DI-26188] - Updated test cases
nikhagra-akamai Jul 16, 2025
4e83163
[DI-26293] - Make alert header conditional, filter alerts by region, …
ankita-akamai Jul 21, 2025
796310e
updated mock data
nikhagra-akamai Jul 22, 2025
6b56d07
Merge branch 'develop' of github.com:linode/manager into alert/contex…
nikhagra-akamai Jul 22, 2025
3ea7f94
Merge branch 'develop' into alert/contextual_view_enhancement
nikhagra-akamai Jul 23, 2025
9f64f37
upcoming: [DI-26188] - Updated confirmation dialog text
nikhagra-akamai Jul 23, 2025
404bd7b
Merge branch 'alert/contextual_view_enhancement' of github.com:nikhag…
nikhagra-akamai Jul 23, 2025
d33d48b
Pass `isLegacyAlertAvailable value to `AclpReusableComponent`
pmakode-akamai Jul 23, 2025
8766a0c
upcoming: [DI-26188] - Updated logic to show beta chip based on flags
nikhagra-akamai Jul 23, 2025
9977a19
upcoming: [DI-26188] - Updated paper padding conditionally
nikhagra-akamai Jul 23, 2025
a164caa
Merge branch 'alert/contextual_view_enhancement' of github.com:nikhag…
nikhagra-akamai Jul 23, 2025
2d6be7b
upcoming: [DI-26188] - Updated logic to called handler function on in…
nikhagra-akamai Jul 23, 2025
c2eae74
Merge branch 'develop' of github.com:linode/manager into alert/contex…
nikhagra-akamai Jul 23, 2025
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
Expand Up @@ -63,6 +63,7 @@ export const AlertConfirmationDialog = React.memo(
disabled: isLoading,
label: 'Cancel',
onClick: handleCancel,
buttonType: 'outlined',
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<AlertInformationActionTable {...props} />);
renderWithTheme(
<AlertInformationActionTable {...props} showConfirmationDialog />
);

// First toggle an alert to make changes
const alert = alerts[0];
Expand All @@ -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(<AlertInformationActionTable {...props} />);

// 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(<AlertInformationActionTable {...props} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
* Service type of the selected entity
*/
serviceType: string;

/**
* Flag to determine if confirmation dialog should be displayed
*/
showConfirmationDialog?: boolean;
}

export interface TableColumnHeader {
Expand Down Expand Up @@ -108,11 +113,11 @@
alerts,
columns,
entityId,
entityName,
error,
orderByColumn,
serviceType,
onToggleAlert,
showConfirmationDialog,
} = props;

const alertsTableRef = React.useRef<HTMLTableElement>(null);
Expand All @@ -125,7 +130,7 @@
const [isLoading, setIsLoading] = React.useState<boolean>(false);

const isEditMode = !!entityId;
const isCreateMode = !!onToggleAlert;
const isCreateMode = !isEditMode;

const { enabledAlerts, setEnabledAlerts, hasUnsavedChanges } =
useContextualAlertsState(alerts, entityId);
Expand All @@ -135,6 +140,13 @@
entityId ?? ''
);

// To send initial state of alerts through toggle handler function
React.useEffect(() => {
if (onToggleAlert) {
onToggleAlert(enabledAlerts);
}
}, []);

Check warning on line 148 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.useEffect has missing dependencies: 'enabledAlerts' and 'onToggleAlert'. Either include them or remove the dependency array. Raw Output: {"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook React.useEffect has missing dependencies: 'enabledAlerts' and 'onToggleAlert'. Either include them or remove the dependency array.","line":148,"column":6,"nodeType":"ArrayExpression","endLine":148,"endColumn":8,"suggestions":[{"desc":"Update the dependencies array to be: [enabledAlerts, onToggleAlert]","fix":{"range":[3931,3933],"text":"[enabledAlerts, onToggleAlert]"}}]}

const handleCancel = () => {
setIsDialogOpen(false);
};
Expand Down Expand Up @@ -274,11 +286,6 @@
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
);
Expand Down Expand Up @@ -312,13 +319,14 @@
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
Expand All @@ -337,13 +345,12 @@
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{' '}
<b>Alerts(Beta)</b> settings.
Are you sure you want to save (Beta) Alerts? <b>Legacy</b> 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? "
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -24,15 +24,26 @@ 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,
}),
...alertFactory.buildList(1, {
entity_ids: [entityId],
service_type: serviceType,
regions: ['us-ord'],
status: 'enabled',
type: 'system',
}),
Expand All @@ -48,6 +59,8 @@ const component = (
<AlertReusableComponent
entityId={entityId}
entityName={entityName}
onToggleAlert={onToggleAlert}
regionId={region}
serviceType={serviceType}
/>
);
Expand Down Expand Up @@ -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(<AlertReusableComponent serviceType={serviceType} />);

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -38,21 +36,37 @@ 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
*/
serviceType: string;
}

export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
const { entityId, entityName, onToggleAlert, serviceType } = props;
const {
entityId,
entityName,
onToggleAlert,
serviceType,
regionId,
isLegacyAlertAvailable,
} = props;
const {
data: alerts,
error,
Expand All @@ -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
Expand All @@ -80,21 +96,24 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
}

return (
<Paper>
<Paper sx={{ p: entityId ? undefined : 0 }}>
<Stack gap={3}>
<Box display="flex" justifyContent="space-between">
<Box alignItems="center" display="flex" gap={0.5}>
<Typography variant="h2">Alerts</Typography>
<BetaChip />
{entityId && (
<Box display="flex" justifyContent="space-between">
<Box alignItems="center" display="flex" gap={0.5}>
<Typography variant="h2">Alerts</Typography>
{aclpBetaServices?.[serviceType]?.alerts && <BetaChip />}
</Box>
<Button
buttonType="outlined"
data-qa-buttons="true"
data-testid="manage-alerts"
onClick={() => history.push('/alerts/definitions')}
>
Manage Alerts
</Button>
</Box>
<Button
buttonType="outlined"
data-testid="manage-alerts"
onClick={() => history.push('/alerts/definitions')}
>
Manage Alerts
</Button>
</Box>
)}
<Stack gap={2}>
<Box display="flex" gap={2}>
<DebouncedSearchTextField
Expand Down Expand Up @@ -133,6 +152,7 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
onToggleAlert={onToggleAlert}
orderByColumn="Alert Name"
serviceType={serviceType}
showConfirmationDialog={isLegacyAlertAvailable}
/>
</Stack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
convertAlertsToTypeSet,
convertSecondsToMinutes,
convertSecondsToOptions,
filterAlertsByStatusAndType,
filterAlerts,
getSchemaWithEntityIdValidation,
getServiceTypeLabel,
handleMultipleError,
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading