mirror of https://github.com/grafana/grafana
commit
c42a232644
@ -0,0 +1,68 @@ |
||||
import React from 'react'; |
||||
import moment from 'moment'; |
||||
import { AlertRuleList } from './AlertRuleList'; |
||||
import { RootStore } from 'app/stores/RootStore/RootStore'; |
||||
import { backendSrv, createNavTree } from 'test/mocks/common'; |
||||
import { mount } from 'enzyme'; |
||||
import toJson from 'enzyme-to-json'; |
||||
|
||||
describe('AlertRuleList', () => { |
||||
let page, store; |
||||
|
||||
beforeAll(() => { |
||||
backendSrv.get.mockReturnValue( |
||||
Promise.resolve([ |
||||
{ |
||||
id: 11, |
||||
dashboardId: 58, |
||||
panelId: 3, |
||||
name: 'Panel Title alert', |
||||
state: 'ok', |
||||
newStateDate: moment() |
||||
.subtract(5, 'minutes') |
||||
.format(), |
||||
evalData: {}, |
||||
executionError: '', |
||||
dashboardUri: 'db/mygool', |
||||
}, |
||||
]) |
||||
); |
||||
|
||||
store = RootStore.create( |
||||
{}, |
||||
{ |
||||
backendSrv: backendSrv, |
||||
navTree: createNavTree('alerting', 'alert-list'), |
||||
} |
||||
); |
||||
|
||||
page = mount(<AlertRuleList {...store} />); |
||||
}); |
||||
|
||||
it('should call api to get rules', () => { |
||||
expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts'); |
||||
}); |
||||
|
||||
it('should render 1 rule', () => { |
||||
page.update(); |
||||
let ruleNode = page.find('.card-item-wrapper'); |
||||
expect(toJson(ruleNode)).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('toggle state should change pause rule if not paused', async () => { |
||||
backendSrv.post.mockReturnValue( |
||||
Promise.resolve({ |
||||
state: 'paused', |
||||
}) |
||||
); |
||||
|
||||
page.find('.fa-pause').simulate('click'); |
||||
|
||||
// wait for api call to resolve
|
||||
await Promise.resolve(); |
||||
page.update(); |
||||
|
||||
expect(store.alertList.rules[0].state).toBe('paused'); |
||||
expect(page.find('.fa-play')).toHaveLength(1); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,145 @@ |
||||
import React from 'react'; |
||||
import classNames from 'classnames'; |
||||
import { inject, observer } from 'mobx-react'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import IContainerProps from 'app/containers/IContainerProps'; |
||||
|
||||
@inject('view', 'nav', 'alertList') |
||||
@observer |
||||
export class AlertRuleList extends React.Component<IContainerProps, any> { |
||||
stateFilters = [ |
||||
{ text: 'All', value: 'all' }, |
||||
{ text: 'OK', value: 'ok' }, |
||||
{ text: 'Not OK', value: 'not_ok' }, |
||||
{ text: 'Alerting', value: 'alerting' }, |
||||
{ text: 'No Data', value: 'no_data' }, |
||||
{ text: 'Paused', value: 'paused' }, |
||||
]; |
||||
|
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.props.nav.load('alerting', 'alert-list'); |
||||
this.fetchRules(); |
||||
} |
||||
|
||||
onStateFilterChanged = evt => { |
||||
this.props.view.updateQuery({ state: evt.target.value }); |
||||
this.fetchRules(); |
||||
}; |
||||
|
||||
fetchRules() { |
||||
this.props.alertList.loadRules({ |
||||
state: this.props.view.query.get('state') || 'all', |
||||
}); |
||||
} |
||||
|
||||
onOpenHowTo = () => { |
||||
appEvents.emit('show-modal', { |
||||
src: 'public/app/features/alerting/partials/alert_howto.html', |
||||
modalClass: 'confirm-modal', |
||||
model: {}, |
||||
}); |
||||
}; |
||||
|
||||
render() { |
||||
const { nav, alertList } = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<PageHeader model={nav as any} /> |
||||
<div className="page-container page-body"> |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form"> |
||||
<label className="gf-form-label">Filter by state</label> |
||||
|
||||
<div className="gf-form-select-wrapper width-13"> |
||||
<select className="gf-form-input" onChange={this.onStateFilterChanged} value={alertList.stateFilter}> |
||||
{this.stateFilters.map(AlertStateFilterOption)} |
||||
</select> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="page-action-bar__spacer" /> |
||||
|
||||
<a className="btn btn-secondary" onClick={this.onOpenHowTo}> |
||||
<i className="fa fa-info-circle" /> How to add an alert |
||||
</a> |
||||
</div> |
||||
|
||||
<section className="card-section card-list-layout-list"> |
||||
<ol className="card-list">{alertList.rules.map(rule => <AlertRuleItem rule={rule} key={rule.id} />)}</ol> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function AlertStateFilterOption({ text, value }) { |
||||
return ( |
||||
<option key={value} value={value}> |
||||
{text} |
||||
</option> |
||||
); |
||||
} |
||||
|
||||
export interface AlertRuleItemProps { |
||||
rule: IAlertRule; |
||||
} |
||||
|
||||
@observer |
||||
export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> { |
||||
toggleState = () => { |
||||
this.props.rule.togglePaused(); |
||||
}; |
||||
|
||||
render() { |
||||
const { rule } = this.props; |
||||
|
||||
let stateClass = classNames({ |
||||
fa: true, |
||||
'fa-play': rule.isPaused, |
||||
'fa-pause': !rule.isPaused, |
||||
}); |
||||
|
||||
let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; |
||||
|
||||
return ( |
||||
<li className="card-item-wrapper"> |
||||
<div className="card-item card-item--alert"> |
||||
<div className="card-item-header"> |
||||
<div className="card-item-type"> |
||||
<a |
||||
className="card-item-cog" |
||||
title="Pausing an alert rule prevents it from executing" |
||||
onClick={this.toggleState} |
||||
> |
||||
<i className={stateClass} /> |
||||
</a> |
||||
<a className="card-item-cog" href={ruleUrl} title="Edit alert rule"> |
||||
<i className="icon-gf icon-gf-settings" /> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div className="card-item-body"> |
||||
<div className="card-item-details"> |
||||
<div className="card-item-name"> |
||||
<a href={ruleUrl}>{rule.name}</a> |
||||
</div> |
||||
<div className="card-item-sub-name"> |
||||
<span className={`alert-list-item-state ${rule.stateClass}`}> |
||||
<i className={rule.stateIcon} /> {rule.stateText} |
||||
</span> |
||||
<span> for {rule.stateAge}</span> |
||||
</div> |
||||
{rule.info && <div className="small muted">{rule.info}</div>} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`AlertRuleList should render 1 rule 1`] = ` |
||||
<li |
||||
className="card-item-wrapper" |
||||
> |
||||
<div |
||||
className="card-item card-item--alert" |
||||
> |
||||
<div |
||||
className="card-item-header" |
||||
> |
||||
<div |
||||
className="card-item-type" |
||||
> |
||||
<a |
||||
className="card-item-cog" |
||||
onClick={[Function]} |
||||
title="Pausing an alert rule prevents it from executing" |
||||
> |
||||
<i |
||||
className="fa fa-pause" |
||||
/> |
||||
</a> |
||||
<a |
||||
className="card-item-cog" |
||||
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert" |
||||
title="Edit alert rule" |
||||
> |
||||
<i |
||||
className="icon-gf icon-gf-settings" |
||||
/> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div |
||||
className="card-item-body" |
||||
> |
||||
<div |
||||
className="card-item-details" |
||||
> |
||||
<div |
||||
className="card-item-name" |
||||
> |
||||
<a |
||||
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert" |
||||
> |
||||
Panel Title alert |
||||
</a> |
||||
</div> |
||||
<div |
||||
className="card-item-sub-name" |
||||
> |
||||
<span |
||||
className="alert-list-item-state alert-state-ok" |
||||
> |
||||
<i |
||||
className="icon-gf icon-gf-online" |
||||
/> |
||||
|
||||
OK |
||||
</span> |
||||
<span> |
||||
for |
||||
5 minutes |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
`; |
||||
@ -0,0 +1,30 @@ |
||||
import React from 'react'; |
||||
import renderer from 'react-test-renderer'; |
||||
import { ServerStats } from './ServerStats'; |
||||
import { RootStore } from 'app/stores/RootStore/RootStore'; |
||||
import { backendSrv, createNavTree } from 'test/mocks/common'; |
||||
|
||||
describe('ServerStats', () => { |
||||
it('Should render table with stats', done => { |
||||
backendSrv.get.mockReturnValue( |
||||
Promise.resolve({ |
||||
dashboards: 10, |
||||
}) |
||||
); |
||||
|
||||
const store = RootStore.create( |
||||
{}, |
||||
{ |
||||
backendSrv: backendSrv, |
||||
navTree: createNavTree('cfg', 'admin', 'server-stats'), |
||||
} |
||||
); |
||||
|
||||
const page = renderer.create(<ServerStats {...store} />); |
||||
|
||||
setTimeout(() => { |
||||
expect(page.toJSON()).toMatchSnapshot(); |
||||
done(); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,45 @@ |
||||
import React from 'react'; |
||||
import { inject, observer } from 'mobx-react'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import IContainerProps from 'app/containers/IContainerProps'; |
||||
|
||||
@inject('nav', 'serverStats') |
||||
@observer |
||||
export class ServerStats extends React.Component<IContainerProps, any> { |
||||
constructor(props) { |
||||
super(props); |
||||
const { nav, serverStats } = this.props; |
||||
|
||||
nav.load('cfg', 'admin', 'server-stats'); |
||||
serverStats.load(); |
||||
} |
||||
|
||||
render() { |
||||
const { nav, serverStats } = this.props; |
||||
return ( |
||||
<div> |
||||
<PageHeader model={nav as any} /> |
||||
<div className="page-container page-body"> |
||||
<table className="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Value</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody>{serverStats.stats.map(StatItem)}</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function StatItem(stat) { |
||||
return ( |
||||
<tr key={stat.name}> |
||||
<td>{stat.name}</td> |
||||
<td>{stat.value}</td> |
||||
</tr> |
||||
); |
||||
} |
||||
@ -0,0 +1,170 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`ServerStats Should render table with stats 1`] = ` |
||||
<div> |
||||
<div |
||||
className="page-header-canvas" |
||||
> |
||||
<div |
||||
className="page-container" |
||||
> |
||||
<div |
||||
className="page-header" |
||||
> |
||||
<div |
||||
className="page-header__inner" |
||||
> |
||||
<span |
||||
className="page-header__logo" |
||||
> |
||||
|
||||
|
||||
</span> |
||||
<div |
||||
className="page-header__info-block" |
||||
> |
||||
<h1 |
||||
className="page-header__title" |
||||
> |
||||
admin-Text |
||||
</h1> |
||||
|
||||
</div> |
||||
</div> |
||||
<nav> |
||||
<div |
||||
className="gf-form-select-wrapper width-20 page-header__select-nav" |
||||
> |
||||
<label |
||||
className="gf-form-select-icon " |
||||
htmlFor="page-header-select-nav" |
||||
/> |
||||
<select |
||||
className="gf-select-nav gf-form-input" |
||||
defaultValue="/url/server-stats" |
||||
id="page-header-select-nav" |
||||
onChange={[Function]} |
||||
> |
||||
<option |
||||
value="/url/server-stats" |
||||
> |
||||
server-stats-Text |
||||
</option> |
||||
</select> |
||||
</div> |
||||
<ul |
||||
className="gf-tabs page-header__tabs" |
||||
> |
||||
<li |
||||
className="gf-tabs-item" |
||||
> |
||||
<a |
||||
className="gf-tabs-link active" |
||||
href="/url/server-stats" |
||||
target={undefined} |
||||
> |
||||
<i |
||||
className="" |
||||
/> |
||||
server-stats-Text |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</nav> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<table |
||||
className="filter-table form-inline" |
||||
> |
||||
<thead> |
||||
<tr> |
||||
<th> |
||||
Name |
||||
</th> |
||||
<th> |
||||
Value |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr> |
||||
<td> |
||||
Total dashboards |
||||
</td> |
||||
<td> |
||||
10 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total users |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Active users (seen last 30 days) |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total orgs |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total playlists |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total snapshots |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total dashboard tags |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total starred dashboards |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total alerts |
||||
</td> |
||||
<td> |
||||
0 |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
`; |
||||
@ -1,12 +1,14 @@ |
||||
import { react2AngularDirective } from 'app/core/utils/react2angular'; |
||||
import { PasswordStrength } from './components/PasswordStrength'; |
||||
import PageHeader from './components/PageHeader/PageHeader'; |
||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; |
||||
import LoginBackground from './components/Login/LoginBackground'; |
||||
import { react2AngularDirective } from "app/core/utils/react2angular"; |
||||
import { PasswordStrength } from "./components/PasswordStrength"; |
||||
import PageHeader from "./components/PageHeader/PageHeader"; |
||||
import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA"; |
||||
import LoginBackground from "./components/Login/LoginBackground"; |
||||
import { SearchResult } from "./components/search/SearchResult"; |
||||
|
||||
export function registerAngularDirectives() { |
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']); |
||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); |
||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); |
||||
react2AngularDirective('loginBackground', LoginBackground, []); |
||||
react2AngularDirective("passwordStrength", PasswordStrength, ["password"]); |
||||
react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]); |
||||
react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]); |
||||
react2AngularDirective("loginBackground", LoginBackground, []); |
||||
react2AngularDirective("searchResult", SearchResult, []); |
||||
} |
||||
|
||||
@ -0,0 +1,86 @@ |
||||
import React from "react"; |
||||
import classNames from "classnames"; |
||||
import { observer } from "mobx-react"; |
||||
import { store } from "app/stores/store"; |
||||
|
||||
export interface SearchResultProps { |
||||
search: any; |
||||
} |
||||
|
||||
@observer |
||||
export class SearchResult extends React.Component<SearchResultProps, any> { |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
search: store.search |
||||
}; |
||||
|
||||
store.search.query(); |
||||
} |
||||
|
||||
render() { |
||||
return this.state.search.sections.map(section => { |
||||
return <SearchResultSection section={section} key={section.id} />; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export interface SectionProps { |
||||
section: any; |
||||
} |
||||
|
||||
@observer |
||||
export class SearchResultSection extends React.Component<SectionProps, any> { |
||||
constructor(props) { |
||||
super(props); |
||||
} |
||||
|
||||
renderItem(item) { |
||||
return ( |
||||
<a className="search-item" href={item.url} key={item.id}> |
||||
<span className="search-item__icon"> |
||||
<i className="fa fa-th-large" /> |
||||
</span> |
||||
<span className="search-item__body"> |
||||
<div className="search-item__body-title">{item.title}</div> |
||||
</span> |
||||
</a> |
||||
); |
||||
} |
||||
|
||||
toggleSection = () => { |
||||
this.props.section.toggle(); |
||||
}; |
||||
|
||||
render() { |
||||
let collapseClassNames = classNames({ |
||||
fa: true, |
||||
"fa-plus": !this.props.section.expanded, |
||||
"fa-minus": this.props.section.expanded, |
||||
"search-section__header__toggle": true |
||||
}); |
||||
|
||||
return ( |
||||
<div className="search-section" key={this.props.section.id}> |
||||
<div className="search-section__header"> |
||||
<i |
||||
className={classNames( |
||||
"search-section__header__icon", |
||||
this.props.section.icon |
||||
)} |
||||
/> |
||||
<span className="search-section__header__text"> |
||||
{this.props.section.title} |
||||
</span> |
||||
<i className={collapseClassNames} onClick={this.toggleSection} /> |
||||
</div> |
||||
{this.props.section.expanded && ( |
||||
<div className="search-section__items"> |
||||
{this.props.section.items.map(this.renderItem)} |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
@ -1,26 +0,0 @@ |
||||
export class BundleLoader { |
||||
lazy: any; |
||||
|
||||
constructor(bundleName) { |
||||
var defer = null; |
||||
|
||||
this.lazy = [ |
||||
'$q', |
||||
'$route', |
||||
'$rootScope', |
||||
($q, $route, $rootScope) => { |
||||
if (defer) { |
||||
return defer.promise; |
||||
} |
||||
|
||||
defer = $q.defer(); |
||||
|
||||
System.import(bundleName).then(() => { |
||||
defer.resolve(); |
||||
}); |
||||
|
||||
return defer.promise; |
||||
}, |
||||
]; |
||||
} |
||||
} |
||||
@ -1,74 +0,0 @@ |
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash'; |
||||
import moment from 'moment'; |
||||
|
||||
import { coreModule, appEvents } from 'app/core/core'; |
||||
import alertDef from './alert_def'; |
||||
|
||||
export class AlertListCtrl { |
||||
alerts: any; |
||||
stateFilters = [ |
||||
{ text: 'All', value: null }, |
||||
{ text: 'OK', value: 'ok' }, |
||||
{ text: 'Not OK', value: 'not_ok' }, |
||||
{ text: 'Alerting', value: 'alerting' }, |
||||
{ text: 'No Data', value: 'no_data' }, |
||||
{ text: 'Paused', value: 'paused' }, |
||||
]; |
||||
filters = { |
||||
state: 'ALL', |
||||
}; |
||||
navModel: any; |
||||
|
||||
/** @ngInject */ |
||||
constructor(private backendSrv, private $location, navModelSrv) { |
||||
this.navModel = navModelSrv.getNav('alerting', 'alert-list', 0); |
||||
|
||||
var params = $location.search(); |
||||
this.filters.state = params.state || null; |
||||
this.loadAlerts(); |
||||
} |
||||
|
||||
filtersChanged() { |
||||
this.$location.search(this.filters); |
||||
} |
||||
|
||||
loadAlerts() { |
||||
this.backendSrv.get('/api/alerts', this.filters).then(result => { |
||||
this.alerts = _.map(result, alert => { |
||||
alert.stateModel = alertDef.getStateDisplayModel(alert.state); |
||||
alert.newStateDateAgo = moment(alert.newStateDate) |
||||
.fromNow() |
||||
.replace(' ago', ''); |
||||
if (alert.evalData && alert.evalData.no_data) { |
||||
alert.no_data = true; |
||||
} |
||||
return alert; |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
pauseAlertRule(alertId: any) { |
||||
var alert = _.find(this.alerts, { id: alertId }); |
||||
|
||||
var payload = { |
||||
paused: alert.state !== 'paused', |
||||
}; |
||||
|
||||
this.backendSrv.post(`/api/alerts/${alert.id}/pause`, payload).then(result => { |
||||
alert.state = result.state; |
||||
alert.stateModel = alertDef.getStateDisplayModel(result.state); |
||||
}); |
||||
} |
||||
|
||||
openHowTo() { |
||||
appEvents.emit('show-modal', { |
||||
src: 'public/app/features/alerting/partials/alert_howto.html', |
||||
modalClass: 'confirm-modal', |
||||
model: {}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
coreModule.controller('AlertListCtrl', AlertListCtrl); |
||||
@ -1,3 +1,2 @@ |
||||
import './alert_list_ctrl'; |
||||
import './notifications_list_ctrl'; |
||||
import './notification_edit_ctrl'; |
||||
|
||||
@ -1,61 +0,0 @@ |
||||
<page-header model="ctrl.navModel"></page-header> |
||||
|
||||
<div class="page-container page-body"> |
||||
|
||||
<div class="page-action-bar"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label">Filter by state</label> |
||||
<div class="gf-form-select-wrapper width-13"> |
||||
<select class="gf-form-input" ng-model="ctrl.filters.state" ng-options="f.value as f.text for f in ctrl.stateFilters" ng-change="ctrl.filtersChanged()"> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="page-action-bar__spacer"> |
||||
</div> |
||||
|
||||
<a class="btn btn-secondary" ng-click="ctrl.openHowTo()"> |
||||
<i class="fa fa-info-circle"></i> |
||||
How to add an alert |
||||
</a> |
||||
</div> |
||||
|
||||
<section class="card-section card-list-layout-list"> |
||||
|
||||
<ol class="card-list" > |
||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.alerts"> |
||||
<div class="card-item card-item--alert"> |
||||
<div class="card-item-header"> |
||||
<div class="card-item-type"> |
||||
<a class="card-item-cog" bs-tooltip="'Pausing an alert rule prevents it from executing'" ng-click="ctrl.pauseAlertRule(alert.id)"> |
||||
<i ng-show="alert.state !== 'paused'" class="fa fa-pause"></i> |
||||
<i ng-show="alert.state === 'paused'" class="fa fa-play"></i> |
||||
</a> |
||||
<a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'"> |
||||
<i class="icon-gf icon-gf-settings"></i> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div class="card-item-body"> |
||||
<div class="card-item-details"> |
||||
<div class="card-item-name"> |
||||
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert"> |
||||
{{alert.name}} |
||||
</a> |
||||
</div> |
||||
<div class="card-item-sub-name"> |
||||
<span class="alert-list-item-state {{alert.stateModel.stateClass}}"> |
||||
<i class="{{alert.stateModel.iconClass}}"></i> |
||||
{{alert.stateModel.text}} <span class="small muted" ng-show="alert.no_data">(due to no data)</span> |
||||
</span> for {{alert.newStateDateAgo}} |
||||
</div> |
||||
<div class="small muted" ng-show="alert.executionError !== ''"> |
||||
Error: "{{alert.executionError}}" |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
</ol> |
||||
</section> |
||||
</div> |
||||
@ -0,0 +1,33 @@ |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import coreModule from 'app/core/core_module'; |
||||
import { store } from 'app/stores/store'; |
||||
import { Provider } from 'mobx-react'; |
||||
|
||||
function WrapInProvider(store, Component, props) { |
||||
return ( |
||||
<Provider {...store}> |
||||
<Component {...props} /> |
||||
</Provider> |
||||
); |
||||
} |
||||
|
||||
/** @ngInject */ |
||||
export function reactContainer($route, $location) { |
||||
return { |
||||
restrict: 'E', |
||||
template: '', |
||||
link(scope, elem) { |
||||
let component = $route.current.locals.component; |
||||
let props = {}; |
||||
|
||||
ReactDOM.render(WrapInProvider(store, component, props), elem[0]); |
||||
|
||||
scope.$on('$destroy', function() { |
||||
ReactDOM.unmountComponentAtNode(elem[0]); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('reactContainer', reactContainer); |
||||
@ -1,4 +1,4 @@ |
||||
import coreModule from '../core_module'; |
||||
import coreModule from 'app/core/core_module'; |
||||
|
||||
export class LoadDashboardCtrl { |
||||
/** @ngInject */ |
||||
@ -0,0 +1,34 @@ |
||||
import { types, getEnv, flow } from 'mobx-state-tree'; |
||||
import { AlertRule } from './AlertRule'; |
||||
import { setStateFields } from './helpers'; |
||||
|
||||
type IAlertRuleType = typeof AlertRule.Type; |
||||
export interface IAlertRule extends IAlertRuleType {} |
||||
|
||||
export const AlertListStore = types |
||||
.model('AlertListStore', { |
||||
rules: types.array(AlertRule), |
||||
stateFilter: types.optional(types.string, 'all'), |
||||
}) |
||||
.actions(self => ({ |
||||
loadRules: flow(function* load(filters) { |
||||
const backendSrv = getEnv(self).backendSrv; |
||||
self.stateFilter = filters.state; // store state filter used in api query
|
||||
const apiRules = yield backendSrv.get('/api/alerts', filters); |
||||
self.rules.clear(); |
||||
|
||||
for (let rule of apiRules) { |
||||
setStateFields(rule, rule.state); |
||||
|
||||
if (rule.executionError) { |
||||
rule.info = 'Execution Error: ' + rule.executionError; |
||||
} |
||||
|
||||
if (rule.evalData && rule.evalData.noData) { |
||||
rule.info = 'Query returned no data'; |
||||
} |
||||
|
||||
self.rules.push(AlertRule.create(rule)); |
||||
} |
||||
}), |
||||
})); |
||||
@ -0,0 +1,41 @@ |
||||
import { types, getEnv } from 'mobx-state-tree'; |
||||
import { NavItem } from './NavItem'; |
||||
|
||||
export const NavStore = types |
||||
.model('NavStore', { |
||||
main: types.maybe(NavItem), |
||||
node: types.maybe(NavItem), |
||||
}) |
||||
.actions(self => ({ |
||||
load(...args) { |
||||
let children = getEnv(self).navTree; |
||||
let main, node; |
||||
let parents = []; |
||||
|
||||
for (let id of args) { |
||||
node = children.find(el => el.id === id); |
||||
|
||||
if (!node) { |
||||
throw new Error(`NavItem with id ${id} not found`); |
||||
} |
||||
|
||||
children = node.children; |
||||
parents.push(node); |
||||
} |
||||
|
||||
main = parents[parents.length - 2]; |
||||
|
||||
if (main.children) { |
||||
for (let item of main.children) { |
||||
item.active = false; |
||||
|
||||
if (item.url === node.url) { |
||||
item.active = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
self.main = NavItem.create(main); |
||||
self.node = NavItem.create(node); |
||||
}, |
||||
})); |
||||
@ -0,0 +1,26 @@ |
||||
import { types } from 'mobx-state-tree'; |
||||
import { SearchStore } from './../SearchStore/SearchStore'; |
||||
import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore'; |
||||
import { NavStore } from './../NavStore/NavStore'; |
||||
import { AlertListStore } from './../AlertListStore/AlertListStore'; |
||||
import { ViewStore } from './../ViewStore/ViewStore'; |
||||
|
||||
export const RootStore = types.model({ |
||||
search: types.optional(SearchStore, { |
||||
sections: [], |
||||
}), |
||||
serverStats: types.optional(ServerStatsStore, { |
||||
stats: [], |
||||
}), |
||||
nav: types.optional(NavStore, {}), |
||||
alertList: types.optional(AlertListStore, { |
||||
rules: [], |
||||
}), |
||||
view: types.optional(ViewStore, { |
||||
path: '', |
||||
query: {}, |
||||
}), |
||||
}); |
||||
|
||||
type IRootStoreType = typeof RootStore.Type; |
||||
export interface IRootStore extends IRootStoreType {} |
||||
@ -0,0 +1,22 @@ |
||||
import { types } from 'mobx-state-tree'; |
||||
import { SearchResultSection } from './SearchResultSection'; |
||||
|
||||
export const SearchStore = types |
||||
.model('SearchStore', { |
||||
sections: types.array(SearchResultSection), |
||||
}) |
||||
.actions(self => ({ |
||||
query() { |
||||
for (let i = 0; i < 100; i++) { |
||||
self.sections.push( |
||||
SearchResultSection.create({ |
||||
id: 'starred' + i, |
||||
title: 'starred', |
||||
icon: 'fa fa-fw fa-star-o', |
||||
expanded: false, |
||||
items: [], |
||||
}) |
||||
); |
||||
} |
||||
}, |
||||
})); |
||||
@ -0,0 +1,46 @@ |
||||
import { types } from 'mobx-state-tree'; |
||||
|
||||
const QueryValueType = types.union(types.string, types.boolean, types.number); |
||||
const urlParameterize = queryObj => { |
||||
const keys = Object.keys(queryObj); |
||||
const newQuery = keys.reduce((acc: string, key: string, idx: number) => { |
||||
const preChar = idx === 0 ? '?' : '&'; |
||||
return acc + preChar + key + '=' + queryObj[key]; |
||||
}, ''); |
||||
|
||||
return newQuery; |
||||
}; |
||||
|
||||
export const ViewStore = types |
||||
.model({ |
||||
path: types.string, |
||||
query: types.map(QueryValueType), |
||||
}) |
||||
.views(self => ({ |
||||
get currentUrl() { |
||||
let path = self.path; |
||||
|
||||
if (self.query.size) { |
||||
path += urlParameterize(self.query.toJS()); |
||||
} |
||||
return path; |
||||
}, |
||||
})) |
||||
.actions(self => { |
||||
function updateQuery(query: any) { |
||||
self.query.clear(); |
||||
for (let key of Object.keys(query)) { |
||||
self.query.set(key, query[key]); |
||||
} |
||||
} |
||||
|
||||
function updatePathAndQuery(path: string, query: any) { |
||||
self.path = path; |
||||
updateQuery(query); |
||||
} |
||||
|
||||
return { |
||||
updateQuery, |
||||
updatePathAndQuery, |
||||
}; |
||||
}); |
||||
@ -0,0 +1,16 @@ |
||||
import { RootStore, IRootStore } from './RootStore/RootStore'; |
||||
import config from 'app/core/config'; |
||||
|
||||
export let store: IRootStore; |
||||
|
||||
export function createStore(backendSrv) { |
||||
store = RootStore.create( |
||||
{}, |
||||
{ |
||||
backendSrv: backendSrv, |
||||
navTree: config.bootData.navTree, |
||||
} |
||||
); |
||||
|
||||
return store; |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
export const backendSrv = { |
||||
get: jest.fn(), |
||||
post: jest.fn(), |
||||
}; |
||||
|
||||
export function createNavTree(...args) { |
||||
let root = []; |
||||
let node = root; |
||||
for (let arg of args) { |
||||
let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; |
||||
node.push(child); |
||||
node = child.children; |
||||
} |
||||
return root; |
||||
} |
||||
Loading…
Reference in new issue