New Vue.js template for dashbord WIP
This commit is contained in:
parent
0c0e7411f7
commit
39abdfe40a
32 changed files with 13983 additions and 20 deletions
|
@ -10,8 +10,8 @@ from platypush.backend.http.app.utils import get_routes
|
||||||
base_folder = os.path.abspath(os.path.join(
|
base_folder = os.path.abspath(os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)), '..'))
|
os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||||
|
|
||||||
template_folder = os.path.join(base_folder, 'templates')
|
template_folder = os.path.join(base_folder, 'dist')
|
||||||
static_folder = os.path.join(base_folder, 'static')
|
static_folder = os.path.join(base_folder, 'dist/static')
|
||||||
|
|
||||||
application = Flask('platypush', template_folder=template_folder,
|
application = Flask('platypush', template_folder=template_folder,
|
||||||
static_folder=static_folder)
|
static_folder=static_folder)
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from flask import Blueprint, request, render_template
|
from flask import Blueprint, request, render_template
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
||||||
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
from platypush.config import Config
|
|
||||||
|
|
||||||
dashboard = Blueprint('dashboard', __name__, template_folder=template_folder)
|
dashboard = Blueprint('dashboard', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
@ -14,18 +12,13 @@ __routes__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dashboard.route('/dashboard', methods=['GET'])
|
@dashboard.route('/dashboard/<name>', methods=['GET'])
|
||||||
@authenticate()
|
@authenticate()
|
||||||
def dashboard():
|
def render_dashboard(*_, **__):
|
||||||
""" Route for the fullscreen dashboard """
|
""" Route for the dashboard """
|
||||||
http_conf = Config.get('backend.http')
|
return render_template('index.html',
|
||||||
dashboard_conf = http_conf.get('dashboard', {})
|
utils=HttpUtils,
|
||||||
|
websocket_port=get_websocket_port())
|
||||||
return render_template('dashboard.html', config=dashboard_conf,
|
|
||||||
utils=HttpUtils, token=Config.get('token'),
|
|
||||||
static_folder=static_folder, template_folder=template_folder,
|
|
||||||
websocket_port=get_websocket_port(),
|
|
||||||
has_ssl=http_conf.get('ssl_cert') is not None)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from platypush.config import Config
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder, static_folder
|
||||||
|
|
||||||
|
|
||||||
img_folder = os.path.join(static_folder, 'resources', 'img')
|
img_folder = os.path.join(template_folder, 'img')
|
||||||
resources = Blueprint('resources', __name__, template_folder=template_folder)
|
resources = Blueprint('resources', __name__, template_folder=template_folder)
|
||||||
favicon = Blueprint('favicon', __name__, template_folder=template_folder)
|
favicon = Blueprint('favicon', __name__, template_folder=template_folder)
|
||||||
img = Blueprint('img', __name__, template_folder=template_folder)
|
img = Blueprint('img', __name__, template_folder=template_folder)
|
||||||
|
@ -57,9 +57,9 @@ def resources_path(path):
|
||||||
|
|
||||||
|
|
||||||
@favicon.route('/favicon.ico', methods=['GET'])
|
@favicon.route('/favicon.ico', methods=['GET'])
|
||||||
def favicon():
|
def serve_favicon():
|
||||||
""" favicon.ico icon """
|
""" favicon.ico icon """
|
||||||
return send_from_directory(img_folder, 'favicon.ico')
|
return send_from_directory(template_folder, 'favicon.ico')
|
||||||
|
|
||||||
@img.route('/img/<path:path>', methods=['GET'])
|
@img.route('/img/<path:path>', methods=['GET'])
|
||||||
def imgpath(path):
|
def imgpath(path):
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Place in this folder the static resources you want to serve over HTTP (images, fonts etc.)
|
|
23
platypush/backend/http/webapp/.gitignore
vendored
Normal file
23
platypush/backend/http/webapp/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
24
platypush/backend/http/webapp/README.md
Normal file
24
platypush/backend/http/webapp/README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# webapp
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
platypush/backend/http/webapp/babel.config.js
Normal file
5
platypush/backend/http/webapp/babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
12758
platypush/backend/http/webapp/package-lock.json
generated
Normal file
12758
platypush/backend/http/webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
platypush/backend/http/webapp/package.json
Normal file
50
platypush/backend/http/webapp/package.json
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "platypush",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
|
"axios": "^0.21.0",
|
||||||
|
"bulma": "^0.9.1",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"mitt": "^2.1.0",
|
||||||
|
"node-sass": "^5.0.0",
|
||||||
|
"sass-loader": "^10.1.0",
|
||||||
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0-rc.3",
|
||||||
|
"vue-skycons": "^4.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^7.0.0-0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
23
platypush/backend/http/webapp/public/index.html
Normal file
23
platypush/backend/http/webapp/public/index.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.config = JSON.parse(`{
|
||||||
|
"websocketPort": "{{ websocket_port }}"
|
||||||
|
}`)
|
||||||
|
</script>
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
67
platypush/backend/http/webapp/src/App.vue
Normal file
67
platypush/backend/http/webapp/src/App.vue
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
<Notifications ref="notifications" />
|
||||||
|
<Events ref="events" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Notifications from "@/components/Notifications";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import { bus } from '@/bus';
|
||||||
|
import Events from "@/Events";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {Events, Notifications},
|
||||||
|
mixins: [Utils],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
config() {
|
||||||
|
const cfg = {
|
||||||
|
websocketPort: 8009,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._config.websocketPort && !this._config.websocketPort.startsWith('{{')) {
|
||||||
|
cfg.websocketPort = parseInt(this._config.websocketPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onNotification(notification) {
|
||||||
|
this.$refs.notifications.create(notification)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
bus.on('notification-create', this.onNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--suppress CssUnusedSymbol -->
|
||||||
|
<style lang="scss">
|
||||||
|
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/solid"; // fas
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/regular"; // far
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/brands"; // fab
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
</style>
|
120
platypush/backend/http/webapp/src/Events.vue
Normal file
120
platypush/backend/http/webapp/src/Events.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div id="__platypush_events"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Events",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ws: null,
|
||||||
|
pending: false,
|
||||||
|
opened: false,
|
||||||
|
timeout: null,
|
||||||
|
reconnectMsecs: 30000,
|
||||||
|
handlers: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
try {
|
||||||
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws'
|
||||||
|
const url = `${protocol}://${location.hostname}:${this.$root.config.websocketPort}`
|
||||||
|
this.ws = new WebSocket(url)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Websocket initialization error')
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending = true
|
||||||
|
|
||||||
|
const onWebsocketTimeout = function(self) {
|
||||||
|
return function() {
|
||||||
|
console.log('Websocket reconnection timed out, retrying')
|
||||||
|
this.pending = false
|
||||||
|
self.close()
|
||||||
|
self.onclose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = setTimeout(
|
||||||
|
onWebsocketTimeout(this.ws), this.reconnectMsecs)
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
const handlers = []
|
||||||
|
event = event.data
|
||||||
|
|
||||||
|
if (typeof event === 'string') {
|
||||||
|
try {
|
||||||
|
event = JSON.parse(event)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Received invalid non-JSON event')
|
||||||
|
console.warn(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(event)
|
||||||
|
if (event.type !== 'event') {
|
||||||
|
// Discard non-event messages
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null in this.handlers) {
|
||||||
|
handlers.push(this.handlers[null])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.args.type in this.handlers) {
|
||||||
|
handlers.push(...this.handlers[event.args.type])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(event.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
if (this.opened) {
|
||||||
|
console.log("There's already an opened websocket connection, closing the newly opened one")
|
||||||
|
this.onclose = () => {}
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Websocket connection successful')
|
||||||
|
this.opened = true
|
||||||
|
|
||||||
|
if (this.pending) {
|
||||||
|
this.pending = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = (event) => {
|
||||||
|
console.error(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
if (event) {
|
||||||
|
console.log('Websocket closed - code: ' + event.code + ' - reason: ' + event.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.opened = false
|
||||||
|
|
||||||
|
if (!this.pending) {
|
||||||
|
this.pending = true
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
9
platypush/backend/http/webapp/src/Utils.vue
Normal file
9
platypush/backend/http/webapp/src/Utils.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<script>
|
||||||
|
import Api from "@/utils/Api";
|
||||||
|
import Notification from "@/utils/Notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Utils",
|
||||||
|
mixins: [Api, Notification],
|
||||||
|
}
|
||||||
|
</script>
|
5
platypush/backend/http/webapp/src/bus.js
Normal file
5
platypush/backend/http/webapp/src/bus.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import mitt from 'mitt';
|
||||||
|
|
||||||
|
const bus = mitt();
|
||||||
|
|
||||||
|
export { bus };
|
87
platypush/backend/http/webapp/src/components/Loading.vue
Normal file
87
platypush/backend/http/webapp/src/components/Loading.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="loading">
|
||||||
|
<div class="icon">
|
||||||
|
<div v-for="n in 4" :key="n" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3em;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: $loading-bg;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
top: 33px;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
left: 8px;
|
||||||
|
animation: lds-ellipsis1 0.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
left: 8px;
|
||||||
|
animation: lds-ellipsis2 0.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
left: 32px;
|
||||||
|
animation: lds-ellipsis2 0.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
left: 56px;
|
||||||
|
animation: lds-ellipsis3 0.6s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-ellipsis1 {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-ellipsis3 {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-ellipsis2 {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(24px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
106
platypush/backend/http/webapp/src/components/Notification.vue
Normal file
106
platypush/backend/http/webapp/src/components/Notification.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div class="notification fade-in" :class="{warning: warning, error: error}" @click="clicked">
|
||||||
|
<div class="title" v-if="title" v-text="title"></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="image col-3" v-if="image || warning || error">
|
||||||
|
<div class="row">
|
||||||
|
<img :src="image.src" v-if="image && image.src" alt="">
|
||||||
|
<i :class="['fa', 'fa-' + image.icon]" :style="image.color ? '--color: ' + image.color : ''"
|
||||||
|
v-else-if="image && image.icon"></i>
|
||||||
|
<i :class="image.iconClass" :style="image.color ? '--color: ' + image.color : ''"
|
||||||
|
v-else-if="image && image.iconClass"></i>
|
||||||
|
<i class="fa fa-exclamation" v-else-if="warning"></i>
|
||||||
|
<i class="fa fa-times" v-else-if="error"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text col-9" v-if="text && !!image" v-text="text"></div>
|
||||||
|
<div class="text col-9" v-if="html && !!image" v-html="html"></div>
|
||||||
|
<div class="text row horizontal-center" v-if="text && !image" v-text="text"></div>
|
||||||
|
<div class="text row horizontal-center" v-if="html && !image" v-html="html"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Notification",
|
||||||
|
props: ['id','text','html','title','image','link','error','warning'],
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
clicked() {
|
||||||
|
if (this.link) {
|
||||||
|
window.open(this.link, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('clicked', this.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.notification {
|
||||||
|
background: $notification-bg;
|
||||||
|
border: $notification-border;
|
||||||
|
border-radius: .5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $notification-hover-bg;
|
||||||
|
&.warning { background: $notification-warning-hover-bg; }
|
||||||
|
&.error { background: $notification-error-hover-bg; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: $notification-warning-bg;
|
||||||
|
border: $notification-warning-border;
|
||||||
|
.image { --color: $notification-warning-icon-color; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: $notification-error-bg;
|
||||||
|
border: $notification-error-border;
|
||||||
|
.image { --color: $notification-error-icon-color; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: .4rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
letter-spacing: .1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
@extend .vertical-center;
|
||||||
|
height: 6em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
letter-spacing: .05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
--color: $notification-icon-color;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
@extend .vertical-center;
|
||||||
|
@extend .horizontal-center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<div class="notifications">
|
||||||
|
<Notification v-for="(notification, id, index) in notifications"
|
||||||
|
:key="index"
|
||||||
|
:id="id"
|
||||||
|
:text="notification.text"
|
||||||
|
:html="notification.html"
|
||||||
|
:title="notification.title"
|
||||||
|
:link="notification.link"
|
||||||
|
:image="notification.image"
|
||||||
|
:warning="notification.warning"
|
||||||
|
:error="notification.error"
|
||||||
|
@clicked="destroy">
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Notification from "@/components/Notification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Notifications",
|
||||||
|
components: {Notification},
|
||||||
|
props: {
|
||||||
|
duration: {
|
||||||
|
// Default notification duration in milliseconds
|
||||||
|
type: Number,
|
||||||
|
default: 10000,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
index: 0,
|
||||||
|
notifications: {},
|
||||||
|
timeouts: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
create: function(args) {
|
||||||
|
const id = this.index++;
|
||||||
|
this.notifications[id] = args;
|
||||||
|
|
||||||
|
if (args.duration == null) {
|
||||||
|
args.duration = this.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = args.duration ? parseInt(args.duration) : 0;
|
||||||
|
if (duration) {
|
||||||
|
this.timeouts[id] = setTimeout(this.destroy.bind(null, id), duration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(id) {
|
||||||
|
delete this.notifications[id];
|
||||||
|
delete this.timeouts[id];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notifications {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 25em;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
</style>
|
7
platypush/backend/http/webapp/src/main.js
Normal file
7
platypush/backend/http/webapp/src/main.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.config.globalProperties._config = window.config
|
||||||
|
app.use(router).mount('#app')
|
22
platypush/backend/http/webapp/src/router/index.js
Normal file
22
platypush/backend/http/webapp/src/router/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { createWebHistory, createRouter } from "vue-router";
|
||||||
|
import Dashboard from "@/views/Dashboard.vue";
|
||||||
|
import NotFound from "@/views/NotFound";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/dashboard/:name",
|
||||||
|
name: "Dashboard",
|
||||||
|
component: Dashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:catchAll(.*)",
|
||||||
|
component: NotFound,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
95
platypush/backend/http/webapp/src/style/layout.scss
Normal file
95
platypush/backend/http/webapp/src/style/layout.scss
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
$widths: (
|
||||||
|
s: '(max-width: 720px)',
|
||||||
|
m: '(max-width: 1024px) and (min-width: 720px)',
|
||||||
|
l: '(min-width: 1024px)',
|
||||||
|
);
|
||||||
|
|
||||||
|
@for $i from 1 through 12 {
|
||||||
|
.col-#{$i} {
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@if $i < 12 {
|
||||||
|
width: (4.66666666667%*$i) + (4% * if($i > 1, $i - 1, 0));
|
||||||
|
margin-left: 4%;
|
||||||
|
} @else {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-no-margin-#{$i} {
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: ((100%/12)*$i);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $i < 12 {
|
||||||
|
.col-offset-#{$i}:first-child {
|
||||||
|
margin-left: (8.66666666667%*$i) !important;
|
||||||
|
}
|
||||||
|
.col-offset-#{$i}:not(first-child) {
|
||||||
|
margin-left: 4% + (8.66666666667%*$i) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $size, $width in $widths {
|
||||||
|
@media #{$width} {
|
||||||
|
@for $i from 1 through 12 {
|
||||||
|
.col-#{$size}-#{$i} {
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@if $i < 12 {
|
||||||
|
width: (4.66666666667%*$i) + (4% * if($i > 1, $i - 1, 0));
|
||||||
|
margin-left: 4%;
|
||||||
|
} @else {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if $i < 12 {
|
||||||
|
.col-offset-#{$size}-#{$i} {
|
||||||
|
margin-left: (8.66666666667%*$i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-no-margin-#{$size}-#{$i} {
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: ((100%/12)*$i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$size}-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$size}-visible {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
23
platypush/backend/http/webapp/src/style/themes/light.scss
Normal file
23
platypush/backend/http/webapp/src/style/themes/light.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//// General
|
||||||
|
$background-color: white;
|
||||||
|
|
||||||
|
//// Notifications
|
||||||
|
$notification-bg: rgba(185, 255, 193, 0.9) !default;
|
||||||
|
$notification-hover-bg: rgba(160,245,178,0.95) !default;
|
||||||
|
$notification-warning-bg: rgba(228, 255, 78, 0.9) !default;
|
||||||
|
$notification-warning-hover-bg: rgba(218, 245, 68, 0.95) !default;
|
||||||
|
$notification-error-bg: rgba(255, 100, 100, 0.9) !default;
|
||||||
|
$notification-error-hover-bg: rgba(245, 90, 90, 0.95) !default;
|
||||||
|
$notification-border: 1px solid rgba(109, 205, 134, 0.62) !default;
|
||||||
|
$notification-warning-border: 1px solid rgba(205, 205, 109, 0.62) !default;
|
||||||
|
$notification-error-border: 1px solid rgba(205, 109, 109, 0.62) !default;
|
||||||
|
$notification-title-border: 1px solid rgba(83, 158, 102, 0.43) !default;
|
||||||
|
$notification-icon-color: black !default;
|
||||||
|
$notification-warning-icon-color: #662 !default;
|
||||||
|
$notification-error-icon-color: #8b0000 !default;
|
||||||
|
|
||||||
|
//// Loading panel
|
||||||
|
$loading-bg: #909090;
|
||||||
|
|
||||||
|
//// Dashboard
|
||||||
|
$dashboard-bg: url('/img/dashboard-bg-light.jpg')
|
66
platypush/backend/http/webapp/src/utils/Api.vue
Normal file
66
platypush/backend/http/webapp/src/utils/Api.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Api",
|
||||||
|
methods: {
|
||||||
|
execute(request, timeout=60000) {
|
||||||
|
const opts = {};
|
||||||
|
|
||||||
|
if (!('target' in request) || !request['target']) {
|
||||||
|
request['target'] = 'localhost'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('type' in request) || !request['type']) {
|
||||||
|
request['type'] = 'request'
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Proper auth/token management
|
||||||
|
// if (window.config.token) {
|
||||||
|
// opts.headers = {
|
||||||
|
// 'X-Token': window.config.token
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
opts.timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post('/execute', request, opts)
|
||||||
|
.then((response) => {
|
||||||
|
response = response.data.response
|
||||||
|
if (!response.errors.length) {
|
||||||
|
resolve(response.output);
|
||||||
|
} else {
|
||||||
|
const error = response.errors[0]
|
||||||
|
this.notify({
|
||||||
|
text: error,
|
||||||
|
error: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.notify({
|
||||||
|
text: error,
|
||||||
|
error: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
request(action, args={}, timeout=60000) {
|
||||||
|
return this.execute({
|
||||||
|
type: 'request',
|
||||||
|
action: action,
|
||||||
|
args: args,
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
21
platypush/backend/http/webapp/src/utils/Notification.vue
Normal file
21
platypush/backend/http/webapp/src/utils/Notification.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { bus } from '@/bus';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Notification",
|
||||||
|
methods: {
|
||||||
|
notify(notification) {
|
||||||
|
bus.emit('notification-create', notification)
|
||||||
|
},
|
||||||
|
|
||||||
|
error(msg) {
|
||||||
|
this.notify({
|
||||||
|
text: msg,
|
||||||
|
error: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
throw msg
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
96
platypush/backend/http/webapp/src/views/Dashboard.vue
Normal file
96
platypush/backend/http/webapp/src/views/Dashboard.vue
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div id="dashboard" class="columns is-mobile" :class="{blurred: loading}" :style="style">
|
||||||
|
<keep-alive v-for="(widget, i) in widgets" :key="i">
|
||||||
|
<Widget :style="widget.style">
|
||||||
|
<component :is="widget.component" v-bind="widget.props" />
|
||||||
|
</Widget>
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import Utils from '@/Utils'
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Widget from "@/widgets/Widget";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Dashboard',
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {Widget, Loading},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
widgets: [],
|
||||||
|
loading: false,
|
||||||
|
style: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseTemplate(name, tmpl) {
|
||||||
|
const node = new DOMParser().parseFromString(tmpl, 'text/xml').childNodes[0]
|
||||||
|
const self = this;
|
||||||
|
this.style = node.attributes.style ? node.attributes.style.nodeValue : undefined;
|
||||||
|
|
||||||
|
[...node.children].forEach((el) => {
|
||||||
|
const component = defineAsyncComponent(
|
||||||
|
() => import(`@/widgets/${el.nodeName}/Index`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const style = el.attributes.style ? el.attributes.style.nodeValue : undefined;
|
||||||
|
const attrs = [...el.attributes].reduce((obj, node) => {
|
||||||
|
if (node.nodeName !== 'style') {
|
||||||
|
obj[node.nodeName] = node.nodeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
self.$options.components[el.nodeName] = component
|
||||||
|
self.widgets.push({
|
||||||
|
component: component,
|
||||||
|
style: style,
|
||||||
|
props: attrs || {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshDashboard() {
|
||||||
|
this.loading = true
|
||||||
|
this.widgets = []
|
||||||
|
const name = this.$route.params.name
|
||||||
|
const template = (await this.request('config.get_dashboard', { name: name }))
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
this.error(`Dashboard ${name} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parseTemplate(name, template)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.refreshDashboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#dashboard {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em 1em 0 1em;
|
||||||
|
background: $dashboard-bg;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
.blurred {
|
||||||
|
filter: blur(0.075em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
10
platypush/backend/http/webapp/src/views/NotFound.vue
Normal file
10
platypush/backend/http/webapp/src/views/NotFound.vue
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<h1>Object not found</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "NotFound"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
<template>
|
||||||
|
<div class="date-time-weather">
|
||||||
|
<div class="date" v-text="formatDate(now)"></div>
|
||||||
|
<div class="time" v-text="formatTime(now)"></div>
|
||||||
|
|
||||||
|
<h1 class="weather">
|
||||||
|
<skycons :condition="weatherIcon" :paused="animPaused" :size="iconSize" v-if="weatherIcon" />
|
||||||
|
<span class="temperature" v-if="weather">
|
||||||
|
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="summary" v-if="weather && weather.summary" v-text="weather.summary"></div>
|
||||||
|
|
||||||
|
<div class="sensors" v-if="Object.keys(sensors).length">
|
||||||
|
<div class="sensor temperature col-6" v-if="sensors.temperature">
|
||||||
|
<i class="fas fa-thermometer-half"></i>
|
||||||
|
<span class="temperature">
|
||||||
|
{{ parseFloat(sensors.temperature).toFixed(1) + '°' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sensor humidity col-6" v-if="sensors.humidity">
|
||||||
|
<i class="fa fa-tint"></i>
|
||||||
|
<span class="humidity">
|
||||||
|
{{ parseFloat(sensors.humidity).toFixed(1) + '%' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Skycons from "vue-skycons"
|
||||||
|
|
||||||
|
// Widget to show date, time, weather and temperature information
|
||||||
|
export default {
|
||||||
|
name: 'DateTimeWeather',
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {Skycons},
|
||||||
|
props: {
|
||||||
|
// If false then the weather icon will be animated.
|
||||||
|
// Otherwise, it will be a static image.
|
||||||
|
paused: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Size of the weather icon in pixels
|
||||||
|
iconSize: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
animPaused() {
|
||||||
|
return !!parseInt(this.paused)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
weather: undefined,
|
||||||
|
sensors: {},
|
||||||
|
now: new Date(),
|
||||||
|
weatherIcon: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
const weather = (await this.request('weather.darksky.get_hourly_forecast')).data[0]
|
||||||
|
this.onWeatherChange(weather)
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTime() {
|
||||||
|
this.now = new Date()
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
return date.toDateString().substring(0, 10)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(date) {
|
||||||
|
return date.toTimeString().substring(0, 8)
|
||||||
|
},
|
||||||
|
|
||||||
|
onWeatherChange(event) {
|
||||||
|
if (!this.weather)
|
||||||
|
this.weather = {}
|
||||||
|
|
||||||
|
this.weather = {...this.weather, ...event}
|
||||||
|
this.weatherIcon = this.weather.icon
|
||||||
|
},
|
||||||
|
|
||||||
|
onSensorData(event) {
|
||||||
|
if ('temperature' in event.data)
|
||||||
|
this.sensors.temperature = event.data.temperature
|
||||||
|
|
||||||
|
if ('humidity' in event.data)
|
||||||
|
this.sensors.temperature = event.data.humidity
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.refresh()
|
||||||
|
setInterval(this.refresh, 900000)
|
||||||
|
setInterval(this.refreshTime, 1000)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// registerEventHandler(this.onWeatherChange, 'platypush.message.event.weather.NewWeatherConditionEvent')
|
||||||
|
// registerEventHandler(this.onSensorData, 'platypush.message.event.sensor.SensorDataChangeEvent')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.date-time-weather {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 0.1em;
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 1.3em;
|
||||||
|
height: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 2em;
|
||||||
|
height: 14%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather {
|
||||||
|
height: 25%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15%;
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 3.1em;
|
||||||
|
margin-left: 0.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
height: 28%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensors {
|
||||||
|
width: 100%;
|
||||||
|
height: 13%;
|
||||||
|
|
||||||
|
.sensor {
|
||||||
|
padding: 0 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
34
platypush/backend/http/webapp/src/widgets/Widget.vue
Normal file
34
platypush/backend/http/webapp/src/widgets/Widget.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div :style="style" class="widget column is-full-mobile is-half-tablet is-one-quarter-desktop">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Widget",
|
||||||
|
props: ['style'],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.widget {
|
||||||
|
background: $background-color;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $tablet){
|
||||||
|
.widget {
|
||||||
|
height: calc(100% - 1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $desktop){
|
||||||
|
.widget {
|
||||||
|
height: calc(50% - 1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
24
platypush/backend/http/webapp/vue.config.js
Normal file
24
platypush/backend/http/webapp/vue.config.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports = {
|
||||||
|
outputDir: "../dist",
|
||||||
|
assetsDir: "static",
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
sass: {
|
||||||
|
additionalData: `
|
||||||
|
@import '~bulma';
|
||||||
|
@import "@/style/themes/light.scss";
|
||||||
|
@import "@/style/layout.scss";
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
proxy: {
|
||||||
|
'/execute': {
|
||||||
|
target: 'http://localhost:8008',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -79,6 +79,10 @@ class Config(object):
|
||||||
self._config['scripts_dir'] = os.path.join(os.path.dirname(cfgfile), 'scripts')
|
self._config['scripts_dir'] = os.path.join(os.path.dirname(cfgfile), 'scripts')
|
||||||
os.makedirs(self._config['scripts_dir'], mode=0o755, exist_ok=True)
|
os.makedirs(self._config['scripts_dir'], mode=0o755, exist_ok=True)
|
||||||
|
|
||||||
|
if 'dashboards_dir' not in self._config:
|
||||||
|
self._config['dashboards_dir'] = os.path.join(os.path.dirname(cfgfile), 'dashboards')
|
||||||
|
os.makedirs(self._config['dashboards_dir'], mode=0o755, exist_ok=True)
|
||||||
|
|
||||||
init_py = os.path.join(self._config['scripts_dir'], '__init__.py')
|
init_py = os.path.join(self._config['scripts_dir'], '__init__.py')
|
||||||
if not os.path.isfile(init_py):
|
if not os.path.isfile(init_py):
|
||||||
with open(init_py, 'w') as f:
|
with open(init_py, 'w') as f:
|
||||||
|
@ -136,10 +140,12 @@ class Config(object):
|
||||||
self.procedures = {}
|
self.procedures = {}
|
||||||
self.constants = {}
|
self.constants = {}
|
||||||
self.cronjobs = {}
|
self.cronjobs = {}
|
||||||
|
self.dashboards = {}
|
||||||
|
|
||||||
self._init_constants()
|
self._init_constants()
|
||||||
self._load_scripts()
|
self._load_scripts()
|
||||||
self._init_components()
|
self._init_components()
|
||||||
|
self._init_dashboards(self._config['dashboards_dir'])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_special_token(token):
|
def _is_special_token(token):
|
||||||
|
@ -265,6 +271,38 @@ class Config(object):
|
||||||
for (key, value) in self._default_constants.items():
|
for (key, value) in self._default_constants.items():
|
||||||
self.constants[key] = value
|
self.constants[key] = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dashboard(name: str, dashboards_dir: Optional[str] = None) -> Optional[str]:
|
||||||
|
global _default_config_instance
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember,PyProtectedMember,PyUnresolvedReferences
|
||||||
|
dashboards_dir = dashboards_dir or _default_config_instance._config['dashboards_dir']
|
||||||
|
abspath = os.path.join(dashboards_dir, name + '.xml')
|
||||||
|
if not os.path.isfile(abspath):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(abspath, 'r') as fp:
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dashboards(cls, dashboards_dir: Optional[str] = None) -> dict:
|
||||||
|
global _default_config_instance
|
||||||
|
dashboards = {}
|
||||||
|
# noinspection PyProtectedMember,PyProtectedMember,PyUnresolvedReferences
|
||||||
|
dashboards_dir = dashboards_dir or _default_config_instance._config['dashboards_dir']
|
||||||
|
for f in os.listdir(dashboards_dir):
|
||||||
|
abspath = os.path.join(dashboards_dir, f)
|
||||||
|
if not os.path.isfile(abspath) or not abspath.endswith('.xml'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = f.split('.xml')[0]
|
||||||
|
dashboards[name] = cls.get_dashboard(name, dashboards_dir)
|
||||||
|
|
||||||
|
return dashboards
|
||||||
|
|
||||||
|
def _init_dashboards(self, dashboards_dir: str):
|
||||||
|
self.dashboards = self.get_dashboards(dashboards_dir)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_backends():
|
def get_backends():
|
||||||
global _default_config_instance
|
global _default_config_instance
|
||||||
|
|
19
platypush/plugins/config.py
Normal file
19
platypush/plugins/config.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from platypush import Config
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigPlugin(Plugin):
|
||||||
|
@action
|
||||||
|
def get(self) -> dict:
|
||||||
|
return Config.get()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def dashboards(self) -> dict:
|
||||||
|
return Config.get_dashboards()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_dashboard(self, name: str) -> str:
|
||||||
|
return Config.get_dashboard(name)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
Loading…
Reference in a new issue