From 68359b88a945650b52f50b0fca236a3ce1a68056 Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <fabio@manganiello.tech>
Date: Tue, 2 May 2023 10:14:03 +0200
Subject: [PATCH] More performance improvements for the entities page.

- Don't recalculate entity groups every time. Instead, keep them in sync
  every time an entity is added or removed.

- Removed `computedChildren` from the entity component - no null nodes
  are guaranteed to be passed now, so there's no need for another
  iteration on the list of children.

- `childrenByParentId` now only looks in the scope of the entity's
  children instead of searching all the entities.
---
 .../src/components/panels/Entities/Entity.vue |  39 +++----
 .../src/components/panels/Entities/Index.vue  | 109 +++++++++---------
 2 files changed, 72 insertions(+), 76 deletions(-)

diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Entity.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Entity.vue
index b0a8323d8..b0d1220aa 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/Entity.vue
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/Entity.vue
@@ -8,7 +8,7 @@
           :is="component"
           :value="value"
           :parent="parent"
-          :children="computedChildren"
+          :children="children"
           :loading="loading"
           ref="instance"
           :error="error || value?.reachable == false"
@@ -25,7 +25,7 @@
     </div>
 
     <div class="children fade-in" v-if="hasChildren && !isCollapsed">
-      <div class="child" v-for="entity in computedChildren" :key="entity.id">
+      <div class="child" v-for="entity in children" :key="entity.id">
         <Entity
          :value="entity"
          :parent="value"
@@ -57,19 +57,12 @@ export default {
   },
 
   computed: {
-    computedChildren() {
-      return Object.values(this.children || {}).filter((child) => child)
-    },
-
     hasChildren() {
-      return !!this.computedChildren.length
+      return !!Object.keys(this.children).length
     },
 
     isCollapsed() {
-      if (!this.hasChildren)
-        return true
-
-      return this.collapsed
+      return !this.hasChildren ? true : this.collapsed
     },
 
     instance() {
@@ -90,16 +83,16 @@ export default {
     },
 
     childrenByParentId(parentId) {
-      return Object.values(this.allEntities || {}).
-        filter(
-          (entity) => entity
-            && entity.parent_id === parentId
-            && !entity.is_configuration
-        ).
-        reduce((obj, entity) => {
+      const parentEntity = this.allEntities?.[parentId]
+      if (!parentEntity)
+        return {}
+
+      return (parentEntity.children_ids || []).reduce((obj, entityId) => {
+        const entity = this.allEntities[entityId]
+        if (entity && !entity.is_configuration)
           obj[entity.id] = entity
-          return obj
-        }, {})
+        return obj
+      }, {})
     },
 
     onClick(event) {
@@ -131,7 +124,7 @@ export default {
       if (!isChildUpdate)
         return
 
-      this.setJustUpdated()
+      this.notifyUpdate()
     },
 
     toggleCollapsed() {
@@ -141,7 +134,7 @@ export default {
         this.instance.collapsed = !this.instance.collapsed
     },
 
-    setJustUpdated() {
+    notifyUpdate() {
       this.justUpdated = true
       const self = this;
       setTimeout(() => self.justUpdated = false, 1000)
@@ -160,7 +153,7 @@ export default {
               if (this.valuesEqual(oldValue, newValue))
                 return false
 
-              this.setJustUpdated()
+              this.notifyUpdate()
               this.$emit('update', {value: newValue})
           }
       )
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Index.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Index.vue
index a8025b6d1..d5303c337 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/Index.vue
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/Index.vue
@@ -55,8 +55,8 @@
 
             <div class="body">
               <div class="entity-frame"
-                  v-for="entity in group.entities"
-                  :key="entity.id">
+                 v-for="entity in Object.values(group.entities).sort((a, b) => a.name.localeCompare(b.name))"
+                 :key="entity.id">
                 <Entity
                   :value="entity"
                   :children="childrenByParentId(entity.id)"
@@ -64,6 +64,7 @@
                   @show-modal="onEntityModal($event)"
                   @input="onEntityInput(entity)"
                   :error="!!errorEntities[entity.id]"
+                  :key="entity.id"
                   :loading="!!loadingEntities[entity.id]"
                   @loading="loadingEntities[entity.id] = $event"
                   v-if="!entity.parent_id"
@@ -122,6 +123,12 @@ export default {
       errorEntities: {},
       entityTimeouts: {},
       entities: {},
+      entityGroups: {
+        id: {},
+        category: {},
+        plugin: {},
+        type: {},
+      },
       modalEntityId: null,
       modalVisible: false,
       variableModalVisible: false,
@@ -141,10 +148,6 @@ export default {
       return icons
     },
 
-    entityTypes() {
-      return this.groupEntities('type')
-    },
-
     typesByCategory() {
       return Object.entries(meta).reduce((obj, [type, meta]) => {
           obj[meta.name_plural] = type
@@ -152,21 +155,10 @@ export default {
       }, {})
     },
 
-    entityGroups() {
-      return {
-        'id': Object.entries(this.groupEntities('id')).reduce((obj, [id, entities]) => {
-          obj[id] = entities[0]
-          return obj
-        }, {}),
-        'category': this.groupEntities('category'),
-        'plugin': this.groupEntities('plugin'),
-      }
-    },
-
     displayGroups() {
       return Object.entries(this.entityGroups[this.selector.grouping]).
         filter(
-          (entry) => entry[1].filter(
+          (entry) => Object.values(entry[1]).filter(
             (e) =>
               !!this.selector.selectedEntities[e.id] && e.parent_id == null
           ).length > 0
@@ -175,7 +167,7 @@ export default {
           ([grouping, entities]) => {
             return {
               name: grouping,
-              entities: entities.filter(
+              entities: Object.values(entities).filter(
                 (e) => e.id in this.selector.selectedEntities
               ),
             }
@@ -186,19 +178,32 @@ export default {
   },
 
   methods: {
-    groupEntities(attr) {
-      return Object.values(this.entities).
-        filter((entity) => entity.parent_id == null).
-        reduce((obj, entity) => {
-          const entities = obj[entity[attr]] || {}
-          entities[entity.id] = entity
+    addEntity(entity) {
+      if (entity.parent_id != null)
+        return  // Only group entities that have no parent
 
-          obj[entity[attr]] = Object.values(entities).sort((a, b) => {
-              return a.name.localeCompare(b.name)
-            })
+      this.entities[entity.id] = entity;
+      ['id', 'type', 'category', 'plugin'].forEach((attr) => {
+        if (entity[attr] == null)
+          return
 
-          return obj
-        }, {})
+        if (!this.entityGroups[attr][entity[attr]])
+          this.entityGroups[attr][entity[attr]] = {}
+        this.entityGroups[attr][entity[attr]][entity.id] = entity
+      })
+    },
+
+    removeEntity(entity) {
+      if (entity.parent_id != null)
+        return  // Only group entities that have no parent
+
+      ['id', 'type', 'category', 'plugin'].forEach((attr) => {
+        if (this.entityGroups[attr][entity[attr]][entity.id])
+          delete this.entityGroups[attr][entity[attr]][entity.id]
+      })
+
+      if (this.entities[entity.id])
+        delete this.entities[entity.id]
     },
 
     _shouldSkipLoading(entity) {
@@ -236,6 +241,7 @@ export default {
           if (this.entityTimeouts[id])
             clearTimeout(this.entityTimeouts[id])
 
+          this.addEntity(entity)
           this.entityTimeouts[id] = setTimeout(() => {
               if (self.loadingEntities[id])
                 delete self.loadingEntities[id]
@@ -266,6 +272,7 @@ export default {
           }
 
           obj[entity.id] = entity
+          this.addEntity(entity)
           return obj
         }, {})
 
@@ -275,30 +282,26 @@ export default {
       }
     },
 
-    childrenByParentId(parentId) {
-      return Object.values(this.entities).
-        filter(
-          (entity) => entity
-            && entity.parent_id === parentId
-            && !entity.is_configuration
-        ).
-        reduce((obj, entity) => {
-          obj[entity.id] = entity
-          return obj
-        }, {})
+    childrenByParentId(parentId, selectConfig) {
+      const entity = this.entities?.[parentId]
+      if (!entity?.children_ids?.length)
+        return {}
+
+      return entity.children_ids.reduce((obj, id) => {
+        const child = this.entities[id]
+        if (
+          child && (
+            (!selectConfig && !child.is_configuration) ||
+            (selectConfig && child.is_configuration)
+          )
+        )
+          obj[id] = this.entities[id]
+        return obj
+      }, {})
     },
 
     configValuesByParentId(parentId) {
-      return Object.values(this.entities).
-        filter(
-            (entity) => entity
-              && entity.parent_id === parentId
-              && entity.is_configuration
-        ).
-        reduce((obj, entity) => {
-          obj[entity.id] = entity
-          return obj
-        }, {})
+      return this.childrenByParentId(parentId, true)
     },
 
     clearEntityTimeouts(entityId) {
@@ -343,7 +346,7 @@ export default {
         ...(event.entity?.meta || {}),
       }
 
-      this.entities[entityId] = entity
+      this.addEntity(entity)
       bus.publishEntity(entity)
     },
 
@@ -354,7 +357,7 @@ export default {
       if (entityId === this.modalEntityId)
         this.modalEntityId = null
       if (this.entities[entityId])
-        delete this.entities[entityId]
+        this.removeEntity(this.entities[entityId])
     },
 
     onEntityModal(entityId) {