Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b38623a
Save progress
pmakode-akamai Feb 5, 2026
bcbdd81
More changes and some clean up...
pmakode-akamai Feb 5, 2026
4b545ce
Merge branch 'develop' into poc/aclp-alerting-change-edit-linode
pmakode-akamai Feb 5, 2026
b9cc270
Handle unified and standalone linode updates
pmakode-akamai Feb 5, 2026
a566693
Fix the ACLP alerts accordion collapse transition issue
pmakode-akamai Feb 6, 2026
db310ef
Remove unnecessary default margins when stacking Accordions in edit flow
pmakode-akamai Feb 6, 2026
978f811
General error handling + some styling fixes
pmakode-akamai Feb 6, 2026
ecea0dc
More changes
pmakode-akamai Feb 6, 2026
b149c6f
Handle root errors as well
pmakode-akamai Feb 7, 2026
b9200eb
Merge branch 'develop' into poc/aclp-alerting-change-edit-linode
pmakode-akamai Feb 26, 2026
bb12569
Add scrollErrorIntoViewV2 for unified errors
pmakode-akamai Feb 26, 2026
43796b1
Some more changes
pmakode-akamai Feb 26, 2026
063cf87
Save progress
pmakode-akamai Feb 26, 2026
78c44e4
New approach
pmakode-akamai Feb 27, 2026
1411620
Allow simultaneous alerting in linode edit flow
pmakode-akamai Mar 2, 2026
f6c0c41
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 2, 2026
4a1bfbc
Some more changes
pmakode-akamai Mar 2, 2026
b933a60
Update mocks and remove the remaining useIsLinodeAclpSubscribed refer…
pmakode-akamai Mar 3, 2026
cb532db
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 3, 2026
885a9bb
Fix CloudPulse ContextualView test cases
pmakode-akamai Mar 5, 2026
1398cb1
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 5, 2026
c78adb2
Update comments
pmakode-akamai Mar 5, 2026
9e78fb3
Fix stale state: reset ACLP alerts to empty when entering ACLP mode
pmakode-akamai Mar 5, 2026
1c9f200
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 5, 2026
150568f
Added changeset: Allow simultaneous v1 (Legacy) and v2 (ACLP) alertin…
pmakode-akamai Mar 5, 2026
7b564a0
Added changeset: The `useIsLinodeAclpSubscribed` hook from the shared…
pmakode-akamai Mar 5, 2026
5d43a62
Added changeset: Simplify `UpdateLinodeAlertsSchema` to support simul…
pmakode-akamai Mar 5, 2026
5039f94
Update comment
pmakode-akamai Mar 5, 2026
aef1f4f
Don't allow unified saving if aclp reusable component is not ready
pmakode-akamai Mar 5, 2026
10c928f
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 9, 2026
e9cadb7
Fix unit tests
pmakode-akamai Mar 9, 2026
a90e580
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 12, 2026
96830de
Few changes to aclp components
pmakode-akamai Mar 12, 2026
3b409cd
Update comments for clarity
pmakode-akamai Mar 12, 2026
c158d85
Update changeset and consolidate imports
pmakode-akamai Mar 13, 2026
37baf8c
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 13, 2026
cea9289
Update comments and exclude fields from payload for bare metal type
pmakode-akamai Mar 17, 2026
fa2a040
Merge branch 'develop' into UIE-10204
pmakode-akamai Mar 17, 2026
72d5e36
Remove Bare Metal related logic from legacy Alerts
pmakode-akamai Mar 17, 2026
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": Upcoming Features
---

Allow simultaneous v1 (Legacy) and v2 (ACLP) alerting in Linode edit flow ([#13455](https://github.com/linode/manager/pull/13455))
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ 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} showConfirmationDialog />
<AlertInformationActionTable
{...props}
serviceType="dbaas"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to use another service type (dbaas) instead of the default linode to make these tests pass since the linode service type does not have a Save button

showConfirmationDialog
/>
);

// First toggle an alert to make changes
Expand All @@ -127,7 +131,9 @@ describe('Alert Listing Reusable Table for contextual view', () => {
});

it('Should hide confirm dialog on save button click when changes are made', async () => {
renderWithTheme(<AlertInformationActionTable {...props} />);
renderWithTheme(
<AlertInformationActionTable {...props} serviceType="dbaas" />
);

// First toggle an alert to make changes
const alert = alerts[0];
Expand All @@ -145,14 +151,22 @@ describe('Alert Listing Reusable Table for contextual view', () => {
});

it('Should have save button in disabled form when no changes are made', () => {
renderWithTheme(<AlertInformationActionTable {...props} />);
renderWithTheme(
<AlertInformationActionTable {...props} serviceType="dbaas" />
);

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} />);
renderWithTheme(
<AlertInformationActionTable
{...props}
alerts={alerts}
serviceType="dbaas"
/>
);

// Toggle entity-level user alert with ID 2 to enable it
const userAlertRow = await screen.findByTestId('9');
Expand All @@ -164,10 +178,38 @@ describe('Alert Listing Reusable Table for contextual view', () => {

// Verify that account and region level alerts are not included in the payload
expect(mockUpdateAlerts).toHaveBeenCalledWith({
alerts: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe servicePayloadTransformerMap may need an update based on the latest behavior. It might make sense for the CloudPulse team to remove linode from this map as part of a separate cleanup.

For the linode service type, we probably don't need to pass the alerts wrapper to the service owners/consumers. Instead, it should just send the raw payload with system_alerts and user_alerts (which is currently working as expected). Also, this should ideally be triggered via the toggle switch, rather than on Save or during alert mutation for the linode service type.

system_alerts: [1, 2, 3, 4, 5, 6, 7],
user_alerts: [9],
});
});

it('Should not render save button for linode service type', () => {
// For linode, save is handled by the service owner component (e.g. LinodeAlerts unified save button).
renderWithTheme(<AlertInformationActionTable {...props} />); // props.serviceType is 'linode'

expect(screen.queryByTestId('save-alerts')).not.toBeInTheDocument();
});

it('Should call onToggleAlert with correct payload when a toggle is clicked for linode service type', async () => {
// Even though the save button is hidden for linode, the toggle still fires onToggleAlert
// so the service owner can collect the payload for its own save flow.
const onToggleAlert = vi.fn();
renderWithTheme(
<AlertInformationActionTable {...props} onToggleAlert={onToggleAlert} />
);

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

// Raw payload passed to service owner: system_alerts unchanged, user_alerts with the newly toggled alert id 9,
// and hasUnsavedChanges is true
expect(onToggleAlert).toHaveBeenCalledWith(
{
system_alerts: [1, 2, 3, 4, 5, 6, 7],
user_alerts: [9],
},
});
true // hasUnsavedChanges
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ export interface AlertRowPropsOptions {
) => void;
}

/**
* Service types whose parent component handles saving alerts externally.
* The internal Save button is hidden for these service types.
* Add a service type here to opt out of the built-in Save button.
*/
const SERVICES_WITH_EXTERNAL_SAVE: CloudPulseServiceType[] = ['linode'];

export const AlertInformationActionTable = (
props: AlertInformationActionTableProps
) => {
Expand Down Expand Up @@ -159,6 +166,12 @@ export const AlertInformationActionTable = (
const updateAlerts = useAlertsMutation(serviceType, entityId ?? '');

React.useEffect(() => {
// To send initial state of alerts through toggle handler function in edit mode.
Comment thread
tanushree-akamai marked this conversation as resolved.
// This ensures the service owner receives the initial enabled-alert state immediately
// when the component is ready, regardless of whether any toggle action is performed.
if (isEditMode && onToggleAlert) {
onToggleAlert(enabledAlerts);
}
return () => {
// Cleanup on unmount (For Edit flow)
if (isEditMode && onToggleAlert) {
Expand Down Expand Up @@ -345,28 +358,37 @@ export const AlertInformationActionTable = (
handleSizeChange={handlePageSizeChange}
page={page}
pageSize={pageSize}
sx={{
// Prevents layout breaks and enables smooth collapse when table with this footer is used inside an Accordion.
// Without this, the PaginationFooter causes layout shifts during the collapse transition.
contain: 'layout',
}}
/>
</Box>
{isEditMode && (
<Box>
<Button
buttonType="primary"
data-qa-buttons="true"
data-testid="save-alerts"
disabled={!hasUnsavedChanges || isLoading}
loading={isLoading}
onClick={() => {
if (showConfirmationDialog) {
setIsDialogOpen(true);
} else {
handleConfirm(enabledAlerts);
}
}}
>
Save
</Button>
</Box>
)}
{/* Show save button only in edit mode. Service types listed in
SERVICES_WITH_EXTERNAL_SAVE manage their own save externally
(e.g. linode handles it in the parent component). */}
{isEditMode &&
!SERVICES_WITH_EXTERNAL_SAVE.includes(serviceType) && (
<Box>
<Button
buttonType="primary"
data-qa-buttons="true"
data-testid="save-alerts"
disabled={!hasUnsavedChanges || isLoading}
loading={isLoading}
onClick={() => {
if (showConfirmationDialog) {
setIsDialogOpen(true);
} else {
handleConfirm(enabledAlerts);
}
}}
>
Save
</Button>
</Box>
)}
</>
)}
</Paginate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,33 @@ describe('Alert Resuable Component for contextual view', () => {
});

it('Should show header for edit mode', async () => {
renderWithTheme(component, {
initialEntries: ['/alerts/definitions'],
initialRoute: '/alerts/definitions',
});
await userEvent.click(screen.getByText('Manage Alerts'));
// For service types not in SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW (e.g. dbaas),
// the 'Alerts' heading and 'Manage Alerts' button appear together in the section header.
renderWithTheme(
<AlertReusableComponent
entityId={entityId}
entityName={entityName}
onToggleAlert={onToggleAlert}
regionId={region}
serviceType="dbaas"
/>,
{
initialEntries: ['/alerts/definitions'],
initialRoute: '/alerts/definitions',
}
);
expect(screen.getByText('Alerts')).toBeVisible();
expect(screen.getByTestId('manage-alerts')).toBeVisible();
});

it('Should not show Alerts heading for linode service type but still show Manage Alerts button in filter row', () => {
// For service types in SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW (e.g. linode), the 'Alerts' heading
// belongs to the service owner and is not rendered here. The Manage Alerts button moves to the filter row.
renderWithTheme(component); // component uses serviceType='linode' with entityId

expect(
screen.queryByRole('heading', { level: 2, name: 'Alerts' })
).not.toBeInTheDocument();
expect(screen.getByTestId('manage-alerts')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
CloudPulseAlertsPayload,
CloudPulseServiceType,
} from '@linode/api-v4';
import type { SxProps, Theme } from '@linode/ui';

interface AlertReusableComponentProps {
/**
Expand All @@ -41,6 +42,14 @@ interface AlertReusableComponentProps {
*/
isLegacyAlertAvailable?: boolean;

/**
Comment thread
tanushree-akamai marked this conversation as resolved.
* Called once when this component is ready - i.e. alerts have loaded
* successfully without error. Always receives `true`.
* Service owners can use this to enable or disable save buttons that depend
* on the readiness of this component before allowing any action.
*/
onStatusChange?: (isReady: boolean) => void;

/**
* Called when an alert is toggled on or off.
* @param payload enabled alerts ids
Expand All @@ -51,6 +60,11 @@ interface AlertReusableComponentProps {
hasUnsavedChanges?: boolean
) => void;

/**
* Custom sx styles for the Paper wrapper component
*/
paperSx?: SxProps<Theme>;

/**
* Region ID for the selected entity
*/
Expand All @@ -62,11 +76,21 @@ interface AlertReusableComponentProps {
serviceType: CloudPulseServiceType;
}

/**
* Service types that display the Manage Alerts button inline with the search/filter row.
* For all other service types, the button appears inline with the Alerts section header.
*/
const SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW: CloudPulseServiceType[] = [
'linode',
];

export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
const {
entityId,
entityName,
onToggleAlert,
onStatusChange,
paperSx,
serviceType,
regionId,
isLegacyAlertAvailable,
Expand All @@ -77,6 +101,13 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
isLoading,
} = useAlertDefinitionByServiceTypeQuery(serviceType);

React.useEffect(() => {
// Only notify the parent when ready
if (!isLoading && !error) {
onStatusChange?.(true);
}
}, [isLoading, error, onStatusChange]);

const [searchText, setSearchText] = React.useState<string>('');
const [selectedType, setSelectedType] = React.useState<
AlertDefinitionType | undefined
Expand All @@ -100,24 +131,26 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
}

return (
<Paper sx={{ p: entityId ? undefined : 0 }}>
<Paper sx={paperSx}>
<Stack gap={3}>
{entityId && (
<Box display="flex" justifyContent="space-between">
<Box alignItems="center" display="flex" gap={0.5}>
<Typography variant="h2">Alerts</Typography>
{aclpServices?.[serviceType]?.alerts?.beta && <BetaChip />}
{/* Not in SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW: Show Manage Alerts button inline with the Alerts section header */}
{entityId &&
!SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW.includes(serviceType) && (
<Box display="flex" justifyContent="space-between">
<Box alignItems="center" display="flex" gap={0.5}>
<Typography variant="h2">Alerts</Typography>
{aclpServices?.[serviceType]?.alerts?.beta && <BetaChip />}
</Box>
<Button
buttonType="outlined"
data-qa-buttons="true"
data-testid="manage-alerts"
onClick={() => navigate({ to: '/alerts/definitions' })}
>
Manage Alerts
</Button>
</Box>
<Button
buttonType="outlined"
data-qa-buttons="true"
data-testid="manage-alerts"
onClick={() => navigate({ to: '/alerts/definitions' })}
>
Manage Alerts
</Button>
</Box>
)}
)}
<Stack gap={2}>
<Box display="flex" gap={2}>
<DebouncedSearchTextField
Expand Down Expand Up @@ -145,6 +178,21 @@ export const AlertReusableComponent = (props: AlertReusableComponentProps) => {
hideLabel: true,
}}
/>
{/* In SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW: Show Manage Alerts button inline with the search/filter row (right-aligned) */}
{entityId &&
SERVICES_WITH_MANAGE_ALERTS_IN_FILTER_ROW.includes(
serviceType
) && (
<Button
buttonType="outlined"
data-qa-buttons="true"
data-testid="manage-alerts"
onClick={() => navigate({ to: '/alerts/definitions' })}
sx={{ marginLeft: 'auto' }}
>
Manage Alerts
</Button>
)}
</Box>

<AlertInformationActionTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ const preferenceConfig: Record<
/**
* - For Alerts, the toggle uses local state, not preferences. We do this because each Linode should manage its own alert mode individually.
* - Create Linode: Toggle defaults to false (legacy mode). It's a simple UI toggle with no persistence.
* - Edit Linode: Toggle defaults based on useIsLinodeAclpSubscribed (true if the Linode is already subscribed to ACLP). Still local state - not saved to preferences.
*
* - For Metrics, we use account-level preferences, since it's a global setting shared across all Linodes.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AlertsPanel } from 'src/features/Linodes/LinodesDetail/LinodeAlerts/Ale
import { useFlags } from 'src/hooks/useFlags';

import { AclpPreferenceToggle } from '../../AclpPreferenceToggle';
import { EMPTY_ACLP_ALERTS } from '../utilities';

import type { LinodeCreateFormValues } from '../utilities';
import type { CloudPulseAlertsPayload } from '@linode/api-v4';
Expand All @@ -27,7 +28,7 @@ export const Alerts = ({
const { field } = useController({
control,
name: 'alerts',
defaultValue: { system_alerts: [], user_alerts: [] },
defaultValue: EMPTY_ACLP_ALERTS,
});

const handleToggleAlert = (updatedAlerts: CloudPulseAlertsPayload) => {
Expand Down Expand Up @@ -71,11 +72,12 @@ export const Alerts = ({
// Beta ACLP Alerts View
<AlertReusableComponent
onToggleAlert={handleToggleAlert}
paperSx={{ p: 0 }}
serviceType="linode"
/>
) : (
// Legacy Alerts View (read-only)
<AlertsPanel />
// Legacy Alerts View (read-only with default values)
<AlertsPanel paperSx={{ p: 0 }} />
)}
</Accordion>
);
Expand Down
Loading
Loading