mirror of https://github.com/grafana/grafana
Migration/AlertList: Migrates AlertList from AngularJS to React (#31872)
* Migration/AlertList: Migrates AlertList from AngularJS to Reactpull/31908/head
parent
93ead2a50c
commit
48d2dff987
@ -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}; |
||||
`,
|
||||
})); |
||||
@ -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 }; |
||||
@ -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); |
||||
@ -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; |
||||
} |
||||
Loading…
Reference in new issue