Added entity info modal and (partial) support for renaming entities

This commit is contained in:
Fabio Manganiello 2022-04-23 01:01:14 +02:00
parent 7d4bd20df0
commit ef6b57df31
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
10 changed files with 378 additions and 17 deletions

View file

@ -0,0 +1,33 @@
<template>
<button class="edit-btn"
@click="proxy($event)" @touch="proxy($event)" @input="proxy($event)"
>
<i class="fas fa-pen-to-square" />
</button>
</template>
<script>
export default {
emits: ['input', 'click', 'touch'],
methods: {
proxy(e) {
this.$emit(e.type, e)
},
},
}
</script>
<style lang="scss" scoped>
.edit-btn {
border: 0;
background: none;
padding: 0 0.25em;
margin-left: 0.25em;
border: 1px solid rgba(0, 0, 0, 0);
&:hover {
background: $hover-bg;
border: 1px solid $selected-fg;
}
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<form @submit.prevent="submit" class="name-editor">
<input type="text" v-model="text" :disabled="disabled">
<button type="submit">
<i class="fas fa-circle-check" />
</button>
<button class="cancel" @click="$emit('cancel')" @touch="$emit('cancel')">
<i class="fas fa-ban" />
</button>
</form>
</template>
<script>
export default {
emits: ['input', 'cancel'],
props: {
value: {
type: String,
},
disabled: {
type: Boolean,
deafult: false,
},
},
data() {
return {
text: null,
}
},
methods: {
proxy(e) {
this.$emit(e.type, e)
},
submit() {
this.$emit('input', this.text)
return false
},
},
mounted() {
this.text = this.value
},
}
</script>
<style lang="scss" scoped>
.name-editor {
background: #00000000;
display: inline-flex;
flex-direction: row;
padding: 0;
border: 0;
border-radius: 0;
box-shadow: none;
button {
border: none;
background: none;
padding: 0 0.5em;
&.confirm {
color: $selected-fg;
}
&.cancel {
color: $error-fg;
}
}
}
</style>

View file

@ -46,6 +46,7 @@ export default {
data() { data() {
return { return {
component: null, component: null,
modalVisible: false,
} }
}, },

View file

@ -8,14 +8,20 @@
</div> </div>
<div class="col-1 right"> <div class="col-1 right">
<button title="Refresh" @click="refresh"> <button title="Refresh" @click="refresh(null)">
<i class="fa fa-sync-alt" /> <i class="fa fa-sync-alt" />
</button> </button>
</div> </div>
</header> </header>
<div class="groups-canvas"> <div class="groups-canvas">
<EntityModal :entity="entities[modalEntityId]"
:visible="modalVisible" @close="onEntityModal(null)"
v-if="modalEntityId"
/>
<NoItems v-if="!Object.keys(displayGroups || {})?.length">No entities found</NoItems> <NoItems v-if="!Object.keys(displayGroups || {})?.length">No entities found</NoItems>
<div class="groups-container" v-else> <div class="groups-container" v-else>
<div class="group fade-in" v-for="group in displayGroups" :key="group.name"> <div class="group fade-in" v-for="group in displayGroups" :key="group.name">
<div class="frame"> <div class="frame">
@ -41,7 +47,8 @@
</div> </div>
<div class="body"> <div class="body">
<div class="entity-frame" v-for="entity in group.entities" :key="entity.id"> <div class="entity-frame" @click="onEntityModal(entity.id)"
v-for="entity in group.entities" :key="entity.id">
<Entity <Entity
:value="entity" :value="entity"
@input="onEntityInput" @input="onEntityInput"
@ -65,12 +72,13 @@ import Icon from "@/components/elements/Icon";
import NoItems from "@/components/elements/NoItems"; import NoItems from "@/components/elements/NoItems";
import Entity from "./Entity.vue"; import Entity from "./Entity.vue";
import Selector from "./Selector.vue"; import Selector from "./Selector.vue";
import EntityModal from "./Modal"
import icons from '@/assets/icons.json' import icons from '@/assets/icons.json'
import meta from './meta.json' import meta from './meta.json'
export default { export default {
name: "Entities", name: "Entities",
components: {Loading, Icon, Entity, Selector, NoItems}, components: {Loading, Icon, Entity, Selector, NoItems, EntityModal},
mixins: [Utils], mixins: [Utils],
props: { props: {
@ -88,6 +96,8 @@ export default {
errorEntities: {}, errorEntities: {},
entityTimeouts: {}, entityTimeouts: {},
entities: {}, entities: {},
modalEntityId: null,
modalVisible: false,
selector: { selector: {
grouping: 'type', grouping: 'type',
selectedEntities: {}, selectedEntities: {},
@ -147,7 +157,7 @@ export default {
}, },
async refresh(group) { async refresh(group) {
const entities = group ? group.entities : this.entities const entities = (group ? group.entities : this.entities) || {}
const args = {} const args = {}
if (group) if (group)
args.plugins = Object.keys(entities.reduce((obj, entity) => { args.plugins = Object.keys(entities.reduce((obj, entity) => {
@ -155,8 +165,9 @@ export default {
return obj return obj
}, {})) }, {}))
this.loadingEntities = Object.entries(entities).reduce((obj, [id, entity]) => { this.loadingEntities = Object.values(entities).reduce((obj, entity) => {
const self = this const self = this
const id = entity.id
if (this.entityTimeouts[id]) if (this.entityTimeouts[id])
clearTimeout(this.entityTimeouts[id]) clearTimeout(this.entityTimeouts[id])
@ -186,6 +197,7 @@ export default {
try { try {
this.entities = (await this.request('entities.get')).reduce((obj, entity) => { this.entities = (await this.request('entities.get')).reduce((obj, entity) => {
entity.name = entity?.meta?.name_override || entity.name
entity.meta = { entity.meta = {
...(meta[entity.type] || {}), ...(meta[entity.type] || {}),
...(entity.meta || {}), ...(entity.meta || {}),
@ -225,13 +237,30 @@ export default {
return return
this.clearEntityTimeouts(entityId) this.clearEntityTimeouts(entityId)
this.entities[entityId] = { const entity = {...event.entity}
...event.entity, if (entity.meta?.name_override?.length)
meta: { entity.name = entity.meta.name_override
...(this.entities[entityId]?.meta || {}), else if (this.entities[entityId]?.meta?.name_override?.length)
...(meta[event.entity.type] || {}), entity.name = this.entities[entityId].meta.name_override
...(event.entity?.meta || {}), else
}, entity.name = event.entity?.name || this.entities[entityId]?.name
entity.meta = {
...(this.entities[entityId]?.meta || {}),
...(meta[event.entity.type] || {}),
...(event.entity?.meta || {}),
}
this.entities[entityId] = entity
},
onEntityModal(entityId) {
if (entityId) {
this.modalEntityId = entityId
this.modalVisible = true
} else {
this.modalEntityId = null
this.modalVisible = false
} }
}, },
}, },
@ -371,5 +400,29 @@ export default {
} }
} }
} }
:deep(.modal) {
@include until($tablet) {
width: 95%;
}
.content {
@include until($tablet) {
width: 100%;
}
@include from($tablet) {
min-width: 30em;
}
.body {
padding: 0;
.table-row {
padding: 0.5em;
}
}
}
}
} }
</style> </style>

View file

@ -0,0 +1,109 @@
<template>
<Modal :visible="visible" :title="entity.name || entity.external_id">
<div class="table-row">
<div class="title">
Name
<EditButton @click="editName = true" v-if="!editName" />
</div>
<div class="value">
<NameEditor :value="entity.name" @input="onRename"
@cancel="editName = false" :disabled="loading" v-if="editName" />
<span v-text="entity.name" v-else />
</div>
</div>
<div class="table-row">
<div class="title">Icon</div>
<div class="value icon-container">
<i class="icon" :class="entity.meta.icon.class" v-if="entity?.meta?.icon?.class" />
</div>
</div>
<div class="table-row">
<div class="title">Plugin</div>
<div class="value" v-text="entity.plugin" />
</div>
<div class="table-row">
<div class="title">Internal ID</div>
<div class="value" v-text="entity.id" />
</div>
<div class="table-row" v-if="entity.external_id">
<div class="title">External ID</div>
<div class="value" v-text="entity.external_id" />
</div>
<div class="table-row" v-if="entity.description">
<div class="title">Description</div>
<div class="value" v-text="entity.description" />
</div>
<div class="table-row" v-if="entity.created_at">
<div class="title">Created at</div>
<div class="value" v-text="formatDateTime(entity.created_at)" />
</div>
<div class="table-row" v-if="entity.updated_at">
<div class="title">Updated at</div>
<div class="value" v-text="formatDateTime(entity.updated_at)" />
</div>
</Modal>
</template>
<script>
import Modal from "@/components/Modal";
import EditButton from "@/components/elements/EditButton";
import NameEditor from "@/components/elements/NameEditor";
import Utils from "@/Utils";
export default {
name: "Entity",
components: {Modal, EditButton, NameEditor},
mixins: [Utils],
emits: ['input', 'loading'],
props: {
entity: {
type: Object,
required: true,
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
loading: false,
editName: false,
editIcon: false,
}
},
methods: {
async onRename(newName) {
this.loading = true
try {
const req = {}
req[this.entity.id] = newName
await this.request('entities.rename', req)
} finally {
this.loading = false
this.editName = false
}
}
},
}
</script>
<style lang="scss" scoped>
:deep(.modal) {
.icon-container {
display: inline-flex;
align-items: center;
}
}
</style>

View file

@ -6,7 +6,7 @@
<div class="col-2 switch pull-right"> <div class="col-2 switch pull-right">
<ToggleSwitch :value="value.state" @input="toggle" <ToggleSwitch :value="value.state" @input="toggle"
:disabled="loading" /> @click.stop :disabled="loading" />
</div> </div>
</div> </div>
</template> </template>

View file

@ -80,3 +80,43 @@ $header-height: 3.5em;
} }
} }
} }
:deep(.table-row) {
width: 100%;
display: flex;
flex-direction: column;
box-shadow: $row-shadow;
&:hover {
background: $hover-bg;
}
@include from($tablet) {
flex-direction: row;
align-items: center;
}
.title,
.value {
width: 100%;
display: flex;
@include from($tablet) {
display: inline-flex;
}
}
.title {
font-weight: bold;
@include from($tablet) {
width: 30%;
}
}
.value {
@include from($tablet) {
justify-content: right;
}
}
}

View file

@ -148,3 +148,5 @@ $scrollbar-track-bg: $slider-bg !default;
$scrollbar-track-shadow: inset 1px 0px 3px 0 $slider-track-shadow !default; $scrollbar-track-shadow: inset 1px 0px 3px 0 $slider-track-shadow !default;
$scrollbar-thumb-bg: #a5a2a2 !default; $scrollbar-thumb-bg: #a5a2a2 !default;
//// Rows
$row-shadow: 0 0 1px 0.5px #cfcfcf !default;

View file

@ -16,7 +16,7 @@ from ._base import Entity
class EntitiesEngine(Thread): class EntitiesEngine(Thread):
# Processing queue timeout in seconds # Processing queue timeout in seconds
_queue_timeout = 5.0 _queue_timeout = 2.0
def __init__(self): def __init__(self):
obj_name = self.__class__.__name__ obj_name = self.__class__.__name__
@ -205,7 +205,12 @@ class EntitiesEngine(Thread):
def merge(entity: Entity, existing_entity: Entity) -> Entity: def merge(entity: Entity, existing_entity: Entity) -> Entity:
columns = [col.key for col in entity.columns] columns = [col.key for col in entity.columns]
for col in columns: for col in columns:
if col not in ('id', 'created_at'): if col == 'meta':
existing_entity.meta = { # type: ignore
**(existing_entity.meta or {}),
**(entity.meta or {}),
}
elif col not in ('id', 'created_at'):
setattr(existing_entity, col, getattr(entity, col)) setattr(existing_entity, col, getattr(entity, col))
return existing_entity return existing_entity

View file

@ -1,10 +1,13 @@
from queue import Queue, Empty from queue import Queue, Empty
from threading import Thread from threading import Thread
from time import time from time import time
from typing import Optional, Any, Collection from typing import Optional, Any, Collection, Mapping
from platypush.context import get_plugin from sqlalchemy.orm import make_transient
from platypush.context import get_plugin, get_bus
from platypush.entities import Entity, get_plugin_entity_registry, get_entities_registry from platypush.entities import Entity, get_plugin_entity_registry, get_entities_registry
from platypush.message.event.entities import EntityUpdateEvent
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -165,5 +168,46 @@ class EntitiesPlugin(Plugin):
assert entity, f'No such entity ID: {id}' assert entity, f'No such entity ID: {id}'
return entity.run(action, *args, **kwargs) return entity.run(action, *args, **kwargs)
@action
def rename(self, **entities: Mapping[str, str]):
"""
Rename a sequence of entities.
Renaming, as of now, is actually done by setting the ``.meta.name_override``
property of an entity rather than fully renaming the entity (which may be owned
by a plugin that doesn't support renaming, therefore the next entity update may
overwrite the name).
:param entities: Entity `id` -> `new_name` mapping.
"""
return self.set_meta(
**{
entity_id: {'name_override': name}
for entity_id, name in entities.items()
}
)
@action
def set_meta(self, **entities):
"""
Update the metadata of a set of entities.
:param entities: Entity `id` -> `new_metadata_fields` mapping.
:return: The updated entities.
"""
entities = {str(k): v for k, v in entities.items()}
with self._get_db().get_session() as session:
objs = session.query(Entity).filter(Entity.id.in_(entities.keys())).all()
for obj in objs:
obj.meta = {**(obj.meta or {}), **(entities.get(str(obj.id), {}))}
session.add(obj)
session.commit()
for obj in objs:
make_transient(obj)
get_bus().post(EntityUpdateEvent(obj))
return [obj.to_json() for obj in objs]
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: