forked from platypush/platypush
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
|
||||
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 flask import Blueprint, render_template
|
||||
|
||||
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.config import Config
|
||||
from platypush.message import Message
|
||||
|
||||
index = Blueprint('index', __name__, template_folder=template_folder)
|
||||
|
||||
|
@ -22,57 +16,7 @@ __routes__ = [
|
|||
@authenticate()
|
||||
def index():
|
||||
""" Route to the main web panel """
|
||||
configured_plugins = Config.get_plugins()
|
||||
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)
|
||||
return render_template('index.html', utils=HttpUtils)
|
||||
|
||||
|
||||
# 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 Login from "@/views/Login";
|
||||
import Register from "@/views/Register";
|
||||
import Panel from "@/views/Panel";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Panel",
|
||||
component: Panel,
|
||||
},
|
||||
|
||||
{
|
||||
path: "/dashboard/:name",
|
||||
name: "Dashboard",
|
||||
|
|
|
@ -55,3 +55,15 @@ $active-glow-bg-2: #9cdfb0 !default;
|
|||
$default-hover-fg: #35b870 !default;
|
||||
$default-hover-fg-2: #38cf80 !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.message import Message
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
|
@ -7,6 +10,18 @@ class ConfigPlugin(Plugin):
|
|||
def get(self) -> dict:
|
||||
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
|
||||
def dashboards(self) -> dict:
|
||||
return Config.get_dashboards()
|
||||
|
@ -15,5 +30,9 @@ class ConfigPlugin(Plugin):
|
|||
def get_dashboard(self, name: str) -> str:
|
||||
return Config.get_dashboard(name)
|
||||
|
||||
@action
|
||||
def get_device_id(self) -> str:
|
||||
return Config.get('device_id')
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
Loading…
Reference in a new issue