Add validation to Alert Rule message templates
parent
49f5c39c76
commit
7e2c7877bb
|
@ -53,6 +53,7 @@
|
|||
1. [#4388](https://github.com/influxdata/chronograf/pull/4388): Fix logs to progressively load results and provide feedback on search
|
||||
1. [#4408](https://github.com/influxdata/chronograf/pull/4408): Render null data point values in Alerts Table as mdashes
|
||||
1. [#4466](https://github.com/influxdata/chronograf/pull/4466): Maintain focus on Flux Editor text area when adding nodes via code
|
||||
1. [#4479](https://github.com/influxdata/chronograf/pull/4479): Add validation to Alert Rule messages
|
||||
|
||||
## v1.6.2 [2018-09-06]
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import ValuesSection from 'src/kapacitor/components/ValuesSection'
|
|||
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
|
||||
import RuleHandlers from 'src/kapacitor/components/RuleHandlers'
|
||||
import RuleMessage from 'src/kapacitor/components/RuleMessage'
|
||||
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
|
||||
|
||||
import {createRule, editRule} from 'src/kapacitor/apis'
|
||||
import buildInfluxQLQuery from 'src/utils/influxql'
|
||||
|
@ -234,6 +235,10 @@ class KapacitorRule extends Component<Props, State> {
|
|||
return notifyAlertRuleRequiresConditionValue()
|
||||
}
|
||||
|
||||
if (rule.message && !isValidMessage(rule.message)) {
|
||||
return 'Please correct template values in the alert message.'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes'
|
|||
|
||||
// Utils
|
||||
import buildQueries from 'src/utils/buildQueriesForGraphs'
|
||||
import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay'
|
||||
import underlayCallback from 'src/kapacitor/utils/ruleGraphUnderlay'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions'
|
||||
import {
|
||||
|
|
|
@ -3,7 +3,7 @@ import ReactTooltip from 'react-tooltip'
|
|||
|
||||
interface Props {
|
||||
onSave: () => void
|
||||
validationError?: string
|
||||
validationError: string
|
||||
}
|
||||
|
||||
const RuleHeaderSave: SFC<Props> = ({onSave, validationError}) => (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {SFC, ChangeEvent} from 'react'
|
||||
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
|
||||
|
||||
import {AlertRule} from 'src/types'
|
||||
|
||||
|
@ -7,16 +8,40 @@ interface Props {
|
|||
updateMessage: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
const RuleMessageText: SFC<Props> = ({rule, updateMessage}) => (
|
||||
<div className="rule-builder--message">
|
||||
<textarea
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
onChange={updateMessage}
|
||||
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}"
|
||||
value={rule.message}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
const RuleMessageText: SFC<Props> = ({rule, updateMessage}) => {
|
||||
const isValid = isValidMessage(rule.message)
|
||||
|
||||
const textAreaClass = isValid ? 'form-malachite' : 'form-volcano'
|
||||
|
||||
const iconName = isValid ? 'checkmark' : 'stop'
|
||||
|
||||
const validationCopy = isValid
|
||||
? 'Alert message is syntactically correct.'
|
||||
: 'Please correct templates in alert message.'
|
||||
|
||||
const outputStatusClass = isValid
|
||||
? 'query-status-output--success'
|
||||
: 'query-status-output--error'
|
||||
|
||||
return (
|
||||
<div className="rule-builder--message">
|
||||
<textarea
|
||||
className={`form-control input-sm monotype ${textAreaClass}`}
|
||||
onChange={updateMessage}
|
||||
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}"
|
||||
value={rule.message}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{rule.message ? (
|
||||
<div className="query-editor--status">
|
||||
<span className={`query-status-output ${outputStatusClass}`}>
|
||||
<span className={`icon ${iconName}`} />
|
||||
{validationCopy}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RuleMessageText
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'lodash'
|
||||
import {
|
||||
Handler,
|
||||
KeyMappings,
|
||||
|
@ -147,8 +148,11 @@ export const RULE_MESSAGE_TEMPLATES: RuleMessageTemplate = {
|
|||
},
|
||||
tags: {
|
||||
label: '{{.Tags}}',
|
||||
text:
|
||||
'Map of tags. Use <code>{{ index .Tags "key" }}</code> to get a specific tag value',
|
||||
text: 'Map of tags.',
|
||||
},
|
||||
tag: {
|
||||
label: '{{ index .Tags "value" }}',
|
||||
text: 'A specific tag value',
|
||||
},
|
||||
level: {
|
||||
label: '{{.Level}}',
|
||||
|
@ -156,15 +160,26 @@ export const RULE_MESSAGE_TEMPLATES: RuleMessageTemplate = {
|
|||
'Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>',
|
||||
},
|
||||
fields: {
|
||||
label: '{{.Fields}}',
|
||||
text: 'Map of fields',
|
||||
},
|
||||
field: {
|
||||
label: '{{ index .Fields "value" }}',
|
||||
text:
|
||||
'Map of fields. Use <code>{{ index .Fields "key" }}</code> to get a specific field value',
|
||||
text: 'A specific field value',
|
||||
},
|
||||
time: {
|
||||
label: '{{.Time}}',
|
||||
text: 'The time of the point that triggered the event',
|
||||
},
|
||||
}
|
||||
|
||||
export const RULE_MESSAGE_TEMPLATE_TEXTS = _.map(RULE_MESSAGE_TEMPLATES, t => {
|
||||
const label = t.label
|
||||
const templateRegexp = /((?:{{)([^{}]+)(?:}}))/g // matches {{*}} where star does not contain '{' or '}'
|
||||
const match = templateRegexp.exec(label)
|
||||
return match[2].trim()
|
||||
})
|
||||
|
||||
// DEFAULT_HANDLERS are empty alert templates for handlers that don't exist in the kapacitor config
|
||||
export const DEFAULT_HANDLERS: Handler[] = [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import _ from 'lodash'
|
||||
import {RULE_MESSAGE_TEMPLATE_TEXTS} from 'src/kapacitor/constants'
|
||||
|
||||
export const isValidTemplate = (template: string): boolean => {
|
||||
const exactMatch = !!_.find(RULE_MESSAGE_TEMPLATE_TEXTS, t => t === template)
|
||||
const fieldsRegex = /(index .Fields ".+")/
|
||||
const tagsRegex = /(index .Tags ".+")/
|
||||
const fuzzyMatch = fieldsRegex.test(template) || tagsRegex.test(template)
|
||||
return exactMatch || fuzzyMatch
|
||||
}
|
||||
|
||||
export const mismatchedBrackets = (str: string): boolean => {
|
||||
const arr = str.split('')
|
||||
const accumulator: string[] = []
|
||||
let isMismatched = false
|
||||
arr.forEach(cur => {
|
||||
if (cur === '{') {
|
||||
accumulator.push('{')
|
||||
}
|
||||
if (cur === '}') {
|
||||
const lastElt = accumulator.pop()
|
||||
if (lastElt !== '{') {
|
||||
isMismatched = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (accumulator.length !== 0) {
|
||||
isMismatched = true
|
||||
}
|
||||
return isMismatched
|
||||
}
|
||||
|
||||
export const isValidMessage = (message: string): boolean => {
|
||||
if (message[message.length] === '}') {
|
||||
message = message + ' '
|
||||
}
|
||||
const malformedTemplateRegexp1 = RegExp('(({{)([^{}]*)(})([^}]+))') // matches {{*} where star does not contain '{' or '}'
|
||||
|
||||
if (malformedTemplateRegexp1.test(message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (mismatchedBrackets(message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const templateRegexp = /((?:{{)([^{}]*)(?:}}))/g // matches {{*}} where star does not contain '{' or '}'
|
||||
const matches = []
|
||||
let match = templateRegexp.exec(message)
|
||||
while (match) {
|
||||
matches[matches.length] = match[2].trim()
|
||||
match = templateRegexp.exec(message)
|
||||
}
|
||||
return _.every(matches, m => isValidTemplate(m))
|
||||
}
|
||||
|
||||
export default isValidMessage
|
|
@ -228,8 +228,10 @@ export interface RuleMessageTemplate {
|
|||
taskName: RuleMessage
|
||||
group: RuleMessage
|
||||
tags: RuleMessage
|
||||
tag: RuleMessage
|
||||
level: RuleMessage
|
||||
fields: RuleMessage
|
||||
field: RuleMessage
|
||||
time: RuleMessage
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
isValidMessage,
|
||||
mismatchedBrackets,
|
||||
isValidTemplate,
|
||||
} from 'src/kapacitor/utils/alertMessageValidation'
|
||||
import {RULE_MESSAGE_TEMPLATE_TEXTS} from 'src/kapacitor/constants'
|
||||
|
||||
describe('kapacitor.utils.alertMessageValidation', () => {
|
||||
describe('isValidMessage', () => {
|
||||
it('accepts message containing one simple template', () => {
|
||||
const isValid = isValidMessage('{{.ID}}')
|
||||
|
||||
expect(isValid).toEqual(true)
|
||||
})
|
||||
|
||||
it('accepts message containing one simple and one complex template', () => {
|
||||
const isValid = isValidMessage('{{ index .Tags "something" }} {{.Name}}')
|
||||
|
||||
expect(isValid).toEqual(true)
|
||||
})
|
||||
|
||||
it('accepts message containing templates and strings mixed', () => {
|
||||
const isValid = isValidMessage(
|
||||
'{{ index .Tags "moo" }} lkajsdflkjasdf {{.Name}}lksjdflsj'
|
||||
)
|
||||
|
||||
expect(isValid).toEqual(true)
|
||||
})
|
||||
|
||||
it('rejects message with invalid template', () => {
|
||||
const isValid = isValidMessage('{{ I am invalid}}')
|
||||
|
||||
expect(isValid).toEqual(false)
|
||||
})
|
||||
|
||||
it('rejects message containing template with missing closing bracket', () => {
|
||||
const isValid = isValidMessage('{{ index .Tags "value" } {{.Name}}')
|
||||
|
||||
expect(isValid).toEqual(false)
|
||||
})
|
||||
|
||||
it('rejects message containing non-matching brackets', () => {
|
||||
const isValid = isValidMessage('{{ index .Tags "value" {{.Name}}')
|
||||
|
||||
expect(isValid).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mismatchedBrackets', () => {
|
||||
it('String containing matched brackets is not mismatched', () => {
|
||||
const isMismatched = mismatchedBrackets('{{}}')
|
||||
|
||||
expect(isMismatched).toEqual(false)
|
||||
})
|
||||
it('String containing matched brackets and other characters is not mismatched', () => {
|
||||
const isMismatched = mismatchedBrackets('asdf{{asdfaasdas}}asdfa')
|
||||
|
||||
expect(isMismatched).toEqual(false)
|
||||
})
|
||||
it('String containing unmatched brackets is mismatched', () => {
|
||||
const isMismatched = mismatchedBrackets('{{}')
|
||||
|
||||
expect(isMismatched).toEqual(true)
|
||||
})
|
||||
it('String containing unmatched brackets and other characters is mismatched', () => {
|
||||
const isMismatched = mismatchedBrackets('asdf{{as}asdfa)')
|
||||
|
||||
expect(isMismatched).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidTemplate', () => {
|
||||
it('is True for an exact match to a valid template', () => {
|
||||
const isValid = isValidTemplate(RULE_MESSAGE_TEMPLATE_TEXTS[0])
|
||||
|
||||
expect(isValid).toEqual(true)
|
||||
})
|
||||
it('is False for a jibberish input', () => {
|
||||
const isValid = isValidTemplate('laslkj;owaiu0294u,mxn')
|
||||
|
||||
expect(isValid).toEqual(false)
|
||||
})
|
||||
it('is True for a fuzzy match to tags', () => {
|
||||
const isValid = isValidTemplate('(index .Tags "lalala")')
|
||||
|
||||
expect(isValid).toEqual(true)
|
||||
})
|
||||
it('is False for distorted version of tags', () => {
|
||||
const isValid = isValidTemplate('(indeasdfx .Tags "lalala")')
|
||||
|
||||
expect(isValid).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue