Merge pull request #725 from influxdata/feature/out-of-range#707
WIP Feature/out of range#707pull/742/head
commit
1f09402b26
|
@ -9,8 +9,8 @@ const (
|
||||||
GreaterThanEqual = "equal to or greater"
|
GreaterThanEqual = "equal to or greater"
|
||||||
Equal = "equal to"
|
Equal = "equal to"
|
||||||
NotEqual = "not equal to"
|
NotEqual = "not equal to"
|
||||||
InsideRange = "is inside range"
|
InsideRange = "inside range"
|
||||||
OutsideRange = "is outside range"
|
OutsideRange = "outside range"
|
||||||
)
|
)
|
||||||
|
|
||||||
// kapaOperator converts UI strings to kapacitor operators
|
// kapaOperator converts UI strings to kapacitor operators
|
||||||
|
|
|
@ -205,7 +205,7 @@ func TestThresholdInsideRange(t *testing.T) {
|
||||||
Trigger: "threshold",
|
Trigger: "threshold",
|
||||||
Alerts: []string{"slack", "victorops", "email"},
|
Alerts: []string{"slack", "victorops", "email"},
|
||||||
TriggerValues: chronograf.TriggerValues{
|
TriggerValues: chronograf.TriggerValues{
|
||||||
Operator: "is inside range",
|
Operator: "inside range",
|
||||||
Value: "90",
|
Value: "90",
|
||||||
RangeValue: "100",
|
RangeValue: "100",
|
||||||
},
|
},
|
||||||
|
@ -352,7 +352,7 @@ func TestThresholdOutsideRange(t *testing.T) {
|
||||||
Trigger: "threshold",
|
Trigger: "threshold",
|
||||||
Alerts: []string{"slack", "victorops", "email"},
|
Alerts: []string{"slack", "victorops", "email"},
|
||||||
TriggerValues: chronograf.TriggerValues{
|
TriggerValues: chronograf.TriggerValues{
|
||||||
Operator: "is outside range",
|
Operator: "outside range",
|
||||||
Value: "90",
|
Value: "90",
|
||||||
RangeValue: "100",
|
RangeValue: "100",
|
||||||
},
|
},
|
||||||
|
|
|
@ -1997,8 +1997,8 @@
|
||||||
"equal to or greater",
|
"equal to or greater",
|
||||||
"equal to",
|
"equal to",
|
||||||
"not equal to",
|
"not equal to",
|
||||||
"is inside range",
|
"inside range",
|
||||||
"is outside range"
|
"outside range"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
|
|
|
@ -110,7 +110,6 @@
|
||||||
'no-new': 2,
|
'no-new': 2,
|
||||||
'no-octal-escape': 2,
|
'no-octal-escape': 2,
|
||||||
'no-octal': 2,
|
'no-octal': 2,
|
||||||
'no-param-reassign': 2,
|
|
||||||
'no-proto': 2,
|
'no-proto': 2,
|
||||||
'no-redeclare': 2,
|
'no-redeclare': 2,
|
||||||
'no-script-url': 2,
|
'no-script-url': 2,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { configure } from '@kadira/storybook';
|
||||||
|
|
||||||
|
function loadStories() {
|
||||||
|
require('../stories');
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(loadStories, module);
|
|
@ -0,0 +1 @@
|
||||||
|
<link href="/style.css" rel="stylesheet">
|
|
@ -0,0 +1,54 @@
|
||||||
|
const path = require('path');
|
||||||
|
var ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||||
|
|
||||||
|
// you can use this file to add your custom webpack plugins, loaders and anything you like.
|
||||||
|
// This is just the basic way to add addional webpack configurations.
|
||||||
|
// For more information refer the docs: https://getstorybook.io/docs/configurations/custom-webpack-config
|
||||||
|
|
||||||
|
// IMPORTANT
|
||||||
|
// When you add this file, we won't add the default configurations which is similar
|
||||||
|
// to "React Create App". This only has babel loader to load JavaScript.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
debug: true,
|
||||||
|
devtool: 'source-map',
|
||||||
|
plugins: [
|
||||||
|
new ExtractTextPlugin("style.css"),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
publicPath: '/',
|
||||||
|
path: path.resolve(__dirname, '../build'),
|
||||||
|
filename: '[name].[chunkhash].dev.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader!resolve-url!sass?sourceMap'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||||
|
options: {
|
||||||
|
limit: 100000,
|
||||||
|
},
|
||||||
|
loader : 'file-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, '..', 'src'),
|
||||||
|
shared: path.resolve(__dirname, '..', 'src', 'shared'),
|
||||||
|
style: path.resolve(__dirname, '..', 'src', 'style'),
|
||||||
|
utils: path.resolve(__dirname, '..', 'src', 'utils'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sassLoader: {
|
||||||
|
includePaths: [path.resolve(__dirname, "node_modules")],
|
||||||
|
},
|
||||||
|
postcss: require('../webpack/postcss'),
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
const express = require('express');
|
||||||
|
const request = require('request');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use('/', (req, res) => {
|
||||||
|
console.log(`${req.method} ${req.url}`);
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*';
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS';
|
||||||
|
headers['Access-Control-Allow-Credentials'] = false;
|
||||||
|
headers['Access-Control-Max-Age'] = '86400'; // 24 hours
|
||||||
|
headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept';
|
||||||
|
res.writeHead(200, headers);
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const url = 'http://localhost:8888' + req.url;
|
||||||
|
req.pipe(request(url)).pipe(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3888, () => {
|
||||||
|
console.log('corsless proxy server now running')
|
||||||
|
});
|
|
@ -14,7 +14,10 @@
|
||||||
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
|
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
|
||||||
"lint": "node_modules/eslint/bin/eslint.js src/",
|
"lint": "node_modules/eslint/bin/eslint.js src/",
|
||||||
"test": "karma start",
|
"test": "karma start",
|
||||||
"clean": "rm -rf build"
|
"clean": "rm -rf build",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook",
|
||||||
|
"proxy": "node ./corsless"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kadira/storybook": "^2.21.0",
|
||||||
"autoprefixer": "^6.3.1",
|
"autoprefixer": "^6.3.1",
|
||||||
"babel-core": "^6.5.1",
|
"babel-core": "^6.5.1",
|
||||||
"babel-eslint": "6.1.2",
|
"babel-eslint": "6.1.2",
|
||||||
|
@ -46,6 +50,7 @@
|
||||||
"eslint": "3.9.1",
|
"eslint": "3.9.1",
|
||||||
"eslint-loader": "1.6.1",
|
"eslint-loader": "1.6.1",
|
||||||
"eslint-plugin-react": "6.6.0",
|
"eslint-plugin-react": "6.6.0",
|
||||||
|
"express": "^4.14.0",
|
||||||
"extract-text-webpack-plugin": "^1.0.1",
|
"extract-text-webpack-plugin": "^1.0.1",
|
||||||
"file-loader": "^0.8.5",
|
"file-loader": "^0.8.5",
|
||||||
"hanson": "^1.1.1",
|
"hanson": "^1.1.1",
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
import AJAX from 'utils/ajax';
|
import AJAX from 'utils/ajax';
|
||||||
|
|
||||||
|
function rangeRule(rule) {
|
||||||
|
const {value, rangeValue, operator} = rule.values;
|
||||||
|
|
||||||
|
if (operator === 'inside range' || operator === 'outside range') {
|
||||||
|
rule.values.value = Math.min(value, rangeValue).toString();
|
||||||
|
rule.values.rangeValue = Math.max(value, rangeValue).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
export function createRule(kapacitor, rule) {
|
export function createRule(kapacitor, rule) {
|
||||||
return AJAX({
|
return AJAX({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: kapacitor.links.rules,
|
url: kapacitor.links.rules,
|
||||||
data: rule,
|
data: rangeRule(rule),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +37,7 @@ export function editRule(rule) {
|
||||||
return AJAX({
|
return AJAX({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: rule.links.self,
|
url: rule.links.self,
|
||||||
data: rule,
|
data: rangeRule(rule),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const RuleGraph = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGraph() {
|
renderGraph() {
|
||||||
const {query, source, timeRange} = this.props;
|
const {query, source, timeRange, rule} = this.props;
|
||||||
const autoRefreshMs = 30000;
|
const autoRefreshMs = 30000;
|
||||||
const queryText = selectStatement({lower: timeRange.queryValue}, query);
|
const queryText = selectStatement({lower: timeRange.queryValue}, query);
|
||||||
const queries = [{host: source.links.proxy, text: queryText}];
|
const queries = [{host: source.links.proxy, text: queryText}];
|
||||||
|
@ -46,6 +46,7 @@ export const RuleGraph = React.createClass({
|
||||||
underlayCallback={this.createUnderlayCallback()}
|
underlayCallback={this.createUnderlayCallback()}
|
||||||
isGraphFilled={false}
|
isGraphFilled={false}
|
||||||
overrideLineColors={kapacitorLineColors}
|
overrideLineColors={kapacitorLineColors}
|
||||||
|
ruleValues={rule.values}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -83,12 +84,28 @@ export const RuleGraph = React.createClass({
|
||||||
highlightEnd = +rule.values.value + width;
|
highlightEnd = +rule.values.value + width;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'outside range': {
|
||||||
|
const {rangeValue, value} = rule.values;
|
||||||
|
highlightStart = Math.min(+value, +rangeValue);
|
||||||
|
highlightEnd = Math.max(+value, +rangeValue);
|
||||||
|
|
||||||
|
canvas.fillStyle = 'rgba(78, 216, 160, 0.3)';
|
||||||
|
canvas.fillRect(area.x, area.y, area.w, area.h);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'inside range': {
|
||||||
|
const {rangeValue, value} = rule.values;
|
||||||
|
highlightStart = Math.min(+value, +rangeValue);
|
||||||
|
highlightEnd = Math.max(+value, +rangeValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bottom = dygraph.toDomYCoord(highlightStart);
|
const bottom = dygraph.toDomYCoord(highlightStart);
|
||||||
const top = dygraph.toDomYCoord(highlightEnd);
|
const top = dygraph.toDomYCoord(highlightEnd);
|
||||||
|
|
||||||
canvas.fillStyle = 'rgba(78,216,160,0.3)';
|
canvas.fillStyle = rule.values.operator === 'out of range' ? 'rgba(41, 41, 51, 1)' : 'rgba(78, 216, 160, 0.3)';
|
||||||
canvas.fillRect(area.x, top, area.w, bottom - top);
|
canvas.fillRect(area.x, top, area.w, bottom - top);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {OPERATORS, PERIODS, CHANGES, SHIFTS} from 'src/kapacitor/constants';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const TABS = ['Threshold', 'Relative', 'Deadman'];
|
const TABS = ['Threshold', 'Relative', 'Deadman'];
|
||||||
|
const mapToItems = (arr, type) => arr.map((text) => ({text, type}));
|
||||||
|
|
||||||
export const ValuesSection = React.createClass({
|
export const ValuesSection = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
rule: PropTypes.shape({
|
rule: PropTypes.shape({
|
||||||
|
@ -65,7 +67,9 @@ const Threshold = React.createClass({
|
||||||
rule: PropTypes.shape({
|
rule: PropTypes.shape({
|
||||||
values: PropTypes.shape({
|
values: PropTypes.shape({
|
||||||
operator: PropTypes.string,
|
operator: PropTypes.string,
|
||||||
|
rangeOperator: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
rangeValue: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -74,26 +78,21 @@ const Threshold = React.createClass({
|
||||||
|
|
||||||
|
|
||||||
handleDropdownChange(item) {
|
handleDropdownChange(item) {
|
||||||
const newValues = Object.assign({}, this.props.rule.values, {[item.type]: item.text});
|
this.props.onChange({...this.props.rule.values, [item.type]: item.text});
|
||||||
this.props.onChange(newValues);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInputChange() {
|
handleInputChange() {
|
||||||
this.props.onChange(Object.assign({}, this.props.rule.values, {
|
this.props.onChange({
|
||||||
|
...this.props.rule.values,
|
||||||
value: this.valueInput.value,
|
value: this.valueInput.value,
|
||||||
}));
|
rangeValue: this.valueRangeInput ? this.valueRangeInput.value : '',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {operator, value} = this.props.rule.values;
|
const {operator, value, rangeValue} = this.props.rule.values;
|
||||||
const {query} = this.props;
|
const {query} = this.props;
|
||||||
|
|
||||||
function mapToItems(arr, type) {
|
|
||||||
return arr.map((text) => {
|
|
||||||
return {text, type};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const operators = mapToItems(OPERATORS, 'operator');
|
const operators = mapToItems(OPERATORS, 'operator');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,7 +101,10 @@ const Threshold = React.createClass({
|
||||||
<span>{query.fields.length ? query.fields[0].field : 'Select a Metric'}</span>
|
<span>{query.fields.length ? query.fields[0].field : 'Select a Metric'}</span>
|
||||||
<p>is</p>
|
<p>is</p>
|
||||||
<Dropdown className="size-176 dropdown-kapacitor" items={operators} selected={operator} onChoose={this.handleDropdownChange} />
|
<Dropdown className="size-176 dropdown-kapacitor" items={operators} selected={operator} onChoose={this.handleDropdownChange} />
|
||||||
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange}></input>
|
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange} />
|
||||||
|
{ (operator === 'inside range' || operator === 'outside range') &&
|
||||||
|
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueRangeInput = r} defaultValue={rangeValue} onKeyUp={this.handleInputChange} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -122,22 +124,16 @@ const Relative = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDropdownChange(item) {
|
handleDropdownChange(item) {
|
||||||
this.props.onChange(Object.assign({}, this.props.rule.values, {[item.type]: item.text}));
|
this.props.onChange({...this.props.rule.values, [item.type]: item.text});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInputChange() {
|
handleInputChange() {
|
||||||
this.props.onChange(Object.assign({}, this.props.rule.values, {value: this.input.value}));
|
this.props.onChange({...this.props.rule.values, value: this.input.value});
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {change, shift, operator, value} = this.props.rule.values;
|
const {change, shift, operator, value} = this.props.rule.values;
|
||||||
|
|
||||||
function mapToItems(arr, type) {
|
|
||||||
return arr.map((text) => {
|
|
||||||
return {text, type};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const changes = mapToItems(CHANGES, 'change');
|
const changes = mapToItems(CHANGES, 'change');
|
||||||
const shifts = mapToItems(SHIFTS, 'shift');
|
const shifts = mapToItems(SHIFTS, 'shift');
|
||||||
const operators = mapToItems(OPERATORS, 'operator');
|
const operators = mapToItems(OPERATORS, 'operator');
|
||||||
|
|
|
@ -11,12 +11,13 @@ export const defaultRuleConfigs = {
|
||||||
threshold: {
|
threshold: {
|
||||||
operator: 'greater than',
|
operator: 'greater than',
|
||||||
value: '',
|
value: '',
|
||||||
|
rangeValue: '',
|
||||||
relation: 'once',
|
relation: 'once',
|
||||||
percentile: '90',
|
percentile: '90',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OPERATORS = ['greater than', 'equal to or greater', 'equal to or less than', 'less than', 'equal to', 'not equal to'];
|
export const OPERATORS = ['greater than', 'equal to or greater', 'equal to or less than', 'less than', 'equal to', 'not equal to', 'inside range', 'outside range'];
|
||||||
// export const RELATIONS = ['once', 'more than ', 'less than'];
|
// export const RELATIONS = ['once', 'more than ', 'less than'];
|
||||||
export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h'];
|
export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h'];
|
||||||
export const CHANGES = ['change', '% change'];
|
export const CHANGES = ['change', '% change'];
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
/* eslint-disable no-magic-numbers */
|
/* eslint-disable no-magic-numbers */
|
||||||
import React, {PropTypes} from 'react';
|
import React, {PropTypes} from 'react';
|
||||||
import Dygraph from '../../external/dygraph';
|
import Dygraph from '../../external/dygraph';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
const {arrayOf, object, array, number, bool, shape} = PropTypes;
|
const {
|
||||||
|
array,
|
||||||
|
arrayOf,
|
||||||
|
number,
|
||||||
|
bool,
|
||||||
|
shape,
|
||||||
|
} = PropTypes;
|
||||||
|
|
||||||
const LINE_COLORS = [
|
const LINE_COLORS = [
|
||||||
'#00C9FF',
|
'#00C9FF',
|
||||||
|
@ -25,16 +32,17 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
ranges: shape({
|
ranges: shape({
|
||||||
y: arrayOf(number.isRequired),
|
y: arrayOf(number),
|
||||||
y2: arrayOf(number.isRequired),
|
y2: arrayOf(number),
|
||||||
}),
|
}),
|
||||||
timeSeries: array.isRequired, // eslint-disable-line react/forbid-prop-types
|
timeSeries: array.isRequired,
|
||||||
labels: array.isRequired, // eslint-disable-line react/forbid-prop-types
|
labels: array.isRequired,
|
||||||
options: object, // eslint-disable-line react/forbid-prop-types
|
options: shape({}),
|
||||||
containerStyle: object, // eslint-disable-line react/forbid-prop-types
|
containerStyle: shape({}),
|
||||||
isGraphFilled: bool,
|
isGraphFilled: bool,
|
||||||
overrideLineColors: array,
|
overrideLineColors: array,
|
||||||
dygraphSeries: shape({}).isRequired,
|
dygraphSeries: shape({}).isRequired,
|
||||||
|
ruleValues: shape({}),
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
|
@ -54,7 +62,7 @@ export default React.createClass({
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const timeSeries = this.getTimeSeries();
|
const timeSeries = this.getTimeSeries();
|
||||||
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
|
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
|
||||||
const {ranges, dygraphSeries} = this.props;
|
const {ranges, dygraphSeries, ruleValues} = this.props;
|
||||||
|
|
||||||
const refs = this.refs;
|
const refs = this.refs;
|
||||||
const graphContainerNode = refs.graphContainer;
|
const graphContainerNode = refs.graphContainer;
|
||||||
|
@ -81,7 +89,7 @@ export default React.createClass({
|
||||||
series: dygraphSeries,
|
series: dygraphSeries,
|
||||||
axes: {
|
axes: {
|
||||||
y: {
|
y: {
|
||||||
valueRange: getRange(timeSeries, ranges.y),
|
valueRange: getRange(timeSeries, ranges.y, _.get(ruleValues, 'value', null), _.get(ruleValues, 'rangeValue', null)),
|
||||||
},
|
},
|
||||||
y2: {
|
y2: {
|
||||||
valueRange: getRange(timeSeries, ranges.y2),
|
valueRange: getRange(timeSeries, ranges.y2),
|
||||||
|
@ -141,14 +149,14 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeSeries = this.getTimeSeries();
|
const timeSeries = this.getTimeSeries();
|
||||||
const {labels, ranges, options, dygraphSeries} = this.props;
|
const {labels, ranges, options, dygraphSeries, ruleValues} = this.props;
|
||||||
|
|
||||||
dygraph.updateOptions({
|
dygraph.updateOptions({
|
||||||
labels,
|
labels,
|
||||||
file: timeSeries,
|
file: timeSeries,
|
||||||
axes: {
|
axes: {
|
||||||
y: {
|
y: {
|
||||||
valueRange: getRange(timeSeries, ranges.y),
|
valueRange: getRange(timeSeries, ranges.y, _.get(ruleValues, 'value', null), _.get(ruleValues, 'rangeValue', null)),
|
||||||
},
|
},
|
||||||
y2: {
|
y2: {
|
||||||
valueRange: getRange(timeSeries, ranges.y2),
|
valueRange: getRange(timeSeries, ranges.y2),
|
||||||
|
@ -172,15 +180,35 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getRange(timeSeries, override) {
|
const PADDING_FACTOR = 0.1;
|
||||||
|
|
||||||
|
function getRange(timeSeries, override, value = null, rangeValue = null) {
|
||||||
if (override) {
|
if (override) {
|
||||||
return override;
|
return override;
|
||||||
}
|
}
|
||||||
|
|
||||||
let max = null;
|
const subtractPadding = (val) => +val - val * PADDING_FACTOR;
|
||||||
let min = null;
|
const addPadding = (val) => +val + val * PADDING_FACTOR;
|
||||||
|
|
||||||
timeSeries.forEach((series) => {
|
const pad = (val, side) => {
|
||||||
|
if (val === null || val === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val < 0) {
|
||||||
|
return side === "top" ? subtractPadding(val) : addPadding(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return side === "top" ? addPadding(val) : subtractPadding(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const points = [
|
||||||
|
...timeSeries,
|
||||||
|
[null, pad(value)],
|
||||||
|
[null, pad(rangeValue, "top")],
|
||||||
|
];
|
||||||
|
|
||||||
|
const range = points.reduce(([min, max], series) => {
|
||||||
for (let i = 1; i < series.length; i++) {
|
for (let i = 1; i < series.length; i++) {
|
||||||
const val = series[i];
|
const val = series[i];
|
||||||
|
|
||||||
|
@ -192,17 +220,19 @@ function getRange(timeSeries, override) {
|
||||||
min = val;
|
min = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val) {
|
if (typeof val === "number") {
|
||||||
min = Math.min(min, val);
|
min = Math.min(min, val);
|
||||||
max = Math.max(max, val);
|
max = Math.max(max, val);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dygraph will not reliably plot X / Y axis labels if min and max are both 0
|
|
||||||
if (min === 0 && max === 0) {
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [min, max];
|
return [min, max];
|
||||||
}
|
}
|
||||||
|
}, [null, null]);
|
||||||
|
|
||||||
|
// Dygraph will not reliably plot X / Y axis labels if min and max are both 0
|
||||||
|
if (range[0] === 0 && range[1] === 0) {
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
|
@ -7,19 +7,34 @@ import _ from 'lodash';
|
||||||
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph';
|
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph';
|
||||||
import lastValues from 'src/shared/parsing/lastValues';
|
import lastValues from 'src/shared/parsing/lastValues';
|
||||||
|
|
||||||
|
const {
|
||||||
|
array,
|
||||||
|
arrayOf,
|
||||||
|
number,
|
||||||
|
bool,
|
||||||
|
shape,
|
||||||
|
string,
|
||||||
|
func,
|
||||||
|
} = PropTypes;
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'LineGraph',
|
displayName: 'LineGraph',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
data: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
|
data: arrayOf(shape({}).isRequired).isRequired,
|
||||||
title: PropTypes.string,
|
ranges: shape({
|
||||||
isFetchingInitially: PropTypes.bool,
|
y: arrayOf(number),
|
||||||
isRefreshing: PropTypes.bool,
|
y2: arrayOf(number),
|
||||||
underlayCallback: PropTypes.func,
|
}),
|
||||||
isGraphFilled: PropTypes.bool,
|
title: string,
|
||||||
overrideLineColors: PropTypes.array,
|
isFetchingInitially: bool,
|
||||||
queries: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
|
isRefreshing: bool,
|
||||||
showSingleStat: PropTypes.bool,
|
underlayCallback: func,
|
||||||
activeQueryIndex: PropTypes.number,
|
isGraphFilled: bool,
|
||||||
|
overrideLineColors: array,
|
||||||
|
queries: arrayOf(shape({}).isRequired).isRequired,
|
||||||
|
showSingleStat: bool,
|
||||||
|
activeQueryIndex: number,
|
||||||
|
ruleValues: shape({}),
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
|
@ -46,7 +61,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {data, isFetchingInitially, isRefreshing, isGraphFilled, overrideLineColors, title, underlayCallback, queries, showSingleStat} = this.props;
|
const {data, ranges, isFetchingInitially, isRefreshing, isGraphFilled, overrideLineColors, title, underlayCallback, queries, showSingleStat, ruleValues} = this.props;
|
||||||
const {labels, timeSeries, dygraphSeries} = this._timeSeries;
|
const {labels, timeSeries, dygraphSeries} = this._timeSeries;
|
||||||
|
|
||||||
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
||||||
|
@ -94,7 +109,8 @@ export default React.createClass({
|
||||||
labels={labels}
|
labels={labels}
|
||||||
options={options}
|
options={options}
|
||||||
dygraphSeries={dygraphSeries}
|
dygraphSeries={dygraphSeries}
|
||||||
ranges={this.getRanges()}
|
ranges={ranges || this.getRanges()}
|
||||||
|
ruleValues={ruleValues}
|
||||||
/>
|
/>
|
||||||
{showSingleStat ? <div className="graph-single-stat single-stat">{roundedValue}</div> : null}
|
{showSingleStat ? <div className="graph-single-stat single-stat">{roundedValue}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,8 +13,10 @@ const rootReducer = combineReducers({
|
||||||
rules: rulesReducer,
|
rules: rulesReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
export default function configureStore(initialState) {
|
export default function configureStore(initialState) {
|
||||||
const createPersistentStore = compose(
|
const createPersistentStore = composeEnhancers(
|
||||||
persistStateEnhancer(),
|
persistStateEnhancer(),
|
||||||
applyMiddleware(thunkMiddleware, makeQueryExecuter()),
|
applyMiddleware(thunkMiddleware, makeQueryExecuter()),
|
||||||
)(createStore);
|
)(createStore);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// CSS
|
||||||
|
import 'src/style/chronograf.scss';
|
||||||
|
|
||||||
|
// Kapacitor Stories
|
||||||
|
import './kapacitor'
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {storiesOf, action, linkTo} from '@kadira/storybook';
|
||||||
|
|
||||||
|
import {spyActions} from './shared'
|
||||||
|
|
||||||
|
// Stubs
|
||||||
|
import kapacitor from './stubs/kapacitor';
|
||||||
|
import source from './stubs/source';
|
||||||
|
import rule from './stubs/rule';
|
||||||
|
import query from './stubs/query';
|
||||||
|
import queryConfigs from './stubs/queryConfigs';
|
||||||
|
|
||||||
|
// Actions for Spies
|
||||||
|
import * as kapacitorActions from 'src/kapacitor/actions/view'
|
||||||
|
import * as queryActions from 'src/chronograf/actions/view';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import KapacitorRule from 'src/kapacitor/components/KapacitorRule';
|
||||||
|
import ValuesSection from 'src/kapacitor/components/ValuesSection';
|
||||||
|
|
||||||
|
const valuesSection = (trigger, values) => (
|
||||||
|
<div className="rule-builder">
|
||||||
|
<ValuesSection
|
||||||
|
rule={rule({
|
||||||
|
trigger,
|
||||||
|
values,
|
||||||
|
})}
|
||||||
|
query={query()}
|
||||||
|
onChooseTrigger={action('chooseTrigger')}
|
||||||
|
onUpdateValues={action('updateRuleValues')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
storiesOf('ValuesSection', module)
|
||||||
|
.add('Threshold', () => (
|
||||||
|
valuesSection('threshold', {
|
||||||
|
"operator": "less than",
|
||||||
|
"rangeOperator": "greater than",
|
||||||
|
"value": "10",
|
||||||
|
"rangeValue": "20",
|
||||||
|
})
|
||||||
|
))
|
||||||
|
.add('Threshold inside Range', () => (
|
||||||
|
valuesSection('threshold', {
|
||||||
|
"operator": "inside range",
|
||||||
|
"rangeOperator": "greater than",
|
||||||
|
"value": "10",
|
||||||
|
"rangeValue": "20",
|
||||||
|
})
|
||||||
|
))
|
||||||
|
// .add('Threshold outside of Range', () => (
|
||||||
|
// valuesSection('threshold', {
|
||||||
|
// "operator": "otuside of range",
|
||||||
|
// "rangeOperator": "less than",
|
||||||
|
// "value": "10",
|
||||||
|
// "rangeValue": "20",
|
||||||
|
// })
|
||||||
|
// ))
|
||||||
|
.add('Relative', () => (
|
||||||
|
valuesSection('relative', {
|
||||||
|
"change": "change",
|
||||||
|
"operator": "greater than",
|
||||||
|
"shift": "1m",
|
||||||
|
"value": "10",
|
||||||
|
})
|
||||||
|
))
|
||||||
|
.add('Deadman', () => (
|
||||||
|
valuesSection('deadman', {
|
||||||
|
"period": "10m",
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
storiesOf('KapacitorRule', module)
|
||||||
|
.add('Threshold', () => (
|
||||||
|
<div className="chronograf-root">
|
||||||
|
<KapacitorRule
|
||||||
|
source={source()}
|
||||||
|
rule={rule({
|
||||||
|
trigger: 'threshold',
|
||||||
|
})}
|
||||||
|
query={query()}
|
||||||
|
queryConfigs={queryConfigs()}
|
||||||
|
kapacitor={kapacitor()}
|
||||||
|
queryActions={spyActions(queryActions)}
|
||||||
|
kapacitorActions={spyActions(kapacitorActions)}
|
||||||
|
addFlashMessage={action('addFlashMessage')}
|
||||||
|
enabledAlerts={['slack']}
|
||||||
|
isEditing={true}
|
||||||
|
router={{
|
||||||
|
push: action('route'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const spyActions = (actions) => Object.keys(actions)
|
||||||
|
.reduce((acc, a) => {
|
||||||
|
acc[a] = (...evt) => {
|
||||||
|
action(a)(...evt);
|
||||||
|
return actions[a](...evt);
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
|
@ -0,0 +1,16 @@
|
||||||
|
const kapacitor = () => {
|
||||||
|
return ({
|
||||||
|
"id": "1",
|
||||||
|
"name": "kapa",
|
||||||
|
"url": "http://chronograf.influxcloud.net:9092",
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "hunter2",
|
||||||
|
"links": {
|
||||||
|
"proxy": "http://localhost:3888/chronograf/v1/sources/2/kapacitors/1/proxy",
|
||||||
|
"self": "http://localhost:3888/chronograf/v1/sources/2/kapacitors/1",
|
||||||
|
"rules": "http://localhost:3888/chronograf/v1/sources/2/kapacitors/1/rules"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default kapacitor;
|
|
@ -0,0 +1,24 @@
|
||||||
|
const query = () => {
|
||||||
|
return ({
|
||||||
|
"id": "ad64c9e3-11d9-4e1a-bb6f-e80e09aec1cf",
|
||||||
|
"database": "telegraf",
|
||||||
|
"measurement": "cpu",
|
||||||
|
"retentionPolicy": "autogen",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field": "usage_idle",
|
||||||
|
"funcs": [
|
||||||
|
"mean"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {},
|
||||||
|
"groupBy": {
|
||||||
|
"time": "10s",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"areTagsAccepted": true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default query;
|
|
@ -0,0 +1,26 @@
|
||||||
|
const queryConfigs = () => {
|
||||||
|
return ({
|
||||||
|
"ad64c9e3-11d9-4e1a-bb6f-e80e09aec1cf": {
|
||||||
|
"id": "ad64c9e3-11d9-4e1a-bb6f-e80e09aec1cf",
|
||||||
|
"database": "telegraf",
|
||||||
|
"measurement": "cpu",
|
||||||
|
"retentionPolicy": "autogen",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field": "usage_idle",
|
||||||
|
"funcs": [
|
||||||
|
"mean"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {},
|
||||||
|
"groupBy": {
|
||||||
|
"time": "10s",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"areTagsAccepted": true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default queryConfigs;
|
|
@ -0,0 +1,54 @@
|
||||||
|
const rule = ({
|
||||||
|
trigger,
|
||||||
|
values,
|
||||||
|
}) => {
|
||||||
|
values = {
|
||||||
|
"rangeOperator": "greater than",
|
||||||
|
"change": "change",
|
||||||
|
"operator": "greater than",
|
||||||
|
"shift": "1m",
|
||||||
|
"value": "10",
|
||||||
|
"rangeValue": "20",
|
||||||
|
"period": "10m",
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ({
|
||||||
|
"id": "chronograf-v1-08cdb16b-7874-4c8f-858d-1c07043cb2f5",
|
||||||
|
"query": {
|
||||||
|
"id": "ad64c9e3-11d9-4e1a-bb6f-e80e09aec1cf",
|
||||||
|
"database": "telegraf",
|
||||||
|
"measurement": "cpu",
|
||||||
|
"retentionPolicy": "autogen",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field": "usage_idle",
|
||||||
|
"funcs": [
|
||||||
|
"mean"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": {},
|
||||||
|
"groupBy": {
|
||||||
|
"time": "10s",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"areTagsAccepted": true
|
||||||
|
},
|
||||||
|
"every": "30s",
|
||||||
|
"alerts": [],
|
||||||
|
"message": "",
|
||||||
|
trigger,
|
||||||
|
values,
|
||||||
|
"name": "Untitled Rule",
|
||||||
|
"tickscript": "var db = 'telegraf'\n\nvar rp = 'autogen'\n\nvar measurement = 'cpu'\n\nvar groupBy = []\n\nvar whereFilter = lambda: TRUE\n\nvar period = 10s\n\nvar every = 30s\n\nvar name = 'Untitled Rule'\n\nvar idVar = name + ':{{.Group}}'\n\nvar message = ''\n\nvar idTag = 'alertID'\n\nvar levelTag = 'level'\n\nvar messageField = 'message'\n\nvar durationField = 'duration'\n\nvar outputDB = 'chronograf'\n\nvar outputRP = 'autogen'\n\nvar outputMeasurement = 'alerts'\n\nvar triggerType = 'threshold'\n\nvar lower = 10\n\nvar upper = 20\n\nvar data = stream\n |from()\n .database(db)\n .retentionPolicy(rp)\n .measurement(measurement)\n .groupBy(groupBy)\n .where(whereFilter)\n |window()\n .period(period)\n .every(every)\n .align()\n |mean('usage_idle')\n .as('value')\n\nvar trigger = data\n |alert()\n .crit(lambda: \"value\" < lower AND \"value\" > upper)\n .stateChangesOnly()\n .message(message)\n .id(idVar)\n .idTag(idTag)\n .levelTag(levelTag)\n .messageField(messageField)\n .durationField(durationField)\n\ntrigger\n |influxDBOut()\n .create()\n .database(outputDB)\n .retentionPolicy(outputRP)\n .measurement(outputMeasurement)\n .tag('alertName', name)\n .tag('triggerType', triggerType)\n\ntrigger\n |httpOut('output')\n",
|
||||||
|
"links": {
|
||||||
|
"self": "/chronograf/v1/sources/2/kapacitors/1/rules/chronograf-v1-08cdb16b-7874-4c8f-858d-1c07043cb2f5",
|
||||||
|
"kapacitor": "/chronograf/v1/sources/2/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-08cdb16b-7874-4c8f-858d-1c07043cb2f5",
|
||||||
|
"output": "/chronograf/v1/sources/2/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-08cdb16b-7874-4c8f-858d-1c07043cb2f5%2Foutput"
|
||||||
|
},
|
||||||
|
"queryID": "ad64c9e3-11d9-4e1a-bb6f-e80e09aec1cf"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default rule;
|
|
@ -0,0 +1,18 @@
|
||||||
|
const source = () => {
|
||||||
|
return ({
|
||||||
|
"id": "2",
|
||||||
|
"name": "test-user",
|
||||||
|
"username": "test-user",
|
||||||
|
"password": "hunter2",
|
||||||
|
"url": "http://chronograf.influxcloud.net:8086",
|
||||||
|
"default": true,
|
||||||
|
"telegraf": "telegraf",
|
||||||
|
"links": {
|
||||||
|
"self": "http://localhost:3888/chronograf/v1/sources/2",
|
||||||
|
"kapacitors": "http://localhost:3888/chronograf/v1/sources/2/kapacitors",
|
||||||
|
"proxy": "http://localhost:3888/chronograf/v1/sources/2/proxy"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default source;
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue