Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve env variable editing uxp #2175

Merged
merged 5 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions frontend/src/layouts/Platform.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
:countdown="a.countdown || 3000" @close="clear($index)"></ff-notification-toast>
</TransitionGroup>
<interview-popup v-if="interview?.enabled" :flag="interview.flag" :payload="interview.payload"></interview-popup>
<ff-dialog ref="dialog" data-el="platform-dialog" :header="dialog.header" :kind="dialog.kind" :disable-primary="dialog.disablePrimary" :confirm-label="dialog.confirmLabel" @cancel="clearDialog" @confirm="dialog.onConfirm">
<ff-dialog ref="dialog" data-el="platform-dialog" :header="dialog.header" :kind="dialog.kind" :disable-primary="dialog.disablePrimary" :confirm-label="dialog.confirmLabel" @cancel="clearDialog(true)" @confirm="dialog.onConfirm">
<p v-if="dialog.text">{{ dialog.text }}</p>
<div class="space-y-2" v-html="dialog.html"></div>
</ff-dialog>
Expand Down Expand Up @@ -48,7 +48,8 @@ export default {
html: null,
confirmLabel: null,
kind: null,
onConfirm: null
onConfirm: null,
onCancel: null
}
}
},
Expand Down Expand Up @@ -90,7 +91,7 @@ export default {
timestamp: Date.now()
})
},
showDialogHandler (msg, onConfirm) {
showDialogHandler (msg, onConfirm, onCancel) {
if (typeof (msg) === 'string') {
this.dialog.content = msg
} else {
Expand All @@ -103,15 +104,20 @@ export default {
this.dialog.disablePrimary = msg.disablePrimary
}
this.dialog.onConfirm = onConfirm
this.dialog.onCancel = onCancel
},
clearDialog () {
clearDialog (cancelled) {
Steve-Mcl marked this conversation as resolved.
Show resolved Hide resolved
if (cancelled) {
this.dialog.onCancel?.()
}
this.dialog = {
header: null,
text: null,
html: null,
confirmLabel: null,
kind: null,
onConfirm: null
onConfirm: null,
onCancel: null
}
},
clear (i) {
Expand Down
32 changes: 25 additions & 7 deletions frontend/src/pages/device/Settings/Environment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<form class="space-y-6">
<TemplateSettingsEnvironment :readOnly="!hasPermission('device:edit-env')" v-model="editable" :editTemplate="false" />
<div v-if="hasPermission('device:edit-env')" class="space-x-4 whitespace-nowrap">
<ff-button size="small" :disabled="!unsavedChanges" @click="saveSettings()">Save Settings</ff-button>
<ff-button size="small" :disabled="!unsavedChanges || hasError" @click="saveSettings()">Save Settings</ff-button>
</div>
</form>
</template>
Expand All @@ -13,12 +13,32 @@ import { mapState } from 'vuex'
import deviceApi from '../../../api/devices.js'
import permissionsMixin from '../../../mixins/Permissions.js'
import TemplateSettingsEnvironment from '../../admin/Template/sections/Environment.vue'
import alerts from '../../../services/alerts.js'
import dialog from '../../../services/dialog.js'

export default {
name: 'DeviceSettingsEnvironment',
props: ['device'],
emits: ['device-updated'],
mixins: [permissionsMixin],
beforeRouteLeave: async function (_to, _from, next) {
if (this.unsavedChanges) {
const dialogOpts = {
header: 'Unsaved changes',
kind: 'danger',
html: '<p>You have unsaved changes. Are you sure you want to leave?</p>',
confirmLabel: 'Yes, lose changes'
}
const answer = await dialog.showAsync(dialogOpts)
if (answer === 'confirm') {
next()
} else {
next(false)
}
} else {
next()
}
},
watch: {
device: 'getSettings',
'editable.settings.env': {
Expand All @@ -44,12 +64,8 @@ export default {
changed = true
}

// if we have an error in one of the keys/values, forbid saving
if (error) {
this.unsavedChanges = false
} else {
this.unsavedChanges = changed
}
this.hasError = error
this.unsavedChanges = changed
}
}
},
Expand All @@ -59,6 +75,7 @@ export default {
data () {
return {
unsavedChanges: false,
hasError: false,
editable: {
name: '',
settings: { env: [] },
Expand Down Expand Up @@ -110,6 +127,7 @@ export default {
})
deviceApi.updateSettings(this.device.id, settings)
this.$emit('device-updated')
alerts.emit('Device settings successfully updated. NOTE: changes will be applied once the device restarts.', 'confirmation', 6000)
}
}
}
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/pages/instance/Settings/Environment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { mapState } from 'vuex'
import InstanceApi from '../../../api/instances.js'
import permissionsMixin from '../../../mixins/Permissions.js'
import alerts from '../../../services/alerts.js'
import dialog from '../../../services/dialog.js'
import TemplateSettingsEnvironment from '../../admin/Template/sections/Environment.vue'
import {
prepareTemplateForEdit
Expand All @@ -24,6 +25,24 @@ export default {
TemplateSettingsEnvironment
},
mixins: [permissionsMixin],
beforeRouteLeave: async function (_to, _from, next) {
if (this.unsavedChanges) {
const dialogOpts = {
header: 'Unsaved changes',
kind: 'danger',
html: '<p>You have unsaved changes. Are you sure you want to leave?</p>',
confirmLabel: 'Yes, lose changes'
}
const answer = await dialog.showAsync(dialogOpts)
if (answer === 'confirm') {
next()
} else {
next(false)
}
} else {
next()
}
},
inheritAttrs: false,
props: {
project: {
Expand Down Expand Up @@ -78,8 +97,6 @@ export default {
changed = true
} else if (original.value !== field.value) {
changed = true
} else if (original.policy !== field.policy) {
changed = true
}
} else {
changed = true
Expand Down Expand Up @@ -150,7 +167,7 @@ export default {
})
await InstanceApi.updateInstance(this.project.id, { settings })
this.$emit('instance-updated')
alerts.emit('Instance successfully updated.', 'confirmation')
alerts.emit('Instance settings successfully updated. NOTE: changes will be applied once the instance is restarted.', 'confirmation', 6000)
}
}
}
Expand Down
74 changes: 61 additions & 13 deletions frontend/src/services/dialog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
let dialog
const subscriptions = []

/**
* @typedef {Object} DialogOptions
* @param {string} header The dialog title
* @param {string} kind The dialog kind (optional)
* @param {string} text The dialog text
* @param {string} html The dialog html (instead of text)
* @param {string} confirmLabel The dialog confirm button label
*/

export default {
// bind this service to a ff-dialog element
// this is used in Platform.vue in order to control the showing/hiding
Expand All @@ -9,22 +18,61 @@ export default {
dialog = el
subscriptions.push(fcn)
},
// .show() function is used across the app in order to
// show and act upon confirmation (via onConfirm) of any dialogs.

// Dialog.show({
// header: '<header title>',
// kind: (optional) 'danger',
// text: 'show this message in the dialog',
// html: 'instead of "text", you can provide html for more custom appearance and content',
// confirmLabel: '<confirm-label>'
// }, async () => {
// // callback goes here
// })
show: async function (msg, onConfirm) {
/**
* Show a dialog
* @param {string|DialogOptions} msg The dialog message or a DialogOptions object
* @param {Function} onConfirm A callback function to call when the confirm button is clicked
* @param {Function} onCancel (optional) A callback function to call when the cancel button is clicked
* @example
* // show a simple dialog
* dialogService.show('Are you sure?', () => { console.log('confirmed') })
* @example
* // show a dialog with a custom header and confirm button label
* dialogService.show({
* header: 'Are you sure?',
* text: 'This action cannot be undone',
* confirmLabel: 'Yes, I am sure'
* }, () => { console.log('confirmed') })
*/
show: async function (msg, onConfirm, onCancel) {
for (let fcn = 0; fcn < subscriptions.length; fcn++) {
subscriptions[fcn](msg, onConfirm)
subscriptions[fcn](msg, onConfirm, onCancel)
}
dialog.show()
},

/**
* Show a dialog and return a promise that resolves when the dialog is closed
* @param {string|DialogOptions} msg The dialog message or a DialogOptions object
* @example
* // show a simple dialog
* const result = await dialogService.showAsync('Are you sure?')
* if (result === 'confirm') {
* console.log('confirmed')
* } else {
* console.log('cancelled')
* }
* @example
* // show a dialog with a custom header and confirm button label
* const result = await dialogService.showAsync({
* header: 'Are you sure?',
* text: 'This action cannot be undone',
* confirmLabel: 'Yes, I am sure'
* })
* if (result === 'confirm') {
* console.log('confirmed')
* } else {
* console.log('cancelled')
* }
*/
showAsync: function (msg) {
return new Promise((resolve, _reject) => {
this.show(msg, () => {
resolve('confirm')
}, () => {
resolve('cancel')
})
})
}
}