commit
bd88803f9f
|
@ -25,6 +25,7 @@
|
|||
1. [#1942](https://github.com/influxdata/chronograf/pull/1942): Polish appearance of optional alert parameters in Kapacitor rule builder
|
||||
1. [#1944](https://github.com/influxdata/chronograf/pull/1944): Add active state for Status page navbar icon
|
||||
1. [#1944](https://github.com/influxdata/chronograf/pull/1944): Improve UX of navigation to a sub-nav item in the navbar
|
||||
1. [#1971](https://github.com/influxdata/chronograf/pull/1971): Resolve confusing deadman trigger alert rule UI
|
||||
|
||||
## v1.3.7.0 [2017-08-23]
|
||||
### Features
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import classnames from 'classnames'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import DatabaseList from 'src/shared/components/DatabaseList'
|
||||
import MeasurementList from 'src/shared/components/MeasurementList'
|
||||
|
@ -8,112 +6,53 @@ import FieldList from 'src/shared/components/FieldList'
|
|||
|
||||
import {defaultEveryFrequency} from 'src/kapacitor/constants'
|
||||
|
||||
export const DataSection = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
kapacitors: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
query: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
addFlashMessage: PropTypes.func,
|
||||
actions: PropTypes.shape({
|
||||
chooseNamespace: PropTypes.func.isRequired,
|
||||
chooseMeasurement: PropTypes.func.isRequired,
|
||||
applyFuncsToField: PropTypes.func.isRequired,
|
||||
chooseTag: PropTypes.func.isRequired,
|
||||
groupByTag: PropTypes.func.isRequired,
|
||||
toggleField: PropTypes.func.isRequired,
|
||||
groupByTime: PropTypes.func.isRequired,
|
||||
toggleTagAcceptance: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
onAddEvery: PropTypes.func.isRequired,
|
||||
onRemoveEvery: PropTypes.func.isRequired,
|
||||
timeRange: PropTypes.shape({}).isRequired,
|
||||
isKapacitorRule: PropTypes.bool,
|
||||
},
|
||||
class DataSection extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
childContextTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
self: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {source: this.props.source}
|
||||
},
|
||||
|
||||
handleChooseNamespace(namespace) {
|
||||
handleChooseNamespace = namespace => {
|
||||
this.props.actions.chooseNamespace(this.props.query.id, namespace)
|
||||
},
|
||||
}
|
||||
|
||||
handleChooseMeasurement(measurement) {
|
||||
handleChooseMeasurement = measurement => {
|
||||
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
|
||||
},
|
||||
}
|
||||
|
||||
handleToggleField(field) {
|
||||
handleToggleField = field => {
|
||||
this.props.actions.toggleField(this.props.query.id, field)
|
||||
// Every is only added when a function has been added to a field.
|
||||
// Here, the field is selected without a function.
|
||||
this.props.onRemoveEvery()
|
||||
// Because there are no functions there is no group by time.
|
||||
this.props.actions.groupByTime(this.props.query.id, null)
|
||||
},
|
||||
}
|
||||
|
||||
handleGroupByTime(time) {
|
||||
handleGroupByTime = time => {
|
||||
this.props.actions.groupByTime(this.props.query.id, time)
|
||||
},
|
||||
}
|
||||
|
||||
handleApplyFuncsToField(fieldFunc) {
|
||||
handleApplyFuncsToField = fieldFunc => {
|
||||
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc)
|
||||
this.props.onAddEvery(defaultEveryFrequency)
|
||||
},
|
||||
}
|
||||
|
||||
handleChooseTag(tag) {
|
||||
handleChooseTag = tag => {
|
||||
this.props.actions.chooseTag(this.props.query.id, tag)
|
||||
},
|
||||
}
|
||||
|
||||
handleToggleTagAcceptance() {
|
||||
handleToggleTagAcceptance = () => {
|
||||
this.props.actions.toggleTagAcceptance(this.props.query.id)
|
||||
},
|
||||
}
|
||||
|
||||
handleGroupByTag(tagKey) {
|
||||
handleGroupByTag = tagKey => {
|
||||
this.props.actions.groupByTag(this.props.query.id, tagKey)
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {query, timeRange: {lower}} = this.props
|
||||
const statement = query.rawText || buildInfluxQLQuery({lower}, query)
|
||||
|
||||
const {query, isKapacitorRule, isDeadman} = this.props
|
||||
return (
|
||||
<div className="rule-section">
|
||||
<h3 className="rule-section--heading">Select a Time Series</h3>
|
||||
<div className="rule-section--body">
|
||||
<pre className="rule-section--border-bottom">
|
||||
<code
|
||||
className={classnames({
|
||||
'metric-placeholder': !statement,
|
||||
})}
|
||||
>
|
||||
{statement || 'Build a query below'}
|
||||
</code>
|
||||
</pre>
|
||||
{this.renderQueryBuilder()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderQueryBuilder() {
|
||||
const {query, isKapacitorRule} = this.props
|
||||
|
||||
return (
|
||||
<div className="query-builder">
|
||||
<div className="query-builder rule-section--border-bottom">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
|
@ -125,16 +64,47 @@ export const DataSection = React.createClass({
|
|||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
/>
|
||||
{isDeadman
|
||||
? null
|
||||
: <FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {string, func, shape, bool} = PropTypes
|
||||
|
||||
DataSection.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
kapacitors: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
query: shape({
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
addFlashMessage: func,
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
}).isRequired,
|
||||
onAddEvery: func.isRequired,
|
||||
onRemoveEvery: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
isKapacitorRule: bool,
|
||||
isDeadman: bool,
|
||||
}
|
||||
|
||||
export default DataSection
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {PERIODS} from 'src/kapacitor/constants'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const periods = PERIODS.map(text => {
|
||||
return {text}
|
||||
})
|
||||
|
||||
const Deadman = ({rule, onChange}) =>
|
||||
<div className="rule-section--row">
|
||||
<p>Send Alert if Data is missing for</p>
|
||||
<Dropdown
|
||||
className="dropdown-80"
|
||||
menuClass="dropdown-malachite"
|
||||
items={periods}
|
||||
selected={rule.values.period}
|
||||
onChoose={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
const {shape, string, func} = PropTypes
|
||||
|
||||
Deadman.propTypes = {
|
||||
rule: shape({
|
||||
values: shape({
|
||||
period: string,
|
||||
}),
|
||||
}),
|
||||
onChange: func.isRequired,
|
||||
}
|
||||
|
||||
export default Deadman
|
|
@ -1,9 +1,7 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import DataSection from 'src/kapacitor/components/DataSection'
|
||||
import ValuesSection from 'src/kapacitor/components/ValuesSection'
|
||||
import RuleHeader from 'src/kapacitor/components/RuleHeader'
|
||||
import RuleGraph from 'src/kapacitor/components/RuleGraph'
|
||||
import RuleMessage from 'src/kapacitor/components/RuleMessage'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
|
@ -11,100 +9,20 @@ import {createRule, editRule} from 'src/kapacitor/apis'
|
|||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
|
||||
export const KapacitorRule = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({}).isRequired,
|
||||
rule: PropTypes.shape({}).isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}).isRequired,
|
||||
queryConfigActions: PropTypes.shape({}).isRequired,
|
||||
ruleActions: PropTypes.shape({}).isRequired,
|
||||
addFlashMessage: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool.isRequired,
|
||||
enabledAlerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
router: PropTypes.shape({
|
||||
push: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
kapacitor: PropTypes.shape({}).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
class KapacitorRule extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
timeRange: timeRanges.find(tr => tr.lower === 'now() - 15m'),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
queryConfigActions,
|
||||
source,
|
||||
enabledAlerts,
|
||||
queryConfigs,
|
||||
query,
|
||||
rule,
|
||||
ruleActions,
|
||||
isEditing,
|
||||
} = this.props
|
||||
const {chooseTrigger, updateRuleValues} = ruleActions
|
||||
const {timeRange} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<RuleHeader
|
||||
rule={rule}
|
||||
actions={ruleActions}
|
||||
onSave={isEditing ? this.handleEdit : this.handleCreate}
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
validationError={this.validationError()}
|
||||
timeRange={timeRange}
|
||||
source={source}
|
||||
/>
|
||||
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className="rule-builder">
|
||||
<DataSection
|
||||
timeRange={timeRange}
|
||||
source={source}
|
||||
query={query}
|
||||
actions={queryConfigActions}
|
||||
onAddEvery={this.handleAddEvery}
|
||||
onRemoveEvery={this.handleRemoveEvery}
|
||||
isKapacitorRule={true}
|
||||
/>
|
||||
<ValuesSection
|
||||
rule={rule}
|
||||
query={queryConfigs[rule.queryID]}
|
||||
onChooseTrigger={chooseTrigger}
|
||||
onUpdateValues={updateRuleValues}
|
||||
/>
|
||||
<RuleGraph
|
||||
timeRange={timeRange}
|
||||
source={source}
|
||||
query={query}
|
||||
rule={rule}
|
||||
/>
|
||||
<RuleMessage
|
||||
rule={rule}
|
||||
actions={ruleActions}
|
||||
enabledAlerts={enabledAlerts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
handleChooseTimeRange = ({lower}) => {
|
||||
const timeRange = timeRanges.find(range => range.lower === lower)
|
||||
this.setState({timeRange})
|
||||
},
|
||||
}
|
||||
|
||||
handleCreate() {
|
||||
handleCreate = () => {
|
||||
const {
|
||||
addFlashMessage,
|
||||
queryConfigs,
|
||||
|
@ -130,9 +48,9 @@ export const KapacitorRule = React.createClass({
|
|||
text: 'There was a problem creating the rule',
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleEdit() {
|
||||
handleEdit = () => {
|
||||
const {addFlashMessage, queryConfigs, rule} = this.props
|
||||
const updatedRule = Object.assign({}, rule, {
|
||||
query: queryConfigs[rule.queryID],
|
||||
|
@ -148,19 +66,19 @@ export const KapacitorRule = React.createClass({
|
|||
text: 'There was a problem updating the rule',
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleAddEvery(frequency) {
|
||||
handleAddEvery = frequency => {
|
||||
const {rule: {id: ruleID}, ruleActions: {addEvery}} = this.props
|
||||
addEvery(ruleID, frequency)
|
||||
},
|
||||
}
|
||||
|
||||
handleRemoveEvery() {
|
||||
handleRemoveEvery = () => {
|
||||
const {rule: {id: ruleID}, ruleActions: {removeEvery}} = this.props
|
||||
removeEvery(ruleID)
|
||||
},
|
||||
}
|
||||
|
||||
validationError() {
|
||||
validationError = () => {
|
||||
const {rule, query} = this.props
|
||||
if (rule.trigger === 'deadman') {
|
||||
return this.deadmanValidation()
|
||||
|
@ -175,16 +93,115 @@ export const KapacitorRule = React.createClass({
|
|||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
}
|
||||
|
||||
deadmanValidation() {
|
||||
deadmanValidation = () => {
|
||||
const {query} = this.props
|
||||
if (query && (!query.database || !query.measurement)) {
|
||||
return 'Deadman requires a database and measurement'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
handleRuleTypeDropdownChange = ({type, text}) => {
|
||||
const {ruleActions, rule} = this.props
|
||||
ruleActions.updateRuleValues(rule.id, rule.trigger, {
|
||||
...this.props.rule.values,
|
||||
[type]: text,
|
||||
})
|
||||
}
|
||||
|
||||
handleRuleTypeInputChange = e => {
|
||||
const {ruleActions, rule} = this.props
|
||||
const {lower, upper} = e.target.form
|
||||
|
||||
ruleActions.updateRuleValues(rule.id, rule.trigger, {
|
||||
...this.props.rule.values,
|
||||
value: lower.value,
|
||||
rangeValue: upper ? upper.value : '',
|
||||
})
|
||||
}
|
||||
|
||||
handleDeadmanChange = ({text}) => {
|
||||
const {ruleActions, rule} = this.props
|
||||
ruleActions.updateRuleValues(rule.id, rule.trigger, {period: text})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rule,
|
||||
source,
|
||||
isEditing,
|
||||
ruleActions,
|
||||
queryConfigs,
|
||||
enabledAlerts,
|
||||
queryConfigActions,
|
||||
} = this.props
|
||||
const {chooseTrigger, updateRuleValues} = ruleActions
|
||||
const {timeRange} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<RuleHeader
|
||||
rule={rule}
|
||||
actions={ruleActions}
|
||||
onSave={isEditing ? this.handleEdit : this.handleCreate}
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
validationError={this.validationError()}
|
||||
timeRange={timeRange}
|
||||
source={source}
|
||||
/>
|
||||
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className="rule-builder">
|
||||
<ValuesSection
|
||||
rule={rule}
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
onChooseTrigger={chooseTrigger}
|
||||
onAddEvery={this.handleAddEvery}
|
||||
onUpdateValues={updateRuleValues}
|
||||
query={queryConfigs[rule.queryID]}
|
||||
onRemoveEvery={this.handleRemoveEvery}
|
||||
queryConfigActions={queryConfigActions}
|
||||
onDeadmanChange={this.handleDeadmanChange}
|
||||
onRuleTypeInputChange={this.handleRuleTypeInputChange}
|
||||
onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange}
|
||||
/>
|
||||
<RuleMessage
|
||||
rule={rule}
|
||||
actions={ruleActions}
|
||||
enabledAlerts={enabledAlerts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
KapacitorRule.propTypes = {
|
||||
source: PropTypes.shape({}).isRequired,
|
||||
rule: PropTypes.shape({
|
||||
values: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}).isRequired,
|
||||
queryConfigActions: PropTypes.shape({}).isRequired,
|
||||
ruleActions: PropTypes.shape({}).isRequired,
|
||||
addFlashMessage: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool.isRequired,
|
||||
enabledAlerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
router: PropTypes.shape({
|
||||
push: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
kapacitor: PropTypes.shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default KapacitorRule
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {CHANGES, OPERATORS, SHIFTS} from 'src/kapacitor/constants'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
const changes = mapToItems(CHANGES, 'change')
|
||||
const shifts = mapToItems(SHIFTS, 'shift')
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
|
||||
const Relative = ({
|
||||
onRuleTypeInputChange,
|
||||
onDropdownChange,
|
||||
rule: {values: {change, shift, operator, value}},
|
||||
}) =>
|
||||
<div className="rule-section--row rule-section--border-bottom">
|
||||
<p>Send Alert when</p>
|
||||
<Dropdown
|
||||
className="dropdown-110"
|
||||
menuClass="dropdown-malachite"
|
||||
items={changes}
|
||||
selected={change}
|
||||
onChoose={onDropdownChange}
|
||||
/>
|
||||
<p>compared to previous</p>
|
||||
<Dropdown
|
||||
className="dropdown-80"
|
||||
menuClass="dropdown-malachite"
|
||||
items={shifts}
|
||||
selected={shift}
|
||||
onChoose={onDropdownChange}
|
||||
/>
|
||||
<p>is</p>
|
||||
<Dropdown
|
||||
className="dropdown-160"
|
||||
menuClass="dropdown-malachite"
|
||||
items={operators}
|
||||
selected={operator}
|
||||
onChoose={onDropdownChange}
|
||||
/>
|
||||
<form style={{display: 'flex'}}>
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px', marginLeft: '6px'}}
|
||||
type="text"
|
||||
name="lower"
|
||||
spellCheck="false"
|
||||
value={value}
|
||||
onChange={onRuleTypeInputChange}
|
||||
required={true}
|
||||
/>
|
||||
</form>
|
||||
{change === CHANGES[1] ? <p>%</p> : null}
|
||||
</div>
|
||||
|
||||
const {shape, string, func} = PropTypes
|
||||
|
||||
Relative.propTypes = {
|
||||
onRuleTypeInputChange: func.isRequired,
|
||||
onDropdownChange: func.isRequired,
|
||||
rule: shape({
|
||||
values: shape({
|
||||
change: string,
|
||||
shift: string,
|
||||
operator: string,
|
||||
value: string,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default Relative
|
|
@ -0,0 +1,72 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {OPERATORS} from 'src/kapacitor/constants'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
|
||||
const Threshold = ({
|
||||
rule: {values: {operator, value, rangeValue}},
|
||||
query,
|
||||
onDropdownChange,
|
||||
onRuleTypeInputChange,
|
||||
}) =>
|
||||
<div className="rule-section--row rule-section--border-bottom">
|
||||
<p>Send Alert where</p>
|
||||
<span className="rule-builder--metric">
|
||||
{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}
|
||||
</span>
|
||||
<p>is</p>
|
||||
<Dropdown
|
||||
className="dropdown-180"
|
||||
menuClass="dropdown-malachite"
|
||||
items={operators}
|
||||
selected={operator}
|
||||
onChoose={onDropdownChange}
|
||||
/>
|
||||
<form style={{display: 'flex'}}>
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px', marginLeft: '6px'}}
|
||||
type="text"
|
||||
name="lower"
|
||||
spellCheck="false"
|
||||
value={value}
|
||||
onChange={onRuleTypeInputChange}
|
||||
placeholder={
|
||||
operator === 'inside range' || operator === 'outside range'
|
||||
? 'Lower'
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{(operator === 'inside range' || operator === 'outside range') &&
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
name="upper"
|
||||
style={{width: '160px'}}
|
||||
placeholder="Upper"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
value={rangeValue}
|
||||
onChange={onRuleTypeInputChange}
|
||||
/>}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
const {shape, string, func} = PropTypes
|
||||
|
||||
Threshold.propTypes = {
|
||||
rule: shape({
|
||||
values: shape({
|
||||
operator: string,
|
||||
rangeOperator: string,
|
||||
value: string,
|
||||
rangeValue: string,
|
||||
}),
|
||||
}),
|
||||
onDropdownChange: func.isRequired,
|
||||
onRuleTypeInputChange: func.isRequired,
|
||||
query: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default Threshold
|
|
@ -1,253 +1,115 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import {Tab, TabList, TabPanels, TabPanel, Tabs} from 'shared/components/Tabs'
|
||||
import {OPERATORS, PERIODS, CHANGES, SHIFTS} from 'src/kapacitor/constants'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import Deadman from 'src/kapacitor/components/Deadman'
|
||||
import Threshold from 'src/kapacitor/components/Threshold'
|
||||
import Relative from 'src/kapacitor/components/Relative'
|
||||
import DataSection from 'src/kapacitor/components/DataSection'
|
||||
import RuleGraph from 'src/kapacitor/components/RuleGraph'
|
||||
|
||||
import {Tab, TabList, TabPanels, TabPanel, Tabs} from 'shared/components/Tabs'
|
||||
|
||||
const TABS = ['Threshold', 'Relative', 'Deadman']
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
|
||||
export const ValuesSection = React.createClass({
|
||||
propTypes: {
|
||||
rule: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
onChooseTrigger: PropTypes.func.isRequired,
|
||||
onUpdateValues: PropTypes.func.isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
},
|
||||
const handleChooseTrigger = (rule, onChooseTrigger) => triggerIndex => {
|
||||
if (TABS[triggerIndex] === rule.trigger) {
|
||||
return
|
||||
}
|
||||
return onChooseTrigger(rule.id, TABS[triggerIndex])
|
||||
}
|
||||
const initialIndex = rule => TABS.indexOf(_.startCase(rule.trigger))
|
||||
const isDeadman = rule => rule.trigger === 'deadman'
|
||||
|
||||
render() {
|
||||
const {rule, query} = this.props
|
||||
const initialIndex = TABS.indexOf(_.startCase(rule.trigger))
|
||||
|
||||
return (
|
||||
<div className="rule-section">
|
||||
<h3 className="rule-section--heading">Rule Conditions</h3>
|
||||
<div className="rule-section--body">
|
||||
<Tabs initialIndex={initialIndex} onSelect={this.handleChooseTrigger}>
|
||||
<TabList isKapacitorTabs="true">
|
||||
{TABS.map(tab =>
|
||||
<Tab key={tab} isKapacitorTab={true}>
|
||||
{tab}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Threshold
|
||||
rule={rule}
|
||||
query={query}
|
||||
onChange={this.handleValuesChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Relative rule={rule} onChange={this.handleValuesChange} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Deadman rule={rule} onChange={this.handleValuesChange} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
const ValuesSection = ({
|
||||
rule,
|
||||
query,
|
||||
source,
|
||||
timeRange,
|
||||
onAddEvery,
|
||||
onRemoveEvery,
|
||||
onDeadmanChange,
|
||||
queryConfigActions,
|
||||
onRuleTypeInputChange,
|
||||
onRuleTypeDropdownChange,
|
||||
onChooseTrigger,
|
||||
}) =>
|
||||
<div className="rule-section">
|
||||
<h3 className="rule-section--heading">Alert Type</h3>
|
||||
<div className="rule-section--body">
|
||||
<Tabs
|
||||
initialIndex={initialIndex(rule)}
|
||||
onSelect={handleChooseTrigger(rule, onChooseTrigger)}
|
||||
>
|
||||
<TabList isKapacitorTabs="true">
|
||||
{TABS.map(tab =>
|
||||
<Tab key={tab} isKapacitorTab={true}>
|
||||
{tab}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<div>
|
||||
<h3 className="rule-builder--sub-header">Time Series</h3>
|
||||
<DataSection
|
||||
query={query}
|
||||
timeRange={timeRange}
|
||||
isKapacitorRule={true}
|
||||
actions={queryConfigActions}
|
||||
onAddEvery={onAddEvery}
|
||||
onRemoveEvery={onRemoveEvery}
|
||||
isDeadman={isDeadman(rule)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
<h3 className="rule-builder--sub-header">Rule Conditions</h3>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Threshold
|
||||
rule={rule}
|
||||
query={query}
|
||||
onDropdownChange={onRuleTypeDropdownChange}
|
||||
onRuleTypeInputChange={onRuleTypeInputChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Relative
|
||||
rule={rule}
|
||||
onDropdownChange={onRuleTypeDropdownChange}
|
||||
onRuleTypeInputChange={onRuleTypeInputChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Deadman rule={rule} onChange={onDeadmanChange} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
{isDeadman(rule)
|
||||
? null
|
||||
: <RuleGraph
|
||||
rule={rule}
|
||||
query={query}
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
/>}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
handleChooseTrigger(triggerIndex) {
|
||||
const {rule, onChooseTrigger} = this.props
|
||||
if (TABS[triggerIndex] === rule.trigger) {
|
||||
return
|
||||
}
|
||||
const {shape, string, func} = PropTypes
|
||||
|
||||
onChooseTrigger(rule.id, TABS[triggerIndex])
|
||||
},
|
||||
|
||||
handleValuesChange(values) {
|
||||
const {onUpdateValues, rule} = this.props
|
||||
onUpdateValues(rule.id, rule.trigger, values)
|
||||
},
|
||||
})
|
||||
|
||||
const Threshold = React.createClass({
|
||||
propTypes: {
|
||||
rule: PropTypes.shape({
|
||||
values: PropTypes.shape({
|
||||
operator: PropTypes.string,
|
||||
rangeOperator: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
rangeValue: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
},
|
||||
|
||||
handleDropdownChange(item) {
|
||||
this.props.onChange({...this.props.rule.values, [item.type]: item.text})
|
||||
},
|
||||
|
||||
handleInputChange() {
|
||||
this.props.onChange({
|
||||
...this.props.rule.values,
|
||||
value: this.valueInput.value,
|
||||
rangeValue: this.valueRangeInput ? this.valueRangeInput.value : '',
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {operator, value, rangeValue} = this.props.rule.values
|
||||
const {query} = this.props
|
||||
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
|
||||
return (
|
||||
<div className="rule-section--row rule-section--border-bottom">
|
||||
<p>Send Alert where</p>
|
||||
<span className="rule-builder--metric">
|
||||
{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}
|
||||
</span>
|
||||
<p>is</p>
|
||||
<Dropdown
|
||||
className="dropdown-180"
|
||||
menuClass="dropdown-malachite"
|
||||
items={operators}
|
||||
selected={operator}
|
||||
onChoose={this.handleDropdownChange}
|
||||
/>
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px'}}
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
ref={r => (this.valueInput = r)}
|
||||
defaultValue={value}
|
||||
onKeyUp={this.handleInputChange}
|
||||
placeholder={
|
||||
operator === 'inside range' || operator === 'outside range'
|
||||
? 'Lower'
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{(operator === 'inside range' || operator === 'outside range') &&
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px'}}
|
||||
placeholder="Upper"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
ref={r => (this.valueRangeInput = r)}
|
||||
defaultValue={rangeValue}
|
||||
onKeyUp={this.handleInputChange}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const Relative = React.createClass({
|
||||
propTypes: {
|
||||
rule: PropTypes.shape({
|
||||
values: PropTypes.shape({
|
||||
change: PropTypes.string,
|
||||
shift: PropTypes.string,
|
||||
operator: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleDropdownChange(item) {
|
||||
this.props.onChange({...this.props.rule.values, [item.type]: item.text})
|
||||
},
|
||||
|
||||
handleInputChange() {
|
||||
this.props.onChange({...this.props.rule.values, value: this.input.value})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {change, shift, operator, value} = this.props.rule.values
|
||||
|
||||
const changes = mapToItems(CHANGES, 'change')
|
||||
const shifts = mapToItems(SHIFTS, 'shift')
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
|
||||
return (
|
||||
<div className="rule-section--row rule-section--border-bottom">
|
||||
<p>Send Alert when</p>
|
||||
<Dropdown
|
||||
className="dropdown-110"
|
||||
menuClass="dropdown-malachite"
|
||||
items={changes}
|
||||
selected={change}
|
||||
onChoose={this.handleDropdownChange}
|
||||
/>
|
||||
<p>compared to previous</p>
|
||||
<Dropdown
|
||||
className="dropdown-80"
|
||||
menuClass="dropdown-malachite"
|
||||
items={shifts}
|
||||
selected={shift}
|
||||
onChoose={this.handleDropdownChange}
|
||||
/>
|
||||
<p>is</p>
|
||||
<Dropdown
|
||||
className="dropdown-160"
|
||||
menuClass="dropdown-malachite"
|
||||
items={operators}
|
||||
selected={operator}
|
||||
onChoose={this.handleDropdownChange}
|
||||
/>
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px'}}
|
||||
ref={r => (this.input = r)}
|
||||
defaultValue={value}
|
||||
onKeyUp={this.handleInputChange}
|
||||
required={true}
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
/>
|
||||
{change === CHANGES[1] ? <p>%</p> : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const Deadman = React.createClass({
|
||||
propTypes: {
|
||||
rule: PropTypes.shape({
|
||||
values: PropTypes.shape({
|
||||
period: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleChange(item) {
|
||||
this.props.onChange({period: item.text})
|
||||
},
|
||||
|
||||
render() {
|
||||
const periods = PERIODS.map(text => {
|
||||
return {text}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rule-section--row">
|
||||
<p>Send Alert if Data is missing for</p>
|
||||
<Dropdown
|
||||
className="dropdown-80"
|
||||
menuClass="dropdown-malachite"
|
||||
items={periods}
|
||||
selected={this.props.rule.values.period}
|
||||
onChoose={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
ValuesSection.propTypes = {
|
||||
rule: shape({
|
||||
id: string,
|
||||
}).isRequired,
|
||||
onChooseTrigger: func.isRequired,
|
||||
onUpdateValues: func.isRequired,
|
||||
query: shape({}).isRequired,
|
||||
onDeadmanChange: func.isRequired,
|
||||
onRuleTypeDropdownChange: func.isRequired,
|
||||
onRuleTypeInputChange: func.isRequired,
|
||||
onAddEvery: func.isRequired,
|
||||
onRemoveEvery: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
queryConfigActions: shape({}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default ValuesSection
|
||||
|
|
|
@ -18,8 +18,6 @@ class KapacitorRulePage extends Component {
|
|||
enabledAlerts: [],
|
||||
kapacitor: {},
|
||||
}
|
||||
|
||||
this.isEditing = ::this.isEditing
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@ -97,7 +95,7 @@ class KapacitorRulePage extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
isEditing = () => {
|
||||
const {params} = this.props
|
||||
return params.ruleID && params.ruleID !== 'new'
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export const TabList = React.createClass({
|
|||
if (this.props.isKapacitorTabs === 'true') {
|
||||
return (
|
||||
<div className="rule-section--row rule-section--row-first rule-section--border-bottom">
|
||||
<p>Alert Type</p>
|
||||
<p>Choose One:</p>
|
||||
<div className="nav nav-tablist nav-tablist-sm nav-tablist-malachite">
|
||||
{children}
|
||||
</div>
|
||||
|
@ -147,11 +147,13 @@ export const Tabs = React.createClass({
|
|||
|
||||
render() {
|
||||
const children = React.Children.map(this.props.children, child => {
|
||||
if (child.type === TabPanels) {
|
||||
if (child && child.type === TabPanels) {
|
||||
return React.cloneElement(child, {
|
||||
activeIndex: this.state.activeIndex,
|
||||
})
|
||||
} else if (child.type === TabList) {
|
||||
}
|
||||
|
||||
if (child && child.type === TabList) {
|
||||
return React.cloneElement(child, {
|
||||
activeIndex: this.state.activeIndex,
|
||||
onActivate: this.handleActivateTab,
|
||||
|
|
|
@ -23,7 +23,7 @@ const getRange = (
|
|||
const subPad = bigNum => bigNum.times(SUB_FACTOR).toNumber()
|
||||
|
||||
const pad = v => {
|
||||
if (v === null || v === '') {
|
||||
if (v === null || v === '' || v === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,9 @@ $rule-builder--radius-lg: 5px;
|
|||
color: $g15-platinum;
|
||||
@include no-user-select();
|
||||
|
||||
&:first-child {margin-left: 0;}
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -59,7 +61,8 @@ $rule-builder--radius-lg: 5px;
|
|||
}
|
||||
.rule-section--heading {
|
||||
margin: 0;
|
||||
padding: $rule-builder--section-gap 0 $rule-builder--padding-md $rule-builder--left-gutter;
|
||||
padding: $rule-builder--section-gap 0 $rule-builder--padding-md
|
||||
$rule-builder--left-gutter;
|
||||
font-size: $page-header-size;
|
||||
font-weight: $page-header-weight;
|
||||
color: $g12-forge;
|
||||
|
@ -127,7 +130,6 @@ $rule-builder--radius-lg: 5px;
|
|||
margin-left: $rule-builder--padding-sm - 2px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Section 1 - Select a Time Series
|
||||
-----------------------------------------------------------------------------
|
||||
|
@ -157,10 +159,9 @@ $rule-builder--radius-lg: 5px;
|
|||
}
|
||||
.query-builder--column {
|
||||
margin-right: 2px;
|
||||
&:last-child {margin-right: 0;}
|
||||
}
|
||||
.query-builder--column:nth-child(1) .query-builder--list {
|
||||
border-bottom-left-radius: $rule-builder--radius-lg;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.query-builder--column:nth-child(4) .query-builder--list,
|
||||
.query-builder--column:nth-child(4) .query-builder--list-empty {
|
||||
|
@ -175,7 +176,9 @@ $rule-builder--radius-lg: 5px;
|
|||
}
|
||||
.group-by-tag.active {
|
||||
background-color: $c-rainforest !important;
|
||||
&:hover {background-color: $c-honeydew !important;}
|
||||
&:hover {
|
||||
background-color: $c-honeydew !important;
|
||||
}
|
||||
}
|
||||
.query-builder--list-item .query-builder--checkbox:after {
|
||||
background-color: $c-rainforest;
|
||||
|
@ -199,6 +202,13 @@ $rule-builder--radius-lg: 5px;
|
|||
Sectiom 2 - Rule Conditions
|
||||
-----------------------------------------------------------------------------
|
||||
*/
|
||||
.rule-builder--sub-header {
|
||||
margin: 0;
|
||||
padding: 30px 0 13px 0;
|
||||
font-size: 19px;
|
||||
font-weight: 400 !important;
|
||||
color: #a4a8b6;
|
||||
}
|
||||
.rule-builder--metric {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
@ -211,8 +221,6 @@ $rule-builder--radius-lg: 5px;
|
|||
@include no-user-select();
|
||||
}
|
||||
.rule-builder--graph {
|
||||
margin-left: $rule-builder--left-gutter;
|
||||
width: calc(100% - #{$rule-builder--left-gutter});
|
||||
background-color: $rule-builder--section-bg;
|
||||
border-radius: 0 0 $rule-builder--radius-lg $rule-builder--radius-lg;
|
||||
padding: 0 $rule-builder--padding-sm;
|
||||
|
@ -234,17 +242,6 @@ $rule-builder--radius-lg: 5px;
|
|||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
width: $rule-builder--accent-line-width;
|
||||
height: 100%;
|
||||
background-color: $rule-builder--accent-line-color;
|
||||
top: 0;
|
||||
left: (($rule-builder--dot / 2) - $rule-builder--left-gutter);
|
||||
}
|
||||
.container--dygraph-legend {
|
||||
transform: translateX(-50%);
|
||||
background-color: $g5-pepper;
|
||||
|
@ -294,8 +291,12 @@ textarea.rule-builder--message {
|
|||
border-radius: 0;
|
||||
@include custom-scrollbar($rule-builder--section-bg,$rule-builder--accent-color);
|
||||
|
||||
&:hover {border-color: $g4-onyx;}
|
||||
&:focus {background-color: $rule-builder--section-bg;}
|
||||
&:hover {
|
||||
border-color: $g4-onyx;
|
||||
}
|
||||
&:focus {
|
||||
background-color: $rule-builder--section-bg;
|
||||
}
|
||||
}
|
||||
.rule-builder--message-template {
|
||||
height: 30px;
|
||||
|
@ -348,9 +349,7 @@ textarea.rule-builder--message {
|
|||
margin: 0;
|
||||
height: 18px;
|
||||
line-height: 20px;
|
||||
transition:
|
||||
background-color 0.4s ease,
|
||||
color 0.4s ease,
|
||||
transition: background-color 0.4s ease, color 0.4s ease,
|
||||
box-shadow 0.4s ease;
|
||||
}
|
||||
.form-group > input.form-control:hover + label {
|
||||
|
@ -364,18 +363,25 @@ textarea.rule-builder--message {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Color coding for alerts in Alert History table
|
||||
-----------------------------------------------------------------------------
|
||||
*/
|
||||
.alert-level-ok {
|
||||
&, &:hover {color: $c-rainforest !important;}
|
||||
&,
|
||||
&:hover {
|
||||
color: $c-rainforest !important;
|
||||
}
|
||||
}
|
||||
.alert-level-warning {
|
||||
&, &:hover {color: $c-pineapple !important;}
|
||||
&,
|
||||
&:hover {
|
||||
color: $c-pineapple !important;
|
||||
}
|
||||
}
|
||||
.alert-level-critical {
|
||||
&, &:hover {color: $c-dreamsicle !important;}
|
||||
&,
|
||||
&:hover {
|
||||
color: $c-dreamsicle !important;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue