forked from platypush/platypush
Added entity info modal and (partial) support for renaming entities
This commit is contained in:
parent
7d4bd20df0
commit
ef6b57df31
10 changed files with 378 additions and 17 deletions
|
@ -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>
|
|
@ -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>
|
|
@ -46,6 +46,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
component: null,
|
component: null,
|
||||||
|
modalVisible: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
else if (this.entities[entityId]?.meta?.name_override?.length)
|
||||||
|
entity.name = this.entities[entityId].meta.name_override
|
||||||
|
else
|
||||||
|
entity.name = event.entity?.name || this.entities[entityId]?.name
|
||||||
|
|
||||||
|
entity.meta = {
|
||||||
...(this.entities[entityId]?.meta || {}),
|
...(this.entities[entityId]?.meta || {}),
|
||||||
...(meta[event.entity.type] || {}),
|
...(meta[event.entity.type] || {}),
|
||||||
...(event.entity?.meta || {}),
|
...(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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue