Merge pull request #1109 from influxdata/feature/alert-time

Alert time range
pull/10616/head
Andrew Watkins 2017-03-30 09:12:23 -07:00 committed by GitHub
commit 15c660076e
11 changed files with 535 additions and 39 deletions

View File

@ -891,6 +891,7 @@
* rimraf 2.5.3 [ISC](http://github.com/isaacs/rimraf)
* rimraf 2.5.4 [ISC](http://github.com/isaacs/rimraf)
* ripemd160 0.2.0 [Unknown](https://github.com/cryptocoinjs/ripemd160)
* rome 2.1.22 [MIT](https://github.com/bevacqua/rome)
* run-async 0.1.0 [MIT](http://github.com/SBoudrias/run-async)
* rx-lite 3.1.2 [Apache License](https://github.com/Reactive-Extensions/RxJS)
* samsam 1.1.2 [BSD](https://github.com/busterjs/samsam)

View File

@ -111,6 +111,7 @@
"react-tooltip": "^3.2.1",
"redux": "^3.3.1",
"redux-thunk": "^1.0.3",
"rome": "^2.1.22",
"updeep": "^0.13.0"
}
}

View File

@ -1,9 +1,9 @@
import {proxy} from 'utils/queryUrlGenerator'
export function getAlerts(proxyLink) {
export function getAlerts(source, timeRange) {
return proxy({
source: proxyLink,
query: "select host, value, level, alertName from alerts order by time desc",
source,
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc`,
db: "chronograf",
})
}

View File

@ -27,18 +27,19 @@ const AlertsTable = React.createClass({
},
componentWillReceiveProps(newProps) {
this.filterAlerts(newProps.alerts, this.state.searchTerm)
this.filterAlerts(this.state.searchTerm, newProps.alerts)
},
filterAlerts(searchTerm) {
const filteredAlerts = this.props.alerts.filter((h) => {
filterAlerts(searchTerm, newAlerts) {
const alerts = newAlerts || this.props.alerts
const filteredAlerts = alerts.filter((h) => {
if (h.host === null || h.name === null || h.level === null) {
return false
}
return h.name.toLowerCase().search((searchTerm).toLowerCase()) !== -1 ||
h.host.toLowerCase().search((searchTerm).toLowerCase()) !== -1 ||
h.level.toLowerCase().search((searchTerm).toLowerCase()) !== -1
h.host.toLowerCase().search((searchTerm).toLowerCase()) !== -1 ||
h.level.toLowerCase().search((searchTerm).toLowerCase()) !== -1
})
this.setState({searchTerm, filteredAlerts})
},

View File

@ -1,31 +1,36 @@
import React, {PropTypes} from 'react'
import AlertsTable from '../components/AlertsTable'
import React, {PropTypes, Component} from 'react'
import SourceIndicator from '../../shared/components/SourceIndicator'
import AlertsTable from '../components/AlertsTable'
import NoKapacitorError from '../../shared/components/NoKapacitorError'
import CustomTimeRange from '../../shared/components/CustomTimeRange'
import {getAlerts} from '../apis'
import AJAX from 'utils/ajax'
import _ from 'lodash'
import NoKapacitorError from '../../shared/components/NoKapacitorError'
import moment from 'moment'
const AlertsApp = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string, // 'influx-enterprise'
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}), // .isRequired,
addFlashMessage: PropTypes.func, // .isRequired,
},
getInitialState() {
return {
class AlertsApp extends Component {
constructor(props) {
super(props)
this.state = {
loading: true,
hasKapacitor: false,
alerts: [],
isTimeOpen: false,
timeRange: {
upper: moment().format(),
lower: moment().subtract(1, 'd').format(),
},
}
},
this.fetchAlerts = ::this.fetchAlerts
this.renderSubComponents = ::this.renderSubComponents
this.handleToggleTime = ::this.handleToggleTime
this.handleCloseTime = ::this.handleCloseTime
this.handleApplyTime = ::this.handleApplyTime
}
// TODO: show a loading screen until we figure out if there is a kapacitor and fetch the alerts
componentDidMount() {
const {source} = this.props
@ -41,10 +46,16 @@ const AlertsApp = React.createClass({
this.setState({loading: false})
}
})
},
}
componentDidUpdate(prevProps, prevState) {
if (!_.isEqual(prevState.timeRange, this.state.timeRange)) {
this.fetchAlerts()
}
}
fetchAlerts() {
getAlerts(this.props.source.links.proxy).then((resp) => {
getAlerts(this.props.source.links.proxy, this.state.timeRange).then((resp) => {
const results = []
const alertSeries = _.get(resp, ['data', 'results', '0', 'series'], [])
@ -70,7 +81,7 @@ const AlertsApp = React.createClass({
})
this.setState({loading: false, alerts: results})
})
},
}
renderSubComponents() {
let component
@ -87,13 +98,24 @@ const AlertsApp = React.createClass({
}
}
return component
},
}
handleToggleTime() {
this.setState({isTimeOpen: !this.state.isTimeOpen})
}
handleCloseTime() {
this.setState({isTimeOpen: false})
}
handleApplyTime(timeRange) {
this.setState({timeRange})
}
render() {
const {source} = this.props
const {timeRange} = this.state
return (
// I stole this from the Hosts page.
// Perhaps we should create an abstraction?
<div className="page">
<div className="page-header">
<div className="page-header__container">
@ -104,6 +126,13 @@ const AlertsApp = React.createClass({
</div>
<div className="page-header__right">
<SourceIndicator sourceName={source.name} />
<CustomTimeRange
isVisible={this.state.isTimeOpen}
onToggle={this.handleToggleTime}
onClose={this.handleCloseTime}
onApplyTimeRange={this.handleApplyTime}
timeRange={timeRange}
/>
</div>
</div>
</div>
@ -111,15 +140,32 @@ const AlertsApp = React.createClass({
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
{ this.renderSubComponents() }
{this.renderSubComponents()}
</div>
</div>
</div>
</div>
</div>
)
},
}
}
})
const {
func,
shape,
string,
} = PropTypes
AlertsApp.propTypes = {
source: shape({
id: string.isRequired,
name: string.isRequired,
type: string, // 'influx-enterprise'
links: shape({
proxy: string.isRequired,
}).isRequired,
}),
addFlashMessage: func,
}
export default AlertsApp

View File

@ -0,0 +1,112 @@
import React, {PropTypes, Component} from 'react'
import rome from 'rome'
import moment from 'moment'
import classNames from 'classnames'
import OnClickOutside from 'react-onclickoutside'
class CustomTimeRange extends Component {
constructor(props) {
super(props)
this.handleClick = ::this.handleClick
}
handleClickOutside() {
this.props.onClose()
}
componentDidMount() {
const {timeRange} = this.props
const lower = rome(this.lower, {
initialValue: this._formatTimeRange(timeRange.lower),
})
const upper = rome(this.upper, {
initialValue: this._formatTimeRange(timeRange.upper),
})
this.lowerCal = lower
this.upperCal = upper
}
// If there is an upper or lower time range set, set the corresponding calendar's value.
componentWillReceiveProps(nextProps) {
const {lower, upper} = nextProps.timeRange
if (lower) {
this.lowerCal.setValue(this._formatTimeRange(lower))
}
if (upper) {
this.upperCal.setValue(this._formatTimeRange(upper))
}
}
render() {
const {isVisible, onToggle, timeRange: {upper, lower}} = this.props
return (
<div className={classNames("custom-time-range", {show: isVisible})} style={{display: 'flex'}}>
<button className="btn btn-sm btn-info custom-time-range--btn" onClick={onToggle}>
<span className="icon clock"></span>
{`${moment(lower).format('MMM Do HH:mm')}${moment(upper).format('MMM Do HH:mm')}`}
<span className="caret"></span>
</button>
<div className="custom-time--container">
<div className="custom-time--dates">
<div className="custom-time--lower" ref={(r) => this.lower = r} />
<div className="custom-time--upper" ref={(r) => this.upper = r} />
</div>
<div className="custom-time--apply btn btn-sm btn-primary" onClick={this.handleClick}>Apply</div>
</div>
</div>
)
}
handleClick() {
const lower = this.lowerCal.getDate().toISOString()
const upper = this.upperCal.getDate().toISOString()
this.props.onApplyTimeRange({lower, upper})
this.props.onClose()
}
/*
* Upper and lower time ranges are passed in with single quotes as part of
* the string literal, i.e. "'2015-09-23T18:00:00.000Z'". Remove them
* before passing the string to be parsed.
*/
_formatTimeRange(timeRange) {
if (!timeRange) {
return ''
}
// If the given time range is relative, create a fixed timestamp based on its value
if (timeRange.match(/^now/)) {
const match = timeRange.match(/\d+\w/)[0]
const duration = match.slice(0, match.length - 1)
const unitOfTime = match[match.length - 1]
return moment().subtract(duration, unitOfTime)
}
return moment(timeRange.replace(/\'/g, '')).format('YYYY-MM-DD HH:mm')
}
}
const {
bool,
func,
shape,
string,
} = PropTypes
CustomTimeRange.propTypes = {
onApplyTimeRange: func.isRequired,
timeRange: shape({
lower: string.isRequired,
upper: string.isRequired,
}).isRequired,
isVisible: bool.isRequired,
onToggle: func.isRequired,
onClose: func.isRequired,
}
export default OnClickOutside(CustomTimeRange)

View File

@ -40,6 +40,7 @@
@import 'components/resizer';
@import 'components/source-indicator';
@import 'components/confirm-buttons';
@import 'components/custom-time-range';
// Pages
@import 'pages/alerts';
@ -52,4 +53,4 @@
@import 'pages/admin';
// TODO
@import 'unsorted';
@import 'unsorted';

View File

@ -0,0 +1,254 @@
/*
Custom Time Range Dropdown
------------------------------------------------------
*/
.custom-time-range {
position: relative;
}
.btn.btn-sm.btn-info.custom-time-range--btn {
padding: 0 30px 0 9px !important;
.caret {
position: absolute;
right: 9px;
top: calc(50% + 1px);
transform: translateY(-50%);
}
}
.custom-time--container {
display: none;
position: absolute;
flex-direction: column;
align-items: center;
top: 35px;
right: 0;
background: $g5-pepper;
border-radius: $radius;
padding: 8px;
z-index: 1000;
box-shadow: 0 2px 5px 0.6px rgba(15, 14, 21, 0.2);
}
.custom-time--dates {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.custom-time--lower {
margin-right: 4px;
}
.custom-time--upper {
margin-left: 4px;
}
$custom-time-arrow: 28px;
$rd-cell-size: 30px;
.rd-container {
display: flex !important;
flex-direction: column;
align-items: center;
}
.rd-date {
position: relative;
}
.rd-back,
.rd-next,
.rd-month-label {
position: absolute;
top: 0;
height: $custom-time-arrow;
line-height: $custom-time-arrow;
}
.rd-back,
.rd-next {
outline: none;
width: $custom-time-arrow;
border: 0;
background-color: transparent;
border-radius: 50%;
color: $g15-platinum;
transition:
background-color 0.25s ease,
color 0.25s ease;
&:after {
font-family: 'icomoon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
color: inherit;
position: absolute;
top: 50%;
transform: translate(-50%,-50%);
font-size: 16px;
}
&:hover {
background-color: $g6-smoke;
color: $g20-white;
}
}
.rd-back {
left: 0;
&:after {
left: calc(50% - 1px);
content: "\e90c";
}
}
.rd-next {
left: calc(100% - #{$custom-time-arrow});
&:after {
left: calc(50% + 1px);
content: "\e911";
}
}
.rd-month-label {
font-weight: 600;
color: $g15-platinum;
left: $custom-time-arrow;
text-align: center;
@include no-user-select();
width: calc(100% - #{($custom-time-arrow * 2)});
}
.rd-days {
margin-top: ($custom-time-arrow + 8px);
background-color: transparent;
border-radius: $radius-small;
/* Cancel out default table styles */
tr:hover {
background-color: transparent !important;
color: inherit !important;
}
thead.rd-days-head th.rd-day-head,
tbody.rd-days-body td.rd-day-body {
padding: 0 !important;
min-height: $rd-cell-size !important;
height: $rd-cell-size !important;
max-height: $rd-cell-size !important;
min-width: $rd-cell-size !important;
width: $rd-cell-size !important;
max-width: $rd-cell-size !important;
vertical-align: middle;
text-align: center;
border: 2px solid $g5-pepper !important;
}
thead.rd-days-head th.rd-day-head {
color: $g15-platinum !important;
background-color: $g5-pepper !important;
}
tbody.rd-days-body td.rd-day-body {
@include no-user-select();
letter-spacing: -1px;
font-family: $code-font;
transition:
background-color 0.25s ease,
color 0.25s ease;
color: $g13-mist !important;
background-color: $g3-castle;
border-radius: 5px;
&:hover {
cursor: $cc-pointer;
color: $g20-white !important;
background-color: $g6-smoke;
}
&.rd-day-next-month,
&.rd-day-prev-month {
cursor: $cc-default;
color: $g8-storm !important;
background-color: $g5-pepper !important;
}
&.rd-day-selected {
background-color: $c-pool !important;
color: $g20-white !important;
}
}
}
.rd-time {
margin: 0 2px;
width: calc(100% - 4px);
height: 30px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.rd-time-selected {
@include no-user-select();
height: 28px;
line-height: 28px;
background-color: $g3-castle;
border-radius: $radius-small;
width: 100%;
letter-spacing: -1px;
font-family: $code-font;
color: $g13-mist;
display: inline-block;
transition:
color 0.25s ease,
background-color 0.25s ease;
text-align: center;
&:hover {
color: $g20-white;
background-color: $g6-smoke;
cursor: $cc-pointer;
}
}
.rd-time-list {
position: absolute;
top: 50%;
left: 50%;
width: 120px;
height: 200px;
transform: translate(-50%,-50%);
overflow: auto;
overflow-x: hidden;
overflow-y: scroll;
@include custom-scrollbar-round($c-pool, $c-laser);
@include gradient-h($c-ocean, $c-pool);
border-radius: $radius;
box-shadow: 0 2px 5px 0.6px rgba(15, 14, 21, 0.2);
}
.rd-time-option {
width: 100%;
height: 24px;
line-height: 24px;
padding-left: $scrollbar-width;
text-align: center;
@include no-user-select();
font-family: $code-font;
color: $c-yeti;
letter-spacing: -1px;
&:hover,
&:active,
&:focus {
color: $g20-white;
cursor: $cc-pointer;
outline: none;
@include gradient-h($c-laser, $c-pool);
}
}
.custom-time--apply {
margin-top: 8px;
width: 120px;
}
/* Show State */
.custom-time-range.show {
.custom-time--container {
display: flex;
}
.custom-time-range--btn {
color: $g20-white !important;
background-color: $g6-smoke;
}
}

View File

@ -41,6 +41,35 @@ $scrollbar-offset: 3px;
@mixin custom-scrollbar($trackColor, $handleColor) {
&::-webkit-scrollbar {
width: $scrollbar-width;
&-button {
background-color: $trackColor;
}
&-track {
background-color: $trackColor;
}
&-track-piece {
background-color: $trackColor;
border: $scrollbar-offset solid $trackColor;
border-radius: ($scrollbar-width / 2);
}
&-thumb {
background-color: $handleColor;
border: $scrollbar-offset solid $trackColor;
border-radius: ($scrollbar-width / 2);
}
&-corner {
background-color: $trackColor;
}
}
&::-webkit-resizer {
background-color: $trackColor;
}
}
@mixin custom-scrollbar-round($trackColor, $handleColor) {
&::-webkit-scrollbar {
width: $scrollbar-width;
border-top-right-radius: $radius;
border-bottom-right-radius: $radius;
&-button {
@ -48,6 +77,7 @@ $scrollbar-offset: 3px;
}
&-track {
background-color: $trackColor;
border-top-right-radius: $radius;
border-bottom-right-radius: $radius;
}
&-track-piece {

View File

@ -267,7 +267,7 @@ input {
padding: 0 !important;
max-height: 290px;
overflow: auto;
@include custom-scrollbar($c-pool, $c-laser);
@include custom-scrollbar-round($c-pool, $c-laser);
@include gradient-h($c-ocean, $c-pool);
box-shadow: 0 2px 5px 0.6px fade-out($g0-obsidian, 0.8);

View File

@ -352,6 +352,10 @@ asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
atoa@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49"
atob@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773"
@ -1540,6 +1544,14 @@ builtin-status-codes@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz#6f22003baacf003ccd287afe6872151fddc58579"
bullseye@1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/bullseye/-/bullseye-1.4.6.tgz#b73f606f7b4273be80ac65acd75295d62606fe24"
dependencies:
crossvent "^1.3.1"
seleccion "2.0.0"
sell "^1.0.0"
bytes@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
@ -1964,6 +1976,13 @@ content-type@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
contra@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/contra/-/contra-1.9.1.tgz#60e498274b3d2d332896d60f82900aefa2ecac8c"
dependencies:
atoa "1.0.0"
ticky "1.0.0"
convert-source-map@^0.3.3:
version "0.3.5"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
@ -2018,6 +2037,12 @@ cross-spawn@^5.0.0:
shebang-command "^1.2.0"
which "^1.2.9"
crossvent@1.5.0, crossvent@^1.3.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.0.tgz#3779c1242699e19417f0414e61b144753a52fd6d"
dependencies:
custom-event "1.0.0"
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -2200,6 +2225,10 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
custom-event@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062"
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@ -4596,7 +4625,7 @@ mocha@^2.4.5:
supports-color "1.2.0"
to-iso-string "0.0.2"
moment@^2.13.0:
moment@^2.13.0, moment@^2.8.2:
version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
@ -6203,6 +6232,15 @@ ripemd160@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce"
rome@^2.1.22:
version "2.1.22"
resolved "https://registry.yarnpkg.com/rome/-/rome-2.1.22.tgz#4bf25318cc0522ae92dd090472ce7a6e0b1f5e02"
dependencies:
bullseye "1.4.6"
contra "1.9.1"
crossvent "1.5.0"
moment "^2.8.2"
run-async@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@ -6243,6 +6281,14 @@ script-loader@~0.6.0:
dependencies:
raw-loader "~0.5.1"
seleccion@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/seleccion/-/seleccion-2.0.0.tgz#0984ac1e8df513e38b41a608e65042e8381e0a73"
sell@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/sell/-/sell-1.0.0.tgz#3baca7e51f78ddee9e22eea1ac747a6368bd1630"
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@ -6842,6 +6888,10 @@ through@^2.3.6, through@~2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
ticky@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.0.tgz#e87f38ee0491ea32f62e8f0567ba9638b29f049c"
timers-browserify@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86"