mirror of https://github.com/grafana/grafana
commit
298c088d57
@ -1,69 +0,0 @@ |
||||
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: '', |
||||
url: 'd/ufkcofof/my-goal', |
||||
canEdit: true, |
||||
}, |
||||
]) |
||||
); |
||||
|
||||
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(); |
||||
const ruleNode = page.find('.alert-rule-item'); |
||||
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); |
||||
}); |
||||
}); |
@ -1,178 +0,0 @@ |
||||
import React from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import classNames from 'classnames'; |
||||
import { inject, observer } from 'mobx-react'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import { AlertRule } from 'app/stores/AlertListStore/AlertListStore'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import ContainerProps from 'app/containers/ContainerProps'; |
||||
import Highlighter from 'react-highlight-words'; |
||||
|
||||
@inject('view', 'nav', 'alertList') |
||||
@observer |
||||
export class AlertRuleList extends React.Component<ContainerProps, 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: {}, |
||||
}); |
||||
}; |
||||
|
||||
onSearchQueryChange = evt => { |
||||
this.props.alertList.setSearchQuery(evt.target.value); |
||||
}; |
||||
|
||||
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 gf-form--grow"> |
||||
<label className="gf-form--has-input-icon gf-form--grow"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input" |
||||
placeholder="Search alerts" |
||||
value={alertList.search} |
||||
onChange={this.onSearchQueryChange} |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<label className="gf-form-label">States</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> |
||||
<ol className="alert-rule-list"> |
||||
{alertList.filteredRules.map(rule => ( |
||||
<AlertRuleItem rule={rule} key={rule.id} search={alertList.search} /> |
||||
))} |
||||
</ol> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function AlertStateFilterOption({ text, value }) { |
||||
return ( |
||||
<option key={value} value={value}> |
||||
{text} |
||||
</option> |
||||
); |
||||
} |
||||
|
||||
export interface AlertRuleItemProps { |
||||
rule: AlertRule; |
||||
search: string; |
||||
} |
||||
|
||||
@observer |
||||
export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> { |
||||
toggleState = () => { |
||||
this.props.rule.togglePaused(); |
||||
}; |
||||
|
||||
renderText(text: string) { |
||||
return ( |
||||
<Highlighter |
||||
highlightClassName="highlight-search-match" |
||||
textToHighlight={text} |
||||
searchWords={[this.props.search]} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
render() { |
||||
const { rule } = this.props; |
||||
|
||||
const stateClass = classNames({ |
||||
fa: true, |
||||
'fa-play': rule.isPaused, |
||||
'fa-pause': !rule.isPaused, |
||||
}); |
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; |
||||
|
||||
return ( |
||||
<li className="alert-rule-item"> |
||||
<span className={`alert-rule-item__icon ${rule.stateClass}`}> |
||||
<i className={rule.stateIcon} /> |
||||
</span> |
||||
<div className="alert-rule-item__body"> |
||||
<div className="alert-rule-item__header"> |
||||
<div className="alert-rule-item__name"> |
||||
<a href={ruleUrl}>{this.renderText(rule.name)}</a> |
||||
</div> |
||||
<div className="alert-rule-item__text"> |
||||
<span className={`${rule.stateClass}`}>{this.renderText(rule.stateText)}</span> |
||||
<span className="alert-rule-item__time"> for {rule.stateAge}</span> |
||||
</div> |
||||
</div> |
||||
{rule.info && <div className="small muted alert-rule-item__info">{this.renderText(rule.info)}</div>} |
||||
</div> |
||||
|
||||
<div className="alert-rule-item__actions"> |
||||
<button |
||||
className="btn btn-small btn-inverse alert-list__btn width-2" |
||||
title="Pausing an alert rule prevents it from executing" |
||||
onClick={this.toggleState} |
||||
> |
||||
<i className={stateClass} /> |
||||
</button> |
||||
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule"> |
||||
<i className="icon-gf icon-gf-settings" /> |
||||
</a> |
||||
</div> |
||||
</li> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default hot(module)(AlertRuleList); |
@ -1,30 +0,0 @@ |
||||
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 backendSrv={backendSrv} {...store} />); |
||||
|
||||
setTimeout(() => { |
||||
expect(page.toJSON()).toMatchSnapshot(); |
||||
done(); |
||||
}); |
||||
}); |
||||
}); |
@ -1,48 +0,0 @@ |
||||
import React from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { inject, observer } from 'mobx-react'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import ContainerProps from 'app/containers/ContainerProps'; |
||||
|
||||
@inject('nav', 'serverStats') |
||||
@observer |
||||
export class ServerStats extends React.Component<ContainerProps, 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> |
||||
); |
||||
} |
||||
|
||||
export default hot(module)(ServerStats); |
@ -0,0 +1,3 @@ |
||||
import { updateLocation } from './location'; |
||||
|
||||
export { updateLocation }; |
@ -0,0 +1,13 @@ |
||||
import { LocationUpdate } from 'app/types'; |
||||
|
||||
export type Action = UpdateLocationAction; |
||||
|
||||
export interface UpdateLocationAction { |
||||
type: 'UPDATE_LOCATION'; |
||||
payload: LocationUpdate; |
||||
} |
||||
|
||||
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({ |
||||
type: 'UPDATE_LOCATION', |
||||
payload: location, |
||||
}); |
@ -0,0 +1,13 @@ |
||||
export type Action = UpdateNavIndexAction; |
||||
|
||||
// this action is not used yet
|
||||
// kind of just a placeholder, will be need for dynamic pages
|
||||
// like datasource edit, teams edit page
|
||||
|
||||
export interface UpdateNavIndexAction { |
||||
type: 'UPDATE_NAV_INDEX'; |
||||
} |
||||
|
||||
export const updateNavIndex = (): UpdateNavIndexAction => ({ |
||||
type: 'UPDATE_NAV_INDEX', |
||||
}); |
@ -0,0 +1,7 @@ |
||||
import { navIndexReducer as navIndex } from './navModel'; |
||||
import { locationReducer as location } from './location'; |
||||
|
||||
export default { |
||||
navIndex, |
||||
location, |
||||
}; |
@ -0,0 +1,33 @@ |
||||
import { Action } from 'app/core/actions/location'; |
||||
import { LocationState, UrlQueryMap } from 'app/types'; |
||||
import { toUrlParams } from 'app/core/utils/url'; |
||||
|
||||
export const initialState: LocationState = { |
||||
url: '', |
||||
path: '', |
||||
query: {}, |
||||
routeParams: {}, |
||||
}; |
||||
|
||||
function renderUrl(path: string, query: UrlQueryMap): string { |
||||
if (Object.keys(query).length > 0) { |
||||
path += '?' + toUrlParams(query); |
||||
} |
||||
return path; |
||||
} |
||||
|
||||
export const locationReducer = (state = initialState, action: Action): LocationState => { |
||||
switch (action.type) { |
||||
case 'UPDATE_LOCATION': { |
||||
const { path, query, routeParams } = action.payload; |
||||
return { |
||||
url: renderUrl(path || state.path, query), |
||||
path: path || state.path, |
||||
query: query || state.query, |
||||
routeParams: routeParams || state.routeParams, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return state; |
||||
}; |
@ -0,0 +1,29 @@ |
||||
import { Action } from 'app/core/actions/navModel'; |
||||
import { NavModelItem, NavIndex } from 'app/types'; |
||||
import config from 'app/core/config'; |
||||
|
||||
export function buildInitialState(): NavIndex { |
||||
const navIndex: NavIndex = {}; |
||||
const rootNodes = config.bootData.navTree as NavModelItem[]; |
||||
buildNavIndex(navIndex, rootNodes); |
||||
return navIndex; |
||||
} |
||||
|
||||
function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) { |
||||
for (const node of children) { |
||||
navIndex[node.id] = { |
||||
...node, |
||||
parentItem: parentItem, |
||||
}; |
||||
|
||||
if (node.children) { |
||||
buildNavIndex(navIndex, node.children, node); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const initialState: NavIndex = buildInitialState(); |
||||
|
||||
export const navIndexReducer = (state = initialState, action: Action): NavIndex => { |
||||
return state; |
||||
}; |
@ -0,0 +1,39 @@ |
||||
import { NavModel, NavModelItem, NavIndex } from 'app/types'; |
||||
|
||||
function getNotFoundModel(): NavModel { |
||||
const node: NavModelItem = { |
||||
id: 'not-found', |
||||
text: 'Page not found', |
||||
icon: 'fa fa-fw fa-warning', |
||||
subTitle: '404 Error', |
||||
url: 'not-found', |
||||
}; |
||||
|
||||
return { |
||||
node: node, |
||||
main: node, |
||||
}; |
||||
} |
||||
|
||||
export function getNavModel(navIndex: NavIndex, id: string): NavModel { |
||||
if (navIndex[id]) { |
||||
const node = navIndex[id]; |
||||
const main = { |
||||
...node.parentItem, |
||||
}; |
||||
|
||||
main.children = main.children.map(item => { |
||||
return { |
||||
...item, |
||||
active: item.url === node.url, |
||||
}; |
||||
}); |
||||
|
||||
return { |
||||
node: node, |
||||
main: main, |
||||
}; |
||||
} else { |
||||
return getNotFoundModel(); |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
import React from 'react'; |
||||
import renderer from 'react-test-renderer'; |
||||
import { ServerStats } from './ServerStats'; |
||||
import { createNavModel } from 'test/mocks/common'; |
||||
import { ServerStat } from './state/apis'; |
||||
|
||||
describe('ServerStats', () => { |
||||
it('Should render table with stats', done => { |
||||
const navModel = createNavModel('Admin', 'stats'); |
||||
const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }]; |
||||
|
||||
const getServerStats = () => { |
||||
return Promise.resolve(stats); |
||||
}; |
||||
|
||||
const page = renderer.create(<ServerStats navModel={navModel} getServerStats={getServerStats} />); |
||||
|
||||
setTimeout(() => { |
||||
expect(page.toJSON()).toMatchSnapshot(); |
||||
done(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,73 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { connect } from 'react-redux'; |
||||
import { NavModel, StoreState } from 'app/types'; |
||||
import { getNavModel } from 'app/core/selectors/navModel'; |
||||
import { getServerStats, ServerStat } from './state/apis'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
|
||||
interface Props { |
||||
navModel: NavModel; |
||||
getServerStats: () => Promise<ServerStat[]>; |
||||
} |
||||
|
||||
interface State { |
||||
stats: ServerStat[]; |
||||
} |
||||
|
||||
export class ServerStats extends PureComponent<Props, State> { |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
stats: [], |
||||
}; |
||||
} |
||||
|
||||
async componentDidMount() { |
||||
try { |
||||
const stats = await this.props.getServerStats(); |
||||
this.setState({ stats }); |
||||
} catch (error) { |
||||
console.error(error); |
||||
} |
||||
} |
||||
|
||||
render() { |
||||
const { navModel } = this.props; |
||||
const { stats } = this.state; |
||||
|
||||
return ( |
||||
<div> |
||||
<PageHeader model={navModel} /> |
||||
<div className="page-container page-body"> |
||||
<table className="filter-table form-inline"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Value</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody>{stats.map(StatItem)}</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function StatItem(stat: ServerStat) { |
||||
return ( |
||||
<tr key={stat.name}> |
||||
<td>{stat.name}</td> |
||||
<td>{stat.value}</td> |
||||
</tr> |
||||
); |
||||
} |
||||
|
||||
const mapStateToProps = (state: StoreState) => ({ |
||||
navModel: getNavModel(state.navIndex, 'server-stats'), |
||||
getServerStats: getServerStats, |
||||
}); |
||||
|
||||
export default hot(module)(connect(mapStateToProps)(ServerStats)); |
@ -0,0 +1,26 @@ |
||||
import { getBackendSrv } from 'app/core/services/backend_srv'; |
||||
|
||||
export interface ServerStat { |
||||
name: string; |
||||
value: number; |
||||
} |
||||
|
||||
export const getServerStats = async (): Promise<ServerStat[]> => { |
||||
try { |
||||
const res = await getBackendSrv().get('api/admin/stats'); |
||||
return [ |
||||
{ name: 'Total users', value: res.users }, |
||||
{ name: 'Total dashboards', value: res.dashboards }, |
||||
{ name: 'Active users (seen last 30 days)', value: res.activeUsers }, |
||||
{ name: 'Total orgs', value: res.orgs }, |
||||
{ name: 'Total playlists', value: res.playlists }, |
||||
{ name: 'Total snapshots', value: res.snapshots }, |
||||
{ name: 'Total dashboard tags', value: res.tags }, |
||||
{ name: 'Total starred dashboards', value: res.stars }, |
||||
{ name: 'Total alerts', value: res.alerts }, |
||||
]; |
||||
} catch (error) { |
||||
console.error(error); |
||||
throw error; |
||||
} |
||||
}; |
@ -0,0 +1,38 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import AlertRuleItem, { Props } from './AlertRuleItem'; |
||||
|
||||
jest.mock('react-redux', () => ({ |
||||
connect: () => params => params, |
||||
})); |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
rule: { |
||||
id: 1, |
||||
dashboardId: 1, |
||||
panelId: 1, |
||||
name: 'Some rule', |
||||
state: 'Open', |
||||
stateText: 'state text', |
||||
stateIcon: 'icon', |
||||
stateClass: 'state class', |
||||
stateAge: 'age', |
||||
url: 'https://something.something.darkside', |
||||
}, |
||||
search: '', |
||||
onTogglePause: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
return shallow(<AlertRuleItem {...props} />); |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
@ -0,0 +1,69 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import Highlighter from 'react-highlight-words'; |
||||
import classNames from 'classnames/bind'; |
||||
import { AlertRule } from '../../types'; |
||||
|
||||
export interface Props { |
||||
rule: AlertRule; |
||||
search: string; |
||||
onTogglePause: () => void; |
||||
} |
||||
|
||||
class AlertRuleItem extends PureComponent<Props> { |
||||
renderText(text: string) { |
||||
return ( |
||||
<Highlighter |
||||
highlightClassName="highlight-search-match" |
||||
textToHighlight={text} |
||||
searchWords={[this.props.search]} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
render() { |
||||
const { rule, onTogglePause } = this.props; |
||||
|
||||
const stateClass = classNames({ |
||||
fa: true, |
||||
'fa-play': rule.state === 'paused', |
||||
'fa-pause': rule.state !== 'paused', |
||||
}); |
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; |
||||
|
||||
return ( |
||||
<li className="alert-rule-item"> |
||||
<span className={`alert-rule-item__icon ${rule.stateClass}`}> |
||||
<i className={rule.stateIcon} /> |
||||
</span> |
||||
<div className="alert-rule-item__body"> |
||||
<div className="alert-rule-item__header"> |
||||
<div className="alert-rule-item__name"> |
||||
<a href={ruleUrl}>{this.renderText(rule.name)}</a> |
||||
</div> |
||||
<div className="alert-rule-item__text"> |
||||
<span className={`${rule.stateClass}`}>{this.renderText(rule.stateText)}</span> |
||||
<span className="alert-rule-item__time"> for {rule.stateAge}</span> |
||||
</div> |
||||
</div> |
||||
{rule.info && <div className="small muted alert-rule-item__info">{this.renderText(rule.info)}</div>} |
||||
</div> |
||||
|
||||
<div className="alert-rule-item__actions"> |
||||
<button |
||||
className="btn btn-small btn-inverse alert-list__btn width-2" |
||||
title="Pausing an alert rule prevents it from executing" |
||||
onClick={onTogglePause} |
||||
> |
||||
<i className={stateClass} /> |
||||
</button> |
||||
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule"> |
||||
<i className="icon-gf icon-gf-settings" /> |
||||
</a> |
||||
</div> |
||||
</li> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default AlertRuleItem; |
@ -0,0 +1,156 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { AlertRuleList, Props } from './AlertRuleList'; |
||||
import { AlertRule, NavModel } from '../../types'; |
||||
import appEvents from '../../core/app_events'; |
||||
|
||||
jest.mock('../../core/app_events', () => ({ |
||||
emit: jest.fn(), |
||||
})); |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
navModel: {} as NavModel, |
||||
alertRules: [] as AlertRule[], |
||||
updateLocation: jest.fn(), |
||||
getAlertRulesAsync: jest.fn(), |
||||
setSearchQuery: jest.fn(), |
||||
togglePauseAlertRule: jest.fn(), |
||||
stateFilter: '', |
||||
search: '', |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = shallow(<AlertRuleList {...props} />); |
||||
|
||||
return { |
||||
wrapper, |
||||
instance: wrapper.instance() as AlertRuleList, |
||||
}; |
||||
}; |
||||
|
||||
describe('Render', () => { |
||||
it('should render component', () => { |
||||
const { wrapper } = setup(); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
|
||||
it('should render alert rules', () => { |
||||
const { wrapper } = setup({ |
||||
alertRules: [ |
||||
{ |
||||
id: 1, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - Always OK', |
||||
state: 'ok', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: {}, |
||||
executionError: '', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
{ |
||||
id: 3, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - ok', |
||||
state: 'ok', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: {}, |
||||
executionError: 'error', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
|
||||
describe('Life cycle', () => { |
||||
describe('component did mount', () => { |
||||
it('should call fetchrules', () => { |
||||
const { instance } = setup(); |
||||
instance.fetchRules = jest.fn(); |
||||
instance.componentDidMount(); |
||||
expect(instance.fetchRules).toHaveBeenCalled(); |
||||
}); |
||||
}); |
||||
|
||||
describe('component did update', () => { |
||||
it('should call fetchrules if props differ', () => { |
||||
const { instance } = setup(); |
||||
instance.fetchRules = jest.fn(); |
||||
|
||||
instance.componentDidUpdate({ stateFilter: 'ok' } as Props); |
||||
|
||||
expect(instance.fetchRules).toHaveBeenCalled(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('Functions', () => { |
||||
describe('Get state filter', () => { |
||||
it('should get all if prop is not set', () => { |
||||
const { instance } = setup(); |
||||
|
||||
const stateFilter = instance.getStateFilter(); |
||||
|
||||
expect(stateFilter).toEqual('all'); |
||||
}); |
||||
|
||||
it('should return state filter if set', () => { |
||||
const { instance } = setup({ |
||||
stateFilter: 'ok', |
||||
}); |
||||
|
||||
const stateFilter = instance.getStateFilter(); |
||||
|
||||
expect(stateFilter).toEqual('ok'); |
||||
}); |
||||
}); |
||||
|
||||
describe('State filter changed', () => { |
||||
it('should update location', () => { |
||||
const { instance } = setup(); |
||||
const mockEvent = { target: { value: 'alerting' } }; |
||||
|
||||
instance.onStateFilterChanged(mockEvent); |
||||
|
||||
expect(instance.props.updateLocation).toHaveBeenCalledWith({ query: { state: 'alerting' } }); |
||||
}); |
||||
}); |
||||
|
||||
describe('Open how to', () => { |
||||
it('should emit show-modal event', () => { |
||||
const { instance } = setup(); |
||||
|
||||
instance.onOpenHowTo(); |
||||
|
||||
expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { |
||||
src: 'public/app/features/alerting/partials/alert_howto.html', |
||||
modalClass: 'confirm-modal', |
||||
model: {}, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('Search query change', () => { |
||||
it('should set search query', () => { |
||||
const { instance } = setup(); |
||||
const mockEvent = { target: { value: 'dashboard' } }; |
||||
|
||||
instance.onSearchQueryChange(mockEvent); |
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard'); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,153 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { connect } from 'react-redux'; |
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader'; |
||||
import AlertRuleItem from './AlertRuleItem'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import { updateLocation } from 'app/core/actions'; |
||||
import { getNavModel } from 'app/core/selectors/navModel'; |
||||
import { NavModel, StoreState, AlertRule } from 'app/types'; |
||||
import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions'; |
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors'; |
||||
|
||||
export interface Props { |
||||
navModel: NavModel; |
||||
alertRules: AlertRule[]; |
||||
updateLocation: typeof updateLocation; |
||||
getAlertRulesAsync: typeof getAlertRulesAsync; |
||||
setSearchQuery: typeof setSearchQuery; |
||||
togglePauseAlertRule: typeof togglePauseAlertRule; |
||||
stateFilter: string; |
||||
search: string; |
||||
} |
||||
|
||||
export class AlertRuleList extends PureComponent<Props, 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' }, |
||||
]; |
||||
|
||||
componentDidMount() { |
||||
this.fetchRules(); |
||||
} |
||||
|
||||
componentDidUpdate(prevProps: Props) { |
||||
if (prevProps.stateFilter !== this.props.stateFilter) { |
||||
this.fetchRules(); |
||||
} |
||||
} |
||||
|
||||
async fetchRules() { |
||||
await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); |
||||
} |
||||
|
||||
getStateFilter(): string { |
||||
const { stateFilter } = this.props; |
||||
if (stateFilter) { |
||||
return stateFilter.toString(); |
||||
} |
||||
return 'all'; |
||||
} |
||||
|
||||
onStateFilterChanged = event => { |
||||
this.props.updateLocation({ |
||||
query: { state: event.target.value }, |
||||
}); |
||||
}; |
||||
|
||||
onOpenHowTo = () => { |
||||
appEvents.emit('show-modal', { |
||||
src: 'public/app/features/alerting/partials/alert_howto.html', |
||||
modalClass: 'confirm-modal', |
||||
model: {}, |
||||
}); |
||||
}; |
||||
|
||||
onSearchQueryChange = event => { |
||||
const { value } = event.target; |
||||
this.props.setSearchQuery(value); |
||||
}; |
||||
|
||||
onTogglePause = (rule: AlertRule) => { |
||||
this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' }); |
||||
}; |
||||
|
||||
alertStateFilterOption = ({ text, value }) => { |
||||
return ( |
||||
<option key={value} value={value}> |
||||
{text} |
||||
</option> |
||||
); |
||||
}; |
||||
|
||||
render() { |
||||
const { navModel, alertRules, search } = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<PageHeader model={navModel} /> |
||||
<div className="page-container page-body"> |
||||
<div className="page-action-bar"> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form--has-input-icon gf-form--grow"> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input" |
||||
placeholder="Search alerts" |
||||
value={search} |
||||
onChange={this.onSearchQueryChange} |
||||
/> |
||||
<i className="gf-form-input-icon fa fa-search" /> |
||||
</label> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<label className="gf-form-label">States</label> |
||||
|
||||
<div className="gf-form-select-wrapper width-13"> |
||||
<select className="gf-form-input" onChange={this.onStateFilterChanged} value={this.getStateFilter()}> |
||||
{this.stateFilters.map(this.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> |
||||
<ol className="alert-rule-list"> |
||||
{alertRules.map(rule => ( |
||||
<AlertRuleItem |
||||
rule={rule} |
||||
key={rule.id} |
||||
search={search} |
||||
onTogglePause={() => this.onTogglePause(rule)} |
||||
/> |
||||
))} |
||||
</ol> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
const mapStateToProps = (state: StoreState) => ({ |
||||
navModel: getNavModel(state.navIndex, 'alert-list'), |
||||
alertRules: getAlertRuleItems(state.alertRules), |
||||
stateFilter: state.location.query.state, |
||||
search: getSearchQuery(state.alertRules), |
||||
}); |
||||
|
||||
const mapDispatchToProps = { |
||||
updateLocation, |
||||
getAlertRulesAsync, |
||||
setSearchQuery, |
||||
togglePauseAlertRule, |
||||
}; |
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); |
@ -1,7 +1,7 @@ |
||||
import _ from 'lodash'; |
||||
import { ThresholdMapper } from './threshold_mapper'; |
||||
import { ThresholdMapper } from './state/ThresholdMapper'; |
||||
import { QueryPart } from 'app/core/components/query_part/query_part'; |
||||
import alertDef from './alert_def'; |
||||
import alertDef from './state/alertDef'; |
||||
import config from 'app/core/config'; |
||||
import appEvents from 'app/core/app_events'; |
||||
|
@ -0,0 +1,256 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`Render should render alert rules 1`] = ` |
||||
<div> |
||||
<PageHeader |
||||
model={Object {}} |
||||
/> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon gf-form--grow" |
||||
> |
||||
<input |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
placeholder="Search alerts" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
</div> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<label |
||||
className="gf-form-label" |
||||
> |
||||
States |
||||
</label> |
||||
<div |
||||
className="gf-form-select-wrapper width-13" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="all" |
||||
> |
||||
<option |
||||
key="all" |
||||
value="all" |
||||
> |
||||
All |
||||
</option> |
||||
<option |
||||
key="ok" |
||||
value="ok" |
||||
> |
||||
OK |
||||
</option> |
||||
<option |
||||
key="not_ok" |
||||
value="not_ok" |
||||
> |
||||
Not OK |
||||
</option> |
||||
<option |
||||
key="alerting" |
||||
value="alerting" |
||||
> |
||||
Alerting |
||||
</option> |
||||
<option |
||||
key="no_data" |
||||
value="no_data" |
||||
> |
||||
No Data |
||||
</option> |
||||
<option |
||||
key="paused" |
||||
value="paused" |
||||
> |
||||
Paused |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<a |
||||
className="btn btn-secondary" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-info-circle" |
||||
/> |
||||
How to add an alert |
||||
</a> |
||||
</div> |
||||
<section> |
||||
<ol |
||||
className="alert-rule-list" |
||||
> |
||||
<AlertRuleItem |
||||
key="1" |
||||
onTogglePause={[Function]} |
||||
rule={ |
||||
Object { |
||||
"dashboardId": 7, |
||||
"dashboardSlug": "alerting-with-testdata", |
||||
"dashboardUid": "ggHbN42mk", |
||||
"evalData": Object {}, |
||||
"evalDate": "0001-01-01T00:00:00Z", |
||||
"executionError": "", |
||||
"id": 1, |
||||
"name": "TestData - Always OK", |
||||
"newStateDate": "2018-09-04T10:01:01+02:00", |
||||
"panelId": 3, |
||||
"state": "ok", |
||||
"url": "/d/ggHbN42mk/alerting-with-testdata", |
||||
} |
||||
} |
||||
search="" |
||||
/> |
||||
<AlertRuleItem |
||||
key="3" |
||||
onTogglePause={[Function]} |
||||
rule={ |
||||
Object { |
||||
"dashboardId": 7, |
||||
"dashboardSlug": "alerting-with-testdata", |
||||
"dashboardUid": "ggHbN42mk", |
||||
"evalData": Object {}, |
||||
"evalDate": "0001-01-01T00:00:00Z", |
||||
"executionError": "error", |
||||
"id": 3, |
||||
"name": "TestData - ok", |
||||
"newStateDate": "2018-09-04T10:01:01+02:00", |
||||
"panelId": 3, |
||||
"state": "ok", |
||||
"url": "/d/ggHbN42mk/alerting-with-testdata", |
||||
} |
||||
} |
||||
search="" |
||||
/> |
||||
</ol> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
||||
exports[`Render should render component 1`] = ` |
||||
<div> |
||||
<PageHeader |
||||
model={Object {}} |
||||
/> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<div |
||||
className="page-action-bar" |
||||
> |
||||
<div |
||||
className="gf-form gf-form--grow" |
||||
> |
||||
<label |
||||
className="gf-form--has-input-icon gf-form--grow" |
||||
> |
||||
<input |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
placeholder="Search alerts" |
||||
type="text" |
||||
value="" |
||||
/> |
||||
<i |
||||
className="gf-form-input-icon fa fa-search" |
||||
/> |
||||
</label> |
||||
</div> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<label |
||||
className="gf-form-label" |
||||
> |
||||
States |
||||
</label> |
||||
<div |
||||
className="gf-form-select-wrapper width-13" |
||||
> |
||||
<select |
||||
className="gf-form-input" |
||||
onChange={[Function]} |
||||
value="all" |
||||
> |
||||
<option |
||||
key="all" |
||||
value="all" |
||||
> |
||||
All |
||||
</option> |
||||
<option |
||||
key="ok" |
||||
value="ok" |
||||
> |
||||
OK |
||||
</option> |
||||
<option |
||||
key="not_ok" |
||||
value="not_ok" |
||||
> |
||||
Not OK |
||||
</option> |
||||
<option |
||||
key="alerting" |
||||
value="alerting" |
||||
> |
||||
Alerting |
||||
</option> |
||||
<option |
||||
key="no_data" |
||||
value="no_data" |
||||
> |
||||
No Data |
||||
</option> |
||||
<option |
||||
key="paused" |
||||
value="paused" |
||||
> |
||||
Paused |
||||
</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
<div |
||||
className="page-action-bar__spacer" |
||||
/> |
||||
<a |
||||
className="btn btn-secondary" |
||||
onClick={[Function]} |
||||
> |
||||
<i |
||||
className="fa fa-info-circle" |
||||
/> |
||||
How to add an alert |
||||
</a> |
||||
</div> |
||||
<section> |
||||
<ol |
||||
className="alert-rule-list" |
||||
/> |
||||
</section> |
||||
</div> |
||||
</div> |
||||
`; |
@ -1,2 +0,0 @@ |
||||
import './notifications_list_ctrl'; |
||||
import './notification_edit_ctrl'; |
@ -1,6 +1,6 @@ |
||||
import { describe, it, expect } from 'test/lib/common'; |
||||
|
||||
import { ThresholdMapper } from '../threshold_mapper'; |
||||
import { ThresholdMapper } from './ThresholdMapper'; |
||||
|
||||
describe('ThresholdMapper', () => { |
||||
describe('with greater than evaluator', () => { |
@ -0,0 +1,47 @@ |
||||
import { getBackendSrv } from 'app/core/services/backend_srv'; |
||||
import { AlertRuleApi, StoreState } from 'app/types'; |
||||
import { ThunkAction } from 'redux-thunk'; |
||||
|
||||
export enum ActionTypes { |
||||
LoadAlertRules = 'LOAD_ALERT_RULES', |
||||
SetSearchQuery = 'SET_SEARCH_QUERY', |
||||
} |
||||
|
||||
export interface LoadAlertRulesAction { |
||||
type: ActionTypes.LoadAlertRules; |
||||
payload: AlertRuleApi[]; |
||||
} |
||||
|
||||
export interface SetSearchQueryAction { |
||||
type: ActionTypes.SetSearchQuery; |
||||
payload: string; |
||||
} |
||||
|
||||
export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({ |
||||
type: ActionTypes.LoadAlertRules, |
||||
payload: rules, |
||||
}); |
||||
|
||||
export const setSearchQuery = (query: string): SetSearchQueryAction => ({ |
||||
type: ActionTypes.SetSearchQuery, |
||||
payload: query, |
||||
}); |
||||
|
||||
export type Action = LoadAlertRulesAction | SetSearchQueryAction; |
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>; |
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> { |
||||
return async dispatch => { |
||||
const rules = await getBackendSrv().get('/api/alerts', options); |
||||
dispatch(loadAlertRules(rules)); |
||||
}; |
||||
} |
||||
|
||||
export function togglePauseAlertRule(id: number, options: { paused: boolean }): ThunkResult<void> { |
||||
return async (dispatch, getState) => { |
||||
await getBackendSrv().post(`/api/alerts/${id}/pause`, options); |
||||
const stateFilter = getState().location.query.state || 'all'; |
||||
dispatch(getAlertRulesAsync({ state: stateFilter.toString() })); |
||||
}; |
||||
} |
@ -0,0 +1,91 @@ |
||||
import { ActionTypes, Action } from './actions'; |
||||
import { alertRulesReducer, initialState } from './reducers'; |
||||
import { AlertRuleApi } from '../../../types'; |
||||
|
||||
describe('Alert rules', () => { |
||||
const payload: AlertRuleApi[] = [ |
||||
{ |
||||
id: 2, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 4, |
||||
name: 'TestData - Always Alerting', |
||||
state: 'alerting', |
||||
newStateDate: '2018-09-04T10:00:30+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: { evalMatches: [{ metric: 'A-series', tags: null, value: 215 }] }, |
||||
executionError: '', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
{ |
||||
id: 1, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - Always OK', |
||||
state: 'ok', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: {}, |
||||
executionError: '', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
{ |
||||
id: 3, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - ok', |
||||
state: 'ok', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: {}, |
||||
executionError: 'error', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
{ |
||||
id: 4, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - Paused', |
||||
state: 'paused', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: {}, |
||||
executionError: 'error', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
{ |
||||
id: 5, |
||||
dashboardId: 7, |
||||
dashboardUid: 'ggHbN42mk', |
||||
dashboardSlug: 'alerting-with-testdata', |
||||
panelId: 3, |
||||
name: 'TestData - Ok', |
||||
state: 'ok', |
||||
newStateDate: '2018-09-04T10:01:01+02:00', |
||||
evalDate: '0001-01-01T00:00:00Z', |
||||
evalData: { |
||||
noData: true, |
||||
}, |
||||
executionError: 'error', |
||||
url: '/d/ggHbN42mk/alerting-with-testdata', |
||||
}, |
||||
]; |
||||
|
||||
it('should set alert rules', () => { |
||||
const action: Action = { |
||||
type: ActionTypes.LoadAlertRules, |
||||
payload: payload, |
||||
}; |
||||
|
||||
const result = alertRulesReducer(initialState, action); |
||||
|
||||
expect(result.items).toEqual(payload); |
||||
}); |
||||
}); |
@ -0,0 +1,50 @@ |
||||
import moment from 'moment'; |
||||
import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types'; |
||||
import { Action, ActionTypes } from './actions'; |
||||
import alertDef from './alertDef'; |
||||
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '' }; |
||||
|
||||
function convertToAlertRule(rule, state): AlertRule { |
||||
const stateModel = alertDef.getStateDisplayModel(state); |
||||
rule.stateText = stateModel.text; |
||||
rule.stateIcon = stateModel.iconClass; |
||||
rule.stateClass = stateModel.stateClass; |
||||
rule.stateAge = moment(rule.newStateDate) |
||||
.fromNow() |
||||
.replace(' ago', ''); |
||||
|
||||
if (rule.state !== 'paused') { |
||||
if (rule.executionError) { |
||||
rule.info = 'Execution Error: ' + rule.executionError; |
||||
} |
||||
if (rule.evalData && rule.evalData.noData) { |
||||
rule.info = 'Query returned no data'; |
||||
} |
||||
} |
||||
|
||||
return rule; |
||||
} |
||||
|
||||
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => { |
||||
switch (action.type) { |
||||
case ActionTypes.LoadAlertRules: { |
||||
const alertRules: AlertRuleApi[] = action.payload; |
||||
|
||||
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => { |
||||
return convertToAlertRule(rule, rule.state); |
||||
}); |
||||
|
||||
return { items: alertRulesViewModel, searchQuery: state.searchQuery }; |
||||
} |
||||
|
||||
case ActionTypes.SetSearchQuery: |
||||
return { items: state.items, searchQuery: action.payload }; |
||||
} |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
export default { |
||||
alertRules: alertRulesReducer, |
||||
}; |
@ -0,0 +1,94 @@ |
||||
import { getSearchQuery, getAlertRuleItems } from './selectors'; |
||||
|
||||
describe('Get search query', () => { |
||||
it('should get search query', () => { |
||||
const state = { searchQuery: 'dashboard' }; |
||||
const result = getSearchQuery(state); |
||||
|
||||
expect(result).toEqual(state.searchQuery); |
||||
}); |
||||
}); |
||||
|
||||
describe('Get alert rule items', () => { |
||||
it('should get alert rule items', () => { |
||||
const state = { |
||||
items: [ |
||||
{ |
||||
id: 1, |
||||
dashboardId: 1, |
||||
panelId: 1, |
||||
name: '', |
||||
state: '', |
||||
stateText: '', |
||||
stateIcon: '', |
||||
stateClass: '', |
||||
stateAge: '', |
||||
url: '', |
||||
}, |
||||
], |
||||
searchQuery: '', |
||||
}; |
||||
|
||||
const result = getAlertRuleItems(state); |
||||
expect(result.length).toEqual(1); |
||||
}); |
||||
|
||||
it('should filter rule items based on search query', () => { |
||||
const state = { |
||||
items: [ |
||||
{ |
||||
id: 1, |
||||
dashboardId: 1, |
||||
panelId: 1, |
||||
name: 'dashboard', |
||||
state: '', |
||||
stateText: '', |
||||
stateIcon: '', |
||||
stateClass: '', |
||||
stateAge: '', |
||||
url: '', |
||||
}, |
||||
{ |
||||
id: 2, |
||||
dashboardId: 3, |
||||
panelId: 1, |
||||
name: 'dashboard2', |
||||
state: '', |
||||
stateText: '', |
||||
stateIcon: '', |
||||
stateClass: '', |
||||
stateAge: '', |
||||
url: '', |
||||
}, |
||||
{ |
||||
id: 3, |
||||
dashboardId: 5, |
||||
panelId: 1, |
||||
name: 'hello', |
||||
state: '', |
||||
stateText: '', |
||||
stateIcon: '', |
||||
stateClass: '', |
||||
stateAge: '', |
||||
url: '', |
||||
}, |
||||
{ |
||||
id: 4, |
||||
dashboardId: 7, |
||||
panelId: 1, |
||||
name: 'test', |
||||
state: '', |
||||
stateText: 'dashboard', |
||||
stateIcon: '', |
||||
stateClass: '', |
||||
stateAge: '', |
||||
url: '', |
||||
}, |
||||
], |
||||
searchQuery: 'dashboard', |
||||
}; |
||||
|
||||
const result = getAlertRuleItems(state); |
||||
expect(result.length).toEqual(3); |
||||
}); |
||||
}); |
@ -0,0 +1,9 @@ |
||||
export const getSearchQuery = state => state.searchQuery; |
||||
|
||||
export const getAlertRuleItems = state => { |
||||
const regex = new RegExp(state.searchQuery, 'i'); |
||||
|
||||
return state.items.filter(item => { |
||||
return regex.test(item.name) || regex.test(item.stateText) || regex.test(item.info); |
||||
}); |
||||
}; |
@ -1,7 +1,7 @@ |
||||
import { PanelCtrl } from 'app/features/panel/panel_ctrl'; |
||||
import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl'; |
||||
import { QueryCtrl } from 'app/features/panel/query_ctrl'; |
||||
import { alertTab } from 'app/features/alerting/alert_tab_ctrl'; |
||||
import { alertTab } from 'app/features/alerting/AlertTabCtrl'; |
||||
import { loadPluginCss } from 'app/features/plugins/plugin_loader'; |
||||
|
||||
export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss }; |
||||
|
@ -1,66 +0,0 @@ |
||||
import { AlertListStore } from './AlertListStore'; |
||||
import { backendSrv } from 'test/mocks/common'; |
||||
import moment from 'moment'; |
||||
|
||||
function getRule(name, state, info) { |
||||
return { |
||||
id: 11, |
||||
dashboardId: 58, |
||||
panelId: 3, |
||||
name: name, |
||||
state: state, |
||||
newStateDate: moment() |
||||
.subtract(5, 'minutes') |
||||
.format(), |
||||
evalData: {}, |
||||
executionError: '', |
||||
url: 'db/mygool', |
||||
stateText: state, |
||||
stateIcon: 'fa', |
||||
stateClass: 'asd', |
||||
stateAge: '10m', |
||||
info: info, |
||||
canEdit: true, |
||||
}; |
||||
} |
||||
|
||||
describe('AlertListStore', () => { |
||||
let store; |
||||
|
||||
beforeAll(() => { |
||||
store = AlertListStore.create( |
||||
{ |
||||
rules: [ |
||||
getRule('Europe', 'OK', 'backend-01'), |
||||
getRule('Google', 'ALERTING', 'backend-02'), |
||||
getRule('Amazon', 'PAUSED', 'backend-03'), |
||||
getRule('West-Europe', 'PAUSED', 'backend-03'), |
||||
], |
||||
search: '', |
||||
}, |
||||
{ |
||||
backendSrv: backendSrv, |
||||
} |
||||
); |
||||
}); |
||||
|
||||
it('search should filter list on name', () => { |
||||
store.setSearchQuery('urope'); |
||||
expect(store.filteredRules).toHaveLength(2); |
||||
}); |
||||
|
||||
it('search should filter list on state', () => { |
||||
store.setSearchQuery('ale'); |
||||
expect(store.filteredRules).toHaveLength(1); |
||||
}); |
||||
|
||||
it('search should filter list on info', () => { |
||||
store.setSearchQuery('-0'); |
||||
expect(store.filteredRules).toHaveLength(4); |
||||
}); |
||||
|
||||
it('search should be equal', () => { |
||||
store.setSearchQuery('alert'); |
||||
expect(store.search).toBe('alert'); |
||||
}); |
||||
}); |
@ -1,47 +0,0 @@ |
||||
import { types, getEnv, flow } from 'mobx-state-tree'; |
||||
import { AlertRule as AlertRuleModel } from './AlertRule'; |
||||
import { setStateFields } from './helpers'; |
||||
|
||||
type AlertRuleType = typeof AlertRuleModel.Type; |
||||
export interface AlertRule extends AlertRuleType {} |
||||
|
||||
export const AlertListStore = types |
||||
.model('AlertListStore', { |
||||
rules: types.array(AlertRuleModel), |
||||
stateFilter: types.optional(types.string, 'all'), |
||||
search: types.optional(types.string, ''), |
||||
}) |
||||
.views(self => ({ |
||||
get filteredRules() { |
||||
const regex = new RegExp(self.search, 'i'); |
||||
return self.rules.filter(alert => { |
||||
return regex.test(alert.name) || regex.test(alert.stateText) || regex.test(alert.info); |
||||
}); |
||||
}, |
||||
})) |
||||
.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 (const rule of apiRules) { |
||||
setStateFields(rule, rule.state); |
||||
|
||||
if (rule.state !== 'paused') { |
||||
if (rule.executionError) { |
||||
rule.info = 'Execution Error: ' + rule.executionError; |
||||
} |
||||
if (rule.evalData && rule.evalData.noData) { |
||||
rule.info = 'Query returned no data'; |
||||
} |
||||
} |
||||
|
||||
self.rules.push(AlertRuleModel.create(rule)); |
||||
} |
||||
}), |
||||
setSearchQuery(query: string) { |
||||
self.search = query; |
||||
}, |
||||
})); |
@ -1,34 +0,0 @@ |
||||
import { types, getEnv, flow } from 'mobx-state-tree'; |
||||
import { setStateFields } from './helpers'; |
||||
|
||||
export const AlertRule = types |
||||
.model('AlertRule', { |
||||
id: types.identifier(types.number), |
||||
dashboardId: types.number, |
||||
panelId: types.number, |
||||
name: types.string, |
||||
state: types.string, |
||||
stateText: types.string, |
||||
stateIcon: types.string, |
||||
stateClass: types.string, |
||||
stateAge: types.string, |
||||
info: types.optional(types.string, ''), |
||||
url: types.string, |
||||
}) |
||||
.views(self => ({ |
||||
get isPaused() { |
||||
return self.state === 'paused'; |
||||
}, |
||||
})) |
||||
.actions(self => ({ |
||||
/** |
||||
* will toggle alert rule paused state |
||||
*/ |
||||
togglePaused: flow(function* togglePaused() { |
||||
const backendSrv = getEnv(self).backendSrv; |
||||
const payload = { paused: !self.isPaused }; |
||||
const res = yield backendSrv.post(`/api/alerts/${self.id}/pause`, payload); |
||||
setStateFields(self, res.state); |
||||
self.info = ''; |
||||
}), |
||||
})); |
@ -1,13 +0,0 @@ |
||||
import moment from 'moment'; |
||||
import alertDef from 'app/features/alerting/alert_def'; |
||||
|
||||
export function setStateFields(rule, state) { |
||||
const stateModel = alertDef.getStateDisplayModel(state); |
||||
rule.state = state; |
||||
rule.stateText = stateModel.text; |
||||
rule.stateIcon = stateModel.iconClass; |
||||
rule.stateClass = stateModel.stateClass; |
||||
rule.stateAge = moment(rule.newStateDate) |
||||
.fromNow() |
||||
.replace(' ago', ''); |
||||
} |
@ -1,22 +0,0 @@ |
||||
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,23 @@ |
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; |
||||
import thunk from 'redux-thunk'; |
||||
import { createLogger } from 'redux-logger'; |
||||
import sharedReducers from 'app/core/reducers'; |
||||
import alertingReducers from 'app/features/alerting/state/reducers'; |
||||
|
||||
const rootReducer = combineReducers({ |
||||
...sharedReducers, |
||||
...alertingReducers, |
||||
}); |
||||
|
||||
export let store; |
||||
|
||||
export function configureStore() { |
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; |
||||
|
||||
if (process.env.NODE_ENV !== 'production') { |
||||
// DEV builds we had the logger middleware
|
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); |
||||
} else { |
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))); |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
//
|
||||
// Location
|
||||
//
|
||||
|
||||
export interface LocationUpdate { |
||||
path?: string; |
||||
query?: UrlQueryMap; |
||||
routeParams?: UrlQueryMap; |
||||
} |
||||
|
||||
export interface LocationState { |
||||
url: string; |
||||
path: string; |
||||
query: UrlQueryMap; |
||||
routeParams: UrlQueryMap; |
||||
} |
||||
|
||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; |
||||
export type UrlQueryMap = { [s: string]: UrlQueryValue }; |
||||
|
||||
//
|
||||
// Alerting
|
||||
//
|
||||
|
||||
export interface AlertRuleApi { |
||||
id: number; |
||||
dashboardId: number; |
||||
dashboardUid: string; |
||||
dashboardSlug: string; |
||||
panelId: number; |
||||
name: string; |
||||
state: string; |
||||
newStateDate: string; |
||||
evalDate: string; |
||||
evalData?: object; |
||||
executionError: string; |
||||
url: string; |
||||
} |
||||
|
||||
export interface AlertRule { |
||||
id: number; |
||||
dashboardId: number; |
||||
panelId: number; |
||||
name: string; |
||||
state: string; |
||||
stateText: string; |
||||
stateIcon: string; |
||||
stateClass: string; |
||||
stateAge: string; |
||||
url: string; |
||||
info?: string; |
||||
executionError?: string; |
||||
evalData?: { noData: boolean }; |
||||
} |
||||
|
||||
//
|
||||
// NavModel
|
||||
//
|
||||
|
||||
export interface NavModelItem { |
||||
text: string; |
||||
url: string; |
||||
subTitle?: string; |
||||
icon?: string; |
||||
img?: string; |
||||
id: string; |
||||
active?: boolean; |
||||
hideFromTabs?: boolean; |
||||
divider?: boolean; |
||||
children?: NavModelItem[]; |
||||
breadcrumbs?: NavModelItem[]; |
||||
target?: string; |
||||
parentItem?: NavModelItem; |
||||
} |
||||
|
||||
export interface NavModel { |
||||
main: NavModelItem; |
||||
node: NavModelItem; |
||||
} |
||||
|
||||
export type NavIndex = { [s: string]: NavModelItem }; |
||||
|
||||
//
|
||||
// Store
|
||||
//
|
||||
|
||||
export interface AlertRulesState { |
||||
items: AlertRule[]; |
||||
searchQuery: string; |
||||
} |
||||
|
||||
export interface StoreState { |
||||
navIndex: NavIndex; |
||||
location: LocationState; |
||||
alertRules: AlertRulesState; |
||||
} |
Loading…
Reference in new issue