@@ -208,6 +208,7 @@
:deleteMarker="field.deleteMarker || ''"
:insertMarker="field.insertMarker || ''"
:readonlyColumns="field.readonlyColumns || []"
+ :insertColumns="field.insertColumns || []"
:isLoading="!['fixed','variable'].includes(dynamicFieldStatus[field.name]) && (field.expression!=undefined || field.query!=undefined)"
:values="form[field.name]||[]"
@input="evaluateDynamicFields(field.name)"
@@ -225,6 +226,7 @@
:type="field.dateType"
value-type="format"
v-model="$v.form[field.name].$model"
+ @change="evaluateDynamicFields(field.name)"
>
@@ -620,20 +622,22 @@
}
},
methods:{
+ // used for enum field, to know the width of the container
calcContainerSize(){
var rect=this.$refs["container"]?.getBoundingClientRect()
if(rect){
this.containerSize.x=rect.x
this.containerSize.width=rect.width
}
-
},
+ // create a url with current values in embedded /-> experimental !
getFormUrl(){
var base64values=btoa(JSON.stringify(this.form))
var url=`${location.protocol}//${location.host}/#/form/?form=${encodeURIComponent(this.currentForm.name)}&base64values=${base64values}`
this.clip(url,true)
},
+ // to execute form events
doAction(a){
var ref=this
const action=Object.keys(a)[0]
@@ -670,6 +674,7 @@
setTimeout(()=>{ref.hideForm=false},wait*1000)
}
},
+ // as the parent to rerender the form vue component
reloadForm(){
this.$emit('rerender')
},
@@ -697,6 +702,7 @@
Vue.set(this.fieldOptions[fieldname],'debug',value) // flag editable
},
// forces any type to visible string
+ // used to visualize expressions
stringify(v){
if(v){
if(Array.isArray(v)){
@@ -733,7 +739,7 @@
&& (el.hide!==true || ref.showHidden))
});
},
- // creates a list of fields per group
+ // creates a list of fields per group & line
filterfieldsByGroupAndLine(group,line){
var ref=this
return this.filterfieldsByGroup(group).filter(function (el) {
@@ -797,12 +803,18 @@
}
// console.log("final => " + result)
if(result){
- Vue.set(ref.visibility,field.name,true)
+ ref.setVisibility(field.name,true)
}else{
- Vue.set(ref.visibility,field.name,false)
+ ref.setVisibility(field.name,false)
}
}
},
+ setVisibility(fieldname,status){
+ if(this.visibility[fieldname]!=status){
+ Vue.set(this.visibility,fieldname,status)
+ this.resetField(fieldname)
+ }
+ },
//----------------------------------------------------------------
// check if an entire group can be shown, based on field depencies
// hide a group if all fields inside are hidden
@@ -838,18 +850,43 @@
// reset to default value
// reset this field status
// console.log(`[${fieldname}] reset`)
+ this.initiateDefaults(fieldname)
this.setFieldStatus(fieldname,undefined)
Vue.set(this.form,fieldname,this.defaults[fieldname])
},
+ // reset all fields
resetFields(){
this.currentForm.fields.forEach((item, i) => {
this.resetField(item.name)
});
-
},
- // load default value in field
- defaultField(fieldname){
+ // new in 4.0.5
+ // instead of taking the default value, see if it needs to be evaluated
+ // allowing dynamic defaults
+ getDefaultValue(fieldname,value){
+ if(value!=undefined){
+ var _value = this.replacePlaceholderInString(value).value
+ // console.log(`${fieldname} -> ${value} -> ${_value}`)
+ if(this.fieldOptions[fieldname].evalDefault){
+ var r=undefined
+ try{
+ r=eval(_value)
+ return r
+ }catch(e){
+ console.log(`Error evaluating default value : ${e}`)
+ }
+ }else{
+ return _value
+ }
+ }else{
+ return value
+ }
+
+ },
+ // load default value in field => only for expressions
+ setFieldToDefault(fieldname){
// reset to default value
+ // console.log(`defaulting ${fieldname}`)
try{
// if there is a default, set "default" status
if(this.defaults[fieldname]!=undefined){
@@ -858,16 +895,20 @@
else{
// if no default, set to undefined
var prevState=this.dynamicFieldStatus[fieldname]
+
if(prevState=="running"){
+ // console.log(`defaulting ${fieldname}`)
// if the field was running, don't re-eval, we just did that
this.setFieldStatus(fieldname,undefined,false)
}else{
// if the field was something diff, re-eval
+ // console.log(`defaulting ${fieldname}`)
this.setFieldStatus(fieldname,undefined,true)
}
}
// set default value
Vue.set(this.form,fieldname,this.defaults[fieldname])
+
}catch(e){
// this error should not hit, unless we have a bug
console.log("Error: " + e)
@@ -877,12 +918,14 @@
// set dynamic field status, only for expressions,query and table
setFieldStatus(fieldname,status,reeval=true){
// console.log(`[${fieldname}] ----> ${status}`)
- var prevState=this.dynamicFieldStatus[fieldname]
- Vue.set(this.dynamicFieldStatus,fieldname,status)
- if(reeval && (prevState!=status)){
- // re-evaluate if need
- this.evaluateDynamicFields(fieldname)
- }
+ if(this.fieldOptions[fieldname]?.isDynamic){
+ var prevState=this.dynamicFieldStatus[fieldname]
+ Vue.set(this.dynamicFieldStatus,fieldname,status)
+ if(reeval && (prevState!=status)){
+ // re-evaluate if need
+ this.evaluateDynamicFields(fieldname)
+ }
+ }
},
// if 2 dependend fields (parent-child) both have defaults
// this can potentially be an issue.
@@ -901,15 +944,17 @@
}
return result
},
+ // new in 4.0.5, we now allow dynamic defaults
// first time run, load all the default values
- initiateDefaults(){
+ initiateDefaults(fieldname=undefined){
var ref=this
- this.currentForm.fields.forEach((item,i) => {
+ this.currentForm.fields.filter(x => !fieldname || fieldname==x.name).forEach((item,i) => {
if(item.name in ref.externalData){
ref.defaults[item.name]=ref.externalData[item.name]
}else{
- ref.defaults[item.name]=item.default
- }
+ // console.log(`defaulting ${item.name} -> ${item.default}`)
+ ref.defaults[item.name]=ref.getDefaultValue(item.name,item.default)
+ }
})
},
// add warnings for bad table values
@@ -920,8 +965,61 @@
var i=(data.length>1)?"are":"is"
this.warnings.push(`Table '${name}' has missing data
${c} '${data}' ${i} missing.`)
},
+ addDynamicFieldDependency(fields,field,foundfield){
+ var ref=this
+ var columnRegex = /([^.]+)\..+/g; // detect a "." in the field
+ var tmpArr=columnRegex.exec(foundfield) // found aaa.bbb
+ if(tmpArr && tmpArr.length>0){
+ // console.log("found dot in " + foundfield + " in " + field)
+ foundfield = tmpArr[1] // aaa
+ // console.log(tmpArr)
+ }else{
+ // console.log("found no in " + foundfield + " in " + field)
+ }
+ foundfield=foundfield.replace(/\[[0-9]*\]/,'') // xxx[y] => xxx
+ if(fields.includes(foundfield)){ // does field xxx exist in our form ?
+ //console.log(foundfield + " is a real field")
+ if(foundfield in ref.dynamicFieldDependencies){ // did we declare it before ?
+ if(ref.dynamicFieldDependencies[foundfield].indexOf(field) === -1) { // allready in there ?
+ ref.dynamicFieldDependencies[foundfield].push(field); // push it
+ if(foundfield==field){
+ // we capture self references
+ ref.warnings.push(`'${foundfield}' has a self reference
This will cause a racing condition`)
+ ref.$toast.error("You defined a self reference on field '"+foundfield+"'")
+ }
+ }
+ }else{
+ ref.dynamicFieldDependencies[foundfield]=[field]
+ if(foundfield==field){
+ // we capture self references
+ ref.warnings.push(`'${foundfield}' has a self reference
This will cause a racing condition`)
+ ref.$toast.error("You defined a self reference on field '"+foundfield+"'")
+ }
+ }
+
+ }else{
+ // we capture bad references
+ if(!Object.keys(ref.form).includes(foundfield))
+ ref.warnings.push(`'${field}' has a reference to unknown field '${foundfield}'
Your form might not function as expected`)
+ }
+ },
+ getPlaceholderMatches(fields,field,s){
+ var ref=this
+ var matches=[]
+ // console.log(typeof s)
+ if(s && typeof s=="string"){
+ var testRegex = /\$\(([^)]+)\)/g;
+ matches = s.matchAll(testRegex)
+ }
+ for(var match of matches){
+ // foundmatch = match[0]; // found $(xxx)
+ var foundfield = match[1]; // found xxx
+ ref.addDynamicFieldDependency(fields,field,foundfield)
+ }
+ },
// Find variable devDependencies
// we analyse which fields are dependent on others
+ // if they change, we then know which other fields to re-evaluate
findVariableDependencies(){
var ref=this
var watchdog=0
@@ -941,6 +1039,7 @@
})
// do the analysis
this.currentForm.fields.forEach((item,i) => {
+ // while we are looping, we also check if there are issues
if(item.dependencies){
item.dependencies.forEach((dep)=>{
if(!(fields.includes(dep.name) || ( dep.name.startsWith("!") && fields.includes(dep.name.slice(1)) ))){
@@ -957,55 +1056,17 @@
if(item.sameAs && !fields.includes(item.sameAs)){
ref.warnings.push(`'${item.name}' has bad 'sameAs' validation
${item.sameAs} is not a valid field name`)
}
- if(["expression","query","enum","table"].includes(item.type)){
- // query type is now deprecated
- if(item.type=='query'){
- ref.warnings.push(`'${item.name}' has the deprecated query type
Use enum type instead.`)
- }
-
- var testRegex = /\$\(([^)]+)\)/g;
- var matches=(item.expression || item.query || "").matchAll(testRegex);
- for(var match of matches){
- foundmatch = match[0]; // found $(xxx)
- foundfield = match[1]; // found xxx
- var columnRegex = /(.+)\.(.+)/g; // detect a "." in the field
- var tmpArr=columnRegex.exec(foundfield) // found aaa.bbb
- if(tmpArr && tmpArr.length>0){
- // console.log("found dot in " + foundfield + " in " + item.name)
- foundfield = tmpArr[1] // aaa
- }else{
- // console.log("found no in " + foundfield + " in " + item.name)
- }
- foundfield=foundfield.replace(/\[[0-9]*\]/,'') // xxx[y] => xxx
- if(fields.includes(foundfield)){ // does field xxx exist in our form ?
- //console.log(foundfield + " is a real field")
- if(foundfield in ref.dynamicFieldDependencies){ // did we declare it before ?
- if(ref.dynamicFieldDependencies[foundfield].indexOf(item.name) === -1) { // allready in there ?
- ref.dynamicFieldDependencies[foundfield].push(item.name); // push it
- if(foundfield==item.name){
- // we capture self references
- ref.warnings.push(`'${foundfield}' has a self reference
This will cause a racing condition`)
- ref.$toast.error("You defined a self reference on field '"+foundfield+"'")
- }
- }
- }else{
- ref.dynamicFieldDependencies[foundfield]=[item.name]
- if(foundfield==item.name){
- // we capture self references
- ref.warnings.push(`'${foundfield}' has a self reference
This will cause a racing condition`)
- ref.$toast.error("You defined a self reference on field '"+foundfield+"'")
- }
- }
-
- }else{
- // we capture bad references
- if(!Object.keys(ref.form).includes(foundfield))
- ref.warnings.push(`'${item.name}' has a reference to unknown field '${foundfield}'
Your form might not function as expected`)
- }
- }
+ // query type is now deprecated
+ if(item.type=='query'){
+ ref.warnings.push(`'${item.name}' has the deprecated query type
Use enum type instead.`)
}
+
+ this.getPlaceholderMatches(fields,item.name,item.expression ?? item.query)
+ this.getPlaceholderMatches(fields,item.name,item.default)
+
})
+ // check self references
while(!finishedFlag){
finishedFlag=true
temp = JSON.parse(JSON.stringify(ref.dynamicFieldDependencies)); // copy dependencies to temp
@@ -1093,15 +1154,18 @@
var keys = undefined
var targetflag=undefined
var hasPlaceholders = false;
+ if(typeof value!=="string"){
+ return {"hasPlaceholders":false,"value":value}
+ }
// console.log("item = " + value)
// console.log(typeof value)
// console.log(testRegex)
matches=[...value.matchAll(testRegex)] // force match array
for(match of matches){
- //console.log("-> match : " + match[0] + "->" + match[1])
+ // console.log("-> match : " + match[0] + "->" + match[1])
foundmatch = match[0]; // found $(xxx)
foundfield = match[1]; // found xxx
- var columnRegex = /(.+)\.(.+)/g; // detect a "." in the field
+ var columnRegex = /([^.]+)\.(.+)/g; // detect a "." in the field
var tmpArr=columnRegex.exec(foundfield) // found aaa.bbb
if(tmpArr && tmpArr.length>0){
foundfield = tmpArr[1] // aaa
@@ -1116,18 +1180,25 @@
targetflag = undefined
// mark the field as a dependent field
if(foundfield in ref.form){ // does field xxx exist in our form ?
- if(ref.fieldOptions[foundfield] && ["expression","table"].includes(ref.fieldOptions[foundfield].type) && (typeof ref.form[foundfield]=="object")){
+ // if the field exists
+ // and it's from an expression or table
+ // or it's deep link in a column (colum has .)
+ // and the reference field is an object
+ if(ref.fieldOptions[foundfield] && (["expression","table"].includes(ref.fieldOptions[foundfield].type)||column.includes(".")) && (typeof ref.form[foundfield]=="object")){
// objects and array should be stringified
- //fieldvalue=JSON.stringify(ref.form[foundfield])
- //console.log(Helpers.replacePlaceholders(match[1],ref.form))
+ fieldvalue=JSON.stringify(ref.form[foundfield])
+ // console.log(Helpers.replacePlaceholders(match[1],ref.form))
fieldvalue=JSON.stringify(Helpers.replacePlaceholders(match[1],ref.form)) // allow full object reference
// drop wrapping quotes
if(typeof fieldvalue=="string"){ // drop quotes if string
fieldvalue=fieldvalue.replace(/^\"+/, '').replace(/\"+$/, ''); // eslint-disable-line
+ }else{
+ // console.log(typeof fieldvalue)
}
+ // in other cases, it's a classic field.column reference
}else{
// other fields, grab a valid value
- fieldvalue = ref.getFieldValue(ref.form[foundfield],column,true);// get value of xxx
+ fieldvalue = ref.getFieldValue(ref.form[foundfield],column,true);// get value of aaa.bbb
}
// get dynamic field status
if(foundfield in ref.dynamicFieldStatus){
@@ -1304,7 +1375,7 @@
ref.checkDependencies(item)
if(ref.visibility[item.name]){ // only if they are visible
// if expression and not processed yet or needs to be reprocessed
- var flag = ref.dynamicFieldStatus[item.name]; // current field status (running/fixed/variable)
+ var flag = ref.dynamicFieldStatus[item.name]; // current field status (running/fixed/variable/default)
var placeholderCheck=undefined; // result for a placeholder check
if(item.expression && (flag==undefined || ref.hasDefaultDependencies(item.name))){ // if expression and not evaluated yet
// console.log("eval expression " + item.name)
@@ -1353,10 +1424,10 @@
}
Vue.delete(ref.queryerrors, item.name);
}catch(err){
- //console.log("Local eval failed : " + err)
+ // console.log("Local eval failed : " + err)
Vue.set(ref.queryerrors, item.name,err);
try{
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}catch(err){
ref.stopLoop("Defaulting " + item.name)
}
@@ -1392,7 +1463,7 @@
// expression returned undefined, so lets set to default if we have one
if(restresult.data.output==undefined && (ref.defaults[item.name]!=undefined)){
if(item.type=="expression"){
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}else{
ref.resetField(item.name)
}
@@ -1412,7 +1483,7 @@
}).catch(function (error) {
// console.log('Error ' + error.message)
try{
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}catch(err){
ref.stopLoop("Defaulting " + item.name)
}
@@ -1423,7 +1494,7 @@
}else{
// console.log(item.name + " is not evaluated yet");
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}
} else if(item.query && flag==undefined){
// console.log("eval query : " + item.name)
@@ -1445,7 +1516,7 @@
if(restresult.status=="error"){
//console.log(restresult.data.error)
if(item.type=="expression"){
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}else{
ref.resetField(item.name)
}
@@ -1471,7 +1542,7 @@
// console.log('Error ' + err.message)
try{
if(item.type=="expression"){
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}else{
ref.resetField(item.name)
}
@@ -1486,7 +1557,7 @@
//console.log(item.name + " is not evaluated yet");
try{
if(item.type=="expression"){
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}else{
ref.resetField(item.name)
}
@@ -1497,7 +1568,7 @@
}
}else{ // not visible field
if(item.type=="expression"){
- ref.defaultField(item.name)
+ ref.setFieldToDefault(item.name)
}else if(item.type=="query" || item.type=="enum" || item.type=="table"){
// console.log("resetting " + item.name)
ref.resetField(item.name)
@@ -1573,6 +1644,7 @@
},
// trigger this when a field has changed and we need to see if it has an impact.
evaluateDynamicFields(fieldname) {
+ // console.log(`${fieldname} changed`)
var ref=this;
// console.log(`[${fieldname}] eval trigger`)
// if this field is dependency
@@ -1587,6 +1659,7 @@
}
})
}
+ this.calcContainerSize();
},
// trigger a job abort
abortJob(id){
@@ -1910,8 +1983,10 @@
}
Vue.set(ref.externalData,item.name,queryValue)
}
+ Vue.set(ref.fieldOptions,item.name,{}) // storing some easy to find options
+ Vue.set(ref.fieldOptions[item.name],"evalDefault",item.evalDefault??false)
if(["expression","query","enum","table"].includes(item.type)){
- Vue.set(ref.fieldOptions,item.name,{}) // storing some easy to find options
+ Vue.set(ref.fieldOptions[item.name],"isDynamic",!!(item.expression??item.query??false))
Vue.set(ref.fieldOptions[item.name],"valueColumn",item.valueColumn||"")
Vue.set(ref.fieldOptions[item.name],"placeholderColumn",item.placeholderColumn||"")
Vue.set(ref.fieldOptions[item.name],"type",item.type)
@@ -1919,7 +1994,7 @@
if(item.refresh && typeof item.refresh=='string' && /[0-9]+s/.test(item.refresh)){
Vue.set(ref.fieldOptions[item.name],"refresh",item.refresh)
}
- Vue.set(ref.form,item.name,ref.externalData[item.name]??item.default)
+ Vue.set(ref.form,item.name,ref.externalData[item.name]??this.getDefaultValue(item.name,item.default))
if(item.type=="table" && !ref.defaults[item.name]){
Vue.set(ref.form,item.name,[])
}
@@ -1927,9 +2002,9 @@
Vue.set(ref.form,item.name,ref.externalData[item.name])
}
}else if(["checkbox"].includes(item.type)){
- Vue.set(ref.form,item.name,ref.externalData[item.name]??item.default??false)
+ Vue.set(ref.form,item.name,ref.externalData[item.name]??this.getDefaultValue(item.name,item.default)??false)
}else{
- Vue.set(ref.form,item.name,ref.externalData[item.name]??item.default??"")
+ Vue.set(ref.form,item.name,ref.externalData[item.name]??this.getDefaultValue(item.name,item.default)??"")
}
Vue.set(ref.visibility,item.name,true)
});
diff --git a/client/src/views/Jobs.vue b/client/src/views/Jobs.vue
index 82777fc5..88eb0abb 100644
--- a/client/src/views/Jobs.vue
+++ b/client/src/views/Jobs.vue
@@ -59,7 +59,7 @@
-
+
@@ -82,7 +82,7 @@
| {{j.user}} ({{j.user_type}}) |
-
+
|
@@ -262,7 +262,7 @@
if(status=="running"){
return "is-info"
}
- if(status=="warning" || status=="aborted"){
+ if(status=="warning" || status=="aborted" || status=="abandoned"){
return "is-warning"
}
if(status=="failed"){
diff --git a/server/package.json b/server/package.json
index 6bc5b760..b9ad5847 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "ansible_forms",
- "version": "4.0.5",
+ "version": "4.0.7",
"repository": {
"type": "git",
"url": "git://github.com/ansibleguy76/ansibleforms.git"
@@ -17,8 +17,8 @@
"clean": "rimraf dist"
},
"dependencies": {
- "core-js": "~3.29.1",
- "axios": "~1.3.2",
+ "core-js": "~3.30.1",
+ "axios": "~1.3.5",
"cors": "~2.8.5",
"bcrypt": "~5.1.0",
"cheerio": "~1.0.0-rc.12",
@@ -30,29 +30,29 @@
"winston-daily-rotate-file": "~4.7.1",
"passport": "~0.6.0",
"modern-passport-http": "~0.3.0",
- "passport-jwt": "~4.0.0",
+ "passport-jwt": "~4.0.1",
"@outlinewiki/passport-azure-ad-oauth2": "~0.1.0",
"mysql2": "~2.3.3",
"ajv": "~6.12.6",
"ajv-error-parser": "~1.0.7",
"lodash": "~4.17.21",
"bluebird": "~3.7.2",
- "node-jq": "~2.3.4",
+ "node-jq": "~2.3.5",
"moment": "~2.29.4",
- "yaml": "~2.1.3",
+ "yaml": "~2.2.1",
"cert-info":"~1.5.1",
"thenby":"~1.3.4",
"mssql":"~8.1.3",
"mongodb":"~4.11.0",
- "pg":"~8.8.0",
+ "pg":"~8.10.0",
"fs-extra":"~10.1.0",
"node-cache":"~5.1.2",
- "ldap-authentication": "~2.3.2",
+ "ldap-authentication": "~2.3.3",
"ldapjs": "~2.3.3",
"read-last-lines": "~1.8.0",
- "swagger-ui-express": "~4.5.0",
- "nodemailer": "~6.8.0",
- "nanoid": "~3.3.4"
+ "swagger-ui-express": "~4.6.2",
+ "nodemailer": "~6.9.1",
+ "nanoid": "~3.3.6"
},
"devDependencies": {
"@babel/core": "7.19.1",
diff --git a/server/schema/forms_schema.json b/server/schema/forms_schema.json
index 8a792991..e8bb5e21 100644
--- a/server/schema/forms_schema.json
+++ b/server/schema/forms_schema.json
@@ -896,6 +896,7 @@
{"type": "integer"}
]
},
+ "evalDefault": {"type": "boolean"},
"dateType": {"type": "string","enum":["date","datetime","year","month","time","week"]},
"noOutput": {"type": "boolean"},
"asCredential": {"type": "boolean"},
@@ -1000,6 +1001,10 @@
"type": "array",
"items": { "type": "string" }
},
+ "insertColumns": {
+ "type": "array",
+ "items": { "type": "string" }
+ },
"sticky": {"type": "boolean"},
"horizontal": {"type": "boolean"},
"editable": {"type": "boolean"},
diff --git a/server/schema/help.yaml b/server/schema/help.yaml
index 79d50193..6e7e5b3e 100644
--- a/server/schema/help.yaml
+++ b/server/schema/help.yaml
@@ -1539,6 +1539,14 @@
default: "2023-12-01"
required: true
help: A help message
+ - type: datetime
+ dateType: datetime
+ name: mydateandtime
+ line: Demo1
+ label: I will default to now
+ evalDefault: true
+ default: "new Date().toISOString().substring(0,19).replace('T',' ')"
+ required: true
- type: datetime
dateType: datetime
name: mydateandtime
@@ -1955,11 +1963,22 @@
description: |
The type of the value depends on the field type.
Can be `string`,`boolean`, `number`, `array` or `object`.
- When using `multiple`, the default is an `array`.
+ When using `multiple`, the default must be an `array`.
When using an `enum` field, you can use the following special values :
* `__auto__` : select the first
* `__none__` : select none
* `__all__` : select all (if multiple is true)
+ changelog:
+ - version: 4.0.5
+ type: added
+ description: A default can now hold placeholders like `$(other_field)`, and together with `evalDefault` be evaluated as javascript.
+ with_types: text, textarea, number, expression, password, enum, radio, checkbox, datetime
+ - name: evalDefault
+ type: boolean
+ default: false
+ short: Evaluates default value
+ description: A default can be treated as javascript and can thus hold expressions like `'$(other_field)'.toUpperCase()` or `Math.PI` or `Date.getDate.toISOString()`
+ version: 4.0.5
with_types: text, textarea, number, expression, password, enum, radio, checkbox, datetime
- name: required
type: boolean
@@ -2529,6 +2548,14 @@
description: |
With a tablefield, some fields might need to be readonly, like an `id` field for example.
Use this property to set the readonly fields.
+ - name: insertColumns
+ type: array
+ with_types: table
+ short: The insert columns in table
+ version: 4.0.5
+ description: |
+ With a tablefield, you can choose which fields are visible during insert. This way you can choose to hide certain fields.
+ Use this property to set the insert fields.
- name: tableFields
type: array
with_types: table
diff --git a/server/src/init/index.js b/server/src/init/index.js
index de388d24..521b7339 100644
--- a/server/src/init/index.js
+++ b/server/src/init/index.js
@@ -1,9 +1,17 @@
const logger=require("../lib/logger");
var Ssh = require('../models/ssh.model');
var Form = require('../models/form.model');
+var Job = require('../models/job.model')
Ssh.generate(false)
.catch((err)=>{
logger.error("Failed to generate ssh keys : " + err)
})
Form.initBackupFolder()
+Job.abandon()
+ .then((changed)=>{
+ logger.warning(`Abandoned ${changed} jobs`)
+ })
+ .catch((err)=>{
+ logger.error("Failed to abandon job : " + err)
+ })
diff --git a/server/src/models/job.model.js b/server/src/models/job.model.js
index 82055d50..38b23092 100644
--- a/server/src/models/job.model.js
+++ b/server/src/models/job.model.js
@@ -170,6 +170,12 @@ Job.create = function (record) {
return mysql.do("INSERT INTO AnsibleForms.`jobs` set ?", record)
.then((res)=>{ return res.insertId})
};
+Job.abandon = function () {
+ // abandon jobs
+ logger.notice(`Abandoning jobs`)
+ return mysql.do("UPDATE AnsibleForms.`jobs` set status='abandoned' where status='running' or status='abort'")
+ .then((res)=>{ return res.changedRows})
+};
Job.update = function (record,id) {
logger.notice(`Updating job ${id} ${record.status}`)
return mysql.do("UPDATE AnsibleForms.`jobs` set ? WHERE id=?", [record,id])
@@ -1205,7 +1211,7 @@ Awx.launchTemplate = async function (template,ev,inventory,tags,limit,c,d,v,cred
})
};
-Awx.trackJob = function (job,jobid,counter,previousoutput,previousoutput2=undefined,lastrun=false) {
+Awx.trackJob = function (job,jobid,counter,previousoutput,previousoutput2=undefined,lastrun=false,retryCount=0) {
return Awx.getConfig()
.catch((err)=>{
logger.error(err)
@@ -1292,16 +1298,35 @@ Awx.trackJob = function (job,jobid,counter,previousoutput,previousoutput2=undefi
})
.catch((err)=>{
message = err.toString()
- return Promise.resolve(message)
+ logger.error(message)
+ retryCount++
+ if(retryCount==10){
+ return Promise.resolve(message)
+ }else{
+ logger.warning(`Retrying jobid ${jobid} [${retryCount}]`)
+ return delay(1000).then(()=>{
+ return Awx.trackJob(job,jobid,counter,previousoutput,previousoutput2,lastrun,retryCount)
+ })
+ }
})
}else{
message=`could not find job with id ${job.id}`
- return Promise.resolve(message)
+ logger.error(message)
+ retryCount++
+ if(retryCount==10){
+ return Promise.resolve(message)
+ }else{
+ logger.warning(`Retrying jobid ${jobid} [${retryCount}]`)
+ return delay(1000).then(()=>{
+ return Awx.trackJob(job,jobid,counter,previousoutput,previousoutput2,lastrun,retryCount)
+ })
+ }
}
})
.catch(function (error) {
- logger.error(error.message)
- return Promise.resolve(error.message)
+ message=error.message
+ logger.error(message)
+ return Promise.resolve(message)
})
})
diff --git a/server/src/swagger.json b/server/src/swagger.json
index 46f706a9..738377bd 100644
--- a/server/src/swagger.json
+++ b/server/src/swagger.json
@@ -2,7 +2,7 @@
"swagger": "2.0",
"info": {
"description": "This is the swagger interface for AnsibleForms.\r\nUse the `/auth/login` api with basic authentication to obtain a JWT token.\r\nThen use the access token, prefixed with the word '**Bearer**' to use all other api's.\r\nNote that the access token is limited in time. You can then either login again and get a new set of tokens or use the `/token` api and the refresh token to obtain a new set (preferred).",
- "version": "4.0.5",
+ "version": "4.0.7",
"title": "AnsibleForms",
"contact": {
"email": "info@ansibleforms.com"