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"
|
||||
Equal = "equal to"
|
||||
NotEqual = "not equal to"
|
||||
InsideRange = "is inside range"
|
||||
OutsideRange = "is outside range"
|
||||
InsideRange = "inside range"
|
||||
OutsideRange = "outside range"
|
||||
)
|
||||
|
||||
// kapaOperator converts UI strings to kapacitor operators
|
||||
|
|
|
@ -205,7 +205,7 @@ func TestThresholdInsideRange(t *testing.T) {
|
|||
Trigger: "threshold",
|
||||
Alerts: []string{"slack", "victorops", "email"},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "is inside range",
|
||||
Operator: "inside range",
|
||||
Value: "90",
|
||||
RangeValue: "100",
|
||||
},
|
||||
|
@ -352,7 +352,7 @@ func TestThresholdOutsideRange(t *testing.T) {
|
|||
Trigger: "threshold",
|
||||
Alerts: []string{"slack", "victorops", "email"},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "is outside range",
|
||||
Operator: "outside range",
|
||||
Value: "90",
|
||||
RangeValue: "100",
|
||||
},
|
||||
|
|
|
@ -1997,8 +1997,8 @@
|
|||
"equal to or greater",
|
||||
"equal to",
|
||||
"not equal to",
|
||||
"is inside range",
|
||||
"is outside range"
|
||||
"inside range",
|
||||
"outside range"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
|
|
|
@ -110,7 +110,6 @@
|
|||
'no-new': 2,
|
||||
'no-octal-escape': 2,
|
||||
'no-octal': 2,
|
||||
'no-param-reassign': 2,
|
||||
'no-proto': 2,
|
||||
'no-redeclare': 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",
|
||||
"lint": "node_modules/eslint/bin/eslint.js src/",
|
||||
"test": "karma start",
|
||||
"clean": "rm -rf build"
|
||||
"clean": "rm -rf build",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook",
|
||||
"proxy": "node ./corsless"
|
||||
},
|
||||
"author": "",
|
||||
"eslintConfig": {
|
||||
|
@ -23,6 +26,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kadira/storybook": "^2.21.0",
|
||||
"autoprefixer": "^6.3.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-eslint": "6.1.2",
|
||||
|
@ -46,6 +50,7 @@
|
|||
"eslint": "3.9.1",
|
||||
"eslint-loader": "1.6.1",
|
||||
"eslint-plugin-react": "6.6.0",
|
||||
"express": "^4.14.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.8.5",
|
||||
"hanson": "^1.1.1",
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
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) {
|
||||
return AJAX({
|
||||
method: 'POST',
|
||||
url: kapacitor.links.rules,
|
||||
data: rule,
|
||||
data: rangeRule(rule),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +37,7 @@ export function editRule(rule) {
|
|||
return AJAX({
|
||||
method: 'PUT',
|
||||
url: rule.links.self,
|
||||
data: rule,
|
||||
data: rangeRule(rule),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export const RuleGraph = React.createClass({
|
|||
},
|
||||
|
||||
renderGraph() {
|
||||
const {query, source, timeRange} = this.props;
|
||||
const {query, source, timeRange, rule} = this.props;
|
||||
const autoRefreshMs = 30000;
|
||||
const queryText = selectStatement({lower: timeRange.queryValue}, query);
|
||||
const queries = [{host: source.links.proxy, text: queryText}];
|
||||
|
@ -46,6 +46,7 @@ export const RuleGraph = React.createClass({
|
|||
underlayCallback={this.createUnderlayCallback()}
|
||||
isGraphFilled={false}
|
||||
overrideLineColors={kapacitorLineColors}
|
||||
ruleValues={rule.values}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -83,12 +84,28 @@ export const RuleGraph = React.createClass({
|
|||
highlightEnd = +rule.values.value + width;
|
||||
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 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);
|
||||
};
|
||||
},
|
||||
|
|
|
@ -5,6 +5,8 @@ import {OPERATORS, PERIODS, CHANGES, SHIFTS} from 'src/kapacitor/constants';
|
|||
import _ from 'lodash';
|
||||
|
||||
const TABS = ['Threshold', 'Relative', 'Deadman'];
|
||||
const mapToItems = (arr, type) => arr.map((text) => ({text, type}));
|
||||
|
||||
export const ValuesSection = React.createClass({
|
||||
propTypes: {
|
||||
rule: PropTypes.shape({
|
||||
|
@ -65,7 +67,9 @@ const Threshold = React.createClass({
|
|||
rule: PropTypes.shape({
|
||||
values: PropTypes.shape({
|
||||
operator: PropTypes.string,
|
||||
rangeOperator: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
rangeValue: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
@ -74,26 +78,21 @@ const Threshold = React.createClass({
|
|||
|
||||
|
||||
handleDropdownChange(item) {
|
||||
const newValues = Object.assign({}, this.props.rule.values, {[item.type]: item.text});
|
||||
this.props.onChange(newValues);
|
||||
this.props.onChange({...this.props.rule.values, [item.type]: item.text});
|
||||
},
|
||||
|
||||
handleInputChange() {
|
||||
this.props.onChange(Object.assign({}, this.props.rule.values, {
|
||||
this.props.onChange({
|
||||
...this.props.rule.values,
|
||||
value: this.valueInput.value,
|
||||
}));
|
||||
rangeValue: this.valueRangeInput ? this.valueRangeInput.value : '',
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
const {operator, value} = this.props.rule.values;
|
||||
const {operator, value, rangeValue} = this.props.rule.values;
|
||||
const {query} = this.props;
|
||||
|
||||
function mapToItems(arr, type) {
|
||||
return arr.map((text) => {
|
||||
return {text, type};
|
||||
});
|
||||
}
|
||||
|
||||
const operators = mapToItems(OPERATORS, 'operator');
|
||||
|
||||
return (
|
||||
|
@ -102,7 +101,10 @@ const Threshold = React.createClass({
|
|||
<span>{query.fields.length ? query.fields[0].field : 'Select a Metric'}</span>
|
||||
<p>is</p>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
|
@ -122,22 +124,16 @@ const Relative = React.createClass({
|
|||
},
|
||||
|
||||
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() {
|
||||
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() {
|
||||
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 shifts = mapToItems(SHIFTS, 'shift');
|
||||
const operators = mapToItems(OPERATORS, 'operator');
|
||||
|
|
|
@ -11,12 +11,13 @@ export const defaultRuleConfigs = {
|
|||
threshold: {
|
||||
operator: 'greater than',
|
||||
value: '',
|
||||
rangeValue: '',
|
||||
relation: 'once',
|
||||
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 PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h'];
|
||||
export const CHANGES = ['change', '% change'];
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
/* eslint-disable no-magic-numbers */
|
||||
import React, {PropTypes} from 'react';
|
||||
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 = [
|
||||
'#00C9FF',
|
||||
|
@ -25,16 +32,17 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
ranges: shape({
|
||||
y: arrayOf(number.isRequired),
|
||||
y2: arrayOf(number.isRequired),
|
||||
y: arrayOf(number),
|
||||
y2: arrayOf(number),
|
||||
}),
|
||||
timeSeries: array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
labels: array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
options: object, // eslint-disable-line react/forbid-prop-types
|
||||
containerStyle: object, // eslint-disable-line react/forbid-prop-types
|
||||
timeSeries: array.isRequired,
|
||||
labels: array.isRequired,
|
||||
options: shape({}),
|
||||
containerStyle: shape({}),
|
||||
isGraphFilled: bool,
|
||||
overrideLineColors: array,
|
||||
dygraphSeries: shape({}).isRequired,
|
||||
ruleValues: shape({}),
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -54,7 +62,7 @@ export default React.createClass({
|
|||
componentDidMount() {
|
||||
const timeSeries = this.getTimeSeries();
|
||||
// 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 graphContainerNode = refs.graphContainer;
|
||||
|
@ -81,7 +89,7 @@ export default React.createClass({
|
|||
series: dygraphSeries,
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, ranges.y),
|
||||
valueRange: getRange(timeSeries, ranges.y, _.get(ruleValues, 'value', null), _.get(ruleValues, 'rangeValue', null)),
|
||||
},
|
||||
y2: {
|
||||
valueRange: getRange(timeSeries, ranges.y2),
|
||||
|
@ -141,14 +149,14 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
const timeSeries = this.getTimeSeries();
|
||||
const {labels, ranges, options, dygraphSeries} = this.props;
|
||||
const {labels, ranges, options, dygraphSeries, ruleValues} = this.props;
|
||||
|
||||
dygraph.updateOptions({
|
||||
labels,
|
||||
file: timeSeries,
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, ranges.y),
|
||||
valueRange: getRange(timeSeries, ranges.y, _.get(ruleValues, 'value', null), _.get(ruleValues, 'rangeValue', null)),
|
||||
},
|
||||
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) {
|
||||
return override;
|
||||
}
|
||||
|
||||
let max = null;
|
||||
let min = null;
|
||||
const subtractPadding = (val) => +val - val * PADDING_FACTOR;
|
||||
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++) {
|
||||
const val = series[i];
|
||||
|
||||
|
@ -192,17 +220,19 @@ function getRange(timeSeries, override) {
|
|||
min = val;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
if (typeof val === "number") {
|
||||
min = Math.min(min, val);
|
||||
max = Math.max(max, val);
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
});
|
||||
}, [null, null]);
|
||||
|
||||
// Dygraph will not reliably plot X / Y axis labels if min and max are both 0
|
||||
if (min === 0 && max === 0) {
|
||||
if (range[0] === 0 && range[1] === 0) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
return range;
|
||||
}
|
||||
|
|
|
@ -7,19 +7,34 @@ import _ from 'lodash';
|
|||
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph';
|
||||
import lastValues from 'src/shared/parsing/lastValues';
|
||||
|
||||
const {
|
||||
array,
|
||||
arrayOf,
|
||||
number,
|
||||
bool,
|
||||
shape,
|
||||
string,
|
||||
func,
|
||||
} = PropTypes;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'LineGraph',
|
||||
propTypes: {
|
||||
data: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
|
||||
title: PropTypes.string,
|
||||
isFetchingInitially: PropTypes.bool,
|
||||
isRefreshing: PropTypes.bool,
|
||||
underlayCallback: PropTypes.func,
|
||||
isGraphFilled: PropTypes.bool,
|
||||
overrideLineColors: PropTypes.array,
|
||||
queries: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired,
|
||||
showSingleStat: PropTypes.bool,
|
||||
activeQueryIndex: PropTypes.number,
|
||||
data: arrayOf(shape({}).isRequired).isRequired,
|
||||
ranges: shape({
|
||||
y: arrayOf(number),
|
||||
y2: arrayOf(number),
|
||||
}),
|
||||
title: string,
|
||||
isFetchingInitially: bool,
|
||||
isRefreshing: bool,
|
||||
underlayCallback: func,
|
||||
isGraphFilled: bool,
|
||||
overrideLineColors: array,
|
||||
queries: arrayOf(shape({}).isRequired).isRequired,
|
||||
showSingleStat: bool,
|
||||
activeQueryIndex: number,
|
||||
ruleValues: shape({}),
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -46,7 +61,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
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;
|
||||
|
||||
// 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}
|
||||
options={options}
|
||||
dygraphSeries={dygraphSeries}
|
||||
ranges={this.getRanges()}
|
||||
ranges={ranges || this.getRanges()}
|
||||
ruleValues={ruleValues}
|
||||
/>
|
||||
{showSingleStat ? <div className="graph-single-stat single-stat">{roundedValue}</div> : null}
|
||||
</div>
|
||||
|
|
|
@ -13,8 +13,10 @@ const rootReducer = combineReducers({
|
|||
rules: rulesReducer,
|
||||
});
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
export default function configureStore(initialState) {
|
||||
const createPersistentStore = compose(
|
||||
const createPersistentStore = composeEnhancers(
|
||||
persistStateEnhancer(),
|
||||
applyMiddleware(thunkMiddleware, makeQueryExecuter()),
|
||||
)(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