refactoring: alert list improvments PR #10452

This commit is contained in:
Torkel Ödegaard 2018-01-10 11:54:47 +01:00
commit 5d5de23025
13 changed files with 393 additions and 235 deletions

View File

@ -150,6 +150,7 @@
"react-dom": "^16.2.0",
"react-grid-layout": "^0.16.1",
"react-popper": "^0.7.5",
"react-highlight-words": "^0.10.0",
"react-sizeme": "^2.3.6",
"remarkable": "^1.7.1",
"rxjs": "^5.4.3",

View File

@ -45,7 +45,7 @@ describe('AlertRuleList', () => {
it('should render 1 rule', () => {
page.update();
let ruleNode = page.find('.card-item-wrapper');
let ruleNode = page.find('.alert-rule-item');
expect(toJson(ruleNode)).toMatchSnapshot();
});

View File

@ -5,6 +5,7 @@ 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';
import Highlighter from 'react-highlight-words';
@inject('view', 'nav', 'alertList')
@observer
@ -23,11 +24,6 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
this.props.nav.load('alerting', 'alert-list');
this.fetchRules();
this.handleTooltipPositionChange = this.handleTooltipPositionChange.bind(this);
this.state = {
tooltipPosition: 'auto',
};
}
onStateFilterChanged = evt => {
@ -49,12 +45,10 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
});
};
handleTooltipPositionChange(evt) {
evt.preventDefault();
this.setState({
tooltipPosition: evt.target.value,
});
}
onSearchQueryChange = evt => {
this.props.alertList.setSearchQuery(evt.target.value);
};
render() {
const { nav, alertList } = this.props;
@ -63,8 +57,20 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
<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 alert"
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">Filter by state</label>
<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}>
@ -80,8 +86,12 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
</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>
<ol className="alert-rule-list">
{alertList.filteredRules.map(rule => (
<AlertRuleItem rule={rule} key={rule.id} search={alertList.search} />
))}
</ol>
</section>
</div>
</div>
@ -99,6 +109,7 @@ function AlertStateFilterOption({ text, value }) {
export interface AlertRuleItemProps {
rule: IAlertRule;
search: string;
}
@observer
@ -107,6 +118,16 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
this.props.rule.togglePaused();
};
renderText(text: string) {
return (
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={text}
searchWords={[this.props.search]}
/>
);
}
render() {
const { rule } = this.props;
@ -119,36 +140,33 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
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>}
<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">
<a
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} />
</a>
<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>
);

View File

@ -2,71 +2,102 @@
exports[`AlertRuleList should render 1 rule 1`] = `
<li
className="card-item-wrapper"
className="alert-rule-item"
>
<span
className="alert-rule-item__icon alert-state-ok"
>
<i
className="icon-gf icon-gf-online"
/>
</span>
<div
className="card-item card-item--alert"
className="alert-rule-item__body"
>
<div
className="card-item-header"
className="alert-rule-item__header"
>
<div
className="card-item-type"
className="alert-rule-item__name"
>
<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"
/>
<Highlighter
highlightClassName="highlight-search-match"
searchWords={
Array [
"",
]
}
textToHighlight="Panel Title alert"
>
<span>
<span
className=""
key="0"
>
Panel Title alert
</span>
</span>
</Highlighter>
</a>
</div>
</div>
<div
className="card-item-body"
>
<div
className="card-item-details"
className="alert-rule-item__text"
>
<div
className="card-item-name"
<span
className="alert-state-ok"
>
<a
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
<Highlighter
highlightClassName="highlight-search-match"
searchWords={
Array [
"",
]
}
textToHighlight="OK"
>
Panel Title alert
</a>
</div>
<div
className="card-item-sub-name"
<span>
<span
className=""
key="0"
>
OK
</span>
</span>
</Highlighter>
</span>
<span
className="alert-rule-item__time"
>
<span
className="alert-list-item-state alert-state-ok"
>
<i
className="icon-gf icon-gf-online"
/>
OK
</span>
<span>
for
5 minutes
</span>
</div>
for
5 minutes
</span>
</div>
</div>
</div>
<div
className="alert-rule-item__actions"
>
<a
className="btn btn-small btn-inverse alert-list__btn width-2"
onClick={[Function]}
title="Pausing an alert rule prevents it from executing"
>
<i
className="fa fa-pause"
/>
</a>
<a
className="btn btn-small btn-inverse alert-list__btn width-2"
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
title="Edit alert rule"
>
<i
className="icon-gf icon-gf-settings"
/>
</a>
</div>
</li>
`;

View File

@ -138,10 +138,6 @@ function getAlertAnnotationInfo(ah) {
return 'Error: ' + ah.data.error;
}
if (ah.data.noData || ah.data.no_data) {
return 'No Data';
}
return '';
}

View File

@ -12,7 +12,7 @@
<li ng-class="{active: ctrl.subTabIndex === 2}">
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
</li>
<li>
<li>
<a ng-click="ctrl.delete()">Delete</a>
</li>
</ul>
@ -143,36 +143,33 @@
<i>No state changes recorded</i>
</div>
<section class="card-section card-list-layout-list">
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
<div class="alert-list card-item card-item--alert">
<div class="alert-list-body">
<div class="alert-list-icon alert-list-item-state {{ah.stateModel.stateClass}}">
<i class="{{ah.stateModel.iconClass}}"></i>
</div>
<div class="alert-list-main alert-list-text">
<span class="alert-list-state {{ah.stateModel.stateClass}}">{{ah.stateModel.text}}</span>
<span class="alert-list-info">{{ah.info}}</span>
</div>
</div>
<div class="alert-list-footer alert-list-text">
<span>{{ah.time}}</span>
<span><!--Img Link--></span>
</div>
</div>
</li>
</ol>
</section>
</div>
</div>
<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}}">
<i class="{{al.stateModel.iconClass}}"></i>
</div>
<div class="alert-rule-item__body">
<div class="alert-rule-item__header">
<div class="alert-rule-item__text">
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
</div>
</div>
<span class="alert-list-info">{{al.info}}</span>
</div>
<div class="alert-rule-item__time">
<span>{{al.time}}</span>
</div>
</li>
</ol>
</div>
</div>
</div>
<div class="gf-form-group" ng-if="!ctrl.alert">
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.enable()">
<i class="icon-gf icon-gf-alert"></i>
Create Alert
</button>
</div>
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.enable()">
<i class="icon-gf icon-gf-alert"></i>
Create Alert
</button>
</div>
</div>

View File

@ -3,24 +3,22 @@
{{ctrl.noAlertsMessage}}
</div>
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
<div class="alert-list card-item card-item--alert">
<div class="alert-list-body">
<div class="alert-list-icon alert-list-item-state {{alert.stateModel.stateClass}}">
<i class="{{alert.stateModel.iconClass}}"></i>
</div>
<div class="alert-list-main">
<p class="alert-list-title">
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
{{alert.name}}
</a>
</p>
<p class="alert-list-text">
<span class="alert-list-state {{alert.stateModel.stateClass}}">{{alert.stateModel.text}}</span>
for {{alert.newStateDateAgo}}
</p>
<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__body">
<div class="alert-rule-item__icon {{alert.stateModel.stateClass}}">
<i class="{{alert.stateModel.iconClass}}"></i>
</div>
<div class="alert-rule-item__header">
<p class="alert-rule-item__name">
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
{{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>
@ -28,30 +26,25 @@
</ol>
</section>
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'changes'">
<ol class="card-list">
<li class="card-item-wrapper" ng-repeat="al in ctrl.alertHistory">
<div class="alert-list card-item card-item--alert">
<div class="alert-list-body">
<div class="alert-list-icon alert-list-item-state {{al.stateModel.stateClass}}">
<i class="{{al.stateModel.iconClass}}"></i>
</div>
<div class="alert-list-main">
<p class="alert-list-title">{{al.alertName}}</p>
<div class="alert-list-text">
<span class="alert-list-state {{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
<span class="alert-list-info alert-list-info-left">{{al.info}}</span>
</div>
</div>
</div>
<div class="alert-list-footer">
<span class="alert-list-text">{{al.time}}</span>
<span class="alert-list-text">
<!--Img Link-->
</span>
</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}}">
<i class="{{al.stateModel.iconClass}}"></i>
</div>
<div class="alert-rule-item__body">
<div class="alert-rule-item__header">
<p class="alert-rule-item__name">{{al.alertName}}</p>
<div class="alert-rule-item__text">
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
</div>
</div>
<span class="alert-rule-item__info">{{al.info}}</span>
</div>
<div class="alert-rule-item__time">
<span>{{al.time}}</span>
</div>
</li>
</ol>
</section>
</div>

View File

@ -0,0 +1,65 @@
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: '',
dashboardUri: 'db/mygool',
stateText: state,
stateIcon: 'fa',
stateClass: 'asd',
stateAge: '10m',
info: info,
};
}
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');
});
});

View File

@ -9,7 +9,16 @@ export const AlertListStore = types
.model('AlertListStore', {
rules: types.array(AlertRule),
stateFilter: types.optional(types.string, 'all'),
search: types.optional(types.string, ''),
})
.views(self => ({
get filteredRules() {
let 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;
@ -31,4 +40,7 @@ export const AlertListStore = types
self.rules.push(AlertRule.create(rule));
}
}),
setSearchQuery(query: string) {
self.search = query;
},
}));

View File

@ -299,7 +299,7 @@ blockquote {
line-height: $line-height-base;
color: $gray-2;
&:before {
content: "\2014 \00A0";
content: '\2014 \00A0';
}
}
@ -316,10 +316,10 @@ blockquote {
}
small {
&:before {
content: "";
content: '';
}
&:after {
content: "\00A0 \2014";
content: '\00A0 \2014';
}
}
}
@ -330,7 +330,7 @@ q:before,
q:after,
blockquote:before,
blockquote:after {
content: "";
content: '';
}
// Addresses
@ -409,3 +409,8 @@ a.external-link {
.no-wrap {
white-space: nowrap;
}
.highlight-search-match {
background: transparent;
color: $yellow;
}

View File

@ -33,6 +33,7 @@ $input-border: 1px solid $input-border-color;
.gf-form--has-input-icon {
position: relative;
margin-right: $gf-form-margin;
.gf-form-input-icon {
position: absolute;

View File

@ -28,69 +28,6 @@
border: 0;
}
// Alert List
.alert-list {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.alert-list-icon {
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
.icon-gf,
.fa {
font-size: 200%;
position: relative;
top: 2px;
}
}
.alert-list-body {
display: flex;
}
.alert-list-main {
padding: 0 2rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.alert-list-title {
font-size: $font-size-base;
margin: 0;
font-weight: 600;
}
.alert-list-state {
font-weight: bold;
}
.alert-list-text {
font-size: $font-size-sm;
margin: 0;
line-height: 1.5rem;
color: $text-color-weak;
}
.alert-list-info {
color: $text-color;
}
.alert-list-info-left {
padding-left: 2rem;
}
.alert-list-footer {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: flex-end;
}
.panel-has-alert {
.panel-alert-icon:before {
content: '\e611';
@ -136,3 +73,94 @@
opacity: 1;
}
}
// Alert List
// Alert List
.alert-rule-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
list-style-type: none;
}
.alert-rule-item {
display: flex;
width: 100%;
height: 100%;
background: $card-background;
box-shadow: $card-shadow;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 4px;
}
.alert-rule-item__body {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.alert-rule-item__icon {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
padding: 0 28px 0 16px;
.icon-gf,
.fa {
font-size: 200%;
position: relative;
top: 2px;
}
}
.alert-rule-item__header {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.alert-rule-item__name {
font-size: $font-size-base;
margin: 0;
font-weight: $font-weight-semi-bold;
}
.alert-list__btn {
margin: 0 2px;
display: flex;
align-items: center;
justify-content: center;
}
.alert-rule-item__text {
font-weight: bold;
font-size: $font-size-sm;
margin: 0;
}
.alert-rule-item__time {
color: $text-color-weak;
font-weight: normal;
white-space: nowrap;
}
.alert-rule-item__info {
//color: $text-color;
font-weight: normal;
flex-grow: 2;
display: flex;
align-items: flex-end;
}
.alert-rule-item__actions {
display: flex;
align-items: center;
}
.alert-tesint {
display: flex;
}

View File

@ -4430,6 +4430,10 @@ header-case@^1.0.0:
no-case "^2.2.0"
upper-case "^1.1.3"
highlight-words-core@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.1.2.tgz#5c2717c4f6c6e7ea2462ab85b43ff8b24f58ec3e"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -7921,7 +7925,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0:
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@ -8134,6 +8138,13 @@ react-popper@^0.7.5:
popper.js "^1.12.5"
prop-types "^15.5.10"
react-highlight-words@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.10.0.tgz#2e905c76c11635237f848ecad00600f1b6f6f4a8"
dependencies:
highlight-words-core "^1.1.0"
prop-types "^15.5.8"
react-resizable@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"