mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Migration: Migrate New notification channel page (#25265)
* creating page * add types select * adding switches * start with converting angular templates to json * converting more alert channels to new format * convert remaining channels * typing the form * add validation, update models * fix default value in type select * fix type * fix issue with validation rule * add missing settings * fix type errors * test notification * add comments to structs * fix selectable value and minor things on each channel * More typings * fix strictnull * rename ModelValue -> PropertyName * rename show -> showWhen * add enums and adding comments * fix comment * break out channel options to component * use try catch * adding default case to OptionElement if element not supported
This commit is contained in:
132
public/app/features/alerting/NewAlertNotificationPage.tsx
Normal file
132
public/app/features/alerting/NewAlertNotificationPage.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { NavModel, SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Form } from '@grafana/ui';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { NewNotificationChannelForm } from './components/NewNotificationChannelForm';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { createNotificationChannel, loadNotificationTypes, testNotificationChannel } from './state/actions';
|
||||
import { NotificationChannel, NotificationChannelDTO, StoreState } from '../../types';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface ConnectedProps {
|
||||
navModel: NavModel;
|
||||
notificationChannels: NotificationChannel[];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
createNotificationChannel: typeof createNotificationChannel;
|
||||
loadNotificationTypes: typeof loadNotificationTypes;
|
||||
testNotificationChannel: typeof testNotificationChannel;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
const defaultValues: NotificationChannelDTO = {
|
||||
name: '',
|
||||
type: { value: 'email', label: 'Email' },
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
settings: {
|
||||
uploadImage: config.rendererAvailable,
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
severity: 'critical',
|
||||
},
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
class NewAlertNotificationPage extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.loadNotificationTypes();
|
||||
}
|
||||
|
||||
onSubmit = (data: NotificationChannelDTO) => {
|
||||
/*
|
||||
Some settings can be options in a select, in order to not save a SelectableValue<T>
|
||||
we need to use check if it is a SelectableValue and use its value.
|
||||
*/
|
||||
const settings = Object.fromEntries(
|
||||
Object.entries(data.settings).map(([key, value]) => {
|
||||
return [key, value.hasOwnProperty('value') ? value.value : value];
|
||||
})
|
||||
);
|
||||
|
||||
this.props.createNotificationChannel({
|
||||
...defaultValues,
|
||||
...data,
|
||||
type: data.type.value,
|
||||
settings: { ...defaultValues.settings, ...settings },
|
||||
});
|
||||
};
|
||||
|
||||
onTestChannel = (data: NotificationChannelDTO) => {
|
||||
this.props.testNotificationChannel({
|
||||
name: data.name,
|
||||
type: data.type.value,
|
||||
frequency: data.frequency ?? defaultValues.frequency,
|
||||
settings: { ...Object.assign(defaultValues.settings, data.settings) },
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navModel, notificationChannels } = this.props;
|
||||
|
||||
/*
|
||||
Need to transform these as we have options on notificationChannels,
|
||||
this will render a dropdown within the select.
|
||||
|
||||
TODO: Memoize?
|
||||
*/
|
||||
const selectableChannels: Array<SelectableValue<string>> = notificationChannels.map(channel => ({
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
description: channel.description,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h2>New Notification Channel</h2>
|
||||
<Form onSubmit={this.onSubmit} validateOn="onChange" defaultValues={defaultValues}>
|
||||
{({ register, errors, control, getValues, watch }) => {
|
||||
const selectedChannel = notificationChannels.find(c => c.value === getValues().type.value);
|
||||
|
||||
return (
|
||||
<NewNotificationChannelForm
|
||||
selectableChannels={selectableChannels}
|
||||
selectedChannel={selectedChannel}
|
||||
onTestChannel={this.onTestChannel}
|
||||
register={register}
|
||||
errors={errors}
|
||||
getValues={getValues}
|
||||
control={control}
|
||||
watch={watch}
|
||||
imageRendererAvailable={config.rendererAvailable}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'channels'),
|
||||
notificationChannels: state.alertRules.notificationChannels,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
createNotificationChannel,
|
||||
loadNotificationTypes,
|
||||
testNotificationChannel,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NewAlertNotificationPage);
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
FormAPI,
|
||||
HorizontalGroup,
|
||||
InfoBox,
|
||||
Input,
|
||||
InputControl,
|
||||
Select,
|
||||
stylesFactory,
|
||||
Switch,
|
||||
useTheme,
|
||||
} from '@grafana/ui';
|
||||
import { NotificationChannel, NotificationChannelDTO } from '../../../types';
|
||||
import { NotificationChannelOptions } from './NotificationChannelOptions';
|
||||
|
||||
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState'> {
|
||||
selectableChannels: Array<SelectableValue<string>>;
|
||||
selectedChannel?: NotificationChannel;
|
||||
imageRendererAvailable: boolean;
|
||||
|
||||
onTestChannel: (data: NotificationChannelDTO) => void;
|
||||
}
|
||||
|
||||
export const NewNotificationChannelForm: FC<Props> = ({
|
||||
control,
|
||||
errors,
|
||||
selectedChannel,
|
||||
selectableChannels,
|
||||
register,
|
||||
watch,
|
||||
getValues,
|
||||
imageRendererAvailable,
|
||||
onTestChannel,
|
||||
}) => {
|
||||
const styles = getStyles(useTheme());
|
||||
|
||||
useEffect(() => {
|
||||
watch(['type', 'settings.priority', 'sendReminder', 'uploadImage']);
|
||||
}, []);
|
||||
|
||||
const currentFormValues = getValues();
|
||||
return (
|
||||
<>
|
||||
<div className={styles.basicSettings}>
|
||||
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||
<Input name="name" ref={register({ required: 'Name is required' })} />
|
||||
</Field>
|
||||
<Field label="Type">
|
||||
<InputControl
|
||||
name="type"
|
||||
as={Select}
|
||||
options={selectableChannels}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Default" description="Use this notification for all alerts">
|
||||
<Switch name="isDefault" ref={register} />
|
||||
</Field>
|
||||
<Field label="Include image" description="Captures an image and include it in the notification">
|
||||
<Switch name="settings.uploadImage" ref={register} />
|
||||
</Field>
|
||||
{currentFormValues.uploadImage && !imageRendererAvailable && (
|
||||
<InfoBox title="No image renderer available/installed">
|
||||
Grafana cannot find an image renderer to capture an image for the notification. Please make sure the Grafana
|
||||
Image Renderer plugin is installed. Please contact your Grafana administrator to install the plugin.
|
||||
</InfoBox>
|
||||
)}
|
||||
<Field
|
||||
label="Disable Resolve Message"
|
||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||
>
|
||||
<Switch name="disableResolveMessage" ref={register} />
|
||||
</Field>
|
||||
<Field label="Send reminders" description="Send additional notifications for triggered alerts">
|
||||
<Switch name="sendReminder" ref={register} />
|
||||
</Field>
|
||||
{currentFormValues.sendReminder && (
|
||||
<>
|
||||
<Field
|
||||
label="Send reminder every"
|
||||
description="Specify how often reminders should be sent, e.g. every 30s, 1m, 10m, 30m or 1h etc."
|
||||
>
|
||||
<Input name="frequency" ref={register} />
|
||||
</Field>
|
||||
<InfoBox>
|
||||
Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently
|
||||
than a configured alert rule evaluation interval.
|
||||
</InfoBox>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedChannel && (
|
||||
<NotificationChannelOptions
|
||||
selectedChannel={selectedChannel}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
||||
Test
|
||||
</Button>
|
||||
<Button type="button" variant="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
basicSettings: css`
|
||||
margin-bottom: ${theme.spacing.xl};
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { FC } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Field, FormAPI, InfoBox } from '@grafana/ui';
|
||||
import { OptionElement } from './OptionElement';
|
||||
import { NotificationChannel, NotificationChannelDTO, Option } from '../../../types';
|
||||
|
||||
interface Props extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch'> {
|
||||
selectedChannel: NotificationChannel;
|
||||
currentFormValues: NotificationChannelDTO;
|
||||
}
|
||||
|
||||
export const NotificationChannelOptions: FC<Props> = ({
|
||||
control,
|
||||
currentFormValues,
|
||||
errors,
|
||||
selectedChannel,
|
||||
register,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{selectedChannel.heading}</h3>
|
||||
{selectedChannel.info !== '' && <InfoBox>{selectedChannel.info}</InfoBox>}
|
||||
{selectedChannel.options.map((option: Option, index: number) => {
|
||||
const key = `${option.label}-${index}`;
|
||||
|
||||
// Some options can be dependent on other options, this determines what is selected in the dependency options
|
||||
// I think this needs more thought.
|
||||
const selectedOptionValue =
|
||||
currentFormValues[`settings.${option.showWhen.field}`] &&
|
||||
(currentFormValues[`settings.${option.showWhen.field}`] as SelectableValue<string>).value;
|
||||
|
||||
if (option.showWhen.field && selectedOptionValue !== option.showWhen.is) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={key}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
invalid={errors.settings && !!errors.settings[option.propertyName]}
|
||||
error={errors.settings && errors.settings[option.propertyName]?.message}
|
||||
>
|
||||
<OptionElement option={option} register={register} control={control} />
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
57
public/app/features/alerting/components/OptionElement.tsx
Normal file
57
public/app/features/alerting/components/OptionElement.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { FC } from 'react';
|
||||
import { FormAPI, Input, InputControl, Select, Switch, TextArea } from '@grafana/ui';
|
||||
import { Option } from '../../../types';
|
||||
|
||||
interface Props extends Pick<FormAPI<any>, 'register' | 'control'> {
|
||||
option: Option;
|
||||
}
|
||||
|
||||
export const OptionElement: FC<Props> = ({ control, option, register }) => {
|
||||
const modelValue = `settings.${option.propertyName}`;
|
||||
switch (option.element) {
|
||||
case 'input':
|
||||
return (
|
||||
<Input
|
||||
type={option.inputType}
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
validate: v => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||
})}
|
||||
placeholder={option.placeholder}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return <InputControl as={Select} options={option.selectOptions} control={control} name={`${modelValue}`} />;
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
validate: v => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
console.error('Element not supported', option.element);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const validateOption = (value: string, validationRule: string) => {
|
||||
return RegExp(validationRule).test(value) ? true : 'Invalid format';
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AlertRuleDTO, ThunkResult } from 'app/types';
|
||||
import { loadAlertRules, loadedAlertRules } from './reducers';
|
||||
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { loadAlertRules, loadedAlertRules, setNotificationChannels } from './reducers';
|
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@@ -17,3 +20,45 @@ export function togglePauseAlertRule(id: number, options: { paused: boolean }):
|
||||
dispatch(getAlertRulesAsync({ state: stateFilter.toString() }));
|
||||
};
|
||||
}
|
||||
|
||||
export function createNotificationChannel(data: any): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
try {
|
||||
await getBackendSrv().post(`/api/alert-notifications`, data);
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
|
||||
dispatch(updateLocation({ path: 'alerting/notifications' }));
|
||||
} catch (error) {
|
||||
appEvents.emit(AppEvents.alertError, [error.data.error]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function testNotificationChannel(data: any): ThunkResult<void> {
|
||||
return async () => {
|
||||
await getBackendSrv().post('/api/alert-notifications/test', data);
|
||||
};
|
||||
}
|
||||
|
||||
export function loadNotificationTypes(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const alertNotifiers: NotifierDTO[] = await getBackendSrv().get(`/api/alert-notifiers`);
|
||||
|
||||
const notificationTypes = alertNotifiers
|
||||
.map((option: NotifierDTO) => {
|
||||
return {
|
||||
value: option.type,
|
||||
label: option.name,
|
||||
...option,
|
||||
typeName: option.type,
|
||||
};
|
||||
})
|
||||
.sort((o1, o2) => {
|
||||
if (o1.name > o2.name) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
dispatch(setNotificationChannels(notificationTypes));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState } from 'app/types';
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState, NotificationChannel } from 'app/types';
|
||||
import alertDef from './alertDef';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false };
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false, notificationChannels: [] };
|
||||
|
||||
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
|
||||
const stateModel = alertDef.getStateDisplayModel(state);
|
||||
@@ -47,10 +47,13 @@ const alertRulesSlice = createSlice({
|
||||
setSearchQuery: (state, action: PayloadAction<string>): AlertRulesState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
},
|
||||
setNotificationChannels: (state, action: PayloadAction<NotificationChannel[]>): AlertRulesState => {
|
||||
return { ...state, notificationChannels: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions;
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery, setNotificationChannels } = alertRulesSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
||||
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
||||
import { validateTitle, validateUid } from '../utils/validation';
|
||||
|
||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
|
||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'watch'> {
|
||||
uidReset: boolean;
|
||||
inputs: DashboardInputs;
|
||||
initialFolderId: number;
|
||||
|
||||
@@ -522,6 +522,15 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
controllerAs: 'ctrl',
|
||||
reloadOnSearch: false,
|
||||
})
|
||||
.when('/alerting/notification/new2', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(/* webpackChunkName: "NewNotificationChannel" */ 'app/features/alerting/NewAlertNotificationPage')
|
||||
),
|
||||
},
|
||||
})
|
||||
.when('/alerting/notification/:id/edit', {
|
||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||
controller: 'AlertNotificationEditCtrl',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface AlertRuleDTO {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
@@ -33,10 +35,83 @@ export interface AlertRule {
|
||||
evalData?: { noData?: boolean; evalMatches?: any };
|
||||
}
|
||||
|
||||
export type NotifierType =
|
||||
| 'discord'
|
||||
| 'hipchat'
|
||||
| 'email'
|
||||
| 'sensu'
|
||||
| 'googlechat'
|
||||
| 'threema'
|
||||
| 'teams'
|
||||
| 'slack'
|
||||
| 'pagerduty'
|
||||
| 'prometheus-alertmanager'
|
||||
| 'telegram'
|
||||
| 'opsgenie'
|
||||
| 'dingding'
|
||||
| 'webhook'
|
||||
| 'victorops'
|
||||
| 'pushover'
|
||||
| 'LINE'
|
||||
| 'kafka';
|
||||
|
||||
export interface NotifierDTO {
|
||||
name: string;
|
||||
description: string;
|
||||
optionsTemplate: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannel {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannelDTO {
|
||||
[key: string]: string | boolean | SelectableValue<string>;
|
||||
name: string;
|
||||
type: SelectableValue<string>;
|
||||
sendReminder: boolean;
|
||||
disableResolveMessage: boolean;
|
||||
frequency: string;
|
||||
settings: ChannelTypeSettings;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelTypeSettings {
|
||||
[key: string]: any;
|
||||
autoResolve: true;
|
||||
httpMethod: string;
|
||||
severity: string;
|
||||
uploadImage: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
element: 'input' | 'select' | 'switch' | 'textarea';
|
||||
inputType: string;
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
propertyName: string;
|
||||
selectOptions: Array<SelectableValue<string>>;
|
||||
showWhen: { field: string; is: string };
|
||||
required: boolean;
|
||||
validationRule: string;
|
||||
}
|
||||
|
||||
export interface AlertRulesState {
|
||||
items: AlertRule[];
|
||||
searchQuery: string;
|
||||
isLoading: boolean;
|
||||
notificationChannels: NotificationChannel[];
|
||||
}
|
||||
|
||||
export interface AlertNotification {
|
||||
|
||||
Reference in New Issue
Block a user