New template for index panel
This commit is contained in:
parent
c3f01c198f
commit
0cd120f492
10 changed files with 425 additions and 60 deletions
|
@ -1,14 +1,8 @@
|
||||||
import json
|
from flask import Blueprint, render_template
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Blueprint, render_template, request
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
|
||||||
from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
|
||||||
|
|
||||||
|
from platypush.backend.http.app import template_folder
|
||||||
|
from platypush.backend.http.app.utils import authenticate
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
from platypush.config import Config
|
|
||||||
from platypush.message import Message
|
|
||||||
|
|
||||||
index = Blueprint('index', __name__, template_folder=template_folder)
|
index = Blueprint('index', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
@ -22,57 +16,7 @@ __routes__ = [
|
||||||
@authenticate()
|
@authenticate()
|
||||||
def index():
|
def index():
|
||||||
""" Route to the main web panel """
|
""" Route to the main web panel """
|
||||||
configured_plugins = Config.get_plugins()
|
return render_template('index.html', utils=HttpUtils)
|
||||||
enabled_templates = {}
|
|
||||||
enabled_scripts = {}
|
|
||||||
enabled_styles = {}
|
|
||||||
|
|
||||||
enabled_plugins = set(request.args.get('enabled_plugins', '').split(','))
|
|
||||||
for plugin in enabled_plugins:
|
|
||||||
if plugin not in configured_plugins:
|
|
||||||
configured_plugins[plugin] = {}
|
|
||||||
|
|
||||||
configured_plugins['execute'] = {}
|
|
||||||
disabled_plugins = set(request.args.get('disabled_plugins', '').split(','))
|
|
||||||
|
|
||||||
js_folder = os.path.abspath(
|
|
||||||
os.path.join(template_folder, '..', 'static', 'js'))
|
|
||||||
style_folder = os.path.abspath(
|
|
||||||
os.path.join(template_folder, '..', 'static', 'css', 'dist'))
|
|
||||||
|
|
||||||
for plugin, conf in configured_plugins.copy().items():
|
|
||||||
if plugin in disabled_plugins:
|
|
||||||
if plugin == 'execute':
|
|
||||||
configured_plugins.pop('execute')
|
|
||||||
continue
|
|
||||||
|
|
||||||
template_file = os.path.join(
|
|
||||||
template_folder, 'plugins', plugin, 'index.html')
|
|
||||||
|
|
||||||
script_file = os.path.join(js_folder, 'plugins', plugin, 'index.js')
|
|
||||||
style_file = os.path.join(style_folder, 'webpanel', 'plugins', plugin+'.css')
|
|
||||||
|
|
||||||
if os.path.isfile(template_file):
|
|
||||||
conf['_template_file'] = '/' + '/'.join(template_file.split(os.sep)[-3:])
|
|
||||||
enabled_templates[plugin] = conf
|
|
||||||
|
|
||||||
if os.path.isfile(script_file):
|
|
||||||
conf['_script_file'] = '/'.join(script_file.split(os.sep)[-4:])
|
|
||||||
enabled_scripts[plugin] = conf
|
|
||||||
|
|
||||||
if os.path.isfile(style_file):
|
|
||||||
conf['_style_file'] = 'css/dist/' + style_file[len(style_folder)+1:]
|
|
||||||
enabled_styles[plugin] = conf
|
|
||||||
|
|
||||||
http_conf = Config.get('backend.http')
|
|
||||||
return render_template('index.html', templates=enabled_templates,
|
|
||||||
scripts=enabled_scripts, styles=enabled_styles,
|
|
||||||
utils=HttpUtils, token=Config.get('token'),
|
|
||||||
websocket_port=get_websocket_port(),
|
|
||||||
template_folder=template_folder, static_folder=static_folder,
|
|
||||||
plugins=Config.get_plugins(), backends=Config.get_backends(),
|
|
||||||
procedures=json.dumps(Config.get_procedures(), cls=Message.Encoder),
|
|
||||||
has_ssl=http_conf.get('ssl_cert') is not None)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
7
platypush/backend/http/webapp/src/assets/icons.json
Normal file
7
platypush/backend/http/webapp/src/assets/icons.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"icons": {
|
||||||
|
"light.hue": {
|
||||||
|
"class": "fas fa-lightbulb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
platypush/backend/http/webapp/src/components/Nav.vue
Normal file
155
platypush/backend/http/webapp/src/components/Nav.vue
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<nav :class="{collapsed: collapsed}">
|
||||||
|
<div class="toggler" @click="collapsed = !collapsed">
|
||||||
|
<i class="fas fa-bars" />
|
||||||
|
<span class="hostname" v-if="!collapsed && hostname" v-text="hostname" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<li v-for="name in Object.keys(panels)" :key="name" class="entry" :class="{selected: name === selectedPanel}"
|
||||||
|
@click="$emit('select', name)">
|
||||||
|
<a :href="`/#${name}`">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="icons[name].class" v-if="icons[name]?.class" />
|
||||||
|
<i class="fas fa-puzzle-piece" v-else />
|
||||||
|
</span>
|
||||||
|
<span class="name" v-if="!collapsed">{{ name }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { icons } from '@/assets/icons.json'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Nav",
|
||||||
|
emits: ['select'],
|
||||||
|
props: {
|
||||||
|
panels: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedPanel: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
hostname: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collapsed: false,
|
||||||
|
icons: icons,
|
||||||
|
host: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--suppress SassScssResolvedByNameOnly -->
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
nav {
|
||||||
|
width: 20%;
|
||||||
|
min-width: 12.5em;
|
||||||
|
max-width: 25em;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: $nav-bg;
|
||||||
|
color: $nav-fg;
|
||||||
|
box-shadow: $nav-box-shadow-main;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $nav-fg;
|
||||||
|
&:hover {
|
||||||
|
color: $nav-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 1em 0.25em;
|
||||||
|
box-shadow: $nav-box-shadow-entry;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $nav-entry-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: $nav-entry-selected-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggler {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.4em;
|
||||||
|
|
||||||
|
.hostname {
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
width: 2.5em;
|
||||||
|
min-width: unset;
|
||||||
|
max-width: unset;
|
||||||
|
background: initial;
|
||||||
|
color: $nav-collapsed-fg;
|
||||||
|
box-shadow: $nav-box-shadow-collapsed;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $nav-collapsed-fg;
|
||||||
|
&:hover {
|
||||||
|
color: $nav-collapsed-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggler {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
border-radius: 1em;
|
||||||
|
margin: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: $nav-entry-collapsed-selected-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $nav-entry-collapsed-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="light-plugin">
|
||||||
|
I'm in the content!
|
||||||
|
{{ pluginName }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Panel from "@/components/panels/Panel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic component for light plugins panels.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "Light",
|
||||||
|
mixins: [Utils, Panel],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
// Set to false if the light plugin doesn't support groups.
|
||||||
|
hasGroups: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set to false if the light plugin doesn't support scenes.
|
||||||
|
hasScenes: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set to false if the light plugin doesn't support animations.
|
||||||
|
hasAnimations: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lights: {},
|
||||||
|
groups: {},
|
||||||
|
scenes: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getLights() {
|
||||||
|
throw "getLights should be implemented by a derived component"
|
||||||
|
},
|
||||||
|
|
||||||
|
async getGroups() {
|
||||||
|
if (!this.hasGroups)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
throw "getGroups should be implemented by a derived component"
|
||||||
|
},
|
||||||
|
|
||||||
|
async getScenes() {
|
||||||
|
if (!this.hasScenes)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
throw "getScenes should be implemented by a derived component"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
[this.lights, this.groups, this.scenes] = await Promise.all([
|
||||||
|
this.getLights(),
|
||||||
|
this.getGroups(),
|
||||||
|
this.getScenes(),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<Light plugin-name="light.hue" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Light from "@/components/panels/Light/Index";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "LightHue",
|
||||||
|
mixins: [Light],
|
||||||
|
components: {Light},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Panel",
|
||||||
|
emits: ['mounted'],
|
||||||
|
props: {
|
||||||
|
// Plugin configuration.
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugin name.
|
||||||
|
pluginName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('mounted', this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -3,8 +3,15 @@ import Dashboard from "@/views/Dashboard.vue";
|
||||||
import NotFound from "@/views/NotFound";
|
import NotFound from "@/views/NotFound";
|
||||||
import Login from "@/views/Login";
|
import Login from "@/views/Login";
|
||||||
import Register from "@/views/Register";
|
import Register from "@/views/Register";
|
||||||
|
import Panel from "@/views/Panel";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Panel",
|
||||||
|
component: Panel,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/dashboard/:name",
|
path: "/dashboard/:name",
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
|
|
|
@ -55,3 +55,15 @@ $active-glow-bg-2: #9cdfb0 !default;
|
||||||
$default-hover-fg: #35b870 !default;
|
$default-hover-fg: #35b870 !default;
|
||||||
$default-hover-fg-2: #38cf80 !default;
|
$default-hover-fg-2: #38cf80 !default;
|
||||||
$hover-bg: #def6ea !default;
|
$hover-bg: #def6ea !default;
|
||||||
|
|
||||||
|
/// Navigator
|
||||||
|
$nav-bg: #002626 !default;
|
||||||
|
$nav-fg: #e8f8e8 !default;
|
||||||
|
$nav-entry-selected-bg: #205046 !default;
|
||||||
|
$nav-entry-hover-bg: #104036 !default;
|
||||||
|
$nav-entry-collapsed-selected-bg: rgba(160, 245, 178, 0.95) !default;
|
||||||
|
$nav-entry-collapsed-hover-bg: rgba(160, 245, 178, 0.60) !default;
|
||||||
|
$nav-box-shadow-main: 1px 0 2px #002626;
|
||||||
|
$nav-box-shadow-entry: 0 0 1px 1px #103824 !default;
|
||||||
|
$nav-box-shadow-collapsed: 1px 0 2px 1px #bbb !default;
|
||||||
|
$nav-collapsed-fg: #5e5e5e;
|
||||||
|
|
101
platypush/backend/http/webapp/src/views/Panel.vue
Normal file
101
platypush/backend/http/webapp/src/views/Panel.vue
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<Nav :panels="components" :selected-panel="selectedPanel" :hostname="hostname" @select="selectedPanel = $event" />
|
||||||
|
|
||||||
|
<div class="panel-container">
|
||||||
|
<div class="panel" v-for="(panel, name) in components" :key="name">
|
||||||
|
<component :is="panel.component" :config="panel.config" :plugin-name="name" v-if="name === selectedPanel" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {defineAsyncComponent} from "vue";
|
||||||
|
import Utils from '@/Utils'
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Nav from "@/components/Nav";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Panel',
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {Nav, Loading},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
plugins: {},
|
||||||
|
backends: {},
|
||||||
|
procedures: {},
|
||||||
|
components: {},
|
||||||
|
hostname: undefined,
|
||||||
|
selectedPanel: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
initPanels() {
|
||||||
|
const self = this
|
||||||
|
this.components = {}
|
||||||
|
|
||||||
|
Object.entries(this.plugins).forEach(async ([name, plugin]) => {
|
||||||
|
const componentName = name.split('.').map((token) => token[0].toUpperCase() + token.slice(1)).join('')
|
||||||
|
let comp = null
|
||||||
|
try {
|
||||||
|
comp = await import(`@/components/panels/${componentName}/Index`)
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = defineAsyncComponent(async () => { return comp })
|
||||||
|
self.$options.components[name] = component
|
||||||
|
self.components[name] = {
|
||||||
|
component: component,
|
||||||
|
pluginName: name,
|
||||||
|
config: plugin,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async parseConfig() {
|
||||||
|
[this.plugins, this.backends, this.procedures, this.hostname] =
|
||||||
|
await Promise.all([
|
||||||
|
this.request('config.get_plugins'),
|
||||||
|
this.request('config.get_backends'),
|
||||||
|
this.request('config.get_procedures'),
|
||||||
|
this.request('config.get_device_id'),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.parseConfig()
|
||||||
|
this.initPanels()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,7 @@
|
||||||
|
import json
|
||||||
|
|
||||||
from platypush import Config
|
from platypush import Config
|
||||||
|
from platypush.message import Message
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +10,18 @@ class ConfigPlugin(Plugin):
|
||||||
def get(self) -> dict:
|
def get(self) -> dict:
|
||||||
return Config.get()
|
return Config.get()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_plugins(self) -> dict:
|
||||||
|
return Config.get_plugins()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_backends(self) -> dict:
|
||||||
|
return Config.get_backends()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_procedures(self) -> dict:
|
||||||
|
return json.loads(json.dumps(Config.get_procedures(), cls=Message.Encoder))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def dashboards(self) -> dict:
|
def dashboards(self) -> dict:
|
||||||
return Config.get_dashboards()
|
return Config.get_dashboards()
|
||||||
|
@ -15,5 +30,9 @@ class ConfigPlugin(Plugin):
|
||||||
def get_dashboard(self, name: str) -> str:
|
def get_dashboard(self, name: str) -> str:
|
||||||
return Config.get_dashboard(name)
|
return Config.get_dashboard(name)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_device_id(self) -> str:
|
||||||
|
return Config.get('device_id')
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
Loading…
Reference in a new issue