Migration/AlertList: Migrates AlertList from AngularJS to React (#31872)

* Migration/AlertList: Migrates AlertList from AngularJS to React
This commit is contained in:
kay delaney 2021-03-11 14:53:13 +00:00 committed by GitHub
parent 93ead2a50c
commit 48d2dff987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 621 additions and 319 deletions

View File

@ -280,11 +280,17 @@ const Tags: FC<ChildProps> = ({ children, styles }) => {
};
Tags.displayName = 'Tags';
const Figure: FC<ChildProps & { align?: 'top' | 'center' }> = ({ children, styles, align = 'top' }) => {
const Figure: FC<ChildProps & { align?: 'top' | 'center'; className?: string }> = ({
children,
styles,
align = 'top',
className,
}) => {
return (
<div
className={cx(
styles?.media,
className,
align === 'center' &&
css`
display: flex;

View File

@ -0,0 +1,272 @@
import React, { useState } from 'react';
import sortBy from 'lodash/sortBy';
import { PanelProps, GrafanaTheme, dateMath, dateTime } from '@grafana/data';
import { Card, CustomScrollbar, Icon, stylesFactory, useStyles } from '@grafana/ui';
import { css, cx } from 'emotion';
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
import { useAsync } from 'react-use';
import alertDef from 'app/features/alerting/state/alertDef';
import { AlertRuleDTO, AnnotationItemDTO } from 'app/types';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { AlertListOptions, ShowOption, SortOrder } from './types';
export function AlertList(props: PanelProps<AlertListOptions>) {
const [noAlertsMessage, setNoAlertsMessage] = useState('');
const currentAlertState = useAsync(async () => {
if (props.options.showOptions !== ShowOption.Current) {
return;
}
const params: any = {
state: getStateFilter(props.options.stateFilter),
};
const panel = getDashboardSrv().getCurrent().getPanelById(props.id)!;
if (props.options.alertName) {
params.query = getTemplateSrv().replace(props.options.alertName, panel.scopedVars);
}
if (props.options.folderId >= 0) {
params.folderId = props.options.folderId;
}
if (props.options.dashboardTitle) {
params.dashboardQuery = props.options.dashboardTitle;
}
if (props.options.dashboardAlerts) {
params.dashboardId = getDashboardSrv().getCurrent().id;
}
if (props.options.tags) {
params.dashboardTag = props.options.tags;
}
const alerts: AlertRuleDTO[] = await getBackendSrv().get(
'/api/alerts',
params,
`alert-list-get-current-alert-state-${props.id}`
);
let currentAlerts = sortAlerts(
props.options.sortOrder,
alerts.map((al) => ({
...al,
stateModel: alertDef.getStateDisplayModel(al.state),
newStateDateAgo: dateTime(al.newStateDate).locale('en').fromNow(true),
}))
);
if (currentAlerts.length > props.options.maxItems) {
currentAlerts = currentAlerts.slice(0, props.options.maxItems);
}
setNoAlertsMessage(currentAlerts.length === 0 ? 'No alerts' : '');
return currentAlerts;
}, [
props.options.showOptions,
props.options.stateFilter.alerting,
props.options.stateFilter.execution_error,
props.options.stateFilter.no_data,
props.options.stateFilter.ok,
props.options.stateFilter.paused,
props.options.stateFilter.pending,
props.options.maxItems,
props.options.tags,
props.options.dashboardAlerts,
props.options.dashboardTitle,
props.options.folderId,
props.options.alertName,
props.options.sortOrder,
]);
const recentStateChanges = useAsync(async () => {
if (props.options.showOptions !== ShowOption.RecentChanges) {
return;
}
const params: any = {
limit: props.options.maxItems,
type: 'alert',
newState: getStateFilter(props.options.stateFilter),
};
const currentDashboard = getDashboardSrv().getCurrent();
if (props.options.dashboardAlerts) {
params.dashboardId = currentDashboard.id;
}
params.from = dateMath.parse(currentDashboard.time.from)!.unix() * 1000;
params.to = dateMath.parse(currentDashboard.time.to)!.unix() * 1000;
const data: AnnotationItemDTO[] = await getBackendSrv().get(
'/api/annotations',
params,
`alert-list-get-state-changes-${props.id}`
);
const alertHistory = sortAlerts(
props.options.sortOrder,
data.map((al) => {
return {
...al,
time: currentDashboard.formatDate(al.time, 'MMM D, YYYY HH:mm:ss'),
stateModel: alertDef.getStateDisplayModel(al.newState),
info: alertDef.getAlertAnnotationInfo(al),
};
})
);
setNoAlertsMessage(alertHistory.length === 0 ? 'No alerts in current time range' : '');
return alertHistory;
}, [
props.options.showOptions,
props.options.maxItems,
props.options.stateFilter.alerting,
props.options.stateFilter.execution_error,
props.options.stateFilter.no_data,
props.options.stateFilter.ok,
props.options.stateFilter.paused,
props.options.stateFilter.pending,
props.options.dashboardAlerts,
]);
const styles = useStyles(getStyles);
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.container}>
{noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
<section>
<ol className={styles.alertRuleList}>
{props.options.showOptions === ShowOption.Current
? !currentAlertState.loading &&
currentAlertState.value &&
currentAlertState.value!.map((alert) => (
<li className={styles.alertRuleItem} key={`alert-${alert.id}`}>
<Card
heading={alert.name}
href={`${alert.url}?viewPanel=${alert.panelId}`}
className={styles.cardContainer}
>
<Card.Figure className={cx(styles.alertRuleItemIcon, alert.stateModel.stateClass)}>
<Icon name={alert.stateModel.iconClass} size="xl" className={styles.alertIcon} />
</Card.Figure>
<Card.Meta>
<div className={styles.alertRuleItemText}>
<span className={alert.stateModel.stateClass}>{alert.stateModel.text}</span>
<span className={styles.alertRuleItemTime}> for {alert.newStateDateAgo}</span>
</div>
</Card.Meta>
</Card>
</li>
))
: !recentStateChanges.loading &&
recentStateChanges.value &&
recentStateChanges.value.map((alert) => (
<li className={styles.alertRuleItem} key={`alert-${alert.id}`}>
<Card heading={alert.alertName} className={styles.cardContainer}>
<Card.Figure className={cx(styles.alertRuleItemIcon, alert.stateModel.stateClass)}>
<Icon name={alert.stateModel.iconClass} size="xl" />
</Card.Figure>
<Card.Meta>
<span className={cx(styles.alertRuleItemText, alert.stateModel.stateClass)}>
{alert.stateModel.text}
</span>
<span>{alert.time}</span>
{alert.info && <span className={styles.alertRuleItemInfo}>{alert.info}</span>}
</Card.Meta>
</Card>
</li>
))}
</ol>
</section>
</div>
</CustomScrollbar>
);
}
function sortAlerts(sortOrder: SortOrder, alerts: any[]) {
if (sortOrder === SortOrder.Importance) {
// @ts-ignore
return sortBy(alerts, (a) => alertDef.alertStateSortScore[a.state || a.newState]);
} else if (sortOrder === SortOrder.TimeAsc) {
return sortBy(alerts, (a) => new Date(a.newStateDate || a.time));
} else if (sortOrder === SortOrder.TimeDesc) {
return sortBy(alerts, (a) => new Date(a.newStateDate || a.time)).reverse();
}
const result = sortBy(alerts, (a) => (a.name || a.alertName).toLowerCase());
if (sortOrder === SortOrder.AlphaDesc) {
result.reverse();
}
return result;
}
function getStateFilter(stateFilter: Record<string, boolean>) {
return Object.entries(stateFilter)
.filter(([_, val]) => val)
.map(([key, _]) => key);
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
cardContainer: css`
padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0;
line-height: ${theme.typography.lineHeight.md};
margin-bottom: 0px;
`,
container: css`
overflow-y: auto;
height: 100%;
`,
alertRuleList: css`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
list-style-type: none;
`,
alertRuleItem: css`
display: flex;
align-items: center;
width: 100%;
height: 100%;
background: ${theme.colors.bg2};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
border-radius: ${theme.border.radius.md};
margin-bottom: ${theme.spacing.xs};
`,
alertRuleItemIcon: css`
display: flex;
justify-content: center;
align-items: center;
width: ${theme.spacing.xl};
padding: 0 ${theme.spacing.xs} 0 ${theme.spacing.xxs};
margin-right: 0px;
`,
alertRuleItemText: css`
font-weight: ${theme.typography.weight.bold};
font-size: ${theme.typography.size.sm};
margin: 0;
`,
alertRuleItemTime: css`
color: ${theme.colors.textWeak};
font-weight: normal;
white-space: nowrap;
`,
alertRuleItemInfo: css`
font-weight: normal;
flex-grow: 2;
display: flex;
align-items: flex-end;
`,
noAlertsMessage: css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`,
alertIcon: css`
margin-right: ${theme.spacing.xs};
`,
}));

View File

@ -0,0 +1,110 @@
import { PanelModel } from '@grafana/data';
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
import { AlertListOptions, ShowOption, SortOrder } from './types';
describe('AlertList Panel Migration', () => {
it('should migrate from < 7.5', () => {
const panel: Omit<PanelModel, 'fieldConfig'> & Record<string, any> = {
id: 7,
links: [],
pluginVersion: '7.4.0',
targets: [],
title: 'Usage',
type: 'alertlist',
nameFilter: 'Customer',
show: 'current',
sortOrder: 1,
stateFilter: ['ok', 'paused'],
dashboardTags: ['tag_a', 'tag_b'],
dashboardFilter: '',
limit: 10,
onlyAlertsOnDashboard: false,
options: {},
};
const newOptions = alertListPanelMigrationHandler(panel as PanelModel);
expect(newOptions).toMatchObject({
showOptions: ShowOption.Current,
maxItems: 10,
sortOrder: SortOrder.AlphaAsc,
dashboardAlerts: false,
alertName: 'Customer',
dashboardTitle: '',
tags: ['tag_a', 'tag_b'],
stateFilter: {
ok: true,
paused: true,
},
folderId: undefined,
});
expect(panel).not.toHaveProperty('show');
expect(panel).not.toHaveProperty('limit');
expect(panel).not.toHaveProperty('sortOrder');
expect(panel).not.toHaveProperty('onlyAlertsOnDashboard');
expect(panel).not.toHaveProperty('nameFilter');
expect(panel).not.toHaveProperty('dashboardFilter');
expect(panel).not.toHaveProperty('folderId');
expect(panel).not.toHaveProperty('dashboardTags');
expect(panel).not.toHaveProperty('stateFilter');
});
it('should handle >= 7.5', () => {
const panel: Omit<PanelModel<AlertListOptions>, 'fieldConfig'> & Record<string, any> = {
id: 7,
links: [],
pluginVersion: '7.5.0',
targets: [],
title: 'Usage',
type: 'alertlist',
options: {
showOptions: ShowOption.Current,
maxItems: 10,
sortOrder: SortOrder.AlphaAsc,
dashboardAlerts: false,
alertName: 'Customer',
dashboardTitle: '',
tags: ['tag_a', 'tag_b'],
stateFilter: {
ok: true,
paused: true,
no_data: false,
execution_error: false,
pending: false,
alerting: false,
},
folderId: 1,
},
};
const newOptions = alertListPanelMigrationHandler(panel as PanelModel);
expect(newOptions).toMatchObject({
showOptions: 'current',
maxItems: 10,
sortOrder: SortOrder.AlphaAsc,
dashboardAlerts: false,
alertName: 'Customer',
dashboardTitle: '',
tags: ['tag_a', 'tag_b'],
stateFilter: {
ok: true,
paused: true,
no_data: false,
execution_error: false,
pending: false,
alerting: false,
},
folderId: 1,
});
expect(panel).not.toHaveProperty('show');
expect(panel).not.toHaveProperty('limit');
expect(panel).not.toHaveProperty('sortOrder');
expect(panel).not.toHaveProperty('onlyAlertsOnDashboard');
expect(panel).not.toHaveProperty('nameFilter');
expect(panel).not.toHaveProperty('dashboardFilter');
expect(panel).not.toHaveProperty('folderId');
expect(panel).not.toHaveProperty('dashboardTags');
expect(panel).not.toHaveProperty('stateFilter');
});
});

View File

@ -0,0 +1,38 @@
import { PanelModel } from '@grafana/data';
import { AlertListOptions } from './types';
export const alertListPanelMigrationHandler = (
panel: PanelModel<AlertListOptions> & Record<string, any>
): Partial<AlertListOptions> => {
const newOptions: AlertListOptions = {
showOptions: panel.options.showOptions ?? panel.show,
maxItems: panel.options.maxItems ?? panel.limit,
sortOrder: panel.options.sortOrder ?? panel.sortOrder,
dashboardAlerts: panel.options.dashboardAlerts ?? panel.onlyAlertsOnDashboard,
alertName: panel.options.alertName ?? panel.nameFilter,
dashboardTitle: panel.options.dashboardTitle ?? panel.dashboardFilter,
folderId: panel.options.folderId ?? panel.folderId,
tags: panel.options.tags ?? panel.dashboardTags,
stateFilter:
panel.options.stateFilter ??
panel.stateFilter.reduce((filterObj: any, curFilter: any) => ({ ...filterObj, [curFilter]: true }), {}),
};
const previousVersion = parseFloat(panel.pluginVersion || '7.4');
if (previousVersion < 7.5) {
const oldProps = [
'show',
'limit',
'sortOrder',
'onlyAlertsOnDashboard',
'nameFilter',
'dashboardFilter',
'folderId',
'dashboardTags',
'stateFilter',
];
oldProps.forEach((prop) => delete panel[prop]);
}
return newOptions;
};

View File

@ -1,64 +0,0 @@
<div>
<div class="section gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form">
<span class="gf-form-label width-8">Show</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.show"
ng-options="f.value as f.text for f in ctrl.showOptions" ng-change="ctrl.onRefresh()"></select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Max items</span>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRefresh()" />
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Sort order</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.sortOrder"
ng-options="f.value as f.text for f in ctrl.sortOrderOptions" ng-change="ctrl.onRefresh()"></select>
</div>
</div>
<gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18"
checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch>
</div>
<div class="section gf-form-group" ng-show="ctrl.panel.show === 'current'">
<h5 class="section-heading">Filter</h5>
<div class="gf-form">
<span class="gf-form-label width-8">Alert name</span>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.nameFilter"
placeholder="Alert name query" ng-change="ctrl.onRefresh()" />
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Dashboard title</span>
<input type="text" class="gf-form-input" placeholder="Dashboard title query" ng-model="ctrl.panel.dashboardFilter"
ng-change="ctrl.onRefresh()" ng-model-onblur>
</div>
<div class="gf-form">
<folder-picker initial-folder-id="ctrl.panel.folderId" on-change="ctrl.onFolderChange" label-class="width-8"
initial-title="'All'" enable-reset="true">
</folder-picker>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Dashboard tags</span>
<bootstrap-tagsinput ng-model="ctrl.panel.dashboardTags" tagclass="label label-tag" placeholder="add tags"
on-tags-updated="ctrl.refresh()">
</bootstrap-tagsinput>
</div>
</div>
<div class="section gf-form-group" ng-show="ctrl.panel.show === 'current'">
<h5 class="section-heading">State filter</h5>
<gf-form-switch class="gf-form" label="Ok" label-class="width-10" checked="ctrl.stateFilter['ok']"
on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Paused" label-class="width-10" checked="ctrl.stateFilter['paused']"
on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="No data" label-class="width-10" checked="ctrl.stateFilter['no_data']"
on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Execution error" label-class="width-10"
checked="ctrl.stateFilter['execution_error']" on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Alerting" label-class="width-10" checked="ctrl.stateFilter['alerting']"
on-change="ctrl.updateStateFilter()"></gf-form-switch>
<gf-form-switch class="gf-form" label="Pending" label-class="width-10" checked="ctrl.stateFilter['pending']"
on-change="ctrl.updateStateFilter()"></gf-form-switch>
</div>
</div>

View File

@ -1,50 +0,0 @@
<div class="panel-alert-list">
<div class="panel-alert-list__no-alerts" ng-show="ctrl.noAlertsMessage">
{{ctrl.noAlertsMessage}}
</div>
<section ng-if="ctrl.panel.show === 'current'">
<ol class="alert-rule-list">
<li class="alert-rule-item" ng-repeat="alert in ctrl.currentAlerts">
<div class="alert-rule-item__icon {{alert.stateModel.stateClass}}">
<icon name="'{{alert.stateModel.iconClass}}'" size="'xl'" style="margin-right: 4px;"></icon>
</div>
<div class="alert-rule-item__body">
<div class="alert-rule-item__header">
<p class="alert-rule-item__name">
<a href="{{alert.url}}?viewPanel={{alert.panelId}}">
{{alert.name}}
</a>
</p>
<div class="alert-rule-item__text">
<span class="{{alert.stateModel.stateClass}}">{{alert.stateModel.text}}</span>
<span class="alert-rule-item__time">for {{alert.newStateDateAgo}}</span>
</div>
</div>
</div>
</li>
</ol>
</section>
<section ng-if="ctrl.panel.show === 'changes'">
<ol class="alert-rule-list">
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
<icon name="'{{al.stateModel.iconClass}}'" size="'xl'"></icon>
</div>
<div class="alert-rule-item__body">
<div class="alert-rule-item__header">
<p class="alert-rule-item__name">{{al.alertName}}</p>
<div class="alert-rule-item__text">
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
</div>
</div>
<span class="alert-rule-item__info">{{al.info}}</span>
</div>
<div class="alert-rule-item__time">
<span>{{al.time}}</span>
</div>
</li>
</ol>
</section>
</div>

View File

@ -1,204 +0,0 @@
import _ from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { dateMath, dateTime, PanelEvents } from '@grafana/data';
import { auto, IScope } from 'angular';
import alertDef from '../../../features/alerting/state/alertDef';
import { PanelCtrl } from 'app/plugins/sdk';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
class AlertListPanel extends PanelCtrl {
static templateUrl = 'module.html';
static scrollable = true;
showOptions = [
{ text: 'Current state', value: 'current' },
{ text: 'Recent state changes', value: 'changes' },
];
sortOrderOptions = [
{ text: 'Alphabetical (asc)', value: 1 },
{ text: 'Alphabetical (desc)', value: 2 },
{ text: 'Importance', value: 3 },
{ text: 'Time (asc)', value: 4 },
{ text: 'Time (desc)', value: 5 },
];
stateFilter: any = {};
currentAlerts: any = [];
alertHistory: any = [];
noAlertsMessage: string;
templateSrv: string;
// Set and populate defaults
panelDefaults: any = {
show: 'current',
limit: 10,
stateFilter: [],
onlyAlertsOnDashboard: false,
sortOrder: 1,
dashboardFilter: '',
nameFilter: '',
folderId: null,
};
/** @ngInject */
constructor($scope: IScope, $injector: auto.IInjectorService) {
super($scope, $injector);
_.defaults(this.panel, this.panelDefaults);
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
this.events.on(PanelEvents.refresh, this.onRefresh.bind(this));
this.templateSrv = this.$injector.get('templateSrv');
for (const key in this.panel.stateFilter) {
this.stateFilter[this.panel.stateFilter[key]] = true;
}
}
sortResult(alerts: any[]) {
if (this.panel.sortOrder === 3) {
return _.sortBy(alerts, (a) => {
// @ts-ignore
return alertDef.alertStateSortScore[a.state || a.newState];
});
} else if (this.panel.sortOrder === 4) {
return _.sortBy(alerts, (a) => {
return new Date(a.newStateDate || a.time);
});
} else if (this.panel.sortOrder === 5) {
return _.sortBy(alerts, (a) => {
return new Date(a.newStateDate || a.time);
}).reverse();
}
const result = _.sortBy(alerts, (a) => {
return (a.name || a.alertName).toLowerCase();
});
if (this.panel.sortOrder === 2) {
result.reverse();
}
return result;
}
updateStateFilter() {
const result = [];
for (const key in this.stateFilter) {
if (this.stateFilter[key]) {
result.push(key);
}
}
this.panel.stateFilter = result;
this.onRefresh();
}
onRefresh() {
let getAlertsPromise;
if (this.panel.show === 'current') {
getAlertsPromise = this.getCurrentAlertState();
} else if (this.panel.show === 'changes') {
getAlertsPromise = this.getStateChanges();
} else {
getAlertsPromise = Promise.resolve();
}
getAlertsPromise.then(() => {
this.renderingCompleted();
});
}
onFolderChange = (folder: any) => {
this.panel.folderId = folder.id;
this.refresh();
};
getStateChanges() {
const params: any = {
limit: this.panel.limit,
type: 'alert',
newState: this.panel.stateFilter,
};
if (this.panel.onlyAlertsOnDashboard) {
params.dashboardId = this.dashboard.id;
}
params.from = dateMath.parse(this.dashboard.time.from)!.unix() * 1000;
params.to = dateMath.parse(this.dashboard.time.to)!.unix() * 1000;
return promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/annotations', params, `alert-list-get-state-changes-${this.panel.id}`)
.then((data) => {
this.alertHistory = this.sortResult(
_.map(data, (al) => {
al.time = this.dashboard.formatDate(al.time, 'MMM D, YYYY HH:mm:ss');
al.stateModel = alertDef.getStateDisplayModel(al.newState);
al.info = alertDef.getAlertAnnotationInfo(al);
return al;
})
);
this.noAlertsMessage = this.alertHistory.length === 0 ? 'No alerts in current time range' : '';
return this.alertHistory;
})
);
}
getCurrentAlertState() {
const params: any = {
state: this.panel.stateFilter,
};
if (this.panel.nameFilter) {
params.query = this.templateSrv.replace(this.panel.nameFilter, this.panel.scopedVars);
}
if (this.panel.folderId >= 0) {
params.folderId = this.panel.folderId;
}
if (this.panel.dashboardFilter) {
params.dashboardQuery = this.panel.dashboardFilter;
}
if (this.panel.onlyAlertsOnDashboard) {
params.dashboardId = this.dashboard.id;
}
if (this.panel.dashboardTags) {
params.dashboardTag = this.panel.dashboardTags;
}
return promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/alerts', params, `alert-list-get-current-alert-state-${this.panel.id}`)
.then((data) => {
this.currentAlerts = this.sortResult(
_.map(data, (al) => {
al.stateModel = alertDef.getStateDisplayModel(al.state);
al.newStateDateAgo = dateTime(al.newStateDate).locale('en').fromNow(true);
return al;
})
);
if (this.currentAlerts.length > this.panel.limit) {
this.currentAlerts = this.currentAlerts.slice(0, this.panel.limit);
}
this.noAlertsMessage = this.currentAlerts.length === 0 ? 'No alerts' : '';
return this.currentAlerts;
})
);
}
onInitEditMode() {
this.addEditorTab('Options', 'public/app/plugins/panel/alertlist/editor.html');
}
}
export { AlertListPanel, AlertListPanel as PanelCtrl };

View File

@ -0,0 +1,142 @@
import React from 'react';
import { PanelPlugin } from '@grafana/data';
import { TagsInput } from '@grafana/ui';
import { AlertList } from './AlertList';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { AlertListOptions, ShowOption, SortOrder } from './types';
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
function showIfCurrentState(options: AlertListOptions) {
return options.showOptions === ShowOption.Current;
}
export const plugin = new PanelPlugin<AlertListOptions>(AlertList)
.setPanelOptions((builder) => {
builder
.addSelect({
name: 'Show',
path: 'showOptions',
settings: {
options: [
{ label: 'Current state', value: ShowOption.Current },
{ label: 'Recent state changes', value: ShowOption.RecentChanges },
],
},
defaultValue: ShowOption.Current,
category: ['Options'],
})
.addNumberInput({
name: 'Max items',
path: 'maxItems',
defaultValue: 10,
category: ['Options'],
})
.addSelect({
name: 'Sort order',
path: 'sortOrder',
settings: {
options: [
{ label: 'Alphabetical (asc)', value: SortOrder.AlphaAsc },
{ label: 'Alphabetical (desc)', value: SortOrder.AlphaDesc },
{ label: 'Importance', value: SortOrder.Importance },
{ label: 'Time (asc)', value: SortOrder.TimeAsc },
{ label: 'Time (desc)', value: SortOrder.TimeDesc },
],
},
defaultValue: SortOrder.AlphaAsc,
category: ['Options'],
})
.addBooleanSwitch({
path: 'dashboardAlerts',
name: 'Alerts from this dashboard',
defaultValue: false,
category: ['Options'],
})
.addTextInput({
path: 'alertName',
name: 'Alert name',
defaultValue: '',
category: ['Filter'],
showIf: showIfCurrentState,
})
.addTextInput({
path: 'dashboardTitle',
name: 'Dashboard title',
defaultValue: '',
category: ['Filter'],
showIf: showIfCurrentState,
})
.addCustomEditor({
path: 'folderId',
name: 'Folder',
id: 'folderId',
defaultValue: null,
editor: function RenderFolderPicker(props) {
return (
<FolderPicker
initialFolderId={props.value}
initialTitle="All"
enableReset={true}
onChange={({ id }) => props.onChange(id)}
/>
);
},
category: ['Filter'],
showIf: showIfCurrentState,
})
.addCustomEditor({
id: 'tags',
path: 'tags',
name: 'Tags',
description: '',
defaultValue: [],
editor(props) {
return <TagsInput tags={props.value} onChange={props.onChange} />;
},
category: ['Filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.ok',
name: 'Ok',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.paused',
name: 'Paused',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.no_data',
name: 'No data',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.execution_error',
name: 'Execution error',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.alerting',
name: 'Alerting',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
})
.addBooleanSwitch({
path: 'stateFilter.pending',
name: 'Pending',
defaultValue: false,
category: ['State filter'],
showIf: showIfCurrentState,
});
})
.setMigrationHandler(alertListPanelMigrationHandler);

View File

@ -0,0 +1,31 @@
export enum SortOrder {
AlphaAsc = 1,
AlphaDesc,
Importance,
TimeAsc,
TimeDesc,
}
export enum ShowOption {
Current = 'current',
RecentChanges = 'changes',
}
export interface AlertListOptions {
showOptions: ShowOption;
maxItems: number;
sortOrder: SortOrder;
dashboardAlerts: boolean;
alertName: string;
dashboardTitle: string;
tags: string[];
stateFilter: {
ok: boolean;
paused: boolean;
no_data: boolean;
execution_error: boolean;
alerting: boolean;
pending: boolean;
};
folderId: number;
}

View File

@ -174,3 +174,24 @@ export interface AlertDefinitionUiState {
rightPaneSize: number;
topPaneSize: number;
}
export interface AnnotationItemDTO {
id: number;
alertId: number;
alertName: string;
dashboardId: number;
panelId: number;
userId: number;
newState: string;
prevState: string;
created: number;
updated: number;
time: number;
timeEnd: number;
text: string;
tags: string[];
login: string;
email: string;
avatarUrl: string;
data: any;
}