New template for index panel

This commit is contained in:
Fabio Manganiello 2020-11-30 20:57:00 +01:00
parent c3f01c198f
commit 0cd120f492
10 changed files with 425 additions and 60 deletions

View file

@ -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:

View file

@ -0,0 +1,7 @@
{
"icons": {
"light.hue": {
"class": "fas fa-lightbulb"
}
}
}

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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",

View file

@ -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;

View 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>

View file

@ -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: