Support for saving/loading configuration/actions

This commit is contained in:
Fabio Manganiello 2020-06-15 01:03:09 +02:00
parent b7baec099d
commit 03076df42b
7 changed files with 244 additions and 36 deletions

View file

@ -5,13 +5,19 @@
:selectedHost="selectedHost"
:selectedHostOption="selectedHostOption"
:isAddHost="isAddHost"
:isBackupMode="isBackupMode"
:isRestoreMode="isRestoreMode"
@select-host="selectHost"
@select-host-option="selectHostOption"
@select-add-host="selectAddHost"
@select-backup-mode="selectBackupMode"
@select-restore-mode="selectRestoreMode"
/>
<div class="body">
<NewHost @add="addHost" v-if="isAddHost" />
<Backup v-else-if="isBackupMode" />
<Restore v-else-if="isRestoreMode" />
<LocalCommands v-else-if="selectedHost >= 0 && selectedHostOption === 'localProc'" />
<RemoteCommands v-else-if="selectedHost >= 0 && selectedHostOption === 'remoteProc'" />
<Run :host="hosts[selectedHost]" v-else-if="selectedHost >= 0 && selectedHostOption === 'run'" />
@ -28,6 +34,8 @@ import NewHost from './NewHost';
import EditHost from './EditHost';
import LocalCommands from './LocalCommands';
import RemoteCommands from './RemoteCommands';
import Backup from './Backup';
import Restore from './Restore';
import Run from './Run';
export default {
@ -39,6 +47,8 @@ export default {
EditHost,
LocalCommands,
RemoteCommands,
Backup,
Restore,
Run,
},
@ -47,6 +57,8 @@ export default {
selectedHost: -1,
selectedHostOption: null,
isAddHost: false,
isBackupMode: false,
isRestoreMode: false,
};
},
@ -64,6 +76,20 @@ export default {
selectAddHost() {
this.selectedHost = -1;
this.isAddHost = true;
this.isBackupMode = false;
this.isRestoreMode = false;
},
selectBackupMode() {
this.isAddHost = false;
this.isBackupMode = true;
this.isRestoreMode = false;
},
selectRestoreMode() {
this.isAddHost = false;
this.isBackupMode = false;
this.isRestoreMode = true;
},
async addHost(form) {
@ -83,7 +109,7 @@ export default {
}
this.hosts.push(host);
await this.saveHosts();
await this.saveHosts(this.hosts);
this.selectedHost = this.hosts.length - 1;
this.isAddHost = false;
} finally {
@ -134,7 +160,10 @@ export default {
},
created() {
this.loadHosts();
const self = this;
this.loadHosts().then(hosts => {
self.hosts = hosts;
});
},
};
</script>

View file

@ -72,23 +72,49 @@ export default {
},
onKeyDown(event) {
if (event.key === 'Enter') {
this.showItems = false;
if (!this.$refs.items) {
return;
if (!this.$refs.items) {
return;
}
const selected = this.$refs.items.querySelector('.selected');
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Tab') {
switch (event.key) {
case 'Enter':
case ' ':
this.showItems = false;
if (!selected) {
return;
}
this.$refs.input.value = selected.innerText;
this.$emit('change', selected.innerText);
break;
case 'Tab':
this.selectNext();
break;
}
const selected = this.$refs.items.querySelector('.selected');
if (!selected) {
return;
}
this.$refs.input.value = selected.innerText;
this.$emit('change', selected.innerText);
event.preventDefault();
}
},
selectPrev() {
if (this.selectedItem > 0) {
this.selectedItem--;
} else {
this.selectedItem = this.filteredItems.length - 1;
}
},
selectNext() {
if (this.selectedItem >= this.filteredItems.length - 1) {
this.selectedItem = this.filteredItems.length ? 0 : -1;
} else {
this.selectedItem++;
}
},
onKeyUp(event) {
if (event.key === 'Escape') {
this.showItems = false;
@ -100,19 +126,10 @@ export default {
if (['ArrowUp', 'ArrowDown'].indexOf(event.key) >= 0) {
switch (event.key) {
case 'ArrowUp':
if (this.selectedItem > 0) {
this.selectedItem--;
} else {
this.selectedItem = this.filteredItems.length - 1;
}
this.selectPrev();
break;
case 'ArrowDown':
if (this.selectedItem >= this.filteredItems.length - 1) {
this.selectedItem = this.filteredItems.length ? 0 : -1;
} else {
this.selectedItem++;
}
this.selectNext();
break;
}

32
src/options/Backup.vue Normal file
View file

@ -0,0 +1,32 @@
<template>
<div class="page backup">
<h2>Backup the current configuration</h2>
<div class="container">
<textarea v-text="loading ? 'Loading...' : config" :disabled="loading" />
</div>
</div>
</template>
<script>
import mixins from '../utils';
export default {
name: 'Backup',
mixins: [mixins],
data() {
return {
config: {},
};
},
created() {
const self = this;
this.loadConfig().then(config => {
self.config = config;
});
},
};
</script>
<!-- vim:sw=2:ts=2:et: -->

View file

@ -8,7 +8,10 @@
</li>
</ul>
</li>
<li class="add" :class="{ selected: isAddHost }" @click="$emit('select-add-host')"><i class="fas fa-plus" /> &nbsp; Add Device</li>
<li class="backup" :class="{ selected: isBackupMode }" @click="$emit('select-backup-mode')"><i class="fas fa-save" /> &nbsp; Backup Configuration</li>
<li class="restore" :class="{ selected: isRestoreMode }" @click="$emit('select-restore-mode')"><i class="fas fa-window-restore" /> &nbsp; Restore Configuration</li>
</ul>
</template>
@ -20,13 +23,15 @@ export default {
selectedHost: Number,
selectedHostOption: String,
isAddHost: Boolean,
isBackupMode: Boolean,
isRestoreMode: Boolean,
},
computed: {
hostOptions() {
return {
localProc: {
displayName: 'Local Procedures',
displayName: 'Local Actions',
iconClass: 'fas fa-puzzle-piece',
},
remoteProc: {

29
src/options/Restore.vue Normal file
View file

@ -0,0 +1,29 @@
<template>
<div class="page restore">
<h2>Load configuration from a previous backup</h2>
<div class="container">
<textarea v-text="loading ? 'Loading...' : config" :disabled="loading" />
</div>
</div>
</template>
<script>
import mixins from '../utils';
export default {
name: 'Restore',
mixins: [mixins],
data() {
return {
config: {},
};
},
created() {
this.config = this.loadConfig();
},
};
</script>
<!-- vim:sw=2:ts=2:et: -->

View file

@ -7,7 +7,7 @@
URL.
</div>
<form ref="form" @submit.prevent="runAction">
<form ref="runForm" @submit.prevent="runAction">
<div class="row action-head">
<div class="action-name">
<Autocomplete :items="Object.keys(actions)" :value="action.name" :disabled="loading" placeholder="Action" @change="onActionChange" />
@ -49,6 +49,27 @@
<button type="button" @click="clearAction" :disabled="loading"><i class="fas fa-times" /> &nbsp; Clear Form</button>
<button type="submit" :disabled="loading"><i class="fas fa-play" /> &nbsp; Run</button>
</div>
<div class="row buttons" v-if="!saveMode">
<button type="button" @click="saveMode = true" :disabled="loading || !(action.name && action.name.length && action.name in actions)">
<i class="fas fa-save" /> &nbsp; Save Action
</button>
</div>
</form>
<form class="save-form" ref="saveForm" @submit.prevent="storeAction" v-if="saveMode">
<div class="row">
<input type="text" name="displayName" placeholder="Action display name" />
</div>
<div class="row">
<input type="text" name="iconClass" placeholder="FontAwesome icon class (e.g. 'fas fa-play')" />
</div>
<div class="row buttons">
<button type="submit" :disabled="loading"><i class="fas fa-save" /> &nbsp; Save Action</button>
<button type="button" @click="saveMode = false" :disabled="loading"><i class="fas fa-times" /> &nbsp; Cancel</button>
</div>
</form>
<div class="code response" v-text="actionResponse" v-if="actionResponse && (actionResponse.length || Object.keys(actionResponse).length)" />
@ -72,6 +93,7 @@ export default {
return {
plugins: {},
pluginsLoading: false,
saveMode: false,
actionResponse: null,
actionError: null,
action: {
@ -119,21 +141,23 @@ export default {
this.actionError = null;
},
async runAction() {
this.loading = true;
const args = [...this.$refs.form.querySelectorAll('[data-type="arg"]')].map(el => {
getActionArgs() {
return [...this.$refs.runForm.querySelectorAll('[data-type="arg"]')].map(el => {
return {
name: el.name,
value: el.value,
};
}, {});
},
async runAction() {
this.loading = true;
try {
this.actionResponse = await this.run(
{
name: this.action.name,
args: [...args, ...this.action.args],
args: this.getActionArgs(),
},
this.host
);
@ -172,6 +196,26 @@ export default {
}
},
async storeAction(event) {
const saveForm = event.target;
const displayName = saveForm.displayName.value.trim();
const iconClass = saveForm.iconClass.value.trim();
if (!displayName.length) {
this.notify('Please specify an action name', 'No action name provided');
return;
}
const action = {
displayName: displayName,
iconClass: iconClass,
name: this.action.name,
args: this.getActionArgs(),
};
await this.saveAction(action);
},
onActionChange(action) {
this.action.name = action;
this.action.args = [];
@ -213,7 +257,6 @@ export default {
form {
position: relative;
max-width: 50em;
.row {
display: flex;
@ -259,6 +302,16 @@ form {
}
}
.save-form {
margin-top: 2em;
input[type='text'] {
width: 60%;
min-width: 20em;
max-width: 35em;
}
}
.code {
padding: 1em;
white-space: pre-wrap;

View file

@ -61,7 +61,7 @@ export default {
return msg.data.response.output;
} catch (e) {
this.notify('Request error', e.toString());
this.notify(e.toString(), 'Request error');
throw e;
}
},
@ -71,14 +71,57 @@ export default {
try {
const response = await browser.storage.local.get('hosts');
this.hosts = JSON.parse(response.hosts);
return JSON.parse(response.hosts);
} finally {
this.loading = false;
}
},
async saveHosts() {
await browser.storage.local.set({ hosts: JSON.stringify(this.hosts) });
async saveHosts(hosts) {
this.loading = true;
try {
await browser.storage.local.set({ hosts: JSON.stringify(hosts) });
} finally {
this.loading = false;
}
},
async loadActions() {
this.loading = true;
try {
const response = await browser.storage.local.get('actions');
return JSON.parse(response.actions);
} finally {
this.loading = false;
}
},
async saveAction(action) {
const actions = await this.loadActions();
if (action.displayName in actions) {
if (!confirm('An action with this name already exists. Do you want to overwrite it?')) {
return;
}
}
actions[action.displayName] = action;
this.loading = true;
try {
await browser.storage.local.set({ actions: JSON.stringify(actions) });
this.notify('You can find this action under the Local Actions menu', 'Action saved');
} finally {
this.loading = false;
}
},
async loadConfig() {
const [hosts, actions] = await Promise.all([this.loadHosts(), this.loadActions()]);
return {
hosts: hosts,
actions: actions,
};
},
formToHost(form) {