From be5c5d365c47bdfa350807bc7177ce0b568918b9 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 8 Dec 2019 16:25:03 +0100 Subject: [PATCH] Added execute tab to webpanel --- platypush/backend/http/app/routes/index.py | 1 + .../static/css/source/common/elements.scss | 1 + .../source/common/elements/autocomplete.scss | 33 +++ .../css/source/common/elements/button.scss | 2 +- .../http/static/css/source/common/vars.scss | 3 + .../webpanel/plugins/execute/index.scss | 173 ++++++++++++++ .../source/webpanel/plugins/execute/vars.scss | 14 ++ .../backend/http/static/js/autocomplete.js | 110 +++++++++ .../http/static/js/plugins/execute/index.js | 215 ++++++++++++++++++ platypush/backend/http/templates/index.html | 7 + platypush/backend/http/templates/nav.html | 7 + .../http/templates/plugins/execute/index.html | 112 +++++++++ platypush/config/__init__.py | 16 +- platypush/message/request/__init__.py | 17 +- platypush/plugins/inspect.py | 160 +++++++++++++ requirements.txt | 3 + setup.py | 2 + 17 files changed, 866 insertions(+), 10 deletions(-) create mode 100644 platypush/backend/http/static/css/source/common/elements/autocomplete.scss create mode 100644 platypush/backend/http/static/css/source/webpanel/plugins/execute/index.scss create mode 100644 platypush/backend/http/static/css/source/webpanel/plugins/execute/vars.scss create mode 100644 platypush/backend/http/static/js/autocomplete.js create mode 100644 platypush/backend/http/static/js/plugins/execute/index.js create mode 100644 platypush/backend/http/templates/plugins/execute/index.html create mode 100644 platypush/plugins/inspect.py diff --git a/platypush/backend/http/app/routes/index.py b/platypush/backend/http/app/routes/index.py index 65ed04415..da6eccf51 100644 --- a/platypush/backend/http/app/routes/index.py +++ b/platypush/backend/http/app/routes/index.py @@ -67,6 +67,7 @@ def index(): websocket_port=get_websocket_port(), template_folder=template_folder, static_folder=static_folder, plugins=Config.get_plugins(), backends=Config.get_backends(), + procedures=Config.get_procedures(), has_ssl=http_conf.get('ssl_cert') is not None) diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss index 3433d461e..f07a9d043 100644 --- a/platypush/backend/http/static/css/source/common/elements.scss +++ b/platypush/backend/http/static/css/source/common/elements.scss @@ -33,4 +33,5 @@ select:-moz-focusring { @import 'common/elements/slider'; @import 'common/elements/text'; @import 'common/elements/dropdown'; +@import 'common/elements/autocomplete'; diff --git a/platypush/backend/http/static/css/source/common/elements/autocomplete.scss b/platypush/backend/http/static/css/source/common/elements/autocomplete.scss new file mode 100644 index 000000000..639867432 --- /dev/null +++ b/platypush/backend/http/static/css/source/common/elements/autocomplete.scss @@ -0,0 +1,33 @@ +.autocomplete { + /*the container must be positioned relative:*/ + position: relative; + display: inline-block; +} + +.autocomplete-items { + position: absolute; + border: $default-border-2; + border-bottom: none; + border-top: none; + z-index: 99; + /*position the autocomplete items to be the same width as the container:*/ + top: 100%; + left: 0; + right: 0; +} + +.autocomplete-items div { + padding: 1em; + cursor: pointer; + border-bottom: $default-border-2; + background-color: $autocomplete-bg; +} + +.autocomplete-items div:hover { + background-color: $hover-bg; +} + +.autocomplete-active { + background-color: $selected-bg !important; +} + diff --git a/platypush/backend/http/static/css/source/common/elements/button.scss b/platypush/backend/http/static/css/source/common/elements/button.scss index 1af86842e..22014cfd8 100644 --- a/platypush/backend/http/static/css/source/common/elements/button.scss +++ b/platypush/backend/http/static/css/source/common/elements/button.scss @@ -7,6 +7,6 @@ button[disabled], .btn-primary { background-color: #d8ffe0 !important; - border: 1px solid #98efb0 !important; + border: 1px solid #c2f0cf !important; } diff --git a/platypush/backend/http/static/css/source/common/vars.scss b/platypush/backend/http/static/css/source/common/vars.scss index b8f26dcab..1f29e3758 100644 --- a/platypush/backend/http/static/css/source/common/vars.scss +++ b/platypush/backend/http/static/css/source/common/vars.scss @@ -107,3 +107,6 @@ $modal-header-bg: #f0f0f0 !default; $modal-header-border: 1px solid #ccc !default; $modal-body-bg: white !default; +//// Autocomplete element +$autocomplete-bg: white !default; + diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/execute/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/execute/index.scss new file mode 100644 index 000000000..ea376b2cb --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/execute/index.scss @@ -0,0 +1,173 @@ +@import 'common/vars'; +@import 'common/layout'; +@import 'webpanel/plugins/execute/vars'; + +.execute-container { + height: 99%; + color: $default-fg-2; + font-weight: 400; + //line-height: 3.8rem; + //letter-spacing: .1rem; + border-bottom: $default-border-2; + border-radius: 0 0 1em 1em; + + .title { + background: $title-bg; + padding: .2em; + border: $title-border; + box-shadow: $title-shadow; + font-size: 1.1em; + } + + .request-type-container { + display: flex; + flex-direction: row; + align-items: baseline; + margin: 1em 0 0 1em; + + label { + margin: 0 1em 0 .5em; + } + } + + .request { + margin: 0 .5em; + + form { + margin-bottom: 0 !important; + } + + .autocomplete { + width: 80%; + max-width: 60em; + } + + .action-name { + box-shadow: $action-name-shadow; + width: 100%; + } + + [type=submit] { + margin-left: 2em; + } + + .options { + display: flex; + margin-top: .5em; + margin-bottom: 1.5em; + padding-top: .5em; + } + + .params { + margin-right: 1.5em; + max-height: 50vh; + overflow: auto; + + .param { + margin-bottom: .25em; + } + + .action-param-value { + width: 100%; + } + } + + .add-param { + width: 100%; + + button { + width: 100%; + background: $extra-params-btn-bg; + border: $title-border; + } + } + + .extra-param { + display: flex; + margin-bottom: .5em; + + .action-extra-param-del { + border: 0; + text-align: right; + padding: 0 .5em; + } + } + + .output-container { + max-height: 50vh; + overflow: auto; + + .response, + .error, + .doc { + padding: .5em .5em 0 .5em; + border-radius: 1em; + } + + .response { + background: $response-bg; + border: $response-border; + } + + .error { + background: $error-bg; + border: $error-border; + } + + .doc { + background: $doc-bg; + border: $doc-border; + } + } + + textarea { + width: 80%; + max-width: 60em; + height: 10em; + border-radius: 1em; + } + } + + .raw-request { + .first-row { + display: flex; + flex-direction: row; + } + } + + .procedures-container { + .procedure { + border-bottom: $default-border-2; + padding: 1.5em .5em; + cursor: pointer; + + &:hover { + background: $hover-bg; + } + + &.selected { + background: $selected-bg; + } + + form { + display: flex; + margin-bottom: 0 !important; + flex-direction: column; + } + + .head { + display: flex; + align-items: center; + } + + .btn-container { + text-align: right; + } + + button { + background: $procedure-submit-btn-bg; + } + } + } +} + diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/execute/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/execute/vars.scss new file mode 100644 index 000000000..fd6fc9e0b --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/execute/vars.scss @@ -0,0 +1,14 @@ +$title-bg: #eee; +$title-border: 1px solid #ddd; +$title-shadow: 0 3px 3px 0 rgba(187,187,187,0.75); +$action-name-shadow: 1px 1px 1px 1px #ddd; +$extra-params-btn-bg: #eee; +$response-bg: #edfff2; +$response-border: 1px dashed #98ff98; +$error-bg: #ffbcbc; +$error-border: 1px dashed #ff5353; +$doc-bg: #e8feff; +$doc-border: 1px dashed #84f9ff; +$procedure-submit-btn-bg: #ebffeb; + + diff --git a/platypush/backend/http/static/js/autocomplete.js b/platypush/backend/http/static/js/autocomplete.js new file mode 100644 index 000000000..a57de31eb --- /dev/null +++ b/platypush/backend/http/static/js/autocomplete.js @@ -0,0 +1,110 @@ +function autocomplete(inp, arr, listener) { + /*the autocomplete function takes two arguments, + the text field element and an array of possible autocompleted values:*/ + var currentFocus; + /*execute a function when someone writes in the text field:*/ + inp.addEventListener("input", function(e) { + var a, b, i, val = this.value; + /*close any already open lists of autocompleted values*/ + closeAllLists(); + if (!val) { return false;} + currentFocus = -1; + /*create a DIV element that will contain the items (values):*/ + a = document.createElement("DIV"); + a.setAttribute("id", this.id + "autocomplete-list"); + a.setAttribute("class", "autocomplete-items"); + /*append the DIV element as a child of the autocomplete container:*/ + this.parentNode.appendChild(a); + /*for each item in the array...*/ + for (i = 0; i < arr.length; i++) { + /*check if the item starts with the same letters as the text field value:*/ + if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) { + /*create a DIV element for each matching element:*/ + b = document.createElement("DIV"); + /*make the matching letters bold:*/ + b.innerHTML = "" + arr[i].substr(0, val.length) + ""; + b.innerHTML += arr[i].substr(val.length); + /*insert a input field that will hold the current array item's value:*/ + b.innerHTML += ""; + /*execute a function when someone clicks on the item value (DIV element):*/ + b.addEventListener("click", function(e) { + /*insert the value for the autocomplete text field:*/ + inp.value = this.getElementsByTagName("input")[0].value; + /*trigger event listener if any:*/ + if (listener) { + listener(e, inp.value); + } + /*close the list of autocompleted values, + (or any other open lists of autocompleted values:*/ + closeAllLists(); + }); + a.appendChild(b); + } + } + }); + + inp.addEventListener("keydown", function(e) { + if (e.keyCode == 9) { + /*Reset the list if tab has been pressed*/ + closeAllLists(); + } + }); + + /*execute a function presses a key on the keyboard:*/ + inp.addEventListener("keydown", function(e) { + var x = document.getElementById(this.id + "autocomplete-list"); + if (x) x = x.getElementsByTagName("div"); + if (e.keyCode == 40) { + /*If the arrow DOWN key is pressed, + increase the currentFocus variable:*/ + currentFocus++; + /*and and make the current item more visible:*/ + addActive(x); + } else if (e.keyCode == 38) { //up + /*If the arrow UP key is pressed, + decrease the currentFocus variable:*/ + currentFocus--; + /*and and make the current item more visible:*/ + addActive(x); + } else if (e.keyCode == 13) { + /*If the ENTER key is pressed, prevent the form from being submitted,*/ + if (currentFocus > -1 && x && x.length) { + e.preventDefault(); + /*and simulate a click on the "active" item:*/ + x[currentFocus].click(); + /*and restore the focus on the input element:*/ + this.focus(); + } + } + }); + function addActive(x) { + /*a function to classify an item as "active":*/ + if (!x) return false; + /*start by removing the "active" class on all items:*/ + removeActive(x); + if (currentFocus >= x.length) currentFocus = 0; + if (currentFocus < 0) currentFocus = (x.length - 1); + /*add class "autocomplete-active":*/ + x[currentFocus].classList.add("autocomplete-active"); + } + function removeActive(x) { + /*a function to remove the "active" class from all autocomplete items:*/ + for (var i = 0; i < x.length; i++) { + x[i].classList.remove("autocomplete-active"); + } + } + function closeAllLists(elmnt) { + /*close all autocomplete lists in the document, + except the one passed as an argument:*/ + var x = document.getElementsByClassName("autocomplete-items"); + for (var i = 0; i < x.length; i++) { + if (elmnt != x[i] && elmnt != inp) { + x[i].parentNode.removeChild(x[i]); + } + } +} +/*execute a function when someone clicks in the document:*/ +document.addEventListener("click", function (e) { + closeAllLists(e.target); +}); +} diff --git a/platypush/backend/http/static/js/plugins/execute/index.js b/platypush/backend/http/static/js/plugins/execute/index.js new file mode 100644 index 000000000..14e229590 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/execute/index.js @@ -0,0 +1,215 @@ +Vue.component('execute', { + template: '#tmpl-execute', + props: ['config'], + data: function() { + return { + loading: false, + running: false, + structuredInput: true, + actionChanged: false, + selectedDoc: undefined, + selectedProcedure: { + name: undefined, + args: {}, + }, + + response: undefined, + error: undefined, + htmlDoc: false, + rawRequest: undefined, + actions: {}, + plugins: {}, + procedures: {}, + action: { + name: undefined, + args: {}, + extraArgs: [], + supportsExtraArgs: false, + }, + }; + }, + + methods: { + refresh: async function() { + this.loading = true; + this.procedures = JSON.parse(this.config); + this.plugins = await request('inspect.get_all_plugins', {html_doc: true}); + + for (const plugin of Object.values(this.plugins)) { + if (plugin.html_doc) + this.htmlDoc = true; + + for (const action of Object.values(plugin.actions)) { + action.name = plugin.name + '.' + action.name; + action.supportsExtraArgs = !!action.has_kwargs; + delete action.has_kwargs; + this.actions[action.name] = action; + } + } + + const self = this; + autocomplete(this.$refs.actionName, Object.keys(this.actions).sort(), (evt, value) => { + this.action.name = value; + self.updateAction(); + }); + + this.loading = false; + }, + + updateAction: function() { + if (!this.actionChanged || !(this.action.name in this.actions)) + return; + + this.loading = true; + this.action = { + ...this.actions[this.action.name], + args: Object.entries(this.actions[this.action.name].args).reduce((args, entry) => { + args[entry[0]] = { + ...entry[1], + value: entry[1].default, + }; + + return args; + }, {}), + extraArgs: [], + }; + + this.selectedDoc = this.action.doc; + this.actionChanged = false; + this.response = undefined; + this.error = undefined; + this.loading = false; + }, + + updateProcedure: function(name) { + if (event.target.getAttribute('type') === 'submit') { + return; + } + + if (this.selectedProcedure.name === name) { + this.selectedProcedure = { + name: undefined, + args: {}, + }; + + return; + } + + if (!(name in this.procedures)) { + console.warn('Procedure not found: ' + name); + return; + } + + this.selectedProcedure = { + name: name, + args: this.procedures[name].args.reduce((args, arg) => { + args[arg] = undefined; + return args; + }, {}), + }; + }, + + addParameter: function() { + this.action.extraArgs.push({ + name: undefined, + value: undefined, + }) + }, + + removeParameter: function(i) { + this.action.extraArgs.pop(i); + }, + + selectAttrDoc: function(name) { + this.response = undefined; + this.error = undefined; + this.selectedDoc = this.action.args[name].doc; + }, + + resetDoc: function() { + this.response = undefined; + this.error = undefined; + this.selectedDoc = this.action.doc; + }, + + onInputTypeChange: function(structuredInput) { + this.structuredInput = structuredInput; + this.response = undefined; + this.error = undefined; + }, + + onResponse: function(response) { + this.response = '
' + JSON.stringify(response, null, 2) + '
'; + this.error = undefined; + }, + + onError: function(error) { + this.response = undefined; + this.error = '
' + error + '
'; + }, + + onDone: function() { + this.running = false; + }, + + executeAction: function() { + if (!this.action.name && !this.rawRequest || this.running) + return; + + this.running = true; + if (this.structuredInput) { + const args = { + ...Object.entries(this.action.args).reduce((args, param) => { + if (param[1].value != null) { + let value = param[1].value; + try {value = JSON.parse(value);} + catch (e) {} + args[param[0]] = value; + } + return args; + }, {}), + + ...this.action.extraArgs.reduce((args, param) => { + let value = args[param.value]; + try {value = JSON.parse(value);} + catch (e) {} + + args[param.name] = value; + return args; + }, {}) + }; + + request(this.action.name, args).then(this.onResponse).catch(this.onError).finally(this.onDone); + } else { + execute(JSON.parse(this.rawRequest)).then(this.onResponse).catch(this.onError).finally(this.onDone); + } + }, + + executeProcedure: function(event) { + if (!this.selectedProcedure.name || this.running) + return; + + event.stopPropagation(); + this.running = true; + const args = { + ...Object.entries(this.selectedProcedure.args).reduce((args, param) => { + if (param[1] != null) { + let value = param[1]; + try {value = JSON.parse(value);} + catch (e) {} + args[param[0]] = value; + } + return args; + }, {}), + }; + + request('procedure.' + this.selectedProcedure.name, args) + .then(this.onResponse).catch(this.onError).finally(this.onDone); + }, + }, + + created: function() { + this.refresh(); + }, +}); + diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index f8f2f9265..910d8b7c7 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -8,6 +8,7 @@ + + {% for style in styles.values() %} {% endfor %} {% include 'elements.html' %} + {% include 'plugins/execute/index.html' %} {% for plugin, conf in templates.items() %} {% with configuration=templates[plugin] %} @@ -56,6 +59,7 @@ {% endwith %} {% endfor %} + {% for script in scripts.values() %} {% endfor %} @@ -69,6 +73,9 @@
+ +