platypush/platypush/backend/http/webapp/src/components/panels/Entities/Index.vue

362 lines
8.8 KiB
Vue

<template>
<div class="row plugin entities-container">
<Loading v-if="loading" />
<header>
<div class="col-11">
<Selector :entity-groups="entityGroups" :value="selector" @input="selector = $event" />
</div>
<div class="col-1 pull-right">
<button title="Refresh" @click="refresh">
<i class="fa fa-sync-alt" />
</button>
</div>
</header>
<div class="groups-canvas">
<NoItems v-if="!Object.keys(displayGroups || {})?.length">No entities found</NoItems>
<div class="groups-container" v-else>
<div class="group fade-in" v-for="group in displayGroups" :key="group.name">
<div class="frame">
<div class="header">
<span class="section left">
<Icon v-bind="entitiesMeta[group.name].icon || {}"
v-if="selector.grouping === 'type' && entitiesMeta[group.name]" />
<Icon :class="pluginIcons[group.name]?.class" :url="pluginIcons[group.name]?.imgUrl"
v-else-if="selector.grouping === 'plugin' && pluginIcons[group.name]" />
</span>
<span class="section center">
<div class="title" v-text="entitiesMeta[group.name].name_plural"
v-if="selector.grouping === 'type' && entitiesMeta[group.name]"/>
<div class="title" v-text="group.name" v-else-if="selector.grouping === 'plugin'"/>
</span>
<span class="section right" />
</div>
<div class="body">
<div class="entity-frame" v-for="entity in group.entities" :key="entity.id">
<Entity
:value="entity"
@input="onEntityInput"
:error="!!errorEntities[entity.id]"
:loading="!!loadingEntities[entity.id]"
@loading="loadingEntities[entity.id] = $event"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Utils from "@/Utils"
import Loading from "@/components/Loading";
import Icon from "@/components/elements/Icon";
import NoItems from "@/components/elements/NoItems";
import Entity from "./Entity.vue";
import Selector from "./Selector.vue";
import icons from '@/assets/icons.json'
import meta from './meta.json'
export default {
name: "Entities",
components: {Loading, Icon, Entity, Selector, NoItems},
mixins: [Utils],
props: {
// Entity scan timeout in seconds
entityScanTimeout: {
type: Number,
default: 30,
},
},
data() {
return {
loading: false,
loadingEntities: {},
errorEntities: {},
entityTimeouts: {},
entities: {},
selector: {
grouping: 'type',
selectedEntities: {},
},
}
},
computed: {
entitiesMeta() {
return meta
},
pluginIcons() {
return icons
},
entityGroups() {
return {
'id': Object.entries(this.groupEntities('id')).reduce((obj, [id, entities]) => {
obj[id] = entities[0]
return obj
}, {}),
'type': this.groupEntities('type'),
'plugin': this.groupEntities('plugin'),
}
},
displayGroups() {
return Object.entries(this.entityGroups[this.selector.grouping]).filter(
(entry) => entry[1].filter(
(e) => !!this.selector.selectedEntities[e.id]
).length > 0
).sort((a, b) => a[0].localeCompare(b[0])).map(
([grouping, entities]) => {
return {
name: grouping,
entities: entities.filter(
(e) => e.id in this.selector.selectedEntities
),
}
}
)
},
},
methods: {
groupEntities(attr) {
return Object.values(this.entities).reduce((obj, entity) => {
const entities = obj[entity[attr]] || {}
entities[entity.id] = entity
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return obj
}, {})
},
async refresh() {
const actions = Object.keys(
Object.values(this.selector.selectedEntities).reduce((obj, entity) => {
if (entity.plugin)
obj[entity.plugin] = true
return obj
}, {})
).map((plugin) => `${plugin}.status`)
this.loadingEntities = {
...this.loadingEntities,
...Object.keys(this.selector.selectedEntities).reduce((obj, id) => {
const self = this
const entity = this.entities[id]
if (this.entityTimeouts[id])
clearTimeout(this.entityTimeouts[id])
this.entityTimeouts[id] = setTimeout(() => {
if (self.loadingEntities[id])
delete self.loadingEntities[id]
if (self.entityTimeouts[id])
delete self.entityTimeouts[id]
self.errorEntities[id] = entity
self.notify({
error: true,
title: entity.plugin,
text: `Scan timeout for ${entity.name}`,
})
}, this.entityScanTimeout * 1000)
obj[id] = true
return obj
}, {}),
}
// Force refresh by calling `.status` on all the selected plugins
await Promise.all(actions.map((act) => this.request(act)))
},
async sync() {
this.loading = true
try {
this.entities = (await this.request('entities.get')).reduce((obj, entity) => {
entity.meta = {
...(meta[entity.type] || {}),
...(entity.meta || {}),
}
obj[entity.id] = entity
return obj
}, {})
this.selector.selectedEntities = this.entityGroups.id
} finally {
this.loading = false
}
},
clearEntityTimeouts(entityId) {
if (this.errorEntities[entityId])
delete this.errorEntities[entityId]
if (this.loadingEntities[entityId])
delete this.loadingEntities[entityId]
if (this.entityTimeouts[entityId]) {
clearTimeout(this.entityTimeouts[entityId])
delete this.entityTimeouts[entityId]
}
},
onEntityInput(entity) {
this.entities[entity.id] = entity
this.clearEntityTimeouts(entity.id)
if (this.loadingEntities[entity.id])
delete this.loadingEntities[entity.id]
},
onEntityUpdate(event) {
const entityId = event.entity.id
if (entityId == null)
return
this.clearEntityTimeouts(entityId)
this.entities[entityId] = {
...event.entity,
meta: {
...(event.entity?.meta || {}),
...(this.entities[entityId]?.meta || {}),
},
}
},
},
mounted() {
this.subscribe(
this.onEntityUpdate,
'on-entity-update',
'platypush.message.event.entities.EntityUpdateEvent'
)
this.sync()
},
}
</script>
<style lang="scss">
@import "vars";
@import "~@/style/items";
.entities-container {
--groups-per-row: 1;
@include from($desktop) {
--groups-per-row: 2;
}
@include from($fullhd) {
--groups-per-row: 3;
}
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
color: $default-fg-2;
font-weight: 400;
button {
background: #ffffff00;
border: 0;
&:hover {
color: $default-hover-fg;
}
}
header {
width: 100%;
height: $selector-height;
display: flex;
background: $default-bg-2;
box-shadow: $border-shadow-bottom;
}
.groups-canvas {
width: 100%;
height: calc(100% - #{$selector-height});
display: flex;
flex-direction: column;
}
.groups-container {
overflow: auto;
@include from($desktop) {
column-count: var(--groups-per-row);
}
}
.group {
width: 100%;
max-height: 100%;
position: relative;
padding: $main-margin 0;
display: flex;
break-inside: avoid;
padding: $main-margin;
.frame {
max-height: calc(100% - #{2 * $main-margin});
display: flex;
flex-direction: column;
flex-grow: 1;
position: relative;
box-shadow: $group-shadow;
border-radius: 1em;
}
.header {
width: 100%;
height: $header-height;
display: table;
background: $header-bg;
box-shadow: $header-shadow;
border-radius: 1em 1em 0 0;
.section {
height: 100%;
display: table-cell;
vertical-align: middle;
&.left, &.right {
width: 10%;
}
&.center {
width: 80%;
text-align: center;
}
}
}
.body {
background: $default-bg-2;
max-height: calc(100% - #{$header-height});
overflow: auto;
flex-grow: 1;
.entity-frame:last-child {
border-radius: 0 0 1em 1em;
}
}
}
}
</style>