diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index e97dc07038..5c9a41b702 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -24,10 +24,17 @@ import { broadcastMessage } from "../common/ipc"; import type { CatalogEntityRegistry } from "./catalog"; import "../common/catalog-entities/kubernetes-cluster"; import { toJS } from "../common/utils"; +import { debounce } from "lodash"; +import type { CatalogEntity } from "../common/catalog"; + + +const broadcaster = debounce((items: CatalogEntity[]) => { + broadcastMessage("catalog:items", items); +}, 1_000, { trailing: true }); export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { return reaction(() => toJS(catalog.items), (items) => { - broadcastMessage("catalog:items", items); + broadcaster(items); }, { fireImmediately: true, }); diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts index 8a26aabc00..4c3bcb48c3 100644 --- a/src/renderer/api/__tests__/catalog-entity-registry.test.ts +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -22,14 +22,36 @@ import { CatalogEntityRegistry } from "../catalog-entity-registry"; import "../../../common/catalog-entities"; import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry"; -import type { CatalogEntityData, CatalogEntityKindData } from "../catalog-entity"; +import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity"; +import { WebLink } from "../../../common/catalog-entities"; class TestCatalogEntityRegistry extends CatalogEntityRegistry { replaceItems(items: Array) { - this.rawItems.replace(items); + this.updateItems(items); } } +class FooBarCategory extends CatalogCategory { + public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; + public readonly kind = "CatalogCategory"; + public metadata = { + name: "FooBars", + icon: "broken" + }; + public spec = { + group: "entity.k8slens.dev", + versions: [ + { + name: "v1alpha1", + entityClass: WebLink + } + ], + names: { + kind: "FooBar" + } + }; +} + describe("CatalogEntityRegistry", () => { describe("updateItems", () => { it("adds new catalog item", () => { @@ -99,6 +121,32 @@ describe("CatalogEntityRegistry", () => { expect(catalog.items[0].status.phase).toEqual("connected"); }); + it("updates activeEntity", () => { + const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.replaceItems(items); + catalog.activeEntity = catalog.items[0]; + expect(catalog.activeEntity.status.phase).toEqual("disconnected"); + + items[0].status.phase = "connected"; + catalog.replaceItems(items); + expect(catalog.activeEntity.status.phase).toEqual("connected"); + }); + it("removes deleted items", () => { const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); const items = [ @@ -175,8 +223,31 @@ describe("CatalogEntityRegistry", () => { ]; catalog.replaceItems(items); - expect(catalog.items.length).toBe(1); }); }); + + it("does return items after matching category is added", () => { + const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); + const items = [ + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "FooBar", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + } + ]; + + catalog.replaceItems(items); + catalogCategoryRegistry.add(new FooBarCategory()); + expect(catalog.items.length).toBe(1); + }); }); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index e2f83058b8..0a30db31a2 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,17 +19,21 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, makeObservable, observable } from "mobx"; +import { computed, observable, makeObservable, action } from "mobx"; import { subscribeToBroadcast } from "../../common/ipc"; import { CatalogCategory, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; -import { iter } from "../utils"; import type { Cluster } from "../../main/cluster"; import { ClusterStore } from "../../common/cluster-store"; export class CatalogEntityRegistry { - protected rawItems = observable.array(); - @observable.ref activeEntity?: CatalogEntity; + @observable.ref activeEntity: CatalogEntity; + protected _entities = observable.map([], { deep: true }); + + /** + * Buffer for keeping entities that don't yet have CatalogCategory synced + */ + protected rawEntities: (CatalogEntityData & CatalogEntityKindData)[] = []; constructor(private categoryRegistry: CatalogCategoryRegistry) { makeObservable(this); @@ -37,20 +41,68 @@ export class CatalogEntityRegistry { init() { subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { - this.rawItems.replace(items); + this.updateItems(items); }); } + @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { + this.rawEntities.length = 0; + + const newIds = new Set(items.map((item) => item.metadata.uid)); + + for (const uid of this._entities.keys()) { + if (!newIds.has(uid)) { + this._entities.delete(uid); + } + } + + for (const item of items) { + this.updateItem(item); + } + } + + @action protected updateItem(item: (CatalogEntityData & CatalogEntityKindData)) { + const existing = this._entities.get(item.metadata.uid); + + if (!existing) { + const entity = this.categoryRegistry.getEntityForData(item); + + if (entity) { + this._entities.set(entity.metadata.uid, entity); + } else { + this.rawEntities.push(item); + } + } else { + existing.metadata = item.metadata; + existing.spec = item.spec; + existing.status = item.status; + } + } + + protected processRawEntities() { + const items = [...this.rawEntities]; + + this.rawEntities.length = 0; + + for (const item of items) { + this.updateItem(item); + } + } + @computed get items() { - return Array.from(iter.filterMap(this.rawItems, rawItem => this.categoryRegistry.getEntityForData(rawItem))); + this.processRawEntities(); + + return Array.from(this._entities.values()); } @computed get entities(): Map { - return new Map(this.items.map(item => [item.metadata.uid, item])); + this.processRawEntities(); + + return this._entities; } - getById(id: string) { - return this.entities.get(id); + getById(id: string) { + return this.entities.get(id) as T; } getItemsForApiKind(apiVersion: string, kind: string): T[] {