From b61ba7ef719f89fbbf3070dc0b70cdd37af09978 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 20 May 2021 01:50:51 -0400 Subject: [PATCH 1/4] Filter out unknown catalog entities (#2816) * Filter out unknown catelog entities - Keep raw data - But filter unknown types until a category is registered Signed-off-by: Sebastian Malton * fix unit tests Signed-off-by: Sebastian Malton * simplify tests Signed-off-by: Sebastian Malton * Remove getOrDefault, consolodate ExtendedMap Signed-off-by: Sebastian Malton --- .../catalog/catalog-category-registry.ts | 40 +++++++++-------- src/common/utils/extended-map.ts | 43 +++++++++++++------ .../__tests__/catalog-entity-registry.test.ts | 12 +++--- src/renderer/api/catalog-entity-registry.ts | 25 +++++------ 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts index 886db3e90a..a0319d773b 100644 --- a/src/common/catalog/catalog-category-registry.ts +++ b/src/common/catalog/catalog-category-registry.ts @@ -19,26 +19,38 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, computed, observable, toJS } from "mobx"; +import { action, computed, observable } from "mobx"; +import { Disposer, ExtendedMap } from "../utils"; import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; export class CatalogCategoryRegistry { - @observable protected categories: CatalogCategory[] = []; + protected categories = observable.set(); - @action add(category: CatalogCategory) { - this.categories.push(category); + @action add(category: CatalogCategory): Disposer { + this.categories.add(category); + + return () => this.categories.delete(category); } - @action remove(category: CatalogCategory) { - this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind); + @computed private get groupKindLookup(): Map> { + // ExtendedMap has the convenience methods `getOrInsert` and `strictSet` + const res = new ExtendedMap>(); + + for (const category of this.categories) { + res + .getOrInsert(category.spec.group, ExtendedMap.new) + .strictSet(category.spec.names.kind, category); + } + + return res; } @computed get items() { - return toJS(this.categories); + return Array.from(this.categories); } - getForGroupKind(group: string, kind: string) { - return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T; + getForGroupKind(group: string, kind: string): T | undefined { + return this.groupKindLookup.get(group)?.get(kind) as T; } getEntityForData(data: CatalogEntityData & CatalogEntityKindData) { @@ -60,17 +72,11 @@ export class CatalogCategoryRegistry { return new specVersion.entityClass(data); } - getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData) { + getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData): T | undefined { const splitApiVersion = data.apiVersion.split("/"); const group = splitApiVersion[0]; - const category = this.categories.find((category) => { - return category.spec.group === group && category.spec.names.kind === data.kind; - }); - - if (!category) return null; - - return category as T; + return this.getForGroupKind(group, data.kind); } } diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index 9d3a0dc7b3..c8b7ff4c65 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -22,19 +22,17 @@ import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx"; export class ExtendedMap extends Map { - constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) { - super(entries); + static new(entries?: readonly (readonly [K, V])[] | null): ExtendedMap { + return new ExtendedMap(entries); } - getOrInsert(key: K, val: V): V { - if (this.has(key)) { - return this.get(key); - } - - return this.set(key, val).get(key); - } - - getOrInsertWith(key: K, getVal: () => V): V { + /** + * Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal` + * @param key The key to insert into the map with + * @param getVal A function that returns a new instance of `V`. + * @returns The value in the map + */ + getOrInsert(key: K, getVal: () => V): V { if (this.has(key)) { return this.get(key); } @@ -42,12 +40,29 @@ export class ExtendedMap extends Map { return this.set(key, getVal()).get(key); } - getOrDefault(key: K): V { + /** + * Set the value associated with `key` iff there was not a previous value + * @throws if `key` already in map + * @returns `this` so that `strictSet` can be chained + */ + strictSet(key: K, val: V): this { if (this.has(key)) { - return this.get(key); + throw new TypeError("Duplicate key in map"); } - return this.set(key, this.getDefault()).get(key); + return this.set(key, val); + } + + /** + * Get the value associated with `key` + * @throws if `key` did not a value associated with it + */ + strictGet(key: K): V { + if (!this.has(key)) { + throw new TypeError("key not in map"); + } + + return this.get(key); } } diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts index 7cc208ad55..359b293993 100644 --- a/src/renderer/api/__tests__/catalog-entity-registry.test.ts +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -42,7 +42,7 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); items.push({ @@ -60,7 +60,7 @@ describe("CatalogEntityRegistry", () => { spec: {} }); - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(2); }); @@ -81,13 +81,13 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("disconnected"); items[0].status.phase = "connected"; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("connected"); }); @@ -125,9 +125,9 @@ describe("CatalogEntityRegistry", () => { } ]; - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); items.splice(0, 1); - catalog.updateItems(items); + (catalog as any).rawItems.replace(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].metadata.uid).toEqual("456"); }); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 42c9b6f224..ac4e6748dc 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,28 +19,25 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, observable } from "mobx"; +import { computed, observable } from "mobx"; import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; +import { iter } from "../utils"; export class CatalogEntityRegistry { - @observable protected _items: CatalogEntity[] = observable.array([], { deep: true }); + protected rawItems = observable.array([], { deep: true }); @observable protected _activeEntity: CatalogEntity; constructor(private categoryRegistry: CatalogCategoryRegistry) {} init() { subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { - this.updateItems(items); + this.rawItems.replace(items); }); broadcastMessage("catalog:broadcast"); } - @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { - this._items = items.map(data => this.categoryRegistry.getEntityForData(data)); - } - set activeEntity(entity: CatalogEntity) { this._activeEntity = entity; } @@ -49,23 +46,27 @@ export class CatalogEntityRegistry { return this._activeEntity; } - get items() { - return this._items; + @computed get items() { + return Array.from(iter.filterMap(this.rawItems, rawItem => this.categoryRegistry.getEntityForData(rawItem))); + } + + @computed get entities(): Map { + return new Map(this.items.map(item => [item.metadata.uid, item])); } getById(id: string) { - return this._items.find((entity) => entity.metadata.uid === id); + return this.entities.get(id); } getItemsForApiKind(apiVersion: string, kind: string): T[] { - const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); return items as T[]; } getItemsForCategory(category: CatalogCategory): T[] { const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`); - const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind); + const items = this.items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind); return items as T[]; } From 1664b393ee1049e0bda6799a96b47b6ca79ab41c Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 20 May 2021 15:02:55 +0300 Subject: [PATCH 2/4] Refactor CatalogEntityRegistry from common to main (#2824) * refactor CatalogEntityRegistry from common to main Signed-off-by: Jari Kolehmainen * test fix Signed-off-by: Jari Kolehmainen --- .../catalog/catalog-category-registry.ts | 24 ++++--- src/common/catalog/index.ts | 1 - src/extensions/core-api/catalog.ts | 3 +- src/extensions/lens-main-extension.ts | 3 +- src/main/catalog-pusher.ts | 2 +- src/main/catalog-sources/kubeconfig-sync.ts | 3 +- .../__tests__/catalog-entity-registry.test.ts | 60 ++++++++++++++++- .../catalog/catalog-entity-registry.ts | 12 ++-- src/main/catalog/index.ts | 22 +++++++ src/main/cluster-manager.ts | 2 +- src/main/index.ts | 2 +- .../__tests__/catalog-entity-registry.test.ts | 65 ++++++++++++++++--- 12 files changed, 163 insertions(+), 36 deletions(-) rename src/{common => main/catalog}/__tests__/catalog-entity-registry.test.ts (61%) rename src/{common => main}/catalog/catalog-entity-registry.ts (79%) create mode 100644 src/main/catalog/index.ts diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts index a0319d773b..3b1d5bb00a 100644 --- a/src/common/catalog/catalog-category-registry.ts +++ b/src/common/catalog/catalog-category-registry.ts @@ -25,24 +25,22 @@ import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./cat export class CatalogCategoryRegistry { protected categories = observable.set(); + protected groupKinds = new ExtendedMap>(); @action add(category: CatalogCategory): Disposer { this.categories.add(category); + this.updateGroupKinds(category); - return () => this.categories.delete(category); + return () => { + this.categories.delete(category); + this.groupKinds.clear(); + }; } - @computed private get groupKindLookup(): Map> { - // ExtendedMap has the convenience methods `getOrInsert` and `strictSet` - const res = new ExtendedMap>(); - - for (const category of this.categories) { - res - .getOrInsert(category.spec.group, ExtendedMap.new) - .strictSet(category.spec.names.kind, category); - } - - return res; + private updateGroupKinds(category: CatalogCategory) { + this.groupKinds + .getOrInsert(category.spec.group, ExtendedMap.new) + .strictSet(category.spec.names.kind, category); } @computed get items() { @@ -50,7 +48,7 @@ export class CatalogCategoryRegistry { } getForGroupKind(group: string, kind: string): T | undefined { - return this.groupKindLookup.get(group)?.get(kind) as T; + return this.groupKinds.get(group)?.get(kind) as T; } getEntityForData(data: CatalogEntityData & CatalogEntityKindData) { diff --git a/src/common/catalog/index.ts b/src/common/catalog/index.ts index 5dee2c22b2..81d166c870 100644 --- a/src/common/catalog/index.ts +++ b/src/common/catalog/index.ts @@ -21,4 +21,3 @@ export * from "./catalog-category-registry"; export * from "./catalog-entity"; -export * from "./catalog-entity-registry"; diff --git a/src/extensions/core-api/catalog.ts b/src/extensions/core-api/catalog.ts index 304ab7bbd0..14ba0c94d9 100644 --- a/src/extensions/core-api/catalog.ts +++ b/src/extensions/core-api/catalog.ts @@ -20,7 +20,8 @@ */ -import { CatalogEntity, catalogEntityRegistry as registry } from "../../common/catalog"; +import type { CatalogEntity } from "../../common/catalog"; +import { catalogEntityRegistry as registry } from "../../main/catalog"; export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry"; export * from "../../common/catalog-entities"; diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 7e0ac71707..023a8b92f9 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -22,7 +22,8 @@ import { LensExtension } from "./lens-extension"; import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; -import { CatalogEntity, catalogEntityRegistry } from "../common/catalog"; +import { catalogEntityRegistry } from "../main/catalog"; +import type { CatalogEntity } from "../common/catalog"; import type { IObservableArray } from "mobx"; import type { MenuRegistration } from "./registries"; diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index 362ba9a770..c2971017cc 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -21,7 +21,7 @@ import { reaction, toJS } from "mobx"; import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc"; -import type { CatalogEntityRegistry} from "../common/catalog"; +import type { CatalogEntityRegistry} from "./catalog"; import "../common/catalog-entities/kubernetes-cluster"; import type { Disposer } from "../common/utils"; diff --git a/src/main/catalog-sources/kubeconfig-sync.ts b/src/main/catalog-sources/kubeconfig-sync.ts index c4a8b0a514..c72b6081f2 100644 --- a/src/main/catalog-sources/kubeconfig-sync.ts +++ b/src/main/catalog-sources/kubeconfig-sync.ts @@ -20,7 +20,8 @@ */ import { action, observable, IComputedValue, computed, ObservableMap, runInAction } from "mobx"; -import { CatalogEntity, catalogEntityRegistry } from "../../common/catalog"; +import type { CatalogEntity } from "../../common/catalog"; +import { catalogEntityRegistry } from "../../main/catalog"; import { watch } from "chokidar"; import fs from "fs"; import fse from "fs-extra"; diff --git a/src/common/__tests__/catalog-entity-registry.test.ts b/src/main/catalog/__tests__/catalog-entity-registry.test.ts similarity index 61% rename from src/common/__tests__/catalog-entity-registry.test.ts rename to src/main/catalog/__tests__/catalog-entity-registry.test.ts index 3c32bc835e..fff3faa3d9 100644 --- a/src/common/__tests__/catalog-entity-registry.test.ts +++ b/src/main/catalog/__tests__/catalog-entity-registry.test.ts @@ -20,8 +20,30 @@ */ import { observable, reaction } from "mobx"; -import { WebLink } from "../catalog-entities"; -import { CatalogEntityRegistry } from "../catalog"; +import { WebLink, WebLinkSpec, WebLinkStatus } from "../../../common/catalog-entities"; +import { catalogCategoryRegistry, CatalogEntity, CatalogEntityMetadata } from "../../../common/catalog"; +import { CatalogEntityRegistry } from "../catalog-entity-registry"; + +class InvalidEntity extends CatalogEntity { + public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public readonly kind = "Invalid"; + + async onRun() { + return; + } + + public onSettingsOpen(): void { + return; + } + + public onDetailsOpen(): void { + return; + } + + public onContextMenuOpen(): void { + return; + } +} describe("CatalogEntityRegistry", () => { let registry: CatalogEntityRegistry; @@ -39,9 +61,23 @@ describe("CatalogEntityRegistry", () => { phase: "valid" } }); + const invalidEntity = new InvalidEntity({ + metadata: { + uid: "invalid", + name: "test-link", + source: "test", + labels: {} + }, + spec: { + url: "https://k8slens.dev" + }, + status: { + phase: "valid" + } + }); beforeEach(() => { - registry = new CatalogEntityRegistry(); + registry = new CatalogEntityRegistry(catalogCategoryRegistry); }); describe("addSource", () => { @@ -79,4 +115,22 @@ describe("CatalogEntityRegistry", () => { expect(registry.items.length).toEqual(0); }); }); + + describe("items", () => { + it("returns added items", () => { + expect(registry.items.length).toBe(0); + + const source = observable.array([entity]); + + registry.addObservableSource("test", source); + expect(registry.items.length).toBe(1); + }); + + it("does not return items without matching category", () => { + const source = observable.array([invalidEntity]); + + registry.addObservableSource("test", source); + expect(registry.items.length).toBe(0); + }); + }); }); diff --git a/src/common/catalog/catalog-entity-registry.ts b/src/main/catalog/catalog-entity-registry.ts similarity index 79% rename from src/common/catalog/catalog-entity-registry.ts rename to src/main/catalog/catalog-entity-registry.ts index 80a6c66a4c..48b8316774 100644 --- a/src/common/catalog/catalog-entity-registry.ts +++ b/src/main/catalog/catalog-entity-registry.ts @@ -20,12 +20,14 @@ */ import { action, computed, observable, IComputedValue, IObservableArray } from "mobx"; -import type { CatalogEntity } from "./catalog-entity"; -import { iter } from "../utils"; +import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog"; +import { iter } from "../../common/utils"; export class CatalogEntityRegistry { protected sources = observable.map>([], { deep: true }); + constructor(private categoryRegistry: CatalogCategoryRegistry) {} + @action addObservableSource(id: string, source: IObservableArray) { this.sources.set(id, computed(() => source)); } @@ -39,7 +41,9 @@ export class CatalogEntityRegistry { } @computed get items(): CatalogEntity[] { - return Array.from(iter.flatMap(this.sources.values(), source => source.get())); + const allItems = Array.from(iter.flatMap(this.sources.values(), source => source.get())); + + return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined); } getItemsForApiKind(apiVersion: string, kind: string): T[] { @@ -49,4 +53,4 @@ export class CatalogEntityRegistry { } } -export const catalogEntityRegistry = new CatalogEntityRegistry(); +export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); diff --git a/src/main/catalog/index.ts b/src/main/catalog/index.ts new file mode 100644 index 0000000000..13e660b878 --- /dev/null +++ b/src/main/catalog/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export * from "./catalog-entity-registry"; diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 211172a2a6..9ef28c8f9f 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -28,7 +28,7 @@ import type { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { Singleton } from "../common/utils"; -import { catalogEntityRegistry } from "../common/catalog"; +import { catalogEntityRegistry } from "./catalog"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; export class ClusterManager extends Singleton { diff --git a/src/main/index.ts b/src/main/index.ts index 73b2ca5fd1..d54babde8b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -50,7 +50,7 @@ import { bindBroadcastHandlers } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { CatalogPusher } from "./catalog-pusher"; -import { catalogEntityRegistry } from "../common/catalog"; +import { catalogEntityRegistry } from "./catalog"; import { HotbarStore } from "../common/hotbar-store"; import { HelmRepoManager } from "./helm/helm-repo-manager"; import { KubeconfigSyncManager } from "./catalog-sources"; diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts index 359b293993..8a26aabc00 100644 --- a/src/renderer/api/__tests__/catalog-entity-registry.test.ts +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -22,11 +22,18 @@ 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"; + +class TestCatalogEntityRegistry extends CatalogEntityRegistry { + replaceItems(items: Array) { + this.rawItems.replace(items); + } +} describe("CatalogEntityRegistry", () => { describe("updateItems", () => { it("adds new catalog item", () => { - const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); const items = [{ apiVersion: "entity.k8slens.dev/v1alpha1", kind: "KubernetesCluster", @@ -42,7 +49,7 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); expect(catalog.items.length).toEqual(1); items.push({ @@ -60,12 +67,12 @@ describe("CatalogEntityRegistry", () => { spec: {} }); - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); expect(catalog.items.length).toEqual(2); }); it("updates existing items", () => { - const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); const items = [{ apiVersion: "entity.k8slens.dev/v1alpha1", kind: "KubernetesCluster", @@ -81,19 +88,19 @@ describe("CatalogEntityRegistry", () => { spec: {} }]; - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("disconnected"); items[0].status.phase = "connected"; - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].status.phase).toEqual("connected"); }); it("removes deleted items", () => { - const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry); const items = [ { apiVersion: "entity.k8slens.dev/v1alpha1", @@ -125,11 +132,51 @@ describe("CatalogEntityRegistry", () => { } ]; - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); items.splice(0, 1); - (catalog as any).rawItems.replace(items); + catalog.replaceItems(items); expect(catalog.items.length).toEqual(1); expect(catalog.items[0].metadata.uid).toEqual("456"); }); }); + + describe("items", () => { + it("does not return items without matching category", () => { + 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: {} + }, + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "FooBar", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + } + ]; + + catalog.replaceItems(items); + + expect(catalog.items.length).toBe(1); + }); + }); }); From 27d230d12b43487b4756b39e61b8313395696252 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 May 2021 15:57:56 +0300 Subject: [PATCH 3/4] Enable deep navigation/routing to catalog (#2818) * Enable route-based navigation to catalog items Signed-off-by: Alex Culliere Co-authored-by: Sebastian Malton * Enable custom protocol handle to process routing in catalog Signed-off-by: Alex Culliere Co-authored-by: Sebastian Malton * Fix type imports Signed-off-by: Alex Culliere Co-authored-by: Alex Culliere --- .../components/+catalog/catalog.route.ts | 8 +++-- src/renderer/components/+catalog/catalog.tsx | 31 +++++++++++++++++-- src/renderer/protocol-handler/app-handlers.ts | 7 +++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/+catalog/catalog.route.ts b/src/renderer/components/+catalog/catalog.route.ts index 39ad958650..b7bbe8c993 100644 --- a/src/renderer/components/+catalog/catalog.route.ts +++ b/src/renderer/components/+catalog/catalog.route.ts @@ -22,8 +22,12 @@ import type { RouteProps } from "react-router"; import { buildURL } from "../../../common/utils/buildUrl"; +export interface ICatalogViewRouteParam { + group?: string; + kind?: string; +} export const catalogRoute: RouteProps = { - path: "/catalog" + path: "/catalog/:group?/:kind?" }; -export const catalogURL = buildURL(catalogRoute.path); +export const catalogURL = buildURL(catalogRoute.path); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 63239eb5d1..5211e034c2 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -23,7 +23,7 @@ import "./catalog.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; -import { action, observable, reaction } from "mobx"; +import { action, observable, reaction, when } from "mobx"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { kebabCase } from "lodash"; @@ -37,18 +37,33 @@ import { ConfirmDialog } from "../confirm-dialog"; import { Tab, Tabs } from "../tabs"; import { catalogCategoryRegistry } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; +import type { RouteComponentProps } from "react-router"; +import type { ICatalogViewRouteParam } from "./catalog.route"; +import { Notifications } from "../notifications"; enum sortBy { name = "name", source = "source", status = "status" } + +interface Props extends RouteComponentProps {} @observer -export class Catalog extends React.Component { +export class Catalog extends React.Component { @observable private catalogEntityStore?: CatalogEntityStore; @observable.deep private contextMenu: CatalogEntityContextMenuContext; @observable activeTab?: string; + get routeActiveTab(): string | undefined { + const { group, kind } = this.props.match.params ?? {}; + + if (group && kind) { + return `${group}/${kind}`; + } + + return undefined; + } + async componentDidMount() { this.contextMenu = { menuItems: [], @@ -57,12 +72,22 @@ export class Catalog extends React.Component { this.catalogEntityStore = new CatalogEntityStore(); disposeOnUnmount(this, [ this.catalogEntityStore.watch(), + when(() => catalogCategoryRegistry.items.length > 0, () => { + const item = catalogCategoryRegistry.items.find(i => i.getId() === this.routeActiveTab); + + if (item || this.routeActiveTab === undefined) { + this.activeTab = this.routeActiveTab; + this.catalogEntityStore.activeCategory = item; + } else { + Notifications.error(

Unknown category: {this.routeActiveTab}

); + } + }), reaction(() => catalogCategoryRegistry.items, (items) => { if (!this.activeTab && items.length > 0) { this.activeTab = items[0].getId(); this.catalogEntityStore.activeCategory = items[0]; } - }, { fireImmediately: true }) + }), ]); } diff --git a/src/renderer/protocol-handler/app-handlers.ts b/src/renderer/protocol-handler/app-handlers.ts index 0e5e2f3ee6..bbd91ab19b 100644 --- a/src/renderer/protocol-handler/app-handlers.ts +++ b/src/renderer/protocol-handler/app-handlers.ts @@ -43,6 +43,13 @@ export function bindProtocolAddRouteHandlers() { .addInternalHandler("/landing", () => { navigate(catalogURL()); }) + .addInternalHandler("/landing/view/:group/:kind", ({ pathname: { group, kind } }) => { + navigate(catalogURL({ + params: { + group, kind + } + })); + }) .addInternalHandler("/cluster", () => { navigate(addClusterURL()); }) From c8421e47403a7c8e8e2946f9a81cfe1b965aabce Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 20 May 2021 16:05:12 +0300 Subject: [PATCH 4/4] handle missing data on disabled hotbar items (#2829) Signed-off-by: Jari Kolehmainen --- src/renderer/components/hotbar/hotbar-icon.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index bdd00859c2..7c567ff681 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -93,6 +93,10 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { }; const getIconString = () => { + if (!title) { + return "??"; + } + const [rawFirst, rawSecond, rawThird] = getNameParts(title); const splitter = new GraphemeSplitter(); const first = splitter.iterateGraphemes(rawFirst); @@ -108,7 +112,7 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { return (
- +