Skip to content

Commit

Permalink
Updated create detector UX (#39)
Browse files Browse the repository at this point in the history
* updated create detector ux

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* removed unused code

Signed-off-by: Amardeepsingh Siglani <[email protected]>

Signed-off-by: Amardeepsingh Siglani <[email protected]>
  • Loading branch information
amsiglan authored Nov 3, 2022
1 parent a6fa8bf commit 25090d3
Show file tree
Hide file tree
Showing 25 changed files with 1,378 additions and 509 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,36 @@

import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiButton, EuiSpacer, EuiTitle } from '@elastic/eui';
import { createDetectorSteps } from '../../../utils/constants';
import {
EMPTY_DEFAULT_ALERT_CONDITION,
MAX_ALERT_CONDITIONS,
MIN_ALERT_CONDITIONS,
} from '../utils/constants';
EuiAccordion,
EuiButton,
EuiHorizontalRule,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { createDetectorSteps } from '../../../utils/constants';
import { EMPTY_DEFAULT_ALERT_CONDITION, MAX_ALERT_CONDITIONS } from '../utils/constants';
import AlertConditionPanel from '../components/AlertCondition';
import { Detector } from '../../../../../../models/interfaces';
import { DetectorCreationStep } from '../../../models/types';
import { CreateDetectorRulesOptions } from '../../../../../models/types';
import { NotificationChannelTypeOptions } from '../models/interfaces';
import { getNotificationChannels, parseNotificationChannelsToOptions } from '../utils/helpers';
import { NotificationsService } from '../../../../../services';

interface ConfigureAlertsProps extends RouteComponentProps {
detector: Detector;
isEdit: boolean;
rulesOptions: CreateDetectorRulesOptions;
changeDetector: (detector: Detector) => void;
updateDataValidState: (step: DetectorCreationStep, isValid: boolean) => void;
notificationsService: NotificationsService;
}

interface ConfigureAlertsState {
loading: boolean;
notificationChannels: string[];
ruleTypes: string[];
notificationChannels: NotificationChannelTypeOptions[];
}

export default class ConfigureAlerts extends Component<ConfigureAlertsProps, ConfigureAlertsState> {
Expand All @@ -35,7 +43,6 @@ export default class ConfigureAlerts extends Component<ConfigureAlertsProps, Con
this.state = {
loading: false,
notificationChannels: [],
ruleTypes: [],
};
}

Expand All @@ -44,14 +51,15 @@ export default class ConfigureAlerts extends Component<ConfigureAlertsProps, Con
detector: { triggers },
} = this.props;
this.getNotificationChannels();
if (triggers.length < MIN_ALERT_CONDITIONS) {
if (triggers.length === 0) {
this.addCondition();
}
};

getNotificationChannels = async () => {
this.setState({ loading: true });
// TODO: fetch notification channels from server.
const channels = await getNotificationChannels(this.props.notificationsService);
this.setState({ notificationChannels: parseNotificationChannelsToOptions(channels) });
this.setState({ loading: false });
};

Expand All @@ -67,23 +75,32 @@ export default class ConfigureAlerts extends Component<ConfigureAlertsProps, Con

onAlertTriggerChanged = (newDetector: Detector): void => {
const isTriggerDataValid = newDetector.triggers.every((trigger) => {
return !!trigger.name && trigger.sev_levels.length > 0;
return !!trigger.name && trigger.severity;
});
this.props.changeDetector(newDetector);
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_ALERTS, isTriggerDataValid);
};

onDelete = (index: number) => {
const {
detector,
detector: { triggers },
} = this.props;
triggers.splice(index, 1);
this.onAlertTriggerChanged({ ...detector, triggers: triggers });
};

render() {
const {
detector: { triggers },
} = this.props;
const { loading, notificationChannels, ruleTypes } = this.state;
const { loading, notificationChannels } = this.state;
return (
<div>
<EuiTitle size={'l'}>
<h3>
{createDetectorSteps[DetectorCreationStep.CONFIGURE_ALERTS].title +
` (${triggers.length}/${MAX_ALERT_CONDITIONS})`}
` (${triggers.length})`}
</h3>
</EuiTitle>

Expand All @@ -92,22 +109,40 @@ export default class ConfigureAlerts extends Component<ConfigureAlertsProps, Con
{triggers.map((alertCondition, index) => (
<div key={index}>
{index > 0 && <EuiSpacer size={'l'} />}
<AlertConditionPanel
{...this.props}
alertCondition={alertCondition}
allNotificationChannels={notificationChannels}
allRuleTypes={ruleTypes}
indexNum={index}
loadingNotifications={loading}
onAlertTriggerChanged={this.onAlertTriggerChanged}
/>
<EuiPanel>
<EuiAccordion
id={`alert-condition-${index}`}
buttonContent={
<EuiTitle>
<h4>Alert trigger</h4>
</EuiTitle>
}
paddingSize={'none'}
initialIsOpen={true}
extraAction={
<EuiButton onClick={() => this.onDelete(index)}>Remove alert trigger</EuiButton>
}
>
<EuiHorizontalRule margin={'xs'} />
<EuiSpacer size={'m'} />
<AlertConditionPanel
{...this.props}
alertCondition={alertCondition}
allNotificationChannels={notificationChannels}
indexNum={index}
loadingNotifications={loading}
onAlertTriggerChanged={this.onAlertTriggerChanged}
refreshNotificationChannels={this.getNotificationChannels}
/>
</EuiAccordion>
</EuiPanel>
</div>
))}

<EuiSpacer size={'m'} />

<EuiButton disabled={triggers.length >= MAX_ALERT_CONDITIONS} onClick={this.addCondition}>
Add another alert condition
{`Add ${triggers.length > 0 ? 'another' : 'an'} alert condition`}
</EuiButton>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export interface NotificationChannelTypeOptions {
label: string;
options: NotificationChannelOption[];
}

export interface NotificationChannelOption {
label: string;
value: string;
type: string;
description: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { AlertCondition } from '../../../../../../models/interfaces';
import { AlertCondition, TriggerAction } from '../../../../../../models/interfaces';

export const MAX_ALERT_CONDITIONS = 10;
export const MIN_ALERT_CONDITIONS = 1;
export const MIN_ALERT_CONDITIONS = 0;

// SEVERITY_OPTIONS have the id, value, label, and text fields because some EUI components
// (e.g, EuiComboBox) require value/label pairings, while others
Expand All @@ -27,12 +27,33 @@ export const RULE_SEVERITY_OPTIONS = {
INFORMATIONAL: { id: '5', value: 'informational', label: 'Info', text: 'Info' },
};

export const EMPTY_DEFAULT_TRIGGER_ACTION: TriggerAction = {
id: '',
name: '',
destination_id: '',
subject_template: {
source: '',
lang: 'mustache',
},
message_template: {
source: '',
lang: 'mustache',
},
throttle_enabled: false,
throttle: {
value: 10,
unit: 'MINUTES',
},
};

export const EMPTY_DEFAULT_ALERT_CONDITION: AlertCondition = {
name: '',
sev_levels: [],
tags: [],
actions: [],
actions: [EMPTY_DEFAULT_TRIGGER_ACTION],
types: [],
severity: '1',
ids: [],
};

export const MIN_NUM_NOTIFICATION_CHANNELS = 1;
Expand All @@ -44,3 +65,5 @@ export const MAX_NUM_RULES = 5;
// Only allows letters. No spaces, numbers, or special characters.
export const MIN_NUM_TAGS = 0;
export const MAX_NUM_TAGS = 5;

export const CHANNEL_TYPES = ['slack', 'email', 'chime', 'webhook', 'ses', 'sns'];
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,47 @@
*/

import { EuiComboBoxOptionOption } from '@elastic/eui';
import { ALERT_SEVERITY_OPTIONS } from './constants';
import { ALERT_SEVERITY_OPTIONS, CHANNEL_TYPES } from './constants';
import { FeatureChannelList } from '../../../../../../server/models/interfaces/Notifications';
import { NotificationChannelTypeOptions } from '../models/interfaces';
import { NotificationsService } from '../../../../../services';

export const parseAlertSeverityToOption = (severity: string): EuiComboBoxOptionOption<string> => {
return Object.values(ALERT_SEVERITY_OPTIONS).find(
(option) => option.label === severity
(option) => option.value === severity
) as EuiComboBoxOptionOption<string>;
};

export const parseAlertSeverityListToOptions = (
severityList: string[]
): EuiComboBoxOptionOption<string>[] => {
return severityList.map((severity) => parseAlertSeverityToOption(severity));
export function createSelectedOptions(optionNames: string[]): EuiComboBoxOptionOption<string>[] {
return optionNames.map((optionName) => ({ id: optionName, label: optionName }));
}

export const getNotificationChannels = async (notificationsService: NotificationsService) => {
try {
const response = await notificationsService.getChannels();
if (response.ok) {
return response.response.channel_list;
} else {
console.error('Failed to retrieve notification channels:', response.error);
}
} catch (e) {
console.error('Failed to retrieve notification channels:', e);
}
return [];
};

export function createSelectedOptions(optionNames: string[]): EuiComboBoxOptionOption<string>[] {
return optionNames.map((optionName) => ({ label: optionName }));
export function parseNotificationChannelsToOptions(
notificationChannels: FeatureChannelList[],
supportedTypes = CHANNEL_TYPES
): NotificationChannelTypeOptions[] {
const allOptions = notificationChannels.map((channel) => ({
label: `[Channel] ${channel.name}`,
value: channel.config_id,
type: channel.config_type,
description: channel.description,
}));
return supportedTypes.map((type) => ({
label: type,
options: allOptions.filter((channel) => channel.type === type),
}));
}
Loading

0 comments on commit 25090d3

Please sign in to comment.