From a480a799987ae4c30ed0bb4654aec072b6f11688 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello <blacklight86@gmail.com> Date: Sat, 13 Jun 2020 17:28:50 +0200 Subject: [PATCH] Split options page into components --- .gitignore | 1 + src/common.scss | 52 ++++++ src/options/App.vue | 318 +++------------------------------ src/options/EditHost.vue | 46 +++++ src/options/LocalCommands.vue | 16 ++ src/options/NewHost.vue | 42 +++++ src/options/RemoteCommands.vue | 16 ++ src/options/Run.vue | 159 +++++++++++++++++ src/options/options.js | 1 + src/utils.js | 56 ++++++ 10 files changed, 410 insertions(+), 297 deletions(-) create mode 100644 src/common.scss create mode 100644 src/options/EditHost.vue create mode 100644 src/options/LocalCommands.vue create mode 100644 src/options/NewHost.vue create mode 100644 src/options/RemoteCommands.vue create mode 100644 src/options/Run.vue diff --git a/.gitignore b/.gitignore index 8e33e27..c680358 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /*.log /dist /dist-zip +Session.vim diff --git a/src/common.scss b/src/common.scss new file mode 100644 index 0000000..388ba73 --- /dev/null +++ b/src/common.scss @@ -0,0 +1,52 @@ +html, +body { + font-size: 14px; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', sans-serif; +} + +a, +a:visited { + color: initial; + text-decoration: underline dotted #888; +} + +a:hover { + opacity: 0.7; +} + +h2 { + font-size: 1.2em; + margin-bottom: 0.75em; + padding-bottom: 0.75em; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +form { + input[type='text'] { + display: block; + margin-bottom: 0.5em; + border-radius: 1em; + padding: 0.4em; + border: 1px solid rgba(0, 0, 0, 0.2); + + &:hover { + border: 1px solid rgba(40, 235, 70, 0.3); + } + + &:focus { + border: 1px solid rgba(40, 235, 70, 0.7); + } + } + + .buttons { + margin-top: 0.5em; + padding-top: 0.5em; + border-top: 1px solid rgba(0, 0, 0, 0.15); + + button { + margin-right: 0.3em; + } + } +} + +// vim:sw=2:ts=2:et: diff --git a/src/options/App.vue b/src/options/App.vue index 846b927..6fdda5c 100644 --- a/src/options/App.vue +++ b/src/options/App.vue @@ -11,97 +11,12 @@ /> <div class="body"> - <div class="page add" v-if="isAddHost"> - <h2>Add a new device</h2> - <form class="host-form" ref="addHostForm" @submit.prevent="addHost"> - <input type="text" name="name" placeholder="Name" autocomplete="off" :disabled="loading" /> - <input type="text" name="address" placeholder="IP or hostname" @keyup="onAddrChange($refs.addHostForm)" autocomplete="off" :disabled="loading" /> - <input type="text" name="port" value="8008" placeholder="HTTP port" @keyup="onPortChange($refs.addHostForm)" autocomplete="off" :disabled="loading" /> - <input type="text" name="websocketPort" value="8009" placeholder="Websocket port" autocomplete="off" :disabled="loading" /> - <input type="text" name="token" placeholder="Access token" autocomplete="off" :disabled="loading" /> - <div class="row ssl"> - <input type="checkbox" name="ssl" :disabled="loading" /> - <label for="ssl">Use SSL</label> - </div> - - <div class="buttons"> - <input type="submit" value="Add" :disabled="loading" /> - </div> - </form> - </div> - - <div class="page local-procedures" v-else-if="selectedHostOption === 'localProc'"> - <h2>Procedures stored on browser</h2> - </div> - - <div class="page remote-procedures" v-else-if="selectedHostOption === 'remoteProc'"> - <h2>Procedures stored on server</h2> - </div> - - <div class="page run" v-else-if="selectedHostOption === 'run'"> - <h2>Run a command on {{ hosts[selectedHost].name }}</h2> - <form class="run-form" ref="runForm" @submit.prevent="runAction"> - <div class="row action-name"> - <input type="text" name="action" v-model="action.name" placeholder="Action" autocomplete="off" :disabled="loading" /> - <span class="help"> - <a href="https://platypush.readthedocs.io/en/latest/plugins.html" target="_blank">Plugins reference</a>. Use <tt>$URL$</tt> as argument value to denote the - current URL. - </span> - </div> - - <div class="row" v-for="(arg, i) in action.args" :key="i"> - <div class="label"> - <input type="text" :name="'arg' + i" v-model="arg.name" placeholder="Name" autocomplete="off" :disabled="loading" /> - </div> - - <div class="value"> - <input type="text" :name="arg.name" v-model="arg.value" data-type="argument" placeholder="Value" autocomplete="off" :disabled="loading" /> - <button type="button" @click="action.args.splice(i, 1)" :disabled="loading"><i class="fas fa-trash" /></button> - </div> - </div> - - <div class="row buttons"> - <button type="button" @click="addActionArgument" :disabled="loading"><i class="fas fa-plus" /> Add Argument</button> - <button type="button" @click="clearAction" :disabled="loading"><i class="fas fa-times" /> Clear Form</button> - <button type="submit" :disabled="loading"><i class="fas fa-play" /> Run</button> - </div> - </form> - - <div class="code response" v-text="actionResponse" v-if="actionResponse && (actionResponse.length || Object.keys(actionResponse).length)" /> - <div class="code error" v-text="actionError" v-if="actionError && actionError.length" /> - </div> - - <div class="page edit" v-else-if="selectedHost >= 0"> - <h2>Edit device {{ hosts[selectedHost].name }}</h2> - <form class="host-form" ref="editHostForm" @submit.prevent="editHost"> - <input type="text" name="name" placeholder="Name" :value="hosts[selectedHost].name" autocomplete="off" :disabled="loading" /> - <input type="text" name="address" placeholder="IP or hostname" :value="hosts[selectedHost].address" autocomplete="off" :disabled="loading" /> - <input - type="text" - name="port" - placeholder="HTTP port" - autocomplete="off" - :value="hosts[selectedHost].port" - @keyup="onPortChange($refs.editHostForm)" - :disabled="loading" - /> - <input type="text" name="websocketPort" :value="hosts[selectedHost].websocketPort" placeholder="Websocket port" autocomplete="off" :disabled="loading" /> - <input type="text" name="token" placeholder="Access token" :value="hosts[selectedHost].token" autocomplete="off" :disabled="loading" /> - <div class="row ssl"> - <input type="checkbox" name="ssl" v-model="hosts[selectedHost].ssl" :disabled="loading" /> - <label for="ssl">Use SSL</label> - </div> - - <div class="buttons"> - <input type="submit" value="Edit" :disabled="loading" /> - <button type="button" @click="removeHost" :disabled="loading">Remove</button> - </div> - </form> - </div> - - <div class="none" v-else> - Select an option from the menu - </div> + <NewHost @add="addHost" v-if="isAddHost" /> + <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'" /> + <EditHost :host="hosts[selectedHost]" @save="editHost" @remove="removeHost" v-else-if="selectedHost >= 0" /> + <div class="none" v-else>Select an option from the menu</div> </div> </div> </template> @@ -109,25 +24,29 @@ <script> import mixins from '../utils'; import Menu from './Menu'; +import NewHost from './NewHost'; +import EditHost from './EditHost'; +import LocalCommands from './LocalCommands'; +import RemoteCommands from './RemoteCommands'; +import Run from './Run'; export default { name: 'App', mixins: [mixins], - components: { Menu }, + components: { + Menu, + NewHost, + EditHost, + LocalCommands, + RemoteCommands, + Run, + }, data() { return { - hosts: [], selectedHost: -1, selectedHostOption: null, isAddHost: false, - loading: false, - actionResponse: null, - actionError: null, - action: { - name: null, - args: [], - }, }; }, @@ -147,59 +66,7 @@ export default { this.isAddHost = true; }, - addActionArgument() { - this.action.args.push({ - name: '', - value: '', - }); - }, - - onAddrChange(form) { - if (form.name.value.length && !form.address.value.startsWith(form.name.value)) { - return; - } - - form.name.value = form.address.value; - }, - - onPortChange(form) { - const port = form.port.value; - if (!this.isPortValid(port)) return; - form.websocketPort.value = '' + (parseInt(port) + 1); - }, - - isPortValid(port) { - port = parseInt(port); - return !isNaN(port) && port > 0 && port < 65536; - }, - - isHostFormValid(form) { - return form.name.value.length && form.address.value.length && this.isPortValid(form.port.value) && this.isPortValid(form.websocketPort.value); - }, - - clearAction() { - this.action.name = null; - this.action.args = []; - this.actionResponse = null; - this.actionError = null; - }, - - async runAction() { - this.loading = true; - - try { - this.actionResponse = await this.run(this.action, this.hosts[this.selectedHost]); - this.actionError = null; - } catch (e) { - this.actionResponse = null; - this.actionError = e.toString(); - } finally { - this.loading = false; - } - }, - - async addHost() { - const form = this.$refs.addHostForm; + async addHost(form) { if (!this.isHostFormValid(form)) { this.notify('Invalid device parameter values', 'Device configuration error'); return; @@ -224,8 +91,7 @@ export default { } }, - async editHost() { - const form = this.$refs.editHostForm; + async editHost(form) { if (!this.isHostFormValid(form)) { this.notify('Invalid device parameter values', 'Device configuration error'); return; @@ -265,32 +131,6 @@ export default { this.loading = false; } }, - - async loadHosts() { - this.loading = true; - - try { - const response = await browser.storage.local.get('hosts'); - this.hosts = JSON.parse(response.hosts); - } finally { - this.loading = false; - } - }, - - async saveHosts() { - await browser.storage.local.set({ hosts: JSON.stringify(this.hosts) }); - }, - - formToHost(form) { - return { - name: form.name.value, - address: form.address.value, - port: parseInt(form.port.value), - websocketPort: parseInt(form.websocketPort.value), - ssl: form.ssl.checked, - token: form.token.value, - }; - }, }, created() { @@ -303,25 +143,6 @@ export default { .container { display: flex; height: 100vh; - font-size: 14px; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', sans-serif; -} - -a, -a:visited { - color: initial; - text-decoration: underline dotted #888; -} - -a:hover { - opacity: 0.7; -} - -h2 { - font-size: 1.2em; - margin-bottom: 0.75em; - padding-bottom: 0.75em; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .body { @@ -345,103 +166,6 @@ h2 { width: 100%; padding: 0 1em; } - -form { - input[type='text'] { - display: block; - margin-bottom: 0.5em; - border-radius: 1em; - padding: 0.4em; - border: 1px solid rgba(0, 0, 0, 0.2); - - &:hover { - border: 1px solid rgba(40, 235, 70, 0.3); - } - - &:focus { - border: 1px solid rgba(40, 235, 70, 0.7); - } - } - - .row.ssl { - display: flex; - align-items: center; - } - - .buttons { - margin-top: 0.5em; - padding-top: 0.5em; - border-top: 1px solid rgba(0, 0, 0, 0.15); - - button { - margin-right: 0.3em; - } - } -} - -.run-form { - position: relative; - max-width: 50em; - - .row { - display: flex; - align-items: center; - margin-bottom: 0.5em; - padding-bottom: 0.5em; - } - - .label { - width: 30%; - input[type='text'] { - width: 90%; - } - } - - .value { - width: 70%; - input[type='text'] { - width: 80%; - } - - button { - background: white; - padding: 0.25em 1.5em; - margin-left: 0.5em; - border: 1px solid rgba(0, 0, 0, 0.3); - border-radius: 1em; - - &:hover { - opacity: 0.8; - } - } - } - - input { - display: inline-flex !important; - margin-bottom: 0 !important; - } - - [type='submit'] { - position: absolute; - right: 0.9em; - } -} - -.code { - padding: 1em; - white-space: pre-wrap; - font-family: monospace; - border: 1px dotted rgba(0, 0, 0, 0.8); - border-radius: 1em; - - &.response { - background: rgba(200, 255, 200, 0.3); - } - - &.error { - background: rgba(255, 200, 200, 0.3); - } -} </style> <!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/EditHost.vue b/src/options/EditHost.vue new file mode 100644 index 0000000..2c96c4f --- /dev/null +++ b/src/options/EditHost.vue @@ -0,0 +1,46 @@ +<template> + <div class="page edit"> + <h2>Edit device {{ host.name }}</h2> + <form class="host-form" ref="form" @submit.prevent="$emit('save', $event.target)"> + <input type="text" name="name" placeholder="Name" :value="host.name" autocomplete="off" :disabled="loading" /> + <input type="text" name="address" placeholder="IP or hostname" :value="host.address" autocomplete="off" :disabled="loading" /> + <input type="text" name="port" placeholder="HTTP port" autocomplete="off" :value="host.port" :disabled="loading" @keyup="onPortChange($refs.form)" /> + <input type="text" name="websocketPort" :value="host.websocketPort" placeholder="Websocket port" autocomplete="off" :disabled="loading" /> + <input type="text" name="token" placeholder="Access token" :value="host.token" autocomplete="off" :disabled="loading" /> + <div class="row ssl"> + <input type="checkbox" name="ssl" v-model="host.ssl" :disabled="loading" /> + <label for="ssl">Use SSL</label> + </div> + + <div class="buttons"> + <input type="submit" value="Edit" :disabled="loading" /> + <button type="button" @click="$emit('remove')" :disabled="loading">Remove</button> + </div> + </form> + </div> +</template> + +<script> +import mixins from '../utils'; + +export default { + name: 'EditHost', + mixins: [mixins], + props: { + host: Object, + }, +}; +</script> + +<style lang="scss" scoped> +form { + input[type='text'] { + .row.ssl { + display: flex; + align-items: center; + } + } +} +</style> + +<!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/LocalCommands.vue b/src/options/LocalCommands.vue new file mode 100644 index 0000000..5019b20 --- /dev/null +++ b/src/options/LocalCommands.vue @@ -0,0 +1,16 @@ +<template> + <div class="page local-procedures"> + <h2>Commands stored on the browser</h2> + </div> +</template> + +<script> +import mixins from '../utils'; + +export default { + name: 'LocalCommands', + mixins: [mixins], +}; +</script> + +<!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/NewHost.vue b/src/options/NewHost.vue new file mode 100644 index 0000000..8e44277 --- /dev/null +++ b/src/options/NewHost.vue @@ -0,0 +1,42 @@ +<template> + <div class="page add"> + <h2>Add a new device</h2> + <form class="host-form" ref="form" @submit.prevent="$emit('add', $event.target)"> + <input type="text" name="name" placeholder="Name" autocomplete="off" :disabled="loading" /> + <input type="text" name="address" placeholder="IP or hostname" @keyup="onAddrChange($refs.form)" autocomplete="off" :disabled="loading" /> + <input type="text" name="port" value="8008" placeholder="HTTP port" @keyup="onPortChange($refs.form)" autocomplete="off" :disabled="loading" /> + <input type="text" name="websocketPort" value="8009" placeholder="Websocket port" autocomplete="off" :disabled="loading" /> + <input type="text" name="token" placeholder="Access token" autocomplete="off" :disabled="loading" /> + <div class="row ssl"> + <input type="checkbox" name="ssl" :disabled="loading" /> + <label for="ssl">Use SSL</label> + </div> + + <div class="buttons"> + <input type="submit" value="Add" :disabled="loading" /> + </div> + </form> + </div> +</template> + +<script> +import mixins from '../utils'; + +export default { + name: 'NewHost', + mixins: [mixins], +}; +</script> + +<style lang="scss" scoped> +form { + input[type='text'] { + .row.ssl { + display: flex; + align-items: center; + } + } +} +</style> + +<!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/RemoteCommands.vue b/src/options/RemoteCommands.vue new file mode 100644 index 0000000..1cedae8 --- /dev/null +++ b/src/options/RemoteCommands.vue @@ -0,0 +1,16 @@ +<template> + <div class="page remote-procedures"> + <h2>Procedures stored on the server</h2> + </div> +</template> + +<script> +import mixins from '../utils'; + +export default { + name: 'RemoteCommands', + mixins: [mixins], +}; +</script> + +<!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/Run.vue b/src/options/Run.vue new file mode 100644 index 0000000..3953230 --- /dev/null +++ b/src/options/Run.vue @@ -0,0 +1,159 @@ +<template> + <div class="page run"> + <h2>Run a command on {{ host.name }}</h2> + <form @submit.prevent="runAction"> + <div class="row action-name"> + <input type="text" name="action" v-model="action.name" placeholder="Action" autocomplete="off" :disabled="loading" /> + <span class="help"> + <a href="https://platypush.readthedocs.io/en/latest/plugins.html" target="_blank">Plugins reference</a>. Use <tt>$URL$</tt> as argument value to denote the current + URL. + </span> + </div> + + <div class="row" v-for="(arg, i) in action.args" :key="i"> + <div class="label"> + <input type="text" :name="'arg' + i" v-model="arg.name" placeholder="Name" autocomplete="off" :disabled="loading" /> + </div> + + <div class="value"> + <input type="text" :name="arg.name" v-model="arg.value" data-type="argument" placeholder="Value" autocomplete="off" :disabled="loading" /> + <button type="button" @click="action.args.splice(i, 1)" :disabled="loading"><i class="fas fa-trash" /></button> + </div> + </div> + + <div class="row buttons"> + <button type="button" @click="addActionArgument" :disabled="loading"><i class="fas fa-plus" /> Add Argument</button> + <button type="button" @click="clearAction" :disabled="loading"><i class="fas fa-times" /> Clear Form</button> + <button type="submit" :disabled="loading"><i class="fas fa-play" /> Run</button> + </div> + </form> + + <div class="code response" v-text="actionResponse" v-if="actionResponse && (actionResponse.length || Object.keys(actionResponse).length)" /> + <div class="code error" v-text="actionError" v-if="actionError && actionError.length" /> + </div> +</template> + +<script> +import mixins from '../utils'; + +export default { + name: 'Run', + mixins: [mixins], + props: { + host: Object, + }, + + data() { + return { + actionResponse: null, + actionError: null, + action: { + name: null, + args: [], + }, + }; + }, + + methods: { + clearAction() { + this.action.name = null; + this.action.args = []; + this.actionResponse = null; + this.actionError = null; + }, + + async runAction() { + this.loading = true; + + try { + this.actionResponse = await this.run(this.action, this.host); + this.actionError = null; + } catch (e) { + this.actionResponse = null; + this.actionError = e.toString(); + } finally { + this.loading = false; + } + }, + + addActionArgument() { + this.action.args.push({ + name: '', + value: '', + }); + }, + }, + + created() { + this.clearAction(); + }, +}; +</script> + +<style lang="scss" scoped> +form { + position: relative; + max-width: 50em; + + .row { + display: flex; + align-items: center; + margin-bottom: 0.5em; + padding-bottom: 0.5em; + } + + .label { + width: 30%; + input[type='text'] { + width: 90%; + } + } + + .value { + width: 70%; + input[type='text'] { + width: 80%; + } + + button { + background: white; + padding: 0.25em 1.5em; + margin-left: 0.5em; + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 1em; + + &:hover { + opacity: 0.8; + } + } + } + + input { + display: inline-flex !important; + margin-bottom: 0 !important; + } + + [type='submit'] { + position: absolute; + right: 0.9em; + } +} + +.code { + padding: 1em; + white-space: pre-wrap; + font-family: monospace; + border: 1px dotted rgba(0, 0, 0, 0.8); + border-radius: 1em; + + &.response { + background: rgba(200, 255, 200, 0.3); + } + + &.error { + background: rgba(255, 200, 200, 0.3); + } +} +</style> + +<!-- vim:sw=2:ts=2:et: --> diff --git a/src/options/options.js b/src/options/options.js index fa15a1e..0cdd1e0 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import App from './App'; +require('../common.scss'); global.browser = require('webextension-polyfill'); /* eslint-disable no-new */ diff --git a/src/utils.js b/src/utils.js index e3b078e..03da864 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,13 @@ import axios from 'axios'; export default { + data() { + return { + loading: false, + hosts: [], + }; + }, + methods: { notify(message, title) { browser.notifications.create({ @@ -51,6 +58,55 @@ export default { throw e; } }, + + async loadHosts() { + this.loading = true; + + try { + const response = await browser.storage.local.get('hosts'); + this.hosts = JSON.parse(response.hosts); + } finally { + this.loading = false; + } + }, + + async saveHosts() { + await browser.storage.local.set({ hosts: JSON.stringify(this.hosts) }); + }, + + formToHost(form) { + return { + name: form.name.value, + address: form.address.value, + port: parseInt(form.port.value), + websocketPort: parseInt(form.websocketPort.value), + ssl: form.ssl.checked, + token: form.token.value, + }; + }, + + onAddrChange(form) { + if (form.name.value.length && !form.address.value.startsWith(form.name.value)) { + return; + } + + form.name.value = form.address.value; + }, + + onPortChange(form) { + const port = form.port.value; + if (!this.isPortValid(port)) return; + form.websocketPort.value = '' + (parseInt(port) + 1); + }, + + isPortValid(port) { + port = parseInt(port); + return !isNaN(port) && port > 0 && port < 65536; + }, + + isHostFormValid(form) { + return form.name.value.length && form.address.value.length && this.isPortValid(form.port.value) && this.isPortValid(form.websocketPort.value); + }, }, };