Support for saving/loading configuration/actions

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

View File

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

View File

@ -72,23 +72,49 @@ export default {
}, },
onKeyDown(event) { onKeyDown(event) {
if (event.key === 'Enter') { if (!this.$refs.items) {
this.showItems = false; 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(); 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) { onKeyUp(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.showItems = false; this.showItems = false;
@ -100,19 +126,10 @@ export default {
if (['ArrowUp', 'ArrowDown'].indexOf(event.key) >= 0) { if (['ArrowUp', 'ArrowDown'].indexOf(event.key) >= 0) {
switch (event.key) { switch (event.key) {
case 'ArrowUp': case 'ArrowUp':
if (this.selectedItem > 0) { this.selectPrev();
this.selectedItem--;
} else {
this.selectedItem = this.filteredItems.length - 1;
}
break; break;
case 'ArrowDown': case 'ArrowDown':
if (this.selectedItem >= this.filteredItems.length - 1) { this.selectNext();
this.selectedItem = this.filteredItems.length ? 0 : -1;
} else {
this.selectedItem++;
}
break; 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> </li>
</ul> </ul>
</li> </li>
<li class="add" :class="{ selected: isAddHost }" @click="$emit('select-add-host')"><i class="fas fa-plus" /> &nbsp; Add Device</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> </ul>
</template> </template>
@ -20,13 +23,15 @@ export default {
selectedHost: Number, selectedHost: Number,
selectedHostOption: String, selectedHostOption: String,
isAddHost: Boolean, isAddHost: Boolean,
isBackupMode: Boolean,
isRestoreMode: Boolean,
}, },
computed: { computed: {
hostOptions() { hostOptions() {
return { return {
localProc: { localProc: {
displayName: 'Local Procedures', displayName: 'Local Actions',
iconClass: 'fas fa-puzzle-piece', iconClass: 'fas fa-puzzle-piece',
}, },
remoteProc: { 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. URL.
</div> </div>
<form ref="form" @submit.prevent="runAction"> <form ref="runForm" @submit.prevent="runAction">
<div class="row action-head"> <div class="row action-head">
<div class="action-name"> <div class="action-name">
<Autocomplete :items="Object.keys(actions)" :value="action.name" :disabled="loading" placeholder="Action" @change="onActionChange" /> <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="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> <button type="submit" :disabled="loading"><i class="fas fa-play" /> &nbsp; Run</button>
</div> </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> </form>
<div class="code response" v-text="actionResponse" v-if="actionResponse && (actionResponse.length || Object.keys(actionResponse).length)" /> <div class="code response" v-text="actionResponse" v-if="actionResponse && (actionResponse.length || Object.keys(actionResponse).length)" />
@ -72,6 +93,7 @@ export default {
return { return {
plugins: {}, plugins: {},
pluginsLoading: false, pluginsLoading: false,
saveMode: false,
actionResponse: null, actionResponse: null,
actionError: null, actionError: null,
action: { action: {
@ -119,21 +141,23 @@ export default {
this.actionError = null; this.actionError = null;
}, },
async runAction() { getActionArgs() {
this.loading = true; return [...this.$refs.runForm.querySelectorAll('[data-type="arg"]')].map(el => {
const args = [...this.$refs.form.querySelectorAll('[data-type="arg"]')].map(el => {
return { return {
name: el.name, name: el.name,
value: el.value, value: el.value,
}; };
}, {}); }, {});
},
async runAction() {
this.loading = true;
try { try {
this.actionResponse = await this.run( this.actionResponse = await this.run(
{ {
name: this.action.name, name: this.action.name,
args: [...args, ...this.action.args], args: this.getActionArgs(),
}, },
this.host 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) { onActionChange(action) {
this.action.name = action; this.action.name = action;
this.action.args = []; this.action.args = [];
@ -213,7 +257,6 @@ export default {
form { form {
position: relative; position: relative;
max-width: 50em;
.row { .row {
display: flex; display: flex;
@ -259,6 +302,16 @@ form {
} }
} }
.save-form {
margin-top: 2em;
input[type='text'] {
width: 60%;
min-width: 20em;
max-width: 35em;
}
}
.code { .code {
padding: 1em; padding: 1em;
white-space: pre-wrap; white-space: pre-wrap;

View File

@ -61,7 +61,7 @@ export default {
return msg.data.response.output; return msg.data.response.output;
} catch (e) { } catch (e) {
this.notify('Request error', e.toString()); this.notify(e.toString(), 'Request error');
throw e; throw e;
} }
}, },
@ -71,14 +71,57 @@ export default {
try { try {
const response = await browser.storage.local.get('hosts'); const response = await browser.storage.local.get('hosts');
this.hosts = JSON.parse(response.hosts); return JSON.parse(response.hosts);
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
async saveHosts() { async saveHosts(hosts) {
await browser.storage.local.set({ hosts: JSON.stringify(this.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) { formToHost(form) {