Merge pull request #725 from influxdata/feature/out-of-range#707

WIP Feature/out of range#707
pull/742/head
Andrew Watkins 2017-01-06 11:36:50 -07:00 committed by GitHub
commit 1f09402b26
25 changed files with 7593 additions and 65 deletions

View File

@ -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

View File

@ -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",
},

View File

@ -1997,8 +1997,8 @@
"equal to or greater",
"equal to",
"not equal to",
"is inside range",
"is outside range"
"inside range",
"outside range"
]
},
"value": {

View File

@ -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,

7
ui/.storybook/config.js Normal file
View File

@ -0,0 +1,7 @@
import { configure } from '@kadira/storybook';
function loadStories() {
require('../stories');
}
configure(loadStories, module);

1
ui/.storybook/head.html Normal file
View File

@ -0,0 +1 @@
<link href="/style.css" rel="stylesheet">

View File

@ -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'),
};

28
ui/corsless.js Normal file
View File

@ -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')
});

View File

@ -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",

View File

@ -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),
});
}

View File

@ -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);
};
},

View File

@ -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');

View File

@ -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'];

View File

@ -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;
}

View File

@ -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>

View File

@ -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);

5
ui/stories/index.js Normal file
View File

@ -0,0 +1,5 @@
// CSS
import 'src/style/chronograf.scss';
// Kapacitor Stories
import './kapacitor'

95
ui/stories/kapacitor.js Normal file
View File

@ -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>
));

8
ui/stories/shared.js Normal file
View File

@ -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;
}, {});

View File

@ -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;

24
ui/stories/stubs/query.js Normal file
View File

@ -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;

View File

@ -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;

54
ui/stories/stubs/rule.js Normal file
View File

@ -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;

View File

@ -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;

7115
ui/yarn.lock Normal file

File diff suppressed because it is too large Load Diff