Skip to content
Snippets Groups Projects
Commit 1e056fb9 authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Erwan Rouchet
Browse files

Split new configuration creation form in two components + clone configuration

parent 51cf49fb
No related branches found
No related tags found
1 merge request!1393Split new configuration creation form in two components + clone configuration
<template>
<form
v-on:submit.prevent="createConfiguration"
>
<ConfigurationForm
v-on:form-errors="disabledTitle = $event"
:worker-version-id="workerVersionId"
:worker-id="workerId"
v-model="formConfiguration"
/>
<div class="help is-danger mb-2" v-if="configurationExistsError">
<p v-if="parsedConfigurationExistsError.name">{{ parsedConfigurationExistsError.name[0] }}</p>
<p v-if="parsedConfigurationExistsError.configuration">
A configuration already exists with this configuration for this worker.
<a v-on:click="$emit('set-configuration', parsedConfigurationExistsError.id)">Use this configuration</a>.
</p>
</div>
<button
type="button"
class="button is-success"
:disabled="disabledTitle || null"
v-on:click="createConfiguration"
:title="!disabledTitle ? 'Create a new configuration' : disabledTitle"
>
Create
</button>
</form>
</template>
<script>
import { isEmpty, cloneDeep } from 'lodash'
import { mapMutations, mapState } from 'vuex'
import { errorParser } from '@/helpers'
import ConfigurationForm from './Form'
export default {
emits: [
'set-configuration',
'config-created'
],
components: {
ConfigurationForm
},
props: {
workerVersionId: {
type: String,
required: true
},
workerId: {
type: String,
required: true
},
newConfiguration: {
type: Object,
required: true
}
},
data: () => ({
loading: false,
configurationExistsError: null,
disabledTitle: '',
formConfiguration: {
name: '',
configuration: {}
}
}),
computed: {
...mapState('process', ['workerConfigurations', 'workerVersions']),
parsedConfigurationExistsError () {
// The error returned by the backend returns the existing configuration's ID like this [ID]
if (!this.configurationExistsError) return
if (this.configurationExistsError.id?.length > 1) throw new Error('Mutiple existing configuration IDs returned.')
const parsed = Object.assign({}, this.configurationExistsError)
if (this.configurationExistsError.id) parsed.id = this.configurationExistsError.id[0]
return parsed
}
},
methods: {
...mapMutations('notifications', ['notify']),
async createConfiguration () {
if (this.loading || this.disabledTitle) return
this.loading = true
try {
const configuration = await this.$store.dispatch('process/createConfiguration', {
workerId: this.workerId,
configuration: {
name: this.formConfiguration.name,
configuration: this.formConfiguration.configuration
}
})
// Go back to configurations list only if the creation succeeded
this.$emit('config-created')
// Select the newly created configuration
this.$emit('set-configuration', configuration.id)
this.notify({ type: 'info', text: 'Configuration created. Save to add it to this worker run.' })
} catch (err) {
this.configurationExistsError = err.response.data
this.$store.commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
this.loading = false
}
}
},
watch: {
newConfiguration: {
immediate: true,
handler (newValue) {
if (!isEmpty(newValue.configuration)) this.formConfiguration = cloneDeep(newValue)
}
}
}
}
</script>
......@@ -65,11 +65,13 @@ export default {
computed: {
valuesDict () {
const aList = []
for (const [k, v] of Object.entries(this.modelValue)) {
const item = {}
item.key = k
item.value = v
aList.push(item)
if (typeof this.modelValue === 'object') {
for (const [k, v] of Object.entries(this.modelValue)) {
const item = {}
item.key = k
item.value = v
aList.push(item)
}
}
aList.push({
key: null,
......
......@@ -96,7 +96,15 @@ export default {
methods: {
addRow () {
this.itemError.push(null)
this.newList.push('')
if (this.field.subtype === 'bool') {
/**
* In the case of a list of booleans, adding a new item should update the validated list
* right away, because there can't be an 'empty' boolean value. If a new item is added to
* the list, a new 'true' value is added to the list.
*/
this.newList.push(true)
this.validatedList.push(true)
} else this.newList.push('')
},
removeItem (i) {
this.newList.splice(i, 1)
......
<template>
<form v-on:submit.prevent="createConfiguration">
<form>
<div class="field">
<label class="label">Name</label>
<input
class="input"
type="text"
v-model="configurationName"
v-model="newConfiguration.name"
v-on:update="$emit('update:modelValue', newConfiguration)"
:disabled="loading || null"
/>
</div>
......@@ -33,7 +34,7 @@
<p class="help" v-else>You can define here any JSON object that will be made available to the worker when it runs.</p>
</div>
</div>
<form class="mb-3" v-else-if="Object.keys(rawFormConfiguration).length">
<form class="mb-3" v-else-if="Object.keys(newConfiguration.configuration).length">
<label class="label">Configuration</label>
<div
class="field"
......@@ -49,41 +50,25 @@
:field="field"
:field-id="key"
:class="{ 'is-danger': Boolean(configurationErrors[key]) }"
v-model="rawFormConfiguration[key]"
v-model="newConfiguration.configuration[key]"
/>
</div>
<p class="help is-danger" v-if="configurationErrors[key]">{{ configurationErrors[key] }}</p>
</div>
</form>
<div class="help is-danger mb-2" v-if="configurationExistsError">
<p v-if="parsedConfigurationExistsError.name">{{ parsedConfigurationExistsError.name }}</p>
<p v-if="parsedConfigurationExistsError.configuration && workerConfigurations[workerId][parsedConfigurationExistsError.id]">
Configuration {{ workerConfigurations[workerId][parsedConfigurationExistsError.id].name }} already exists with this
configuration for this worker. <a v-on:click="$emit('set-configuration', parsedConfigurationExistsError.id)">Use this configuration</a>.
</p>
</div>
<button
type="button"
class="button is-success"
:disabled="!allowCreate || null"
v-on:click="createConfiguration"
:title="allowCreate ? 'Create a new configuration' : disabledTitle"
>
Create
</button>
</form>
</template>
<script>
import { isPlainObject, isEmpty, isEqual, cloneDeep, sortBy } from 'lodash'
import { ConfigurationValidationError, errorParser } from '@/helpers'
import { mapMutations, mapState, mapActions } from 'vuex'
import { ConfigurationValidationError } from '@/helpers'
import { mapMutations, mapState } from 'vuex'
import FIELDS from './Fields'
export default {
emits: [
'set-configuration',
'toggle-archived'
'form-errors',
'update:modelValue'
],
props: {
workerVersionId: {
......@@ -93,22 +78,43 @@ export default {
workerId: {
type: String,
required: true
},
modelValue: {
type: Object,
required: true
}
},
data: () => ({
FIELDS,
loading: false,
configurationName: '',
newConfiguration: {
name: '',
configuration: {}
},
// a configuration entered in free JSON string mode (unvalidated)
stringConfiguration: '',
// a configuration entered using the form (unvalidated)
rawFormConfiguration: {},
JSONStringToggled: false,
configurationExistsError: null
JSONStringToggled: false
}),
mounted () {
if (!this.schema || !this.defaultConfiguration) return
this.rawFormConfiguration = cloneDeep(this.defaultConfiguration)
// handle the case where there is no schema (which also means no form display)
if (!this.schema) {
this.newConfiguration = cloneDeep(this.modelValue)
this.stringConfiguration = JSON.stringify(this.modelValue.configuration, null, 2)
return
}
// check that the input configuration matches an existing schema
try {
this.validateFilling(this.modelValue.configuration)
this.newConfiguration = cloneDeep(this.modelValue)
} catch (e) {
this.notify({ type: 'warning', text: 'Cloned configuration does not match schema. Switching to default configuration values.' })
this.newConfiguration = {
name: '',
configuration: cloneDeep(this.defaultConfiguration)
}
}
// fill configuration form with default configuration if none was passed from the parent component
if (isEmpty(this.newConfiguration.configuration)) this.newConfiguration.configuration = cloneDeep(this.defaultConfiguration)
},
computed: {
...mapState('process', ['workerConfigurations', 'workerVersions']),
......@@ -127,15 +133,10 @@ export default {
}
return filledForm
},
formConfiguration () {
if (this.JSONStringToggled && this.stringConfiguration && !this.JSONConfigError) return JSON.parse(this.stringConfiguration)
else if (this.rawFormConfiguration && !this.hasConfigurationError) return this.rawFormConfiguration
else return this.defaultConfiguration
},
configurationErrors () {
if (!Object.keys(this.rawFormConfiguration).length) return {}
if (!this.newConfiguration.configuration || !Object.keys(this.newConfiguration.configuration).length || !this.schema) return {}
try {
this.validateFields(this.rawFormConfiguration)
this.validateFields(this.newConfiguration.configuration)
} catch (e) {
if (e instanceof ConfigurationValidationError) return e.message
throw e
......@@ -182,19 +183,14 @@ export default {
isDefault () {
let currentConfiguration = {}
if (!this.defaultConfiguration) return false
if (!this.JSONStringMode) currentConfiguration = this.formConfiguration
if (!this.JSONStringMode) currentConfiguration = this.newConfiguration.configuration
else currentConfiguration = this.validStringConfig
return isEqual(currentConfiguration, this.defaultConfiguration)
},
allowCreate () {
const allowed = !this.loading && this.configurationName.trim() && !this.isDefault
if (this.JSONStringMode) return allowed && !this.JSONConfigError && this.stringConfiguration.trim()
else return allowed && !this.hasConfigurationError
},
disabledTitle () {
const name = this.configurationName.trim()
const name = this.newConfiguration.name.trim()
if (!name) {
if (!this.stringConfiguration && !this.formConfiguration) return 'Please fill out the creation form'
if (!this.stringConfiguration && !this.newConfiguration.configuration) return 'Please fill out the creation form'
else return 'Please name your configuration'
}
if (this.hasConfigurationError) return 'Please enter a valid configuration.'
......@@ -202,25 +198,16 @@ export default {
if (this.JSONStringMode && this.JSONConfigError) return this.JSONConfigError
if (this.isDefault) return 'This already is the default configuration'
else return ''
},
parsedConfigurationExistsError () {
// The error returned by the backend returns the existing configuration's ID like this [ID]
if (!this.configurationExistsError) return
if (this.configurationExistsError.id.length > 1) throw new Error('Mutiple existing configuration IDs returned.')
const parsed = Object.assign({}, this.configurationExistsError)
parsed.id = this.configurationExistsError.id[0]
return parsed
}
},
methods: {
...mapMutations('notifications', ['notify']),
...mapActions('process', ['getConfiguration']),
toggleJSONStringMode () {
if (!this.JSONStringToggled) {
this.stringConfiguration = JSON.stringify(this.formConfiguration, null, 2)
this.stringConfiguration = JSON.stringify(this.newConfiguration.configuration, null, 2)
} else if (!this.JSONConfigError) {
if (!this.stringConfiguration.trim()) this.rawFormConfiguration = this.defaultConfiguration
else this.rawFormConfiguration = JSON.parse(this.stringConfiguration)
if (!this.stringConfiguration.trim()) this.newConfiguration.configuration = this.defaultConfiguration
else this.newConfiguration.configuration = JSON.parse(this.stringConfiguration)
}
this.JSONStringToggled = !this.JSONStringToggled
},
......@@ -233,7 +220,7 @@ export default {
for (const [k, v] of Object.entries(config)) {
const field = this.schema[k]
if (!field) errors[k] = `Unrecognised field: ${k}`
if (!FIELDS[field.type]) {
else if (!FIELDS[field.type]) {
errors[k] = `Unknown field type: ${k}`
} else {
if (field.required === true) {
......@@ -257,47 +244,36 @@ export default {
}
}
if (Object.keys(errors).length) throw new ConfigurationValidationError(errors)
},
async createConfiguration () {
if (this.loading || !this.allowCreate) return
this.loading = true
let newConfiguration = {}
if (this.JSONStringMode) newConfiguration = JSON.parse(this.stringConfiguration)
else newConfiguration = this.formConfiguration
try {
const configuration = await this.$store.dispatch('process/createConfiguration', {
workerId: this.workerId,
configuration: {
name: this.configurationName,
configuration: newConfiguration
}
else {
this.$emit('update:modelValue', {
name: this.newConfiguration.name,
configuration: config
})
// Go back to configurations list only if the creation succeeded
this.createConfig = false
this.workerConfig = { name: '', configuration: '' }
// If the archivedFilter was active, turn it off
this.$emit('toggle-archived', false)
// Select the newly created configuration
this.$emit('set-configuration', configuration.id)
this.notify({ type: 'info', text: 'Configuration created. Save to add it to this worker run.' })
} catch (err) {
const errorData = err.response.data
if (errorData.configuration) {
/*
* If an existing configuration is returned by the backend but isn't found in the store, retrieve it.
* If the configuration does exist and is archived, emit a custom event to switch to the archived
* configurations tab in the parent component.
*/
if (!(Object.values(this.workerConfigurations[this.workerId]).some((id) => id === errorData.id))) {
await this.getConfiguration({ workerId: this.workerId, configurationId: errorData.id })
if (this.workerConfigurations[this.workerId][errorData.id]?.archived) {
this.$emit('toggle-archived', true)
}
}
}
},
validateFilling (config) {
/*
* This method ensures that an existing configuration which is passed to the form (through cloning)
* is consistent with the worker version's configuration schema. This is because different worker
* versions can have different user configuration schemas, and the user configuration definition
* can also have changed since a configuration was created.
*/
if (Object.keys(config).some(item => Object.keys(this.schema).indexOf(item) === -1)) throw new Error('Undefined field found in configuration')
this.validateFields(config)
}
},
watch: {
disabledTitle: {
immediate: true,
handler () { this.$emit('form-errors', this.disabledTitle) }
},
stringConfiguration: {
immediate: true,
handler () {
if (this.stringConfiguration.trim() && !this.JSONConfigError) {
this.newConfiguration.configuration = JSON.parse(this.stringConfiguration)
this.$emit('update:modelValue', this.newConfiguration)
}
this.configurationExistsError = errorData
this.$store.commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
this.loading = false
}
}
}
......
......@@ -42,7 +42,7 @@
</template>
<li>
<a
v-on:click="configCreate = true"
v-on:click="createNewConfiguration"
:class="{ 'is-active': configCreate }"
>
<i class="icon-add-square"></i>
......@@ -53,14 +53,27 @@
</aside>
</div>
<div class="column">
<!-- Do not put any spaces or line breaks between the <pre> and its contents, or the JSON indentation will be messed up -->
<pre v-if="selectedConfigurationId && selectedConfiguration && !configCreate">{{ prettify(selectedConfiguration.configuration) }}</pre>
<template v-if="selectedConfigurationId && selectedConfiguration && !configCreate">
<div class="buttons is-right">
<button
class="button mb-0"
title="Clone this configuration"
v-on:click="cloneConfiguration"
>
Clone
</button>
</div>
<!-- Do not put any spaces or line breaks between the <pre> and its contents, or the JSON indentation will be messed up -->
<pre>{{ prettify(selectedConfiguration.configuration) }}</pre>
</template>
<div v-else-if="configCreate">
<CreateForm
v-on:set-configuration="setConfiguration"
v-on:toggle-archived="archivedFilter = $event"
v-on:config-created="configCreate = false, archivedFilter = false"
:worker-id="workerId"
:worker-version-id="workerVersionId"
:new-configuration="newConfiguration"
/>
</div>
<div v-else-if="configurationsError" class="notification is-danger">
......@@ -98,7 +111,7 @@
</template>
<script>
import CreateForm from './Form'
import CreateForm from './Create'
import Modal from '@/components/Modal.vue'
import { errorParser } from '@/helpers'
import { mapActions, mapState } from 'vuex'
......@@ -148,7 +161,11 @@ export default {
loading: false,
configurationsError: null,
// selectedConfigurationId stores the current selected configuration before it is confirmed by the user (Save)
selectedConfigurationId: null
selectedConfigurationId: null,
newConfiguration: {
name: '',
configuration: {}
}
}),
mounted () {
// Ensure that properties have been passed correctly
......@@ -222,9 +239,19 @@ export default {
...mapActions('process', [
'listConfigurations',
'updateConfiguration',
'updateWorkerRun'
'updateWorkerRun',
'getConfiguration'
]),
createNewConfiguration () {
// When clicking on "New configuration" the pre-filled value is reset
this.newConfiguration = {
name: '',
configuration: {}
}
this.configCreate = true
},
async retrieveConfigurations () {
if (!this.workerId) return
this.configurationsError = null
......@@ -241,8 +268,22 @@ export default {
}
},
setConfiguration (configId) {
if (this.selectedConfigurationId !== configId) this.selectedConfigurationId = configId
async setConfiguration (configId) {
if (this.selectedConfigurationId !== configId) {
if (!this.workerConfigurations[this.workerId][configId]) {
this.loading = true
try {
await this.getConfiguration({ workerId: this.workerId, configurationId: configId })
this.archivedFilter = this.workerConfigurations[this.workerId][configId]
} catch (err) {
this.$store.commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
return
} finally {
this.loading = false
}
}
this.selectedConfigurationId = configId
}
if (this.configCreate) this.configCreate = false
},
......@@ -313,7 +354,16 @@ export default {
prettify: function (value) {
return JSON.stringify(value, null, 2)
},
cloneConfiguration () {
this.newConfiguration = {
configuration: this.selectedConfiguration.configuration,
name: 'Copy of ' + this.selectedConfiguration.name
}
this.configCreate = true
}
},
watch: {
workerId: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment