mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Migration/AlertList: Migrates AlertList from AngularJS to React (#31872)
* Migration/AlertList: Migrates AlertList from AngularJS to React
This commit is contained in:
parent
93ead2a50c
commit
48d2dff987
@ -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;
|
||||
|
272
public/app/plugins/panel/alertlist/AlertList.tsx
Normal file
272
public/app/plugins/panel/alertlist/AlertList.tsx
Normal 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};
|
||||
`,
|
||||
}));
|
110
public/app/plugins/panel/alertlist/AlertListMigration.test.ts
Normal file
110
public/app/plugins/panel/alertlist/AlertListMigration.test.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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;
|
||||
};
|
@ -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>
|
@ -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>
|
@ -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 };
|
142
public/app/plugins/panel/alertlist/module.tsx
Normal file
142
public/app/plugins/panel/alertlist/module.tsx
Normal 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);
|
31
public/app/plugins/panel/alertlist/types.ts
Normal file
31
public/app/plugins/panel/alertlist/types.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user