Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [DI-22184] - Added Metric Criteria, Metric components #11392

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Property names, and types of the CreateAlertDefinitionPayload and Alert interfaces ([#11392](https://github.com/linode/manager/pull/11392))
26 changes: 23 additions & 3 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ export type AlertServiceType = 'linode' | 'dbaas';
type DimensionFilterOperatorType = 'eq' | 'neq' | 'startswith' | 'endswith';
export type AlertDefinitionType = 'system' | 'user';
export type AlertStatusType = 'enabled' | 'disabled';
export type CriteriaConditionType = 'ALL';
export type MetricUnitType =
| 'number'
| 'byte'
| 'second'
| 'percent'
| 'bit_per_second'
| 'millisecond'
| 'KB'
| 'MB'
| 'GB';
export interface Dashboard {
id: number;
label: string;
Expand Down Expand Up @@ -142,23 +153,30 @@ export interface ServiceTypesList {

export interface CreateAlertDefinitionPayload {
label: string;
tags?: string[];
description?: string;
entity_ids?: string[];
severity: AlertSeverityType;
rule_criteria: {
rules: MetricCriteria[];
};
trigger_condition: TriggerCondition;
trigger_conditions: TriggerCondition;
channel_ids: number[];
}
export interface MetricCriteria {
metric: string;
aggregation_type: MetricAggregationType;
operator: MetricOperatorType;
threshold: number;
dimension_filters: DimensionFilter[];
}

export interface AlertDefinitionMetricCriteria extends MetricCriteria {
unit: string;
label: string;
}
export interface DimensionFilter {
label: string;
dimension_label: string;
operator: DimensionFilterOperatorType;
value: string;
Expand All @@ -168,10 +186,12 @@ export interface TriggerCondition {
polling_interval_seconds: number;
evaluation_period_seconds: number;
trigger_occurrences: number;
criteria_condition: CriteriaConditionType;
}
export interface Alert {
id: number;
label: string;
tags: string[];
description: string;
has_more_resources: boolean;
status: AlertStatusType;
Expand All @@ -180,9 +200,9 @@ export interface Alert {
service_type: AlertServiceType;
entity_ids: string[];
rule_criteria: {
rules: MetricCriteria[];
rules: AlertDefinitionMetricCriteria[];
};
triggerCondition: TriggerCondition;
trigger_conditions: TriggerCondition;
channels: {
id: string;
label: string;
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11392-added-1733853776123.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Metric, MetricCriteria, ClearIconButton components with Unit Tests ([#11392](https://github.com/linode/manager/pull/11392))
4 changes: 3 additions & 1 deletion packages/manager/src/factories/cloudpulse/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export const alertFactory = Factory.Sync.makeFactory<Alert>({
service_type: 'linode',
severity: 0,
status: 'enabled',
triggerCondition: {
tags: ['tag1', 'tag2'],
trigger_conditions: {
criteria_condition: 'ALL',
evaluation_period_seconds: 0,
polling_interval_seconds: 0,
trigger_occurrences: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
import { CreateAlertDefinition } from './CreateAlertDefinition';
describe('AlertDefinition Create', () => {
it('should render input components', async () => {
const { getByLabelText } = renderWithTheme(<CreateAlertDefinition />);
const { getByLabelText, getByText } = renderWithTheme(
<CreateAlertDefinition />
);

expect(getByText('1. General Information')).toBeVisible();
expect(getByLabelText('Name')).toBeVisible();
expect(getByLabelText('Description (optional)')).toBeVisible();
expect(getByLabelText('Severity')).toBeVisible();
expect(getByLabelText('Service')).toBeVisible();
expect(getByLabelText('Region')).toBeVisible();
expect(getByLabelText('Resources')).toBeVisible();
expect(getByText('2. Criteria')).toBeVisible();
expect(getByText('Metric Threshold')).toBeVisible();
expect(getByLabelText('Data Field')).toBeVisible();
expect(getByLabelText('Aggregation Type')).toBeVisible();
expect(getByLabelText('Operator')).toBeVisible();
expect(getByLabelText('Threshold')).toBeVisible();
});

it('should be able to enter a value in the textbox', async () => {
const { getByLabelText } = renderWithTheme(<CreateAlertDefinition />);
const input = getByLabelText('Name');
Expand All @@ -23,17 +36,40 @@ describe('AlertDefinition Create', () => {
);
expect(specificInput).toHaveAttribute('value', 'text');
});

it('should render client side validation errors', async () => {
const { getByText } = renderWithTheme(<CreateAlertDefinition />);
const user = userEvent.setup();
const container = renderWithTheme(<CreateAlertDefinition />);
const input = container.getByLabelText('Threshold');
const submitButton = container.getByText('Submit').closest('button');

await userEvent.click(submitButton!);

expect(container.getByText('Name is required.')).toBeVisible();
expect(container.getByText('Severity is required.')).toBeVisible();
expect(container.getByText('Service is required.')).toBeVisible();
expect(container.getByText('Region is required.')).toBeVisible();
expect(
container.getByText('At least one resource is needed.')
).toBeVisible();
expect(container.getByText('Metric Data Field is required.')).toBeVisible();
expect(container.getByText('Aggregation type is required.')).toBeVisible();
expect(container.getByText('Criteria Operator is required.')).toBeVisible();

await user.clear(input);
await user.type(input, '-3');
await userEvent.click(submitButton!);

const submitButton = getByText('Submit').closest('button');
expect(
await container.findByText('Threshold value cannot be negative.')
).toBeVisible();

await user.clear(input);
await user.type(input, 'sdgf');
await userEvent.click(submitButton!);

expect(getByText('Name is required.')).toBeVisible();
expect(getByText('Severity is required.')).toBeVisible();
expect(getByText('Service is required.')).toBeVisible();
expect(getByText('Region is required.')).toBeVisible();
expect(getByText('At least one resource is needed.')).toBeVisible();
expect(
await container.findByText('Threshold value should be a number.')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,31 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';

import { MetricCriteriaField } from './Criteria/MetricCriteria';
import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect';
import { EngineOption } from './GeneralInformation/EngineOption';
import { CloudPulseRegionSelect } from './GeneralInformation/RegionSelect';
import { CloudPulseMultiResourceSelect } from './GeneralInformation/ResourceMultiSelect';
import { CloudPulseServiceSelect } from './GeneralInformation/ServiceTypeSelect';
import { CreateAlertDefinitionFormSchema } from './schemas';
import { filterFormValues, filterMetricCriteriaFormValues } from './utilities';
import { filterFormValues } from './utilities';

import type { CreateAlertDefinitionForm, MetricCriteriaForm } from './types';
import type { TriggerCondition } from '@linode/api-v4/lib/cloudpulse/types';
import type { ObjectSchema } from 'yup';

const triggerConditionInitialValues: TriggerCondition = {
criteria_condition: 'ALL',
evaluation_period_seconds: 0,
polling_interval_seconds: 0,
trigger_occurrences: 0,
};
const criteriaInitialValues: MetricCriteriaForm = {
aggregation_type: null,
dimension_filters: [],
metric: '',
metric: null,
operator: null,
threshold: 0,
};
const initialValues: CreateAlertDefinitionForm = {
channel_ids: [],
Expand All @@ -39,11 +42,12 @@ const initialValues: CreateAlertDefinitionForm = {
label: '',
region: '',
rule_criteria: {
rules: filterMetricCriteriaFormValues(criteriaInitialValues),
rules: [criteriaInitialValues],
},
serviceType: null,
severity: null,
trigger_condition: triggerConditionInitialValues,
tags: [''],
trigger_conditions: triggerConditionInitialValues,
};

const overrides = [
Expand Down Expand Up @@ -83,6 +87,11 @@ export const CreateAlertDefinition = () => {
getValues('serviceType')!
);

/**
* The maxScrapeInterval variable will be required for the Trigger Conditions part of the Critieria section.
*/
const [maxScrapeInterval, setMaxScrapeInterval] = React.useState<number>(0);

const serviceTypeWatcher = watch('serviceType');
const onSubmit = handleSubmit(async (values) => {
try {
Expand All @@ -96,6 +105,9 @@ export const CreateAlertDefinition = () => {
if (error.field) {
setError(error.field, { message: error.reason });
} else {
enqueueSnackbar(`Alert failed: ${error.reason}`, {
variant: 'error',
});
setError('root', { message: error.reason });
}
}
Expand Down Expand Up @@ -152,6 +164,13 @@ export const CreateAlertDefinition = () => {
serviceType={serviceTypeWatcher}
/>
<CloudPulseAlertSeveritySelect name="severity" />
<MetricCriteriaField
setMaxInterval={(interval: number) =>
setMaxScrapeInterval(interval)
}
name="rule_criteria.rules"
serviceType={serviceTypeWatcher!}
/>
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { ClearIconButton } from './ClearIconButton';

describe('Clear Icon Button', () => {
it('should render the icon', () => {
const { getByTestId } = renderWithTheme(
<ClearIconButton handleClick={vi.fn()} />
);
expect(getByTestId('clear-icon')).toBeInTheDocument();
expect(getByTestId('ClearOutlinedIcon')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ClearOutlineOutlined from '@mui/icons-material/ClearOutlined';
import { IconButton } from '@mui/material';
import * as React from 'react';
interface ClearIconButtonProps {
/**
* method passed from the parent component to control the onClick function
* @returns void
*/
handleClick: () => void;
}
export const ClearIconButton = (props: ClearIconButtonProps) => {
const { handleClick } = props;
return (
<IconButton
sx={{
padding: 0,
}}
data-testid="clear-icon"
onClick={handleClick}
>
<ClearOutlineOutlined />
</IconButton>
);
};
Loading
Loading