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

UI enhancements across Alerting feature: Notification Error message, Limits for Metrics, Dimensions, Notifications, and other UI enhancements with relevant unit test cases([#11773](https://github.com/linode/manager/pull/11773))
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ describe('Create Alert', () => {
cy.visitWithLogin('monitor/alerts/definitions/create');

// Enter Name and Description
cy.findByPlaceholderText('Enter Name')
cy.findByPlaceholderText('Enter a Name')
.should('be.visible')
.type(customAlertDefinition.label);

cy.findByPlaceholderText('Enter Description')
cy.findByPlaceholderText('Enter a Description')
.should('be.visible')
.type(customAlertDefinition.description ?? '');

Expand Down Expand Up @@ -227,7 +227,7 @@ describe('Create Alert', () => {
const cpuUsageMetricDetails = {
aggregationType: 'Average',
dataField: 'CPU Utilization',
operator: '==',
operator: '=',
ruleIndex: 0,
threshold: '1000',
};
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('Create Alert', () => {
const memoryUsageMetricDetails = {
aggregationType: 'Average',
dataField: 'Memory Usage',
operator: '==',
operator: '=',
ruleIndex: 1,
threshold: '1000',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ queryMocks.useEditAlertDefinition.mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
reset: vi.fn(),
});

const mockScroll = vi.fn();
describe('Alert List Table test', () => {
it('should render the alert landing table ', async () => {
const { getByText } = renderWithTheme(
<AlertsListTable alerts={[]} isLoading={false} services={[]} />
<AlertsListTable
alerts={[]}
isLoading={false}
scrollToElement={mockScroll}
services={[]}
/>
);
expect(getByText('Alert Name')).toBeVisible();
expect(getByText('Service')).toBeVisible();
Expand All @@ -40,6 +45,7 @@ describe('Alert List Table test', () => {
alerts={[]}
error={[{ reason: 'Error in fetching the alerts' }]}
isLoading={false}
scrollToElement={mockScroll}
services={[]}
/>
);
Expand All @@ -60,6 +66,7 @@ describe('Alert List Table test', () => {
}),
]}
isLoading={false}
scrollToElement={mockScroll}
services={[{ label: 'Linode', value: 'linode' }]}
/>
);
Expand All @@ -82,6 +89,7 @@ describe('Alert List Table test', () => {
<AlertsListTable
alerts={[alert]}
isLoading={false}
scrollToElement={mockScroll}
services={[{ label: 'Linode', value: 'linode' }]}
/>
);
Expand All @@ -98,6 +106,7 @@ describe('Alert List Table test', () => {
<AlertsListTable
alerts={[alert]}
isLoading={false}
scrollToElement={mockScroll}
services={[{ label: 'Linode', value: 'linode' }]}
/>
);
Expand All @@ -118,6 +127,7 @@ describe('Alert List Table test', () => {
<AlertsListTable
alerts={[alert]}
isLoading={false}
scrollToElement={mockScroll}
services={[{ label: 'Linode', value: 'linode' }]}
/>
);
Expand All @@ -139,6 +149,7 @@ describe('Alert List Table test', () => {
<AlertsListTable
alerts={[alert]}
isLoading={false}
scrollToElement={mockScroll}
services={[{ label: 'Linode', value: 'linode' }]}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ export interface AlertsListTableProps {
* A boolean indicating whether the alerts are loading
*/
isLoading: boolean;
/**
* Callback to scroll to the button element on page change
*/
scrollToElement: () => void;
/**
* The list of services to display in the table
*/
services: Item<string, AlertServiceType>[];
}

export const AlertsListTable = React.memo((props: AlertsListTableProps) => {
const { alerts, error, isLoading, services } = props;
const { alerts, error, isLoading, scrollToElement, services } = props;
const _error = error
? getAPIErrorOrDefault(error, 'Error in fetching the alerts.')
: undefined;
Expand Down Expand Up @@ -85,7 +89,7 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => {
<OrderBy
data={alerts}
order="asc"
orderBy="service"
orderBy="service_type"
preferenceKey="alerts-landing"
>
{({ data: orderedData, handleOrderChange, order, orderBy }) => (
Expand All @@ -109,11 +113,16 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => {
<TableRow>
{AlertListingTableLabelMap.map((value) => (
<TableSortCell
handleClick={(orderBy, order) => {
if (order) {
handleOrderChange(orderBy, order);
handlePageChange(1);
}
}}
active={orderBy === value.label}
data-qa-header={value.label}
data-qa-sorting={value.label}
direction={order}
handleClick={handleOrderChange}
key={value.label}
label={value.label}
noWrap
Expand Down Expand Up @@ -147,12 +156,24 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => {
</Table>
</Grid>
<PaginationFooter
handlePageChange={(page) => {
handlePageChange(page);
requestAnimationFrame(() => {
scrollToElement();
});
}}
handleSizeChange={(pageSize) => {
handlePageSizeChange(pageSize);
handlePageChange(1);
requestAnimationFrame(() => {
scrollToElement();
});
}}
count={count}
eventCategory="Alert Definitions Table"
handlePageChange={handlePageChange}
handleSizeChange={handlePageSizeChange}
page={page}
pageSize={pageSize}
sx={{ border: 0 }}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import { useAllAlertDefinitionsQuery } from 'src/queries/cloudpulse/alerts';
import { useCloudPulseServiceTypes } from 'src/queries/cloudpulse/services';

import { alertStatusOptions } from '../constants';
import { scrollToElement } from '../Utils/AlertResourceUtils';
import { AlertsListTable } from './AlertListTable';

import type { Item } from '../constants';
import type { Alert, AlertServiceType, AlertStatusType } from '@linode/api-v4';

const searchAndSelectSx = {
lg: '250px',
md: '300px',
sm: '500px',
sm: '400px',
xs: '300px',
};

Expand All @@ -30,6 +32,7 @@ export const AlertListing = () => {
isLoading: serviceTypesLoading,
} = useCloudPulseServiceTypes(true);

const topRef = React.useRef<HTMLButtonElement>(null);
const getServicesList = React.useMemo((): Item<
string,
AlertServiceType
Expand Down Expand Up @@ -146,6 +149,7 @@ export const AlertListing = () => {
flexWrap="wrap"
gap={3}
justifyContent="space-between"
ref={topRef}
>
<Box
flexDirection={{
Expand Down Expand Up @@ -184,7 +188,7 @@ export const AlertListing = () => {
data-qa-filter="alert-service-filter"
data-testid="alert-service-filter"
label=""
limitTags={2}
limitTags={1}
loading={serviceTypesLoading}
multiple
noMarginTop
Expand All @@ -203,6 +207,7 @@ export const AlertListing = () => {
data-qa-filter="alert-status-filter"
data-testid="alert-status-filter"
label=""
limitTags={1}
multiple
noMarginTop
options={alertStatusOptions}
Expand All @@ -219,7 +224,7 @@ export const AlertListing = () => {
paddingBottom: 0,
paddingTop: 0,
whiteSpace: 'noWrap',
width: { md: '150px', xs: '200px' },
width: { lg: '120px', md: '120px', sm: '150px', xs: '150px' },
}}
buttonType="primary"
data-qa-button="create-alert"
Expand All @@ -233,6 +238,7 @@ export const AlertListing = () => {
alerts={getAlertsList}
error={error ?? undefined}
isLoading={isLoading}
scrollToElement={() => scrollToElement(topRef.current ?? null)}
services={getServicesList}
/>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const queryMocks = vi.hoisted(() => ({
}));

beforeEach(() => {
Element.prototype.scrollIntoView = vi.fn();
queryMocks.useResourcesQuery.mockReturnValue({
data: [],
isError: false,
Expand Down Expand Up @@ -79,9 +80,9 @@ describe('AlertDefinition Create', () => {
await user.click(
container.getByRole('button', { name: 'Add dimension filter' })
);
const submitButton = container.getByText('Submit').closest('button');
const submitButton = container.getByText('Submit');
await user.click(submitButton!);
expect(container.getAllByText('This field is required.').length).toBe(10);
expect(container.getAllByText('This field is required.').length).toBe(11);
container.getAllByText(errorMessage).forEach((element) => {
expect(element).toBeVisible();
});
Expand All @@ -101,5 +102,32 @@ describe('AlertDefinition Create', () => {
expect(
await container.findByText('The value should be a number.')
).toBeInTheDocument();

expect(
await container.findByText(
'At least one notification channel is required.'
)
);
});

it('should validate the checks of Alert Name and Description', async () => {
const user = userEvent.setup();
const container = renderWithTheme(<CreateAlertDefinition />);
const nameInput = container.getByLabelText('Name');
const descriptionInput = container.getByLabelText('Description (optional)');
await user.type(nameInput, '*#&+:<>"?@%');
await user.type(
descriptionInput,
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
);
await user.click(container.getByText('Submit'));
expect(
await container.findByText(
'Name cannot contain special characters: * # & + : \< \> ? @ % { } \\ /.'
)
).toBeVisible();
expect(
await container.findByText('Description must be 100 characters or less.')
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { isEmpty } from '@linode/api-v4';
import { Paper, TextField, Typography } from '@linode/ui';
import { useSnackbar } from 'notistack';
import * as React from 'react';
Expand All @@ -9,6 +10,7 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';
import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView';

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.

There was some error with using scrollErrorIntoViewV2 with the react-hook-form. So using the scrollErrorIntoView even though it is deprecated. Seems like this is a issue that has happened previously as well. #11357 (comment).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you create a backlog ticket to investigate this? We don't want to settle into a pattern of bringing back a deprecated utility and maintaining two utilities that are supposed to accomplish the same objective


import { MetricCriteriaField } from './Criteria/MetricCriteria';
import { TriggerConditions } from './Criteria/TriggerConditions';
Expand Down Expand Up @@ -77,8 +79,15 @@ export const CreateAlertDefinition = () => {
CreateAlertDefinitionFormSchema as ObjectSchema<CreateAlertDefinitionForm>
),
});
const {
control,
formState: { errors, isSubmitting, submitCount },
getValues,
handleSubmit,
setError,
setValue,
} = formMethods;

const { control, formState, getValues, handleSubmit, setError } = formMethods;
const { enqueueSnackbar } = useSnackbar();
const { mutateAsync: createAlert } = useCreateAlertDefinition(
getValues('serviceType')!
Expand Down Expand Up @@ -109,6 +118,26 @@ export const CreateAlertDefinition = () => {
}
});

const previousSubmitCount = React.useRef<number>(0);
React.useEffect(() => {
if (!isEmpty(errors) && submitCount > previousSubmitCount.current) {
scrollErrorIntoView(undefined, { behavior: 'smooth' });
}
}, [errors, submitCount]);

const handleServiceTypeChange = React.useCallback(() => {
// Reset the criteria to initial state
setValue('rule_criteria.rules', [
{
aggregate_function: null,
dimension_filters: [],
metric: null,
operator: null,
threshold: 0,
},
]);
}, [setValue]);

return (
<React.Fragment>
<DocumentTitleSegment segment="Create an Alert" />
Expand All @@ -128,7 +157,7 @@ export const CreateAlertDefinition = () => {
name="label"
onBlur={field.onBlur}
onChange={(e) => field.onChange(e.target.value)}
placeholder="Enter Name"
placeholder="Enter a Name"
value={field.value ?? ''}
/>
)}
Expand All @@ -144,14 +173,17 @@ export const CreateAlertDefinition = () => {
onBlur={field.onBlur}
onChange={(e) => field.onChange(e.target.value)}
optional
placeholder="Enter Description"
placeholder="Enter a Description"
value={field.value ?? ''}
/>
)}
control={control}
name="description"
/>
<CloudPulseServiceSelect name="serviceType" />
<CloudPulseServiceSelect
handleServiceTypeChange={handleServiceTypeChange}
name="serviceType"
/>
<CloudPulseAlertSeveritySelect name="severity" />
<CloudPulseModifyAlertResources name="entity_ids" />
<MetricCriteriaField
Expand All @@ -169,7 +201,7 @@ export const CreateAlertDefinition = () => {
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
loading: formState.isSubmitting,
loading: isSubmitting,
type: 'submit',
}}
secondaryButtonProps={{
Expand Down
Loading