Skip to content

Commit

Permalink
upcoming: [DI-22558] - Added Trigger condition and Dimension Filter c…
Browse files Browse the repository at this point in the history
…omponents (#11445)

* upcoming: [DI-22558] - Added Trigger condition and Dimension Filter changes

* removed unnecessary console log statement

* upcoming: [DI-22558] - Importing right interface for Trigger condition for error validation and minor changes

* upcoming: [DI-22588] - Added the Unit Tests for the components

* upcoming: [DI-22588] - Added changesets and few changes to the messages in validation schema

* upcoming: [DI-22588] - Review comments and made some minor UX changes to the component

* Updated stylings

* upcoming: [DI-22588] - UX changes

* upcoming: [DI-22588] - Fixed the validation error resolution for Metric Data Field and Dimension Filter Data Field

* upcoming: [DI-22588] - Review changes

* upcoming: [DI-22588] - Moved mockData to individual test files instead of exporting to fix the failing tests

* upcoming: [DI-22558] - Typescript issue fixes

* Update constants.ts

---------

Co-authored-by: nikhagra-akamai <nagrawal@akamai.com>
Co-authored-by: vmangalr <vmangalr@akamai.com>
Co-authored-by: venkatmano-akamai <chk-Venkatesh@outlook.com>
  • Loading branch information
4 people authored Jan 7, 2025
1 parent 6dc152f commit b47661f
Show file tree
Hide file tree
Showing 22 changed files with 1,261 additions and 163 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-11445-changed-1734706802321.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Changed MetricCritera, DimensionFilter and Alert Interfaces ([#11445](https://github.com/linode/manager/pull/11445))
16 changes: 12 additions & 4 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ export type AlertSeverityType = 0 | 1 | 2 | 3;
export type MetricAggregationType = 'avg' | 'sum' | 'min' | 'max' | 'count';
export type MetricOperatorType = 'eq' | 'gt' | 'lt' | 'gte' | 'lte';
export type AlertServiceType = 'linode' | 'dbaas';
type DimensionFilterOperatorType = 'eq' | 'neq' | 'startswith' | 'endswith';
export type DimensionFilterOperatorType =
| 'eq'
| 'neq'
| 'startswith'
| 'endswith';
export type AlertDefinitionType = 'system' | 'user';
export type AlertStatusType = 'enabled' | 'disabled';
export type CriteriaConditionType = 'ALL';
Expand Down Expand Up @@ -164,20 +168,24 @@ export interface MetricCriteria {
aggregation_type: MetricAggregationType;
operator: MetricOperatorType;
threshold: number;
dimension_filters: DimensionFilter[];
dimension_filters?: DimensionFilter[];
}

export interface AlertDefinitionMetricCriteria extends MetricCriteria {
export interface AlertDefinitionMetricCriteria
extends Omit<MetricCriteria, 'dimension_filters'> {
unit: string;
label: string;
dimension_filters?: AlertDefinitionDimensionFilter[];
}
export interface DimensionFilter {
label: string;
dimension_label: string;
operator: DimensionFilterOperatorType;
value: string;
}

export interface AlertDefinitionDimensionFilter extends DimensionFilter {
label: string;
}
export interface TriggerCondition {
polling_interval_seconds: number;
evaluation_period_seconds: number;
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11445-added-1734705630196.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

DimensionFilter, DimensionFilterField, TriggerCondition component along with Unit Tests ([#11445](https://github.com/linode/manager/pull/11445))
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb';
import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts';

import { MetricCriteriaField } from './Criteria/MetricCriteria';
import { TriggerConditions } from './Criteria/TriggerConditions';
import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect';
import { EngineOption } from './GeneralInformation/EngineOption';
import { CloudPulseRegionSelect } from './GeneralInformation/RegionSelect';
Expand All @@ -18,14 +19,17 @@ import { CloudPulseServiceSelect } from './GeneralInformation/ServiceTypeSelect'
import { CreateAlertDefinitionFormSchema } from './schemas';
import { filterFormValues } from './utilities';

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

const triggerConditionInitialValues: TriggerCondition = {
const triggerConditionInitialValues: TriggerConditionForm = {
criteria_condition: 'ALL',
evaluation_period_seconds: 0,
polling_interval_seconds: 0,
evaluation_period_seconds: null,
polling_interval_seconds: null,
trigger_occurrences: 0,
};
const criteriaInitialValues: MetricCriteriaForm = {
Expand Down Expand Up @@ -164,8 +168,10 @@ export const CreateAlertDefinition = () => {
name="rule_criteria.rules"
serviceType={serviceTypeWatcher!}
/>
{/* This is just being displayed to pass the typecheck-manager test. In the next PR maxScrapeInterval will be used by another component */}
{maxScrapeInterval}
<TriggerConditions
maxScrapingInterval={maxScrapeInterval}
name="trigger_conditions"
/>
<ActionsPanel
primaryButtonProps={{
label: 'Submit',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

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

import { DimensionFilters } from './DimensionFilter';

import type { CreateAlertDefinitionForm } from '../types';
import type { MetricDefinition } from '@linode/api-v4';

const mockData: MetricDefinition[] = [
{
available_aggregate_functions: ['min', 'max', 'avg'],
dimensions: [
{
dimension_label: 'cpu',
label: 'CPU name',
values: [],
},
{
dimension_label: 'state',
label: 'State of CPU',
values: [
'user',
'system',
'idle',
'interrupt',
'nice',
'softirq',
'steal',
'wait',
],
},
{
dimension_label: 'LINODE_ID',
label: 'Linode ID',
values: [],
},
],
label: 'CPU utilization',
metric: 'system_cpu_utilization_percent',
metric_type: 'gauge',
scrape_interval: '2m',
unit: 'percent',
},
];
const dimensionFilterButton = 'Add dimension filter';
describe('DimensionFilterField', () => {
const user = userEvent.setup();

it('render the fields properly', async () => {
const container = renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>(
{
component: (
<DimensionFilters
dataFieldDisabled={true}
dimensionOptions={mockData[0].dimensions}
name={`rule_criteria.rules.${0}.dimension_filters`}
/>
),
useFormOptions: {
defaultValues: {
rule_criteria: {
rules: [mockData[0]],
},
serviceType: 'linode',
},
},
}
);
expect(screen.getByText('Dimension Filter')).toBeVisible();
expect(screen.getByText('(optional)')).toBeVisible();
await user.click(
container.getByRole('button', { name: 'Add dimension filter' })
);
expect(screen.getByLabelText('Data Field')).toBeVisible();
expect(screen.getByLabelText('Operator')).toBeVisible();
expect(screen.getByLabelText('Value')).toBeVisible();
});

it('does not render the dimension filed directly with Metric component', async () => {
const container = renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>(
{
component: (
<DimensionFilters
dataFieldDisabled={true}
dimensionOptions={mockData[0].dimensions}
name={`rule_criteria.rules.${0}.dimension_filters`}
/>
),
useFormOptions: {
defaultValues: {
rule_criteria: {
rules: [mockData[0]],
},
serviceType: 'linode',
},
},
}
);
const dimensionFilterID = 'rule_criteria.rules.0.dimension_filters.0-id';
expect(container.queryByTestId(dimensionFilterID)).not.toBeInTheDocument();
});

it('adds and removes dimension filter fields dynamically', async () => {
const container = renderWithThemeAndHookFormContext<CreateAlertDefinitionForm>(
{
component: (
<DimensionFilters
dataFieldDisabled={true}
dimensionOptions={mockData[0].dimensions}
name={`rule_criteria.rules.${0}.dimension_filters`}
/>
),
useFormOptions: {
defaultValues: {
rule_criteria: {
rules: [mockData[0]],
},
serviceType: 'linode',
},
},
}
);

const dimensionFilterID = 'rule_criteria.rules.0.dimension_filters.1-id';
await user.click(
container.getByRole('button', { name: dimensionFilterButton })
);
await user.click(
container.getByRole('button', { name: dimensionFilterButton })
);
expect(container.getByTestId(dimensionFilterID)).toBeInTheDocument();
await user.click(
within(container.getByTestId(dimensionFilterID)).getByTestId('clear-icon')
);
await waitFor(() =>
expect(container.queryByTestId(dimensionFilterID)).not.toBeInTheDocument()
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Box } from '@linode/ui';
import { Button, Stack, Typography } from '@linode/ui';
import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';

import { DimensionFilterField } from './DimensionFilterField';

import type { CreateAlertDefinitionForm, DimensionFilterForm } from '../types';
import type { Dimension } from '@linode/api-v4';
import type { FieldPathByValue } from 'react-hook-form';

interface DimensionFilterProps {
/**
* boolean value to disable the Data Field in dimension filter
*/
dataFieldDisabled: boolean;
/**
* dimension filter data for the selected metric
*/
dimensionOptions: Dimension[];
/**
* name used for the component to set in the form
*/
name: FieldPathByValue<CreateAlertDefinitionForm, DimensionFilterForm[]>;
}
export const DimensionFilters = (props: DimensionFilterProps) => {
const { dataFieldDisabled, dimensionOptions, name } = props;
const { control } = useFormContext<CreateAlertDefinitionForm>();

const { append, fields, remove } = useFieldArray({
control,
name,
});
return (
<Box display="flex" flexDirection="column" gap={1}>
<Typography variant="h3">
Dimension Filter
<Typography component="span"> (optional)</Typography>
</Typography>

<Stack gap={1}>
{fields?.length > 0 &&
fields.map((field, index) => (
<DimensionFilterField
dataFieldDisabled={dataFieldDisabled}
dimensionOptions={dimensionOptions}
key={field.id}
name={`${name}.${index}`}
onFilterDelete={() => remove(index)}
/>
))}
</Stack>
<Button
onClick={() =>
append({
dimension_label: null,
operator: null,
value: null,
})
}
buttonType="secondary"
compactX
size="small"
sx={{ justifyContent: 'start' }}
>
Add dimension filter
</Button>
</Box>
);
};
Loading

0 comments on commit b47661f

Please sign in to comment.