Added support for custom dashboard components [see #129]
This commit is contained in:
parent
eb486df1ee
commit
177c697f83
27 changed files with 15612 additions and 28 deletions
|
@ -3,6 +3,12 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/-/wikis/Backends#creating-custom-widgets).
|
||||||
|
|
||||||
## [0.20.7] - 2021-03-26
|
## [0.20.7] - 2021-03-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
9
platypush/backend/http/webapp/dist/static/css/chunk-b8b1872e.355b7f9b.css
vendored
Normal file
9
platypush/backend/http/webapp/dist/static/css/chunk-b8b1872e.355b7f9b.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/app.0816b5b2.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/app.0816b5b2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/app.0816b5b2.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app.0816b5b2.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
||||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-9f884670"],{"0279":function(e,t,a){"use strict";var c=a("7a23"),n=Object(c["K"])("data-v-8fae7678");Object(c["u"])("data-v-8fae7678");var s=Object(c["h"])("div",{class:"switch"},[Object(c["h"])("div",{class:"dot"})],-1),i={class:"label"};Object(c["s"])();var o=n((function(e,t,a,n,o,l){return Object(c["r"])(),Object(c["e"])("div",{class:["power-switch",{disabled:a.disabled}],onClick:t[1]||(t[1]=function(){return l.onInput.apply(l,arguments)})},[Object(c["h"])("input",{type:"checkbox",checked:a.value},null,8,["checked"]),Object(c["h"])("label",null,[s,Object(c["h"])("span",i,[Object(c["y"])(e.$slots,"default")])])],2)})),l={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(e.stopPropagation(),this.disabled)return!1;this.$emit("input",e)}}};a("5b0a");l.render=o,l.__scopeId="data-v-8fae7678";t["a"]=l},"5b0a":function(e,t,a){"use strict";a("7ef9")},"7ef9":function(e,t,a){}}]);
|
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-06539e5d"],{"0279":function(e,t,a){"use strict";var c=a("7a23"),n=Object(c["K"])("data-v-8fae7678");Object(c["u"])("data-v-8fae7678");var s=Object(c["h"])("div",{class:"switch"},[Object(c["h"])("div",{class:"dot"})],-1),i={class:"label"};Object(c["s"])();var o=n((function(e,t,a,n,o,l){return Object(c["r"])(),Object(c["e"])("div",{class:["power-switch",{disabled:a.disabled}],onClick:t[1]||(t[1]=function(){return l.onInput.apply(l,arguments)})},[Object(c["h"])("input",{type:"checkbox",checked:a.value},null,8,["checked"]),Object(c["h"])("label",null,[s,Object(c["h"])("span",i,[Object(c["y"])(e.$slots,"default")])])],2)})),l={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(e.stopPropagation(),this.disabled)return!1;this.$emit("input",e)}}};a("5b0a");l.render=o,l.__scopeId="data-v-8fae7678";t["a"]=l},"5b0a":function(e,t,a){"use strict";a("7ef9a")},"7ef9a":function(e,t,a){}}]);
|
||||||
//# sourceMappingURL=chunk-9f884670.9830b044.js.map
|
//# sourceMappingURL=chunk-06539e5d.1a0f4e72.js.map
|
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,2 @@
|
||||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-31bc5041"],{6341:function(e,n,t){"use strict";t.r(n);t("b64b");var c=t("7a23"),r=Object(c["K"])("data-v-eac2ea44");Object(c["u"])("data-v-eac2ea44");var i={class:"switches-container"},s={class:"switch-plugins"},u={key:0,class:"no-content"},a={key:0,class:"refresh col-2"},o=Object(c["h"])("i",{class:"fa fa-sync"},null,-1),l={class:"refresh-button"},d=Object(c["h"])("i",{class:"fa fa-sync"},null,-1);Object(c["s"])();var b=r((function(e,n,t,r,b,h){var f=Object(c["z"])("Loading");return Object(c["r"])(),Object(c["e"])("div",i,[b.loading?(Object(c["r"])(),Object(c["e"])(f,{key:0})):Object(c["f"])("",!0),Object(c["h"])("div",s,[Object.keys(b.plugins).length?Object(c["f"])("",!0):(Object(c["r"])(),Object(c["e"])("div",u,"No switch plugins configured")),(Object(c["r"])(!0),Object(c["e"])(c["a"],null,Object(c["x"])(Object.keys(b.plugins),(function(e){return Object(c["r"])(),Object(c["e"])("div",{class:"switch-plugin",key:e,onClick:function(n){return b.selectedPlugin=b.selectedPlugin===e?null:e}},[Object(c["h"])("div",{class:["header",{selected:b.selectedPlugin===e}]},[Object(c["h"])("div",{class:"name col-10",textContent:Object(c["C"])(e)},null,8,["textContent"]),b.selectedPlugin===e?(Object(c["r"])(),Object(c["e"])("div",a,[Object(c["h"])("button",{onClick:Object(c["J"])((function(n){return b.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:b.loading},[o],8,["onClick","disabled"])])):Object(c["f"])("",!0)],2),Object(c["h"])("div",{class:["body",{hidden:b.selectedPlugin!==e}]},[(Object(c["r"])(),Object(c["e"])(Object(c["A"])(b.components[e]),{config:b.plugins[e],"plugin-name":e,selected:b.selectedPlugin===e,bus:b.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,["onClick"])})),128))]),Object(c["h"])("div",l,[Object(c["h"])("button",{onClick:n[1]||(n[1]=function(){return h.refresh.apply(h,arguments)}),disabled:b.loading,title:"Refresh plugins"},[d],8,["disabled"])])])})),h=(t("4160"),t("a15b"),t("d81d"),t("fb6a"),t("d3b7"),t("ac1f"),t("1276"),t("159b"),t("96cf"),t("1da1")),f=t("3a5e"),p=t("3e54"),O=t("14b7"),j={name:"Switches",components:{Loading:f["a"]},mixins:[p["a"]],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:Object(O["a"])()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=Object(h["a"])(regeneratorRuntime.mark((function n(r){var i,s,u;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=r.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join(""),s=null,n.prev=2,n.next=5,t("c1da")("./".concat(i,"/Index"));case 5:s=n.sent,n.next=11;break;case 8:return n.prev=8,n.t0=n["catch"](2),n.abrupt("return");case 11:u=Object(c["i"])(Object(h["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",s);case 1:case"end":return e.stop()}}),e)})))),e.$options.components[r]=u,e.components[r]=u;case 14:case"end":return n.stop()}}),n,null,[[2,8]])})));return function(e){return n.apply(this,arguments)}}())},refresh:function(){var e=this;return Object(h["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.loading=!0,n.prev=1,n.next=4,e.request("utils.get_switch_plugins");case 4:e.plugins=n.sent,e.initPanels();case 6:return n.prev=6,e.loading=!1,n.finish(6);case 9:case"end":return n.stop()}}),n,null,[[1,,6,9]])})))()}},mounted:function(){this.refresh()}};t("84aa");j.render=b,j.__scopeId="data-v-eac2ea44";n["default"]=j},"7ac9":function(e,n,t){},"84aa":function(e,n,t){"use strict";t("7ac9")},c1da:function(e,n,t){var c={"./LightHue/Index":["0219","chunk-9f884670","chunk-5d632024","chunk-e017dc3e"],"./Smartthings/Index":["6e68","chunk-9f884670","chunk-5d632024","chunk-972487d6"],"./SwitchSwitchbot/Index":["5083","chunk-9f884670","chunk-5d632024","chunk-0021f7ee"],"./SwitchTplink/Index":["d11f","chunk-9f884670","chunk-5d632024","chunk-c4aee99e"],"./SwitchWemo/Index":["bedd","chunk-9f884670","chunk-5d632024","chunk-60dbbc82"],"./ZigbeeMqtt/Index":["65d6","chunk-9f884670","chunk-5d632024","chunk-07773226"],"./Zwave/Index":["e170","chunk-9f884670","chunk-5d632024","chunk-0827360a"]};function r(e){if(!t.o(c,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=c[e],r=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(r)}))}r.keys=function(){return Object.keys(c)},r.id="c1da",e.exports=r}}]);
|
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-31bc5041"],{6341:function(e,n,t){"use strict";t.r(n);t("b64b");var c=t("7a23"),r=Object(c["K"])("data-v-eac2ea44");Object(c["u"])("data-v-eac2ea44");var i={class:"switches-container"},s={class:"switch-plugins"},u={key:0,class:"no-content"},a={key:0,class:"refresh col-2"},o=Object(c["h"])("i",{class:"fa fa-sync"},null,-1),l={class:"refresh-button"},d=Object(c["h"])("i",{class:"fa fa-sync"},null,-1);Object(c["s"])();var b=r((function(e,n,t,r,b,h){var f=Object(c["z"])("Loading");return Object(c["r"])(),Object(c["e"])("div",i,[b.loading?(Object(c["r"])(),Object(c["e"])(f,{key:0})):Object(c["f"])("",!0),Object(c["h"])("div",s,[Object.keys(b.plugins).length?Object(c["f"])("",!0):(Object(c["r"])(),Object(c["e"])("div",u,"No switch plugins configured")),(Object(c["r"])(!0),Object(c["e"])(c["a"],null,Object(c["x"])(Object.keys(b.plugins),(function(e){return Object(c["r"])(),Object(c["e"])("div",{class:"switch-plugin",key:e,onClick:function(n){return b.selectedPlugin=b.selectedPlugin===e?null:e}},[Object(c["h"])("div",{class:["header",{selected:b.selectedPlugin===e}]},[Object(c["h"])("div",{class:"name col-10",textContent:Object(c["C"])(e)},null,8,["textContent"]),b.selectedPlugin===e?(Object(c["r"])(),Object(c["e"])("div",a,[Object(c["h"])("button",{onClick:Object(c["J"])((function(n){return b.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:b.loading},[o],8,["onClick","disabled"])])):Object(c["f"])("",!0)],2),Object(c["h"])("div",{class:["body",{hidden:b.selectedPlugin!==e}]},[(Object(c["r"])(),Object(c["e"])(Object(c["A"])(b.components[e]),{config:b.plugins[e],"plugin-name":e,selected:b.selectedPlugin===e,bus:b.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,["onClick"])})),128))]),Object(c["h"])("div",l,[Object(c["h"])("button",{onClick:n[1]||(n[1]=function(){return h.refresh.apply(h,arguments)}),disabled:b.loading,title:"Refresh plugins"},[d],8,["disabled"])])])})),h=(t("4160"),t("a15b"),t("d81d"),t("fb6a"),t("d3b7"),t("ac1f"),t("1276"),t("159b"),t("96cf"),t("1da1")),f=t("3a5e"),p=t("3e54"),O=t("14b7"),j={name:"Switches",components:{Loading:f["a"]},mixins:[p["a"]],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:Object(O["a"])()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=Object(h["a"])(regeneratorRuntime.mark((function n(r){var i,s,u;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=r.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join(""),s=null,n.prev=2,n.next=5,t("c1da")("./".concat(i,"/Index"));case 5:s=n.sent,n.next=11;break;case 8:return n.prev=8,n.t0=n["catch"](2),n.abrupt("return");case 11:u=Object(c["i"])(Object(h["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",s);case 1:case"end":return e.stop()}}),e)})))),e.$options.components[r]=u,e.components[r]=u;case 14:case"end":return n.stop()}}),n,null,[[2,8]])})));return function(e){return n.apply(this,arguments)}}())},refresh:function(){var e=this;return Object(h["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.loading=!0,n.prev=1,n.next=4,e.request("utils.get_switch_plugins");case 4:e.plugins=n.sent,e.initPanels();case 6:return n.prev=6,e.loading=!1,n.finish(6);case 9:case"end":return n.stop()}}),n,null,[[1,,6,9]])})))()}},mounted:function(){this.refresh()}};t("84aa");j.render=b,j.__scopeId="data-v-eac2ea44";n["default"]=j},"7ac9":function(e,n,t){},"84aa":function(e,n,t){"use strict";t("7ac9")},c1da:function(e,n,t){var c={"./LightHue/Index":["0219","chunk-06539e5d","chunk-5d632024","chunk-e017dc3e"],"./Smartthings/Index":["6e68","chunk-06539e5d","chunk-5d632024","chunk-972487d6"],"./SwitchSwitchbot/Index":["5083","chunk-06539e5d","chunk-5d632024","chunk-0021f7ee"],"./SwitchTplink/Index":["d11f","chunk-06539e5d","chunk-5d632024","chunk-c4aee99e"],"./SwitchWemo/Index":["bedd","chunk-06539e5d","chunk-5d632024","chunk-60dbbc82"],"./ZigbeeMqtt/Index":["65d6","chunk-06539e5d","chunk-5d632024","chunk-07773226"],"./Zwave/Index":["e170","chunk-06539e5d","chunk-5d632024","chunk-0827360a"]};function r(e){if(!t.o(c,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=c[e],r=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(r)}))}r.keys=function(){return Object.keys(c)},r.id="c1da",e.exports=r}}]);
|
||||||
//# sourceMappingURL=chunk-31bc5041.f4934e0d.js.map
|
//# sourceMappingURL=chunk-31bc5041.821f5281.js.map
|
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/chunk-b8b1872e.33831618.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-b8b1872e.33831618.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/chunk-b8b1872e.33831618.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-b8b1872e.33831618.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14949
platypush/backend/http/webapp/package-lock.json
generated
14949
platypush/backend/http/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -67,6 +67,8 @@ export default {
|
||||||
for (let handler of handlers) {
|
for (let handler of handlers) {
|
||||||
if (handler instanceof Array)
|
if (handler instanceof Array)
|
||||||
handler = handler[0]
|
handler = handler[0]
|
||||||
|
else if (handler instanceof Object)
|
||||||
|
handler = Object.values(handler)[0]
|
||||||
|
|
||||||
handler(event.args)
|
handler(event.args)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
<template>
|
||||||
|
<div class="component-widget">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<div class="container" ref="container" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import components from './index'
|
||||||
|
import { createApp, h } from "vue";
|
||||||
|
import mitt from 'mitt';
|
||||||
|
|
||||||
|
const bus = mitt();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Elements",
|
||||||
|
components: {Loading},
|
||||||
|
mixins: [Utils],
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
unwatch: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_parseActions(element) {
|
||||||
|
const actionsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'actions')
|
||||||
|
const children = actionsTags?.length ? actionsTags[0].children : element.children
|
||||||
|
const actionTags = [...children].filter((node) => node.tagName?.toLowerCase() === 'action')
|
||||||
|
|
||||||
|
if (!actionTags?.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
return [...actionTags]
|
||||||
|
.map((actionTag) => {
|
||||||
|
return {
|
||||||
|
action: actionTag.attributes.name.value,
|
||||||
|
args: [...actionTag.children].reduce((obj, arg) => {
|
||||||
|
let value = undefined
|
||||||
|
try {
|
||||||
|
value = JSON.parse(arg.innerText)
|
||||||
|
} catch (e) {
|
||||||
|
if (arg.innerText?.length)
|
||||||
|
value = arg.innerText
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[arg.tagName.toLowerCase()] = value
|
||||||
|
return obj
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_parseVars(element) {
|
||||||
|
const varsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'vars')
|
||||||
|
if (!varsTags?.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
return [...varsTags[0].children].reduce((vars, varTag) => {
|
||||||
|
let value = undefined
|
||||||
|
try {
|
||||||
|
value = JSON.parse(varTag.innerText)
|
||||||
|
} catch (e) {
|
||||||
|
if (varTag.innerText?.length)
|
||||||
|
value = varTag.innerText
|
||||||
|
}
|
||||||
|
vars[varTag.tagName.toLowerCase()] = value
|
||||||
|
return vars
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
_parseHandlers(element) {
|
||||||
|
const handlers = {}
|
||||||
|
const parseHndlScript = (hndlText) => {
|
||||||
|
return (app) => {
|
||||||
|
return eval(`// noinspection JSUnusedLocalSymbols
|
||||||
|
(async function (self) {
|
||||||
|
${hndlText}
|
||||||
|
})`)(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseEventHndl = (hndlText) => {
|
||||||
|
return (app) => {
|
||||||
|
return (event) => {
|
||||||
|
return eval(`// noinspection JSUnusedLocalSymbols
|
||||||
|
(async function (self, event) {
|
||||||
|
${hndlText}
|
||||||
|
})`)(app, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hndlTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'handlers')
|
||||||
|
if (hndlTags?.length) {
|
||||||
|
const mounted = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'mounted')
|
||||||
|
if (mounted?.length)
|
||||||
|
handlers.mounted = parseHndlScript(mounted[0].innerText)
|
||||||
|
|
||||||
|
const refresh = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'refresh')
|
||||||
|
if (refresh?.length) {
|
||||||
|
handlers.refresh = {
|
||||||
|
handler: parseHndlScript(refresh[0].innerText),
|
||||||
|
interval: refresh[0].attributes.interval?.value || 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'event')
|
||||||
|
if (events?.length)
|
||||||
|
handlers.events = events.reduce((events, hndlTag) => {
|
||||||
|
events[hndlTag.attributes.type.value] = parseEventHndl(hndlTag.innerText)
|
||||||
|
return events
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'actions')
|
||||||
|
if (actionsTags?.length) {
|
||||||
|
const beforeActionsTags = [...actionsTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'before')
|
||||||
|
if (beforeActionsTags?.length)
|
||||||
|
handlers.beforeActions = parseHndlScript(beforeActionsTags[0].innerText)
|
||||||
|
|
||||||
|
const afterActionsTags = [...actionsTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'after')
|
||||||
|
if (afterActionsTags?.length)
|
||||||
|
handlers.afterActions = parseHndlScript(afterActionsTags[0].innerText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
},
|
||||||
|
|
||||||
|
_parseProps(element) {
|
||||||
|
return [...element.attributes].reduce((obj, attr) => {
|
||||||
|
obj[attr.name] = attr.value
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
|
propagateEvent(event) {
|
||||||
|
bus.emit('event', event)
|
||||||
|
},
|
||||||
|
|
||||||
|
_addEventHandler() {
|
||||||
|
this.unwatch = this.subscribe((event) => {
|
||||||
|
bus.emit('event', event)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeEventHandler() {
|
||||||
|
if (this.unwatch) {
|
||||||
|
this.unwatch()
|
||||||
|
this.unwatch = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loading = true
|
||||||
|
this._addEventHandler()
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$refs.container.innerHTML = this.content
|
||||||
|
|
||||||
|
Object.entries(components).forEach(([name, component]) => {
|
||||||
|
this.$options.components[name] = component;
|
||||||
|
[...this.$refs.container.getElementsByTagName(name)].forEach((element) => {
|
||||||
|
const props = this._parseProps(element)
|
||||||
|
props.actions = this._parseActions(element)
|
||||||
|
props.handlers = this._parseHandlers(element)
|
||||||
|
props._vars = this._parseVars(element)
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
render() { return h(component, props) },
|
||||||
|
data() {
|
||||||
|
return { bus: bus }
|
||||||
|
},
|
||||||
|
}).mount(element)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const tagName of ['handlers', 'actions', 'vars'])
|
||||||
|
this.$refs.container.getElementsByTagName(tagName).forEach((hndlTag) => {
|
||||||
|
hndlTag.parentNode.removeChild(hndlTag)
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this._removeEventHandler()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.component-widget {
|
||||||
|
margin: -.75em 0 0 -.75em !important;
|
||||||
|
padding: 0;
|
||||||
|
width: calc(100% + 1.5em);
|
||||||
|
height: calc(100% + 1.5em);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="run component-row" @click="run">
|
||||||
|
<div class="col-1 icon-container" v-if="hasIcon">
|
||||||
|
<img class="icon" :src="iconUrl" :alt="name" v-if="iconUrl?.length">
|
||||||
|
<i class="icon" :class="iconClass" :style="iconStyle" v-else />
|
||||||
|
</div>
|
||||||
|
<div :class="{'col-11': hasIcon, 'col-12': !hasIcon}" v-text="name" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mixins from './mixins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is used to run one or more actions.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "Run",
|
||||||
|
mixins: [mixins],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "mixins";
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="sensor component-row" @click="run">
|
||||||
|
<div class="col-1 icon-container" v-if="hasIcon">
|
||||||
|
<img class="icon" :src="iconUrl" :alt="name" v-if="iconUrl?.length">
|
||||||
|
<i class="icon" :class="iconClass" :style="iconStyle" v-else />
|
||||||
|
</div>
|
||||||
|
<div :class="{'col-8': hasIcon, 'col-9': !hasIcon}" v-text="name" />
|
||||||
|
<div class="col-3 value-container">
|
||||||
|
<div class="value">
|
||||||
|
{{ value }}
|
||||||
|
<span v-if="unit" v-text="unit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mixins from './mixins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is used to monitor values from sensors.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "Sensor",
|
||||||
|
mixins: [mixins],
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Optional unit used for the sensor value
|
||||||
|
*/
|
||||||
|
unit: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async run() {
|
||||||
|
if (this.handlers.beforeActions)
|
||||||
|
await this.handlers.beforeActions(this)
|
||||||
|
|
||||||
|
if (this.actions?.length)
|
||||||
|
for (const action of this.actions)
|
||||||
|
await this.request_(action)
|
||||||
|
else
|
||||||
|
await this.refresh()
|
||||||
|
|
||||||
|
if (this.handlers.afterActions) {
|
||||||
|
await this.handlers.afterActions(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
|
.sensor {
|
||||||
|
.value-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="slider-root component-row">
|
||||||
|
<div class="col-1 icon-container" v-if="hasIcon">
|
||||||
|
<img class="icon" :src="iconUrl" :alt="name" v-if="iconUrl?.length">
|
||||||
|
<i class="icon" :class="iconClass" :style="iconStyle" v-else />
|
||||||
|
</div>
|
||||||
|
<div :class="{'col-6': hasIcon, 'col-7': !hasIcon}" v-text="name" />
|
||||||
|
<div class="col-5 slider-container">
|
||||||
|
<div class="slider">
|
||||||
|
<SliderElement :value="value" :range="[parseFloat(min), parseFloat(max)]" @mouseup="run" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mixins from './mixins';
|
||||||
|
import SliderElement from "@/components/elements/Slider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component can be used to run action on the basis of a
|
||||||
|
* numeric value included in a specified interval (i.e. a slider).
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "Slider",
|
||||||
|
components: {SliderElement},
|
||||||
|
mixins: [mixins],
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Minimum value for the slider (default: 0).
|
||||||
|
*/
|
||||||
|
min: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum value for the slider.
|
||||||
|
*/
|
||||||
|
max: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async run(event) {
|
||||||
|
this.value = parseFloat(event.target.value)
|
||||||
|
|
||||||
|
if (this.handlers.beforeActions)
|
||||||
|
await this.handlers.beforeActions(this)
|
||||||
|
for (const action of this.actions)
|
||||||
|
await this.request_(action)
|
||||||
|
if (this.handlers.afterActions) {
|
||||||
|
await this.handlers.afterActions(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
|
.slider-root {
|
||||||
|
.slider-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="switch component-row" @click="run">
|
||||||
|
<div class="col-1 icon-container" v-if="hasIcon">
|
||||||
|
<img class="icon" :src="iconUrl" :alt="name" v-if="iconUrl?.length">
|
||||||
|
<i class="icon" :class="iconClass" :style="iconStyle" v-else />
|
||||||
|
</div>
|
||||||
|
<div :class="{'col-9': hasIcon, 'col-10': !hasIcon}" v-text="name" />
|
||||||
|
<div class="col-2 toggle-container">
|
||||||
|
<div class="toggle">
|
||||||
|
<ToggleSwitch :value="value" @input.stop="run" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mixins from './mixins';
|
||||||
|
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component can be used to trigger toggle actions on
|
||||||
|
* entities with a binary (ON/OFF) state.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "Switch",
|
||||||
|
components: {ToggleSwitch},
|
||||||
|
mixins: [mixins],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
.toggle-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,183 @@
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Utils],
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Component name
|
||||||
|
*/
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '[Unnamed sensor]',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action (FontAwesome) icon class (default: `fa fa-play`)
|
||||||
|
*/
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action icon URL (default: `fa fa-play`)
|
||||||
|
*/
|
||||||
|
iconUrl: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action icon color override, for FontAwesome icons
|
||||||
|
*/
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to run upon interaction with the widget. Format:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "action": "light.hue.toggle",
|
||||||
|
* "args": {
|
||||||
|
* "lights": ["Bulb 1", "Bulb 2"]
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "action": "music.mpd.pause"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => { return [] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of variables used by this component, in the form
|
||||||
|
* variable_name -> variable_value.
|
||||||
|
*/
|
||||||
|
_vars: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { return {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of handlers, in the form of event_type -> functions.
|
||||||
|
* Supported event handler types:
|
||||||
|
*
|
||||||
|
* - mounted: Function to execute when the component is mounted.
|
||||||
|
* - beforeActions: Function to execute before the component action is run.
|
||||||
|
* - afterActions: Function to execute after the component action is run.
|
||||||
|
* - refresh: Function to be called at startup (if mounted is also specified
|
||||||
|
* then refresh will be called after mounted when the component is
|
||||||
|
* first mounted) and at regular intervals defined on the
|
||||||
|
* interval property (default: 10 seconds).
|
||||||
|
* - events: This is a mapping of functions that react to Platypush
|
||||||
|
* platform events published on the websocket (e.g. lights or
|
||||||
|
* switches toggles, media events etc.). The form is
|
||||||
|
* platypush_event_type -> function.
|
||||||
|
*/
|
||||||
|
handlers: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { return {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event bus
|
||||||
|
*/
|
||||||
|
bus: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
vars: {...(this._vars || {})},
|
||||||
|
_interval: undefined,
|
||||||
|
refresh: null,
|
||||||
|
refreshInterval: null,
|
||||||
|
value: null,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
iconStyle() {
|
||||||
|
if (!this.iconClass?.length && this.iconColor?.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
return {'color': this.iconColor}
|
||||||
|
},
|
||||||
|
|
||||||
|
hasIcon() {
|
||||||
|
return this.iconUrl?.length || this.iconClass?.length
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async run() {
|
||||||
|
if (this.handlers.input)
|
||||||
|
return this.handlers.input(this)(this.value)
|
||||||
|
|
||||||
|
if (this.handlers.beforeActions)
|
||||||
|
await this.handlers.beforeActions(this)
|
||||||
|
for (const action of this.actions)
|
||||||
|
await this.request_(action)
|
||||||
|
if (this.handlers.afterActions) {
|
||||||
|
await this.handlers.afterActions(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async request_(action) {
|
||||||
|
const args = Object.entries(action.args).reduce((args, [key, value]) => {
|
||||||
|
if (value.trim) {
|
||||||
|
value = value.trim()
|
||||||
|
const m = value.match(/^{{\s*(.*)\s*}}/)
|
||||||
|
if (m) {
|
||||||
|
value = eval(`// noinspection JSUnusedLocalSymbols
|
||||||
|
(function (self) {
|
||||||
|
return ${m[1]}
|
||||||
|
})`)(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args[key] = value
|
||||||
|
return args
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
await this.request(action.action, args)
|
||||||
|
},
|
||||||
|
|
||||||
|
async processEvent(event) {
|
||||||
|
const hndl = (this.handlers.events || {})[event.type]
|
||||||
|
if (hndl)
|
||||||
|
await hndl(this)(event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$root.bus.on('event', this.processEvent)
|
||||||
|
|
||||||
|
if (this.handlers.mounted)
|
||||||
|
await this.handlers.mounted(this)
|
||||||
|
|
||||||
|
if (this.handlers.refresh) {
|
||||||
|
this.refreshInterval = (this.handlers.refresh?.interval || 0) * 1000
|
||||||
|
this.refresh = () => {
|
||||||
|
this.handlers.refresh.handler(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refresh()
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
const self = this
|
||||||
|
const wrapper = () => { return self.refresh() }
|
||||||
|
this._interval = setInterval(wrapper, this.refreshInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
if (this._interval)
|
||||||
|
clearInterval(this._interval)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
@mixin component-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .75em .5em;
|
||||||
|
border-bottom: $default-border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-row {
|
||||||
|
@include component-row;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Run from './components/Run'
|
||||||
|
import Switch from './components/Switch'
|
||||||
|
import Slider from './components/Slider'
|
||||||
|
import Sensor from "@/components/widgets/Component/components/Sensor";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Run,
|
||||||
|
Switch,
|
||||||
|
Slider,
|
||||||
|
Sensor,
|
||||||
|
}
|
|
@ -40,7 +40,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
generateId() {
|
generateId() {
|
||||||
return btoa([...Array(16).keys()].forEach(() => String.fromCharCode(Math.round(Math.random() * 255))))
|
return btoa([...Array(11).keys()].map(() => String.fromCharCode(Math.round(Math.random() * 255))))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,27 +59,29 @@ export default {
|
||||||
parseTemplate(name, tmpl) {
|
parseTemplate(name, tmpl) {
|
||||||
const node = new DOMParser().parseFromString(tmpl, 'text/xml').childNodes[0]
|
const node = new DOMParser().parseFromString(tmpl, 'text/xml').childNodes[0]
|
||||||
const self = this
|
const self = this
|
||||||
this.style = node.attributes.style ? node.attributes.style.nodeValue : undefined
|
this.style = node.attributes.style?.nodeValue
|
||||||
this.class = node.attributes.class ? node.attributes.class.nodeValue : undefined
|
this.class = node.attributes.class?.nodeValue
|
||||||
|
|
||||||
this.rows = [...node.getElementsByTagName('Row')].map((row) => {
|
this.rows = [...node.getElementsByTagName('Row')].map((row) => {
|
||||||
return {
|
return {
|
||||||
style: row.attributes.style ? row.attributes.style.nodeValue : undefined,
|
style: row.attributes.style?.nodeValue,
|
||||||
class: row.attributes.class ? row.attributes.class.nodeValue : undefined,
|
class: row.attributes.class?.nodeValue,
|
||||||
widgets: [...row.children].map((el) => {
|
widgets: [...row.children].map((el) => {
|
||||||
const component = defineAsyncComponent(
|
const component = defineAsyncComponent(
|
||||||
() => import(`@/components/widgets/${el.nodeName}/Index`)
|
() => import(`@/components/widgets/${el.nodeName}/Index`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const style = el.attributes.style ? el.attributes.style.nodeValue : undefined
|
const style = el.attributes.style?.nodeValue
|
||||||
const classes = el.attributes.class ? el.attributes.class.nodeValue : undefined
|
const classes = el.attributes.class?.nodeValue
|
||||||
const attrs = [...el.attributes].reduce((obj, node) => {
|
const attrs = [...el.attributes].reduce((obj, node) => {
|
||||||
if (node.nodeName !== 'style') {
|
if (node.nodeName !== 'style') {
|
||||||
obj[node.nodeName] = node.nodeValue
|
obj[node.nodeName] = node.nodeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
}, {})
|
}, {
|
||||||
|
content: el.innerHTML,
|
||||||
|
})
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
component: component,
|
component: component,
|
||||||
|
|
Loading…
Reference in a new issue