[editor] Add validation for template fields

closes #38

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-04-02 14:55:27 +02:00
parent 0dc648d02a
commit 4f12b5c206
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
5 changed files with 100 additions and 6 deletions

View file

@ -172,6 +172,10 @@ func patchConfig(filename, authorName, authorEmail, summary string, patcher func
return errors.Wrap(err, "patching config") return errors.Wrap(err, "patching config")
} }
if err = cfgFile.runLoadChecks(); err != nil {
return errors.Wrap(err, "checking config after patch")
}
return errors.Wrap( return errors.Wrap(
writeConfigToYAML(filename, authorName, authorEmail, summary, cfgFile), writeConfigToYAML(filename, authorName, authorEmail, summary, cfgFile),
"replacing config", "replacing config",

View file

@ -13,6 +13,7 @@ import (
var frontendReloadHooks = newHooker() var frontendReloadHooks = newHooker()
//nolint:funlen // Just contains a collection of objects
func registerEditorGlobalMethods() { func registerEditorGlobalMethods() {
for _, rd := range []plugins.HTTPRouteRegistrationArgs{ for _, rd := range []plugins.HTTPRouteRegistrationArgs{
{ {
@ -100,6 +101,23 @@ func registerEditorGlobalMethods() {
}, },
ResponseType: plugins.HTTPRouteResponseTypeTextPlain, ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
}, },
{
Description: "Validate a template expression against the built in template function library",
HandlerFunc: configEditorGlobalValidateTemplate,
Method: http.MethodPut,
Module: "config-editor",
Name: "Validate template expression",
Path: "/validate-template",
QueryParams: []plugins.HTTPRouteParamDocumentation{
{
Description: "The template expression to test",
Name: "template",
Required: true,
Type: "string",
},
},
ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
},
} { } {
if err := registerRoute(rd); err != nil { if err := registerRoute(rd); err != nil {
log.WithError(err).Fatal("Unable to register config editor route") log.WithError(err).Fatal("Unable to register config editor route")
@ -215,3 +233,12 @@ func configEditorGlobalValidateRegex(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
func configEditorGlobalValidateTemplate(w http.ResponseWriter, r *http.Request) {
if err := validateTemplate(r.FormValue("template")); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}

View file

@ -124,6 +124,7 @@
id="formAutoMessageMessage" id="formAutoMessageMessage"
v-model="models.autoMessage.message" v-model="models.autoMessage.message"
:state="models.autoMessage.message ? models.autoMessage.message.length <= validateAutoMessageMessageLength : false" :state="models.autoMessage.message ? models.autoMessage.message.length <= validateAutoMessageMessageLength : false"
@valid-template="valid => updateTemplateValid('autoMessage.message', valid)"
/> />
<div slot="description"> <div slot="description">
<font-awesome-icon <font-awesome-icon
@ -227,6 +228,7 @@
<template-editor <template-editor
id="formAutoMessageDisableOnTemplate" id="formAutoMessageDisableOnTemplate"
v-model="models.autoMessage.disable_on_template" v-model="models.autoMessage.disable_on_template"
@valid-template="valid => updateTemplateValid('autoMessage.disable_on_template', valid)"
/> />
</b-form-group> </b-form-group>
</b-col> </b-col>
@ -284,6 +286,10 @@ export default {
return false return false
} }
if (Object.entries(this.templateValid).filter(e => !e[1]).length > 0) {
return false
}
return true return true
}, },
@ -344,6 +350,7 @@ export default {
}, },
showAutoMessageEditModal: false, showAutoMessageEditModal: false,
templateValid: {},
} }
}, },
@ -376,6 +383,7 @@ export default {
...msg, ...msg,
sendMode: msg.cron ? 'cron' : 'lines', sendMode: msg.cron ? 'cron' : 'lines',
}) })
this.templateValid = {}
this.showAutoMessageEditModal = true this.showAutoMessageEditModal = true
}, },
@ -392,6 +400,7 @@ export default {
newAutoMessage() { newAutoMessage() {
Vue.set(this.models, 'autoMessage', {}) Vue.set(this.models, 'autoMessage', {})
this.templateValid = {}
this.showAutoMessageEditModal = true this.showAutoMessageEditModal = true
}, },
@ -422,6 +431,10 @@ export default {
}) })
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err)) .catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
}, },
updateTemplateValid(id, valid) {
Vue.set(this.templateValid, id, valid)
},
}, },
mounted() { mounted() {

View file

@ -398,6 +398,7 @@
<template-editor <template-editor
id="formRuleDisableOnTemplate" id="formRuleDisableOnTemplate"
v-model="models.rule.disable_on_template" v-model="models.rule.disable_on_template"
@valid-template="valid => updateTemplateValid('rule.disable_on_template', valid)"
/> />
</b-form-group> </b-form-group>
</b-tab> </b-tab>
@ -603,6 +604,7 @@
:id="`${models.rule.uuid}-action-${idx}-${field.key}`" :id="`${models.rule.uuid}-action-${idx}-${field.key}`"
v-model="models.rule.actions[idx].attributes[field.key]" v-model="models.rule.actions[idx].attributes[field.key]"
:state="validateActionArgument(idx, field.key)" :state="validateActionArgument(idx, field.key)"
@valid-template="valid => updateTemplateValid(`${models.rule.uuid}-action-${idx}-${field.key}`, valid)"
/> />
</b-form-group> </b-form-group>
@ -781,6 +783,7 @@ export default {
showRuleEditModal: false, showRuleEditModal: false,
showRuleSubscribeModal: false, showRuleSubscribeModal: false,
templateValid: {},
validateReason: null, validateReason: null,
} }
}, },
@ -850,6 +853,7 @@ export default {
cooldown: this.fixDurationRepresentationToString(msg.cooldown), cooldown: this.fixDurationRepresentationToString(msg.cooldown),
user_cooldown: this.fixDurationRepresentationToString(msg.user_cooldown), user_cooldown: this.fixDurationRepresentationToString(msg.user_cooldown),
}) })
this.templateValid = {}
this.showRuleEditModal = true this.showRuleEditModal = true
this.validateMatcherRegex() this.validateMatcherRegex()
}, },
@ -976,6 +980,7 @@ export default {
newRule() { newRule() {
Vue.set(this.models, 'rule', { match_message__validation: true }) Vue.set(this.models, 'rule', { match_message__validation: true })
this.templateValid = {}
this.showRuleEditModal = true this.showRuleEditModal = true
}, },
@ -1094,6 +1099,10 @@ export default {
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err)) .catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
}, },
updateTemplateValid(id, valid) {
Vue.set(this.templateValid, id, valid)
},
validateActionArgument(idx, key) { validateActionArgument(idx, key) {
const action = this.models.rule.actions[idx] const action = this.models.rule.actions[idx]
const def = this.getActionDefinitionByType(action.type) const def = this.getActionDefinitionByType(action.type)
@ -1189,6 +1198,11 @@ export default {
}, },
validateRule() { validateRule() {
if (Object.entries(this.templateValid).filter(e => !e[1]).length > 0) {
this.validateReason = 'templateValid'
return false
}
if (!this.models.rule.match_message__validation) { if (!this.models.rule.match_message__validation) {
this.validateReason = 'rule.match_message__validation' this.validateReason = 'rule.match_message__validation'
return false return false

View file

@ -1,11 +1,20 @@
<template> <template>
<div>
<div :class="wrapClasses"> <div :class="wrapClasses">
<div ref="editor" /> <div ref="editor" />
</div> </div>
<div
v-if="!isValid && validationError"
class="d-block invalid-feedback"
>
{{ validationError }}
</div>
</div>
</template> </template>
<script> <script>
import * as constants from './const.js' import * as constants from './const.js'
import axios from 'axios'
import { CodeJar } from 'codejar/codejar.js' import { CodeJar } from 'codejar/codejar.js'
import Prism from 'prismjs' import Prism from 'prismjs'
import { withLineNumbers } from 'codejar/linenumbers.js' import { withLineNumbers } from 'codejar/linenumbers.js'
@ -38,8 +47,8 @@ export default {
wrapClasses() { wrapClasses() {
return { return {
'form-control': true, 'form-control': true,
'is-invalid': this.state === false, 'is-invalid': this.state === false || !this.isValid,
'is-valid': this.state === true, 'is-valid': this.state === true && this.isValid,
'template-editor': true, 'template-editor': true,
} }
}, },
@ -48,7 +57,9 @@ export default {
data() { data() {
return { return {
emittedCode: '', emittedCode: '',
isValid: true,
jar: null, jar: null,
validationError: '',
} }
}, },
@ -57,6 +68,27 @@ export default {
const code = editor.textContent const code = editor.textContent
editor.innerHTML = Prism.highlight(code, this.grammar, 'template') editor.innerHTML = Prism.highlight(code, this.grammar, 'template')
}, },
validateTemplate(template) {
if (template === '') {
this.isValid = true
this.validationError = ''
this.$emit('valid-template', true)
return
}
return axios.put(`config-editor/validate-template?template=${encodeURIComponent(template)}`)
.then(() => {
this.isValid = true
this.validationError = ''
this.$emit('valid-template', true)
})
.catch(resp => {
this.isValid = false
this.validationError = resp.response.data.split(':1:')[1]
this.$emit('valid-template', false)
})
},
}, },
mounted() { mounted() {
@ -65,6 +97,7 @@ export default {
tab: ' '.repeat(2), tab: ' '.repeat(2),
}) })
this.jar.onUpdate(code => { this.jar.onUpdate(code => {
this.validateTemplate(code)
this.emittedCode = code this.emittedCode = code
this.$emit('input', code) this.$emit('input', code)
}) })
@ -101,8 +134,6 @@ export default {
<style> <style>
.template-editor { .template-editor {
background-color: #fff;
border-radius: 0.25rem;
color: #444; color: #444;
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
font-size: 87.5%; font-size: 87.5%;
@ -110,6 +141,11 @@ export default {
padding: 0; padding: 0;
} }
.template-editor .codejar-wrap {
background-color: #fff;
border-radius: 0.25rem;
}
.template-editor .codejar-linenumbers { .template-editor .codejar-linenumbers {
padding-right: 0.5em; padding-right: 0.5em;
text-align: right; text-align: right;