mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)
* Turn on strict mode in tsconfig.json - Add route, clusterRoute, and payloadValidatedClusterRoute helper functions to improve types with backend routes - Turn on the following new lints: - react/jsx-first-prop-new-line - react/jsx-wrap-multilines - react/jsx-one-expression-per-line - react/jsx-max-props-per-line - react/jsx-indent - react/jsx-indent-props Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace KubeObject scope strings with enum Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert package.json version changes Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move hostedCluster(Id) Signed-off-by: Sebastian Malton <sebastian@malton.name> * change some type param names to be not single letters Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove copy-extension-themes Signed-off-by: Sebastian Malton <sebastian@malton.name> * add new make clean action Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build to not use webpack for generating types Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move fileNameMigration to index Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix ref logic error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts test by overriding createStorage to not touch file system Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster.test.ts tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube=api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixed hotbar-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kubeconfig-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix daemonset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicaset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulsets/dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicasets/scale-dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments/scale/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cronjob.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stateful-set.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployment.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix job.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix scroll-spy.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix welcome.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix verify-that-all-routes-have-route-component.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pod-tolerations.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * better fix for previous 3 fixes, plus also select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix app-paths.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix isReactNode typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sub-title.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix drawer.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix list-layout.tsx and header.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix error-boundary.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix upgrade-chart/store.ts and dock-tab.store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix install-chart/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix edit-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix create-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select-filter.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crd-list.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix wrong types for extensions Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency on catalogCategoryRegistry Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-kube Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors, most <Select /> errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixing more type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * some more fixing type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeApis to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * factor out into a common file all the exports Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeObjectStores to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unused legacy KubeApi globals Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bad previous commit Signed-off-by: Sebastian Malton <sebastian@malton.name> * more crash fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * try and fix behavioural tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-core.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-kubernetes-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-application-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-extension-specific-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-telemetry-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix closing-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts - Fix other type errors too Signed-off-by: Sebastian Malton <sebastian@malton.name> * final tweaks Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add more tsconfig files, fix bug in <Catalog> - Make all of history, navigation injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of kube-details-params/ and navigate/ to injectable - This fixes a runtime error that was encountered during testing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix runtime errors on renderer - remove all static uses of `createPageParam` (and then removed the legacy global) - Made LensRendererExtension and LensMainExtension just used dependencies and not the getLegacyDi - Fixed circular dep in extension-loader Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move registerStore calls to after injectMany Signed-off-by: Sebastian Malton <sebastian@malton.name> * replace all the rest of the legacy uses of apiManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stack overflow and cycles in DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix NamespaceSelectFilter not opening Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix WizardStep and AddNamespaceDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix KubeApi's not being registered Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup WindowManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * Proper fix for Wizard, fix NamespaceStore.subscribe Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite withTooltip to be more type correct - Fixes mobx related "too many recursive actions" error - Change all the uses of withTooltips to be functional components Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add e2e test to cover kube api registration Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup internal-commands Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove cast in <Animate> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix command-palette e2e test Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test name Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix code to help CodeQL scanner Signed-off-by: Sebastian Malton <sebastian@malton.name> * update intree extension lock files Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build-extensions picking wrong @types/react Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix tests from rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigSyncManager more injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crash in test mode for Dialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * make Select snapshots deterministic Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix new type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object.store.test.ts typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix merge build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix snapshots after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * reexport BaseKubeJsonApiObjectMetadata Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo in terminalSpawningPool Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove duplicate license header Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo to waitUntilDefined Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove iter use from getLegacyGlobalDiForExtensionApi Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove complex createStorage override Signed-off-by: Sebastian Malton <sebastian@malton.name> * override logger with mocks only when needed for tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove specialized overrideStore flags for getDiForUnitTesting Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary | undefined types from the exactOptionalFieldTypes experiment Signed-off-by: Sebastian Malton <sebastian@malton.name> * use more descriptive name for local test mocks Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary addition to 'make clean' target Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove oddity of KubeObjectStore.getById(undefined) being allowed Signed-off-by: Sebastian Malton <sebastian@malton.name> * rename KubeObject.getDescriptor in favour of name without fundemental JS meaning Signed-off-by: Sebastian Malton <sebastian@malton.name> * Simplify legacyRegisterApi when working in behaviour unit tests - Don't emit within main environment as there should be no auto registering there Signed-off-by: Sebastian Malton <sebastian@malton.name> * change confusing variable name in ReactiveDuration Signed-off-by: Sebastian Malton <sebastian@malton.name> * make visitor pattern more explicit for Entity contextMenuOpen Signed-off-by: Sebastian Malton <sebastian@malton.name> * toggleDetails -> toggleKubeDetailsPane is more specific Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove outdated comment Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bug where LensExtension dependencies are not set Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests from the revert of react 18 Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix more tests from merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typings with new is-compatible-extension tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * more type fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert in-tree extension versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve name of guarding injectable for stores and apis - New name better implies that it is just a guard state and does not do anything Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add helper for <Select>.isMulti for storing in a Set<Value> Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix is-compatible-extension.test.ts types Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
381d77c633
commit
dfcb7c3330
1062 files changed
+26512
-28689
No files matched your search
@@ -3,21 +3,19 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||
import type { CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
||||
import { CatalogCategory } from "../catalog-entity";
|
||||
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
||||
import { observable } from "mobx";
|
||||
import type { CatalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { categoryVersion } from "../../../common/catalog";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import type { CatalogEntityRegistry } from "../catalog/entity/registry";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity/registry.injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
|
||||
|
||||
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
|
||||
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
|
||||
this.updateItems(items);
|
||||
}
|
||||
}
|
||||
|
||||
class FooBarCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly apiVersion = "entityRegistry.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "FooBars",
|
||||
@@ -26,10 +24,7 @@ class FooBarCategory extends CatalogCategory {
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink,
|
||||
},
|
||||
categoryVersion("v1alpha1", WebLink),
|
||||
],
|
||||
names: {
|
||||
kind: "FooBar",
|
||||
@@ -81,9 +76,18 @@ const entitykc = new KubernetesCluster({
|
||||
});
|
||||
|
||||
describe("CatalogEntityRegistry", () => {
|
||||
let entityRegistry: CatalogEntityRegistry;
|
||||
let catalogCategoryRegistry: CatalogCategoryRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
|
||||
});
|
||||
|
||||
describe("updateItems", () => {
|
||||
it("adds new catalog item", () => {
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
@@ -99,8 +103,8 @@ describe("CatalogEntityRegistry", () => {
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toEqual(1);
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
|
||||
items.push({
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
@@ -117,12 +121,11 @@ describe("CatalogEntityRegistry", () => {
|
||||
spec: {},
|
||||
});
|
||||
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toEqual(2);
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(2);
|
||||
});
|
||||
|
||||
it("updates existing items", () => {
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
@@ -138,19 +141,18 @@ describe("CatalogEntityRegistry", () => {
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toEqual(1);
|
||||
expect(catalog.items[0].status.phase).toEqual("disconnected");
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[0].status.phase).toEqual("disconnected");
|
||||
|
||||
items[0].status.phase = "connected";
|
||||
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toEqual(1);
|
||||
expect(catalog.items[0].status.phase).toEqual("connected");
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[0].status.phase).toEqual("connected");
|
||||
});
|
||||
|
||||
it("updates activeEntity", () => {
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
@@ -166,17 +168,16 @@ describe("CatalogEntityRegistry", () => {
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
catalog.activeEntity = catalog.items[0];
|
||||
expect(catalog.activeEntity.status.phase).toEqual("disconnected");
|
||||
entityRegistry.updateItems(items);
|
||||
entityRegistry.activeEntity = entityRegistry.items.get()[0];
|
||||
expect(entityRegistry.activeEntity.status.phase).toEqual("disconnected");
|
||||
|
||||
items[0].status.phase = "connected";
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.activeEntity.status.phase).toEqual("connected");
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.activeEntity.status.phase).toEqual("connected");
|
||||
});
|
||||
|
||||
it("removes deleted items", () => {
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
@@ -208,17 +209,16 @@ describe("CatalogEntityRegistry", () => {
|
||||
},
|
||||
];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
entityRegistry.updateItems(items);
|
||||
items.splice(0, 1);
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toEqual(1);
|
||||
expect(catalog.items[0].metadata.uid).toEqual("456");
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[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",
|
||||
@@ -250,13 +250,12 @@ describe("CatalogEntityRegistry", () => {
|
||||
},
|
||||
];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
expect(catalog.items.length).toBe(1);
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("does return items after matching category is added", () => {
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
@@ -274,29 +273,28 @@ describe("CatalogEntityRegistry", () => {
|
||||
},
|
||||
];
|
||||
|
||||
catalog.replaceItems(items);
|
||||
entityRegistry.updateItems(items);
|
||||
catalogCategoryRegistry.add(new FooBarCategory());
|
||||
expect(catalog.items.length).toBe(1);
|
||||
expect(entityRegistry.items.get().length).toBe(1);
|
||||
});
|
||||
|
||||
it("does not return items that are filtered out", () => {
|
||||
const source = observable.array([entity, entity2, entitykc]);
|
||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||
|
||||
catalog.replaceItems(source);
|
||||
entityRegistry.updateItems(source);
|
||||
|
||||
expect(catalog.items.length).toBe(3);
|
||||
expect(catalog.filteredItems.length).toBe(3);
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(3);
|
||||
|
||||
const d = catalog.addCatalogFilter(entity => entity.kind === KubernetesCluster.kind);
|
||||
const d = entityRegistry.addCatalogFilter(entity => entity.kind === KubernetesCluster.kind);
|
||||
|
||||
expect(catalog.items.length).toBe(3);
|
||||
expect(catalog.filteredItems.length).toBe(1);
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(1);
|
||||
|
||||
// Remove filter
|
||||
d();
|
||||
|
||||
expect(catalog.items.length).toBe(3);
|
||||
expect(catalog.filteredItems.length).toBe(3);
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -3,5 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export { catalogCategoryRegistry } from "../../common/catalog";
|
||||
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export type { CategoryFilter } from "../../common/catalog";
|
||||
|
||||
export const catalogCategoryRegistry = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||
|
||||
const catalogEntityRegistryInjectable = getInjectable({
|
||||
id: "catalog-entity-registry",
|
||||
instantiate: () => catalogEntityRegistry,
|
||||
});
|
||||
|
||||
export default catalogEntityRegistryInjectable;
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export { catalogEntityRunContext } from "./catalog-entity-registry";
|
||||
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||
export type {
|
||||
CatalogEntityData,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
const activeEntityInjectable = getInjectable({
|
||||
id: "active-entity",
|
||||
instantiate: (di) => {
|
||||
const registry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
return computed(() => registry.activeEntity);
|
||||
},
|
||||
});
|
||||
|
||||
export default activeEntityInjectable;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
const catalogEnitiesInjectable = getInjectable({
|
||||
id: "catalog-enities",
|
||||
instantiate: (di) => di.inject(catalogEntityRegistryInjectable).items,
|
||||
});
|
||||
|
||||
export default catalogEnitiesInjectable;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
export type GetActiveClusterEntity = () => Cluster | undefined;
|
||||
|
||||
const getActiveClusterEntityInjectable = getInjectable({
|
||||
id: "get-active-cluster-entity",
|
||||
instantiate: (di): GetActiveClusterEntity => {
|
||||
const store = di.inject(clusterStoreInjectable);
|
||||
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
return () => store.getById(entityRegistry.activeEntity?.getId());
|
||||
},
|
||||
});
|
||||
|
||||
export default getActiveClusterEntityInjectable;
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import getActiveClusterEntityInjectable from "./get-active-cluster-entity.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(getActiveClusterEntityInjectable)` instead
|
||||
*/
|
||||
export const getActiveClusterEntity = asLegacyGlobalFunctionForExtensionApi(getActiveClusterEntityInjectable);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable";
|
||||
import navigateInjectable from "../../../navigation/navigate.injectable";
|
||||
import { CatalogEntityRegistry } from "./registry";
|
||||
|
||||
const catalogEntityRegistryInjectable = getInjectable({
|
||||
id: "catalog-entity-registry",
|
||||
instantiate: (di) => new CatalogEntityRegistry({
|
||||
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default catalogEntityRegistryInjectable;
|
||||
+45
-45
@@ -4,56 +4,57 @@
|
||||
*/
|
||||
|
||||
import { computed, observable, makeObservable, action } from "mobx";
|
||||
import { ipcRendererOn } from "../../common/ipc";
|
||||
import type { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||
import { catalogCategoryRegistry } from "../../common/catalog";
|
||||
import "../../common/catalog-entities";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { ClusterStore } from "../../common/cluster-store/cluster-store";
|
||||
import { iter } from "../utils";
|
||||
import type { Disposer } from "../utils";
|
||||
import { ipcRendererOn } from "../../../../common/ipc";
|
||||
import type { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogCategoryRegistry, CatalogEntityKindData } from "../../../../common/catalog";
|
||||
import "../../../../common/catalog-entities";
|
||||
import { iter } from "../../../utils";
|
||||
import type { Disposer } from "../../../utils";
|
||||
import { once } from "lodash";
|
||||
import logger from "../../common/logger";
|
||||
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||
import logger from "../../../../common/logger";
|
||||
import { CatalogRunEvent } from "../../../../common/catalog/catalog-run-event";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../common/ipc/catalog";
|
||||
import { navigate } from "../navigation";
|
||||
import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../../../common/ipc/catalog";
|
||||
import { isMainFrame } from "process";
|
||||
import type { Navigate } from "../../../navigation/navigate.injectable";
|
||||
|
||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||
|
||||
export const catalogEntityRunContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||
catalogEntityRegistry.activeEntity = entity;
|
||||
},
|
||||
};
|
||||
interface Dependencies {
|
||||
navigate: Navigate;
|
||||
readonly categoryRegistry: CatalogCategoryRegistry;
|
||||
}
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@observable protected activeEntityId: string | undefined = undefined;
|
||||
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||
protected filters = observable.set<EntityFilter>([], {
|
||||
protected readonly activeEntityId = observable.box<string | undefined>(undefined);
|
||||
protected readonly _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||
protected readonly filters = observable.set<EntityFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
protected onBeforeRunHooks = observable.set<CatalogEntityOnBeforeRun>([], {
|
||||
protected readonly onBeforeRunHooks = observable.set<CatalogEntityOnBeforeRun>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
||||
*/
|
||||
protected rawEntities: (CatalogEntityData & CatalogEntityKindData)[] = [];
|
||||
protected readonly rawEntities: (CatalogEntityData & CatalogEntityKindData)[] = [];
|
||||
|
||||
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
protected getActiveEntityById() {
|
||||
return this._entities.get(this.activeEntityId) || null;
|
||||
const activeEntityId = this.activeEntityId.get();
|
||||
|
||||
if (!activeEntityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._entities.get(activeEntityId);
|
||||
}
|
||||
|
||||
get activeEntity(): CatalogEntity | null {
|
||||
get activeEntity(): CatalogEntity | undefined {
|
||||
const entity = this.getActiveEntityById();
|
||||
|
||||
// If the entity was not found but there are rawEntities to be processed,
|
||||
@@ -68,15 +69,15 @@ export class CatalogEntityRegistry {
|
||||
return entity;
|
||||
}
|
||||
|
||||
set activeEntity(raw: CatalogEntity | string | null) {
|
||||
set activeEntity(raw: CatalogEntity | string | undefined) {
|
||||
if (raw) {
|
||||
const id = typeof raw === "string"
|
||||
? raw
|
||||
: raw.getId();
|
||||
|
||||
this.activeEntityId = id;
|
||||
this.activeEntityId.set(id);
|
||||
} else {
|
||||
this.activeEntityId = undefined;
|
||||
this.activeEntityId.set(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ export class CatalogEntityRegistry {
|
||||
const existing = this._entities.get(item.metadata.uid);
|
||||
|
||||
if (!existing) {
|
||||
const entity = this.categoryRegistry.getEntityForData(item);
|
||||
const entity = this.dependencies.categoryRegistry.getEntityForData(item);
|
||||
|
||||
if (entity) {
|
||||
this._entities.set(entity.getId(), entity);
|
||||
@@ -143,25 +144,25 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
readonly items = computed(() => {
|
||||
this.processRawEntities();
|
||||
|
||||
return Array.from(this._entities.values());
|
||||
}
|
||||
});
|
||||
|
||||
@computed get filteredItems() {
|
||||
return Array.from(
|
||||
iter.reduce(
|
||||
this.filters,
|
||||
iter.filter,
|
||||
this.items.values(),
|
||||
this.items.get().values(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@computed get entities(): Map<string, CatalogEntity> {
|
||||
return new Map(
|
||||
this.items.map(entity => [entity.getId(), entity]),
|
||||
this.items.get().map(entity => [entity.getId(), entity]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,13 +172,13 @@ export class CatalogEntityRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
getById<T extends CatalogEntity>(id: string) {
|
||||
return this.entities.get(id) as T;
|
||||
getById(id: string) {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string, { filtered = false } = {}): T[] {
|
||||
const byApiKind = (item: CatalogEntity) => item.apiVersion === apiVersion && item.kind === kind;
|
||||
const entities = filtered ? this.filteredItems : this.items;
|
||||
const entities = filtered ? this.filteredItems : this.items.get();
|
||||
|
||||
return entities.filter(byApiKind) as T[];
|
||||
}
|
||||
@@ -185,7 +186,7 @@ export class CatalogEntityRegistry {
|
||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory, { filtered = false } = {}): T[] {
|
||||
const supportedVersions = new Set(category.spec.versions.map((v) => `${category.spec.group}/${v.name}`));
|
||||
const byApiVersionKind = (item: CatalogEntity) => supportedVersions.has(item.apiVersion) && item.kind === category.spec.names.kind;
|
||||
const entities = filtered ? this.filteredItems : this.items;
|
||||
const entities = filtered ? this.filteredItems : this.items.get();
|
||||
|
||||
return entities.filter(byApiVersionKind) as T[];
|
||||
}
|
||||
@@ -245,7 +246,12 @@ export class CatalogEntityRegistry {
|
||||
this.onBeforeRun(entity)
|
||||
.then(doOnRun => {
|
||||
if (doOnRun) {
|
||||
return entity.onRun?.(catalogEntityRunContext);
|
||||
return entity.onRun?.({
|
||||
navigate: this.dependencies.navigate,
|
||||
setCommandPaletteContext: (entity) => {
|
||||
this.activeEntity = entity;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logger.debug(`onBeforeRun for ${entity.getId()} returned false`);
|
||||
}
|
||||
@@ -253,9 +259,3 @@ export class CatalogEntityRegistry {
|
||||
.catch(error => logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error));
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
|
||||
export function getActiveClusterEntity(): Cluster | undefined {
|
||||
return ClusterStore.getInstance().getById(catalogEntityRegistry.activeEntity?.getId());
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import hostedClusterIdInjectable from "../../common/cluster-store/hosted-cluster-id.injectable";
|
||||
import type { TerminalApiQuery } from "./terminal-api";
|
||||
import { TerminalApi } from "./terminal-api";
|
||||
|
||||
export type CreateTerminalApi = (query: TerminalApiQuery) => TerminalApi;
|
||||
|
||||
const createTerminalApiInjectable = getInjectable({
|
||||
id: "create-terminal-api",
|
||||
instantiate: (di): CreateTerminalApi => {
|
||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||
|
||||
return (query) => {
|
||||
assert(hostedClusterId, "Can only create terminal APIs within a cluster frame");
|
||||
|
||||
return new TerminalApi({
|
||||
hostedClusterId,
|
||||
}, query);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createTerminalApiInjectable;
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { when } from "mobx";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import type { GeneralEntity } from "../../../common/catalog-entities";
|
||||
|
||||
export async function setEntityOnRouteMatch() {
|
||||
await when(() => catalogEntityRegistry.entities.size > 0);
|
||||
|
||||
const entities: GeneralEntity[] = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General"));
|
||||
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
||||
|
||||
if (activeEntity) {
|
||||
catalogEntityRegistry.activeEntity = activeEntity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { reaction, when } from "mobx";
|
||||
import generalCategoryInjectable from "../../../common/catalog/categories/general.injectable";
|
||||
import isActiveRouteInjectable from "../../navigation/is-route-active.injectable";
|
||||
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
|
||||
import type { Disposer } from "../../utils";
|
||||
import { disposer } from "../../utils";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity/registry.injectable";
|
||||
|
||||
export type WatchForGeneralEntityNavigation = () => Disposer;
|
||||
|
||||
const watchForGeneralEntityNavigationInjectable = getInjectable({
|
||||
id: "watch-for-general-entity-navigation",
|
||||
instantiate: (di): WatchForGeneralEntityNavigation => {
|
||||
const observableHistory = di.inject(observableHistoryInjectable);
|
||||
const isActiveRoute = di.inject(isActiveRouteInjectable);
|
||||
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
const generalCategory = di.inject(generalCategoryInjectable);
|
||||
|
||||
return () => {
|
||||
const dispose = disposer();
|
||||
|
||||
dispose.push(when(
|
||||
() => entityRegistry.entities.size > 0,
|
||||
() => {
|
||||
dispose.push(reaction(
|
||||
() => observableHistory.location,
|
||||
() => {
|
||||
const entities = entityRegistry.getItemsForCategory(generalCategory);
|
||||
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
||||
|
||||
if (activeEntity) {
|
||||
entityRegistry.activeEntity = activeEntity;
|
||||
}
|
||||
},
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
));
|
||||
},
|
||||
));
|
||||
|
||||
return dispose;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default watchForGeneralEntityNavigationInjectable;
|
||||
@@ -3,22 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { JsonApiErrorParsed } from "../../common/k8s-api/json-api";
|
||||
import type { Response } from "node-fetch";
|
||||
import { Notifications } from "../components/notifications";
|
||||
import { apiBase, apiKube } from "../../common/k8s-api";
|
||||
export { apiBase, apiKube } from "../../common/k8s-api";
|
||||
|
||||
|
||||
// Common handler for HTTP api errors
|
||||
export function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
switch (res.status) {
|
||||
case 403:
|
||||
error.isUsedForNotification = true;
|
||||
Notifications.error(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiBase) apiBase.onError.addListener(onApiError);
|
||||
if (apiKube) apiKube.onError.addListener(onApiError);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Response } from "node-fetch";
|
||||
import type { JsonApiErrorParsed } from "../../common/k8s-api/json-api";
|
||||
import { Notifications } from "../components/notifications";
|
||||
|
||||
export function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
switch (res.status) {
|
||||
case 403:
|
||||
error.isUsedForNotification = true;
|
||||
Notifications.error(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiBase } from "../../common/k8s-api";
|
||||
import { onApiError } from "./on-api-error";
|
||||
|
||||
const setupOnApiErrorListenersInjectable = getInjectable({
|
||||
id: "setup-on-api-error-listeners",
|
||||
setup: () => {
|
||||
apiBase?.onError.addListener(onApiError);
|
||||
},
|
||||
instantiate: () => undefined,
|
||||
});
|
||||
|
||||
export default setupOnApiErrorListenersInjectable;
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getHostedClusterId } from "../utils";
|
||||
import type { WebSocketEvents } from "./websocket-api";
|
||||
import { WebSocketApi } from "./websocket-api";
|
||||
import isEqual from "lodash/isEqual";
|
||||
@@ -49,23 +48,27 @@ enum TerminalColor {
|
||||
NO_COLOR = "\u001b[0m",
|
||||
}
|
||||
|
||||
export type TerminalApiQuery = Record<string, string> & {
|
||||
export interface TerminalApiQuery extends Record<string, string | undefined> {
|
||||
id: string;
|
||||
node?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TerminalEvents extends WebSocketEvents {
|
||||
ready: () => void;
|
||||
connected: () => void;
|
||||
}
|
||||
|
||||
export interface TerminalApiDependencies {
|
||||
readonly hostedClusterId: string;
|
||||
}
|
||||
|
||||
export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
||||
protected size: { width: number; height: number };
|
||||
protected size?: { width: number; height: number };
|
||||
|
||||
@observable public isReady = false;
|
||||
|
||||
constructor(protected query: TerminalApiQuery) {
|
||||
constructor(protected readonly dependencies: TerminalApiDependencies, protected readonly query: TerminalApiQuery) {
|
||||
super({
|
||||
flushOnOpen: false,
|
||||
pingInterval: 30,
|
||||
@@ -86,7 +89,7 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
||||
this.emitStatus("Connecting ...");
|
||||
}
|
||||
|
||||
const authTokenArray = await ipcRenderer.invoke("cluster:shell-api", getHostedClusterId(), this.query.id);
|
||||
const authTokenArray = await ipcRenderer.invoke("cluster:shell-api", this.dependencies.hostedClusterId, this.query.id);
|
||||
|
||||
if (!(authTokenArray instanceof Uint8Array)) {
|
||||
throw new TypeError("ShellApi token is not a Uint8Array");
|
||||
@@ -114,11 +117,15 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
||||
|
||||
// data is undefined if the event that was handled is "connected"
|
||||
if (data === undefined) {
|
||||
/**
|
||||
* Output the last line, the makes sure that the terminal isn't completely
|
||||
* empty when the user refreshes.
|
||||
*/
|
||||
this.emit("data", window.localStorage.getItem(`${this.query.id}:last-data`));
|
||||
const lastData = window.localStorage.getItem(`${this.query.id}:last-data`);
|
||||
|
||||
if (lastData) {
|
||||
/**
|
||||
* Output the last line, the makes sure that the terminal isn't completely
|
||||
* empty when the user refreshes.
|
||||
*/
|
||||
this.emit("data", lastData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,7 +133,9 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
||||
this.prependListener("connected", onReady);
|
||||
|
||||
super.connect(socketUrl);
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.socket!.binaryType = "arraybuffer";
|
||||
}
|
||||
|
||||
sendMessage(message: TerminalMessage) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import EventEmitter from "events";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import type { Arguments } from "typed-emitter";
|
||||
import { isDevelopment } from "../../common/vars";
|
||||
import type { Defaulted } from "../utils";
|
||||
|
||||
interface WebsocketApiParams {
|
||||
/**
|
||||
@@ -62,18 +63,16 @@ export interface WebSocketEvents {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
type Defaulted<Params, DefaultParams extends keyof Params> = Required<Pick<Params, DefaultParams>> & Omit<Params, DefaultParams>;
|
||||
|
||||
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
|
||||
protected socket?: WebSocket | null;
|
||||
protected socket: WebSocket | null = null;
|
||||
protected pendingCommands: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = [];
|
||||
protected reconnectTimer?: any;
|
||||
protected pingTimer?: any;
|
||||
protected reconnectTimer?: number;
|
||||
protected pingTimer?: number;
|
||||
protected params: Defaulted<WebsocketApiParams, keyof typeof WebSocketApi["defaultParams"]>;
|
||||
|
||||
@observable readyState = WebSocketApiState.PENDING;
|
||||
|
||||
private static defaultParams = {
|
||||
private static readonly defaultParams = {
|
||||
logging: isDevelopment,
|
||||
reconnectDelay: 10,
|
||||
flushOnOpen: true,
|
||||
@@ -83,18 +82,25 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
||||
constructor(params: WebsocketApiParams) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
this.params = Object.assign({}, WebSocketApi.defaultParams, params);
|
||||
this.params = {
|
||||
...WebSocketApi.defaultParams,
|
||||
...params,
|
||||
};
|
||||
const { pingInterval } = this.params;
|
||||
|
||||
if (pingInterval) {
|
||||
this.pingTimer = setInterval(() => this.ping(), pingInterval * 1000);
|
||||
this.pingTimer = window.setInterval(() => this.ping(), pingInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected() {
|
||||
protected getIsConnected(): this is (WebSocketApi<Events> & { socket: WebSocket }) {
|
||||
return this.socket?.readyState === WebSocket.OPEN && this.isOnline;
|
||||
}
|
||||
|
||||
get isConnected() {
|
||||
return this.getIsConnected();
|
||||
}
|
||||
|
||||
get isOnline() {
|
||||
return navigator.onLine;
|
||||
}
|
||||
@@ -144,7 +150,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
||||
}
|
||||
|
||||
send(command: string | ArrayBufferLike | Blob | ArrayBufferView) {
|
||||
if (this.isConnected) {
|
||||
if (this.getIsConnected()) {
|
||||
this.socket.send(command);
|
||||
} else {
|
||||
this.pendingCommands.push(command);
|
||||
@@ -186,7 +192,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
|
||||
|
||||
this.writeLog("will reconnect in", `${reconnectDelay}s`);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => this.connect(url), reconnectDelay * 1000);
|
||||
this.reconnectTimer = window.setTimeout(() => this.connect(url), reconnectDelay * 1000);
|
||||
this.readyState = WebSocketApiState.RECONNECTING;
|
||||
}
|
||||
} else {
|
||||
|
||||
+6
-2
@@ -5,12 +5,16 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
|
||||
import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel";
|
||||
import type { Channel } from "../../../common/ipc-channel/channel";
|
||||
|
||||
export type GetValueFromRegisteredChannel = <TChannel extends Channel<TInstance>, TInstance>(channel: TChannel) => Promise<TChannel["_template"]>;
|
||||
|
||||
const getValueFromRegisteredChannelInjectable = getInjectable({
|
||||
id: "get-value-from-registered-channel",
|
||||
|
||||
instantiate: (di) =>
|
||||
getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }),
|
||||
instantiate: (di) => getValueFromRegisteredChannel({
|
||||
ipcRenderer: di.inject(ipcRendererInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default getValueFromRegisteredChannelInjectable;
|
||||
+4
-5
@@ -9,9 +9,8 @@ interface Dependencies {
|
||||
ipcRenderer: IpcRenderer;
|
||||
}
|
||||
|
||||
export const getValueFromRegisteredChannel =
|
||||
({ ipcRenderer }: Dependencies) =>
|
||||
export const getValueFromRegisteredChannel = ({ ipcRenderer }: Dependencies) =>
|
||||
<TChannel extends Channel<TInstance>, TInstance>(
|
||||
channel: TChannel,
|
||||
): Promise<TChannel["_template"]> =>
|
||||
ipcRenderer.invoke(channel.name);
|
||||
channel: TChannel,
|
||||
): Promise<TChannel["_template"]> =>
|
||||
ipcRenderer.invoke(channel.name);
|
||||
+24
-10
@@ -36,13 +36,16 @@ import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-fram
|
||||
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
|
||||
import { Router } from "react-router";
|
||||
import historyInjectable from "./navigation/history.injectable";
|
||||
import themeStoreInjectable from "./theme-store.injectable";
|
||||
import themeStoreInjectable from "./themes/store.injectable";
|
||||
import navigateToAddClusterInjectable from "../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
|
||||
import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
|
||||
import { bindEvents } from "./navigation/events";
|
||||
import deleteClusterDialogModelInjectable from "./components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
|
||||
import openDeleteClusterDialogInjectable from "./components/delete-cluster-dialog/open.injectable";
|
||||
import { init } from "@sentry/electron/renderer";
|
||||
import kubernetesClusterCategoryInjectable from "../common/catalog/categories/kubernetes-cluster.injectable";
|
||||
import autoRegistrationInjectable from "../common/k8s-api/api-manager/auto-registration.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
configurePackages(); // global packages
|
||||
registerCustomThemes(); // monaco editor themes
|
||||
@@ -71,6 +74,20 @@ export async function bootstrap(di: DiContainer) {
|
||||
const rootElem = document.getElementById("app");
|
||||
const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`;
|
||||
|
||||
assert(rootElem, "#app MUST exist");
|
||||
|
||||
|
||||
/**
|
||||
* This is injected here to initialize it for the side effect.
|
||||
*
|
||||
* The side effect CANNOT be within `apiManagerInjectable` itself since that causes circular
|
||||
* dependencies with the current need for legacy di use.
|
||||
*
|
||||
* This also MUST be done before anything else so that it can start listening for the events for
|
||||
* auto initialization.
|
||||
*/
|
||||
di.inject(autoRegistrationInjectable);
|
||||
|
||||
// TODO: Remove temporal dependencies to make timing of initialization not important
|
||||
di.inject(userStoreInjectable);
|
||||
|
||||
@@ -89,19 +106,17 @@ export async function bootstrap(di: DiContainer) {
|
||||
logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`);
|
||||
initializers.initCatalogEntityDetailRegistry();
|
||||
|
||||
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
||||
const addSyncEntries = di.inject(addSyncEntriesInjectable);
|
||||
|
||||
logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`);
|
||||
initializers.initCatalogCategoryRegistryEntries({
|
||||
navigateToAddCluster,
|
||||
addSyncEntries,
|
||||
navigateToAddCluster: di.inject(navigateToAddClusterInjectable),
|
||||
addSyncEntries: di.inject(addSyncEntriesInjectable),
|
||||
kubernetesClusterCategory: di.inject(kubernetesClusterCategoryInjectable),
|
||||
});
|
||||
|
||||
logger.info(`${logPrefix} initializing Catalog`);
|
||||
initializers.initCatalog({
|
||||
openCommandDialog: di.inject(commandOverlayInjectable).open,
|
||||
deleteClusterDialogModel: di.inject(deleteClusterDialogModelInjectable),
|
||||
openDeleteClusterDialog: di.inject(openDeleteClusterDialogInjectable),
|
||||
});
|
||||
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
@@ -144,7 +159,6 @@ export async function bootstrap(di: DiContainer) {
|
||||
// TODO: Introduce proper architectural boundaries between root and cluster iframes
|
||||
if (process.isMainFrame) {
|
||||
initializeApp = di.inject(initRootFrameInjectable);
|
||||
|
||||
App = (await import("./frames/root-frame/root-frame")).RootFrame;
|
||||
} else {
|
||||
initializeApp = di.inject(initClusterFrameInjectable);
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { ClusterFrameContext } from "./cluster-frame-context";
|
||||
import namespaceStoreInjectable from "../components/+namespaces/namespace-store/namespace-store.injectable";
|
||||
import namespaceStoreInjectable from "../components/+namespaces/store.injectable";
|
||||
import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
const clusterFrameContextInjectable = getInjectable({
|
||||
id: "cluster-frame-context",
|
||||
@@ -13,13 +14,11 @@ const clusterFrameContextInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const cluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
return new ClusterFrameContext(
|
||||
cluster,
|
||||
assert(cluster, "This can only be injected within a cluster frame");
|
||||
|
||||
{
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
},
|
||||
);
|
||||
return new ClusterFrameContext(cluster, {
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { NamespaceStore } from "../components/+namespaces/namespace-store/namespace.store";
|
||||
import type { NamespaceStore } from "../components/+namespaces/store";
|
||||
import type { ClusterContext } from "../../common/k8s-api/cluster-context";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as uuid from "uuid";
|
||||
import { appEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { iter } from "../../utils";
|
||||
import { isDefined, iter } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { Notifications } from "../notifications";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
@@ -66,7 +66,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
return [
|
||||
...this.errors,
|
||||
...iter.map(this.kubeContexts.values(), ({ error }) => error),
|
||||
].filter(Boolean);
|
||||
].filter(isDefined);
|
||||
}
|
||||
|
||||
readonly refreshContexts = debounce(action(() => {
|
||||
@@ -106,8 +106,18 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
<SettingLayout className={styles.AddClusters} data-testid="add-cluster-page">
|
||||
<h2>Add Clusters from Kubeconfig</h2>
|
||||
<p>
|
||||
Clusters added here are <b>not</b> merged into the <code>~/.kube/config</code> file.{" "}
|
||||
<a href={`${docsUrl}/catalog/add-clusters/`} rel="noreferrer" target="_blank">Read more about adding clusters</a>.
|
||||
{"Clusters added here are "}
|
||||
<b>not</b>
|
||||
{" merged into the "}
|
||||
<code>~/.kube/config</code>
|
||||
{" file. "}
|
||||
<a
|
||||
href={`${docsUrl}/catalog/add-clusters/`}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read more about adding clusters.
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex column">
|
||||
<MonacoEditor
|
||||
|
||||
@@ -11,16 +11,16 @@ import rendererExtensionsInjectable from "../../../../extensions/renderer-extens
|
||||
import { CatalogCategory } from "../../../api/catalog-entity";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
|
||||
import type { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable";
|
||||
import getCategoryColumnsInjectable from "../get-category-columns.injectable";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
import type { CategoryColumns, GetCategoryColumnsParams } from "../columns/get.injectable";
|
||||
import getCategoryColumnsInjectable from "../columns/get.injectable";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
|
||||
|
||||
class TestCategory extends CatalogCategory {
|
||||
apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
kind = "CatalogCategory";
|
||||
metadata: {
|
||||
name: "Test";
|
||||
icon: "question_mark";
|
||||
metadata = {
|
||||
name: "Test",
|
||||
icon: "question_mark",
|
||||
};
|
||||
spec: CatalogCategorySpec = {
|
||||
group: "foo.bar.bat",
|
||||
@@ -32,7 +32,10 @@ class TestCategory extends CatalogCategory {
|
||||
|
||||
constructor(columns?: CategoryColumnRegistration[]) {
|
||||
super();
|
||||
this.spec.displayColumns = columns;
|
||||
this.spec = {
|
||||
displayColumns: columns,
|
||||
...this.spec,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,19 +57,19 @@ describe("Custom Category Columns", () => {
|
||||
});
|
||||
|
||||
it("should contain a kind column if activeCategory is falsy", () => {
|
||||
expect(getCategoryColumns({ activeCategory: null }).renderTableHeader.find(elem => elem.title === "Kind")).toBeTruthy();
|
||||
expect(getCategoryColumns({ activeCategory: null }).renderTableHeader.find(elem => elem?.title === "Kind")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not contain a kind column if activeCategory is truthy", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "Kind")).toBeFalsy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "Kind")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should include the default columns if the provided category doesn't provide any", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "Source")).toBeTruthy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "Source")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not include the default columns if the provided category provides any", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory([]) }).renderTableHeader.find(elem => elem.title === "Source")).toBeFalsy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory([]) }).renderTableHeader.find(elem => elem?.title === "Source")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should include the displayColumns from the provided category", () => {
|
||||
@@ -80,7 +83,7 @@ describe("Custom Category Columns", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory(columns) }).renderTableHeader.find(elem => elem.title === "Foo")).toBeTruthy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory(columns) }).renderTableHeader.find(elem => elem?.title === "Foo")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,11 +120,11 @@ describe("Custom Category Columns", () => {
|
||||
});
|
||||
|
||||
it("should include columns from extensions that match", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "High")).toBeTruthy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "High")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not include columns from extensions that don't match", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "High2")).toBeFalsy();
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem?.title === "High2")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,8 +20,8 @@ describe("Custom Category Views", () => {
|
||||
});
|
||||
|
||||
it("should order items correctly over all extensions", () => {
|
||||
const component1 = (): React.ReactElement => null;
|
||||
const component2 = (): React.ReactElement => null;
|
||||
const component1 = (): React.ReactNode => null;
|
||||
const component2 = (): React.ReactNode => null;
|
||||
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||
{
|
||||
@@ -51,15 +51,15 @@ describe("Custom Category Views", () => {
|
||||
] as LensRendererExtension[]));
|
||||
|
||||
const customCategoryViews = di.inject(customCategoryViewsInjectable);
|
||||
const { after } = customCategoryViews.get().get("foo").get("bar");
|
||||
const { after = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {};
|
||||
|
||||
expect(after[0].View).toBe(component2);
|
||||
expect(after[1].View).toBe(component1);
|
||||
});
|
||||
|
||||
it("should put put priority < 50 items in before", () => {
|
||||
const component1 = (): React.ReactElement => null;
|
||||
const component2 = (): React.ReactElement => null;
|
||||
const component1 = (): React.ReactNode => null;
|
||||
const component2 = (): React.ReactNode => null;
|
||||
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||
{
|
||||
@@ -89,7 +89,7 @@ describe("Custom Category Views", () => {
|
||||
] as LensRendererExtension[]));
|
||||
|
||||
const customCategoryViews = di.inject(customCategoryViewsInjectable);
|
||||
const { before } = customCategoryViews.get().get("foo").get("bar");
|
||||
const { before = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {};
|
||||
|
||||
expect(before[0].View).toBe(component1);
|
||||
});
|
||||
|
||||
@@ -114,18 +114,20 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
onClick={this.onButtonClick}
|
||||
>
|
||||
{this.items.map((menuItem, index) => {
|
||||
return <SpeedDialAction
|
||||
key={index}
|
||||
icon={<Icon material={menuItem.icon}/>}
|
||||
tooltipTitle={menuItem.title}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
menuItem.onClick();
|
||||
}}
|
||||
TooltipClasses={{
|
||||
popper: "catalogSpeedDialPopper",
|
||||
}}
|
||||
/>;
|
||||
return (
|
||||
<SpeedDialAction
|
||||
key={index}
|
||||
icon={<Icon material={menuItem.icon}/>}
|
||||
tooltipTitle={menuItem.title}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
menuItem.onClick();
|
||||
}}
|
||||
TooltipClasses={{
|
||||
popper: "catalogSpeedDialPopper",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SpeedDial>
|
||||
);
|
||||
|
||||
@@ -14,16 +14,22 @@ import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||
import { isDevelopment } from "../../../common/vars";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Avatar } from "../avatar";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
import type { GetLabelBadges } from "./get-label-badges.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import getLabelBadgesInjectable from "./get-label-badges.injectable";
|
||||
|
||||
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
|
||||
entity: T;
|
||||
export interface CatalogEntityDetailsProps<Entity extends CatalogEntity> {
|
||||
entity: Entity;
|
||||
hideDetails(): void;
|
||||
onRun: () => void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
getLabelBadges: GetLabelBadges;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CatalogEntityDetails<T extends CatalogEntity> extends Component<CatalogEntityDetailsProps<T>> {
|
||||
class NonInjectedCatalogEntityDetails<Entity extends CatalogEntity> extends Component<CatalogEntityDetailsProps<Entity> & Dependencies> {
|
||||
categoryIcon(category: CatalogCategory) {
|
||||
if (Icon.isSvg(category.metadata.icon)) {
|
||||
return <Icon svg={category.metadata.icon} smallest />;
|
||||
@@ -32,11 +38,11 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Cat
|
||||
}
|
||||
}
|
||||
|
||||
renderContent(entity: T) {
|
||||
const { onRun, hideDetails } = this.props;
|
||||
renderContent(entity: Entity) {
|
||||
const { onRun, hideDetails, getLabelBadges } = this.props;
|
||||
const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion);
|
||||
const details = detailItems.map(({ components }, index) => <components.Details entity={entity} key={index} />);
|
||||
const showDefaultDetails = detailItems.find((item) => item.priority > 999) === undefined;
|
||||
const showDefaultDetails = detailItems.find((item) => item.priority ?? 50 > 999) === undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -109,3 +115,10 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Cat
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CatalogEntityDetails = withInjectables<Dependencies, CatalogEntityDetailsProps<CatalogEntity>>(NonInjectedCatalogEntityDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
getLabelBadges: di.inject(getLabelBadgesInjectable),
|
||||
}),
|
||||
}) as <Entity extends CatalogEntity>(props: CatalogEntityDetailsProps<Entity>) => React.ReactElement;
|
||||
@@ -4,45 +4,42 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { cssNames, hasDefiniteField } from "../../utils";
|
||||
import type { MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
import type { CatalogEntity, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||
import type { CatalogEntity, CatalogEntityContextMenu } from "../../api/catalog-entity";
|
||||
import { observer } from "mobx-react";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observable } from "mobx";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
|
||||
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { Navigate } from "../../navigation/navigate.injectable";
|
||||
import navigateInjectable from "../../navigation/navigate.injectable";
|
||||
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
|
||||
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
||||
|
||||
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
|
||||
entity: T;
|
||||
export interface CatalogEntityDrawerMenuProps<Entity extends CatalogEntity> extends MenuActionsProps {
|
||||
entity: Entity;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
|
||||
navigate: Navigate;
|
||||
visitEntityContextMenu: VisitEntityContextMenu;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Component<CatalogEntityDrawerMenuProps<T> & Dependencies> {
|
||||
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||
|
||||
constructor(props: CatalogEntityDrawerMenuProps<T> & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
|
||||
|
||||
componentDidMount() {
|
||||
this.contextMenu = {
|
||||
menuItems: [],
|
||||
this.props.visitEntityContextMenu(this.props.entity, {
|
||||
menuItems: this.menuItems,
|
||||
navigate: this.props.navigate,
|
||||
};
|
||||
this.props.entity?.onContextMenuOpen(this.contextMenu);
|
||||
});
|
||||
}
|
||||
|
||||
getMenuItems(entity: T): React.ReactChild[] {
|
||||
@@ -50,9 +47,9 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = this.contextMenu.menuItems
|
||||
.filter(menuItem => menuItem.icon)
|
||||
const items = this.menuItems
|
||||
.map(this.props.normalizeMenuItem)
|
||||
.filter(hasDefiniteField("icon"))
|
||||
.map(menuItem => (
|
||||
<MenuItem key={menuItem.title} onClick={menuItem.onClick}>
|
||||
<Icon
|
||||
@@ -67,8 +64,22 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
|
||||
<HotbarToggleMenuItem
|
||||
key="hotbar-toggle"
|
||||
entity={entity}
|
||||
addContent={<Icon material="push_pin" interactive small tooltip="Add to Hotbar"/>}
|
||||
removeContent={<Icon svg="push_off" interactive small tooltip="Remove from Hotbar"/>}
|
||||
addContent={(
|
||||
<Icon
|
||||
material="push_pin"
|
||||
interactive
|
||||
small
|
||||
tooltip="Add to Hotbar"
|
||||
/>
|
||||
)}
|
||||
removeContent={(
|
||||
<Icon
|
||||
svg="push_off"
|
||||
interactive
|
||||
small
|
||||
tooltip="Remove from Hotbar"
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -78,7 +89,7 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
|
||||
render() {
|
||||
const { className, entity, ...menuProps } = this.props;
|
||||
|
||||
if (!this.contextMenu || !entity.isEnabled()) {
|
||||
if (!this.menuItems.length || !entity.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -97,7 +108,8 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
|
||||
export const CatalogEntityDrawerMenu = withInjectables<Dependencies, CatalogEntityDrawerMenuProps<CatalogEntity>>(NonInjectedCatalogEntityDrawerMenu, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
||||
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
}),
|
||||
}) as <Entity extends CatalogEntity>(props: CatalogEntityDrawerMenuProps<Entity>) => React.ReactElement;
|
||||
}) as <Entity extends CatalogEntity>(props: CatalogEntityDrawerMenuProps<Entity>) => JSX.Element;
|
||||
+6
-4
@@ -3,14 +3,16 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||
import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable";
|
||||
import { catalogEntityStore } from "./catalog-entity.store";
|
||||
import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable";
|
||||
|
||||
const catalogEntityStoreInjectable = getInjectable({
|
||||
id: "catalog-entity-store",
|
||||
|
||||
instantiate: (di) => new CatalogEntityStore({
|
||||
registry: di.inject(catalogEntityRegistryInjectable),
|
||||
instantiate: (di) => catalogEntityStore({
|
||||
entityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
catalogRegistry: di.inject(catalogCategoryRegistryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,61 +3,81 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||
import type { IComputedValue, IObservableValue } from "mobx";
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
|
||||
import type { CatalogEntity } from "../../../api/catalog-entity";
|
||||
import { ItemStore } from "../../../../common/item.store";
|
||||
import type { CatalogCategory } from "../../../../common/catalog";
|
||||
import { catalogCategoryRegistry } from "../../../../common/catalog";
|
||||
import { autoBind, disposer } from "../../../../common/utils";
|
||||
import type { CatalogCategory, CatalogCategoryRegistry } from "../../../../common/catalog";
|
||||
import type { Disposer } from "../../../../common/utils";
|
||||
import { disposer } from "../../../../common/utils";
|
||||
import type { ItemListStore } from "../../item-object-list";
|
||||
|
||||
interface Dependencies {
|
||||
registry: CatalogEntityRegistry;
|
||||
entityRegistry: CatalogEntityRegistry;
|
||||
catalogRegistry: CatalogCategoryRegistry;
|
||||
}
|
||||
|
||||
export class CatalogEntityStore extends ItemStore<CatalogEntity> {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
export type CatalogEntityStore = ItemListStore<CatalogEntity, false> & {
|
||||
readonly entities: IComputedValue<CatalogEntity[]>;
|
||||
readonly activeCategory: IObservableValue<CatalogCategory | undefined>;
|
||||
readonly selectedItemId: IObservableValue<string | undefined>;
|
||||
readonly selectedItem: IComputedValue<CatalogEntity | undefined>;
|
||||
watch(): Disposer;
|
||||
onRun(entity: CatalogEntity): void;
|
||||
};
|
||||
|
||||
@observable activeCategory?: CatalogCategory;
|
||||
@observable selectedItemId?: string;
|
||||
export function catalogEntityStore({
|
||||
entityRegistry,
|
||||
catalogRegistry,
|
||||
}: Dependencies): CatalogEntityStore {
|
||||
const activeCategory = observable.box<CatalogCategory | undefined>(undefined);
|
||||
const selectedItemId = observable.box<string | undefined>(undefined);
|
||||
const entities = computed(() => {
|
||||
const category = activeCategory.get();
|
||||
|
||||
@computed get entities() {
|
||||
if (!this.activeCategory) {
|
||||
return this.dependencies.registry.filteredItems;
|
||||
return category
|
||||
? entityRegistry.getItemsForCategory(category, { filtered: true })
|
||||
: entityRegistry.filteredItems;
|
||||
});
|
||||
const selectedItem = computed(() => {
|
||||
const id = selectedItemId.get();
|
||||
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.dependencies.registry.getItemsForCategory(this.activeCategory, { filtered: true });
|
||||
}
|
||||
return entities.get().find(entity => entity.getId() === id);
|
||||
});
|
||||
const loadAll = () => {
|
||||
const category = activeCategory.get();
|
||||
|
||||
@computed get selectedItem() {
|
||||
return this.entities.find(e => e.getId() === this.selectedItemId);
|
||||
}
|
||||
|
||||
watch() {
|
||||
return disposer(
|
||||
reaction(() => this.entities, () => this.loadAll()),
|
||||
reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100 }),
|
||||
);
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
if (this.activeCategory) {
|
||||
this.activeCategory.emit("load");
|
||||
if (category) {
|
||||
category.emit("load");
|
||||
} else {
|
||||
for (const category of catalogCategoryRegistry.items) {
|
||||
for (const category of catalogRegistry.items) {
|
||||
category.emit("load");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// concurrency is true to fix bug if catalog filter is removed and added at the same time
|
||||
return this.loadItems(() => this.entities, undefined, true);
|
||||
}
|
||||
|
||||
onRun(entity: CatalogEntity): void {
|
||||
this.dependencies.registry.onRun(entity);
|
||||
}
|
||||
return {
|
||||
entities,
|
||||
selectedItem,
|
||||
activeCategory,
|
||||
selectedItemId,
|
||||
watch: () => disposer(
|
||||
reaction(() => entities.get(), loadAll),
|
||||
reaction(() => activeCategory.get(), loadAll, { delay: 100 }),
|
||||
),
|
||||
onRun: entity => entityRegistry.onRun(entity),
|
||||
failedLoading: false,
|
||||
getTotalCount: () => entityRegistry.filteredItems.length,
|
||||
isLoaded: true,
|
||||
isSelected: (item) => item.getId() === selectedItemId.get(),
|
||||
isSelectedAll: () => false,
|
||||
pickOnlySelected: () => [],
|
||||
toggleSelection: () => {},
|
||||
toggleSelectionAll: () => {},
|
||||
removeSelectedItems: async () => {},
|
||||
};
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import { observer } from "mobx-react";
|
||||
import { CatalogCategoryLabel } from "./catalog-category-label";
|
||||
|
||||
export interface CatalogMenuProps {
|
||||
activeItem: string;
|
||||
activeTab: string | undefined;
|
||||
onItemClick: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,14 @@ export const CatalogMenu = observer((props: CatalogMenuProps) => {
|
||||
defaultExpanded={["catalog"]}
|
||||
defaultCollapseIcon={<Icon material="expand_more"/>}
|
||||
defaultExpandIcon={<Icon material="chevron_right" />}
|
||||
selected={props.activeItem || "browse"}
|
||||
selected={props.activeTab || "browse"}
|
||||
>
|
||||
<Item nodeId="browse" label="Browse" data-testid="*-tab" onClick={() => props.onItemClick("*")}/>
|
||||
<Item
|
||||
nodeId="browse"
|
||||
label="Browse"
|
||||
data-testid="*-tab"
|
||||
onClick={() => props.onItemClick("*")}
|
||||
/>
|
||||
<Item
|
||||
nodeId="catalog"
|
||||
label={<div className={styles.parent}>Categories</div>}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const catalogPreviousActiveTabStorageInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
|
||||
return createStorage(
|
||||
return createStorage<string | null>(
|
||||
"catalog-previous-active-tab",
|
||||
browseCatalogTab,
|
||||
);
|
||||
|
||||
@@ -9,24 +9,25 @@ import userEvent from "@testing-library/user-event";
|
||||
import { Catalog } from "./catalog";
|
||||
import { mockWindow } from "../../../../__mocks__/windowMock";
|
||||
import type { CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
|
||||
import { CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CatalogEntity } from "../../../common/catalog";
|
||||
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
||||
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../api/catalog-entity-registry/catalog-entity-registry.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
|
||||
import type { DiRender } from "../test-utils/renderFor";
|
||||
import { renderFor } from "../test-utils/renderFor";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import mockFs from "mock-fs";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
|
||||
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
|
||||
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import { computed } from "mobx";
|
||||
import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
|
||||
mockWindow();
|
||||
jest.mock("electron", () => ({
|
||||
@@ -60,57 +61,57 @@ class MockCatalogEntity extends CatalogEntity {
|
||||
constructor(data: CatalogEntityData, public onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
public onContextMenuOpen(): void | Promise<void> {}
|
||||
public onSettingsOpen(): void | Promise<void> {}
|
||||
function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
|
||||
return new MockCatalogEntity({
|
||||
metadata: {
|
||||
uid: "a_catalogEntity_uid",
|
||||
name: "a catalog entity",
|
||||
labels: {
|
||||
test: "label",
|
||||
},
|
||||
},
|
||||
status: {
|
||||
phase: "",
|
||||
},
|
||||
spec: {},
|
||||
}, onRun);
|
||||
}
|
||||
|
||||
describe("<Catalog />", () => {
|
||||
function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
|
||||
return new MockCatalogEntity({
|
||||
metadata: {
|
||||
uid: "a_catalogEntity_uid",
|
||||
name: "a catalog entity",
|
||||
labels: {
|
||||
test: "label",
|
||||
},
|
||||
},
|
||||
status: {
|
||||
phase: "",
|
||||
},
|
||||
spec: {},
|
||||
}, onRun);
|
||||
}
|
||||
|
||||
let di: DiContainer;
|
||||
let catalogEntityStore: CatalogEntityStore;
|
||||
let catalogEntityRegistry: CatalogEntityRegistry;
|
||||
let emitEvent: (event: AppEvent) => void;
|
||||
let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise<void>>;
|
||||
let catalogEntityItem: MockCatalogEntity;
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(appVersionInjectable);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
mockFs();
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
CatalogEntityDetailRegistry.createInstance();
|
||||
|
||||
render = renderFor(di);
|
||||
onRun = jest.fn();
|
||||
catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
|
||||
catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
UserStore.createInstance(); // TODO: replace with DI
|
||||
|
||||
di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry);
|
||||
di.override(ipcRendererInjectable, () => ({
|
||||
on: jest.fn(),
|
||||
invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
|
||||
} as never));
|
||||
|
||||
emitEvent = jest.fn();
|
||||
|
||||
@@ -119,27 +120,20 @@ describe("<Catalog />", () => {
|
||||
}));
|
||||
|
||||
catalogEntityStore = di.inject(catalogEntityStoreInjectable);
|
||||
Object.assign(catalogEntityStore, {
|
||||
selectedItem: computed(() => catalogEntityItem),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
CatalogEntityDetailRegistry.resetInstance();
|
||||
|
||||
UserStore.resetInstance();
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => {
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
(event) => {
|
||||
expect(event.target.getId()).toBe("a_catalogEntity_uid");
|
||||
@@ -152,22 +146,12 @@ describe("<Catalog />", () => {
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<Catalog />,
|
||||
);
|
||||
render(<Catalog />);
|
||||
|
||||
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||
});
|
||||
|
||||
it("onBeforeRun prevents event => onRun wont be triggered", (done) => {
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
(e) => {
|
||||
setTimeout(() => {
|
||||
@@ -184,14 +168,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnBeforeRun throw an exception => onRun will be triggered", (done) => {
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
@@ -209,13 +185,7 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => {
|
||||
const onRun = jest.fn(() => done());
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
onRun.mockImplementation(() => done());
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
async () => {
|
||||
@@ -229,14 +199,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and prevents event wont be triggered", (done) => {
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
async (e) => {
|
||||
expect(onRun).not.toBeCalled();
|
||||
@@ -256,14 +218,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and reject => onRun will be triggered", (done) => {
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||
.mockImplementation(() => catalogEntityItem);
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
async () => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,13 +12,12 @@ import type { IComputedValue } from "mobx";
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
||||
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||
import { MenuItem, MenuActions } from "../menu";
|
||||
import type { CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||
import type { CatalogEntityContextMenu } from "../../api/catalog-entity";
|
||||
import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
||||
import { CatalogAddButton } from "./catalog-add-button";
|
||||
import { Notifications } from "../notifications";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import type { StorageLayer } from "../../utils";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||
import { CatalogMenu } from "./catalog-menu";
|
||||
@@ -29,8 +28,8 @@ import { Avatar } from "../avatar";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||
import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
|
||||
import getCategoryColumnsInjectable from "./get-category-columns.injectable";
|
||||
import type { GetCategoryColumnsParams, CategoryColumns } from "./columns/get.injectable";
|
||||
import getCategoryColumnsInjectable from "./columns/get.injectable";
|
||||
import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable";
|
||||
import customCategoryViewsInjectable from "./custom-views.injectable";
|
||||
import type { CustomCategoryViewComponents } from "./custom-views";
|
||||
@@ -40,14 +39,18 @@ import catalogRouteParametersInjectable from "./catalog-route-parameters.injecta
|
||||
import { browseCatalogTab } from "./catalog-browse-tab";
|
||||
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
|
||||
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbars/store";
|
||||
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
|
||||
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
import type { Navigate } from "../../navigation/navigate.injectable";
|
||||
import navigateInjectable from "../../navigation/navigate.injectable";
|
||||
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
|
||||
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void; get: () => string };
|
||||
catalogPreviousActiveTabStorage: StorageLayer<string | null>;
|
||||
catalogEntityStore: CatalogEntityStore;
|
||||
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
|
||||
@@ -58,13 +61,15 @@ interface Dependencies {
|
||||
};
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
hotbarStore: HotbarStore;
|
||||
catalogCategoryRegistry: CatalogCategoryRegistry;
|
||||
visitEntityContextMenu: VisitEntityContextMenu;
|
||||
navigate: Navigate;
|
||||
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
|
||||
@observable activeTab?: string;
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
@@ -93,42 +98,50 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.contextMenu = {
|
||||
menuItems: observable.array([]),
|
||||
navigate: this.props.navigate,
|
||||
};
|
||||
const {
|
||||
catalogEntityStore,
|
||||
catalogPreviousActiveTabStorage,
|
||||
catalogCategoryRegistry,
|
||||
} = this.props;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
this.props.catalogEntityStore.watch(),
|
||||
catalogEntityStore.watch(),
|
||||
reaction(() => this.routeActiveTab, async (routeTab) => {
|
||||
catalogPreviousActiveTabStorage.set(this.routeActiveTab);
|
||||
|
||||
try {
|
||||
await when(() => (routeTab === browseCatalogTab || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load
|
||||
const item = catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab);
|
||||
|
||||
runInAction(() => {
|
||||
this.activeTab = routeTab;
|
||||
this.props.catalogEntityStore.activeCategory = item;
|
||||
catalogEntityStore.activeCategory.set(item);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notifications.error(<p>Unknown category: {routeTab}</p>);
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"Unknown category: "}
|
||||
{routeTab}
|
||||
</p>
|
||||
));
|
||||
}
|
||||
}, { fireImmediately: true }),
|
||||
// If active category is filtered out, automatically switch to the first category
|
||||
reaction(() => catalogCategoryRegistry.filteredItems, () => {
|
||||
if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === catalogEntityStore.activeCategory.get()?.getId())) {
|
||||
const item = catalogCategoryRegistry.filteredItems[0];
|
||||
|
||||
runInAction(() => {
|
||||
if (item) {
|
||||
this.activeTab = item.getId();
|
||||
this.props.catalogEntityStore.activeCategory.set(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
// If active category is filtered out, automatically switch to the first category
|
||||
disposeOnUnmount(this, reaction(() => catalogCategoryRegistry.filteredItems, () => {
|
||||
if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === this.props.catalogEntityStore.activeCategory.getId())) {
|
||||
const item = catalogCategoryRegistry.filteredItems[0];
|
||||
|
||||
runInAction(() => {
|
||||
if (item) {
|
||||
this.activeTab = item.getId();
|
||||
this.props.catalogEntityStore.activeCategory = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.props.emitEvent({
|
||||
name: "catalog",
|
||||
action: "open",
|
||||
@@ -144,15 +157,15 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
onDetails = (entity: CatalogEntity) => {
|
||||
if (this.props.catalogEntityStore.selectedItemId) {
|
||||
this.props.catalogEntityStore.selectedItemId = null;
|
||||
if (this.props.catalogEntityStore.selectedItemId.get()) {
|
||||
this.props.catalogEntityStore.selectedItemId.set(undefined);
|
||||
} else {
|
||||
this.props.catalogEntityStore.onRun(entity);
|
||||
}
|
||||
};
|
||||
|
||||
get categories() {
|
||||
return catalogCategoryRegistry.items;
|
||||
return this.props.catalogCategoryRegistry.items;
|
||||
}
|
||||
|
||||
onTabChange = action((tabId: string | null) => {
|
||||
@@ -175,26 +188,25 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
}
|
||||
});
|
||||
|
||||
renderNavigation() {
|
||||
return (
|
||||
<CatalogMenu activeItem={this.activeTab} onItemClick={this.onTabChange} />
|
||||
);
|
||||
}
|
||||
|
||||
renderItemMenu = (entity: CatalogEntity) => {
|
||||
const onOpen = () => {
|
||||
this.contextMenu.menuItems = [];
|
||||
|
||||
entity.onContextMenuOpen(this.contextMenu);
|
||||
this.menuItems.clear();
|
||||
this.props.visitEntityContextMenu(entity, {
|
||||
menuItems: this.menuItems,
|
||||
navigate: this.props.navigate,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuActions onOpen={onOpen}>
|
||||
<MenuItem key="open-details" onClick={() => this.props.catalogEntityStore.selectedItemId = entity.getId()}>
|
||||
<MenuItem
|
||||
key="open-details"
|
||||
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())}
|
||||
>
|
||||
View Details
|
||||
</MenuItem>
|
||||
{
|
||||
this.contextMenu.menuItems
|
||||
this.menuItems
|
||||
.map(this.props.normalizeMenuItem)
|
||||
.map((menuItem, index) => (
|
||||
<MenuItem key={index} onClick={menuItem.onClick}>
|
||||
@@ -231,7 +243,6 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
<Icon
|
||||
small
|
||||
className={styles.pinIcon}
|
||||
material={!isItemInHotbar && "push_pin"}
|
||||
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||
onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(entity) : this.addToHotbar(entity))}
|
||||
@@ -240,15 +251,12 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
);
|
||||
}
|
||||
|
||||
renderViews = () => {
|
||||
const { catalogEntityStore, customCategoryViews } = this.props;
|
||||
const { activeCategory } = catalogEntityStore;
|
||||
|
||||
renderViews = (activeCategory: CatalogCategory | undefined) => {
|
||||
if (!activeCategory) {
|
||||
return this.renderList();
|
||||
return this.renderList(activeCategory);
|
||||
}
|
||||
|
||||
const customViews = customCategoryViews.get()
|
||||
const customViews = this.props.customCategoryViews.get()
|
||||
.get(activeCategory.spec.group)
|
||||
?.get(activeCategory.spec.names.kind);
|
||||
const renderView = ({ View }: CustomCategoryViewComponents, index: number) => (
|
||||
@@ -261,15 +269,14 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
return (
|
||||
<>
|
||||
{customViews?.before.map(renderView)}
|
||||
{this.renderList()}
|
||||
{this.renderList(activeCategory)}
|
||||
{customViews?.after.map(renderView)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderList() {
|
||||
renderList(activeCategory: CatalogCategory | undefined) {
|
||||
const { catalogEntityStore, getCategoryColumns } = this.props;
|
||||
const { activeCategory } = catalogEntityStore;
|
||||
const tableId = activeCategory
|
||||
? `catalog-items-${activeCategory.metadata.name.replace(" ", "")}`
|
||||
: "catalog-items";
|
||||
@@ -279,14 +286,15 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemListLayout
|
||||
<ItemListLayout<CatalogEntity, false>
|
||||
className={styles.Catalog}
|
||||
tableId={tableId}
|
||||
renderHeaderTitle={activeCategory?.metadata.name ?? "Browse All"}
|
||||
isSelectable={false}
|
||||
isConfigurable={true}
|
||||
preloadStores={false}
|
||||
store={catalogEntityStore}
|
||||
getItems={() => catalogEntityStore.entities}
|
||||
getItems={() => catalogEntityStore.entities.get()}
|
||||
customizeTableRowProps={entity => ({
|
||||
disabled: !entity.isEnabled(),
|
||||
})}
|
||||
@@ -302,27 +310,37 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedEntity = this.props.catalogEntityStore.selectedItem;
|
||||
const activeCategory = this.props.catalogEntityStore.activeCategory.get();
|
||||
const selectedItem = this.props.catalogEntityStore.selectedItem.get();
|
||||
|
||||
return (
|
||||
<MainLayout sidebar={this.renderNavigation()}>
|
||||
<MainLayout
|
||||
sidebar={(
|
||||
<CatalogMenu
|
||||
activeTab={this.activeTab}
|
||||
onItemClick={this.onTabChange}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div className={styles.views}>
|
||||
{this.renderViews()}
|
||||
{this.renderViews(activeCategory)}
|
||||
</div>
|
||||
{
|
||||
selectedEntity
|
||||
? <CatalogEntityDetails
|
||||
entity={selectedEntity}
|
||||
hideDetails={() => this.props.catalogEntityStore.selectedItemId = null}
|
||||
onRun={() => this.props.catalogEntityStore.onRun(selectedEntity)}
|
||||
/>
|
||||
: (
|
||||
<RenderDelay>
|
||||
<CatalogAddButton
|
||||
category={this.props.catalogEntityStore.activeCategory}
|
||||
/>
|
||||
</RenderDelay>
|
||||
selectedItem
|
||||
? (
|
||||
<CatalogEntityDetails
|
||||
entity={selectedItem}
|
||||
hideDetails={() => this.props.catalogEntityStore.selectedItemId.set(undefined)}
|
||||
onRun={() => this.props.catalogEntityStore.onRun(selectedItem)}
|
||||
/>
|
||||
)
|
||||
: activeCategory
|
||||
? (
|
||||
<RenderDelay>
|
||||
<CatalogAddButton category={activeCategory} />
|
||||
</RenderDelay>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</MainLayout>
|
||||
);
|
||||
@@ -340,6 +358,8 @@ export const Catalog = withInjectables<Dependencies>(NonInjectedCatalog, {
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
emitEvent: di.inject(appEventBusInjectable).emit,
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
|
||||
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import namedCategoryColumnInjectable from "./named-category.injectable";
|
||||
import defaultCategoryColumnsInjectable from "./default-category.injectable";
|
||||
|
||||
const defaultBrowseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "kind",
|
||||
priority: 5,
|
||||
renderCell: entity => entity.kind,
|
||||
titleProps: {
|
||||
id: "kind",
|
||||
sortBy: "kind",
|
||||
title: "Kind",
|
||||
},
|
||||
sortCallback: entity => entity.kind,
|
||||
},
|
||||
];
|
||||
|
||||
const browseAllColumnsInjectable = getInjectable({
|
||||
id: "browse-all-columns",
|
||||
instantiate: (di) => [
|
||||
...defaultBrowseAllColumns,
|
||||
di.inject(namedCategoryColumnInjectable),
|
||||
...di.inject(defaultCategoryColumnsInjectable),
|
||||
],
|
||||
});
|
||||
|
||||
export default browseAllColumnsInjectable;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import styles from "../catalog.module.scss";
|
||||
import React from "react";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
|
||||
import { KubeObject } from "../../../../common/k8s-api/kube-object";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import getLabelBadgesInjectable from "../get-label-badges.injectable";
|
||||
|
||||
const defaultCategoryColumnsInjectable = getInjectable({
|
||||
id: "default-category-columns",
|
||||
instantiate: (di): RegisteredAdditionalCategoryColumn[] => {
|
||||
const getLabelBadges = di.inject(getLabelBadgesInjectable);
|
||||
|
||||
return [
|
||||
{
|
||||
id: "source",
|
||||
priority: 10,
|
||||
renderCell: entity => entity.getSource(),
|
||||
titleProps: {
|
||||
title: "Source",
|
||||
className: styles.sourceCell,
|
||||
id: "source",
|
||||
sortBy: "source",
|
||||
},
|
||||
sortCallback: entity => entity.getSource(),
|
||||
searchFilter: entity => `source=${entity.getSource()}`,
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
priority: 20,
|
||||
renderCell: getLabelBadges,
|
||||
titleProps: {
|
||||
id: "labels",
|
||||
title: "Labels",
|
||||
className: `${styles.labelsCell} scrollable`,
|
||||
},
|
||||
searchFilter: entity => KubeObject.stringifyLabels(entity.metadata.labels),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
priority: 30,
|
||||
renderCell: entity => (
|
||||
<span key="phase" className={entity.status.phase}>
|
||||
{entity.status.phase}
|
||||
</span>
|
||||
),
|
||||
titleProps: {
|
||||
title: "Status",
|
||||
className: styles.statusCell,
|
||||
id: "status",
|
||||
sortBy: "status",
|
||||
},
|
||||
searchFilter: entity => entity.status.phase,
|
||||
sortCallback: entity => entity.status.phase,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export default defaultCategoryColumnsInjectable;
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { orderBy } from "lodash";
|
||||
import type { CatalogCategory, CatalogEntity } from "../../../../common/catalog";
|
||||
import type { ItemListLayoutProps } from "../../item-object-list";
|
||||
import browseAllColumnsInjectable from "./browse-all.injectable";
|
||||
import getColumnsForCategoryInjectable from "./specific-category.injectable";
|
||||
|
||||
export interface GetCategoryColumnsParams {
|
||||
activeCategory: CatalogCategory | null | undefined;
|
||||
}
|
||||
|
||||
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
|
||||
export type GetCategoryColumns = (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
const getCategoryColumnsInjectable = getInjectable({
|
||||
id: "get-category-columns",
|
||||
|
||||
instantiate: (di): GetCategoryColumns => {
|
||||
const getColumnsForCategory = di.inject(getColumnsForCategoryInjectable);
|
||||
const browseAllColumns = di.inject(browseAllColumnsInjectable);
|
||||
|
||||
return ({ activeCategory }) => {
|
||||
const allRegistrations = orderBy(
|
||||
activeCategory
|
||||
? getColumnsForCategory(activeCategory)
|
||||
: browseAllColumns,
|
||||
"priority",
|
||||
"asc",
|
||||
);
|
||||
|
||||
const sortingCallbacks: CategoryColumns["sortingCallbacks"] = {};
|
||||
const searchFilters: CategoryColumns["searchFilters"] = [];
|
||||
const renderTableHeader: CategoryColumns["renderTableHeader"] = [];
|
||||
const tableRowRenderers: ((entity: CatalogEntity) => React.ReactNode)[] = [];
|
||||
|
||||
for (const registration of allRegistrations) {
|
||||
if (registration.sortCallback) {
|
||||
sortingCallbacks[registration.id] = registration.sortCallback;
|
||||
}
|
||||
|
||||
if (registration.searchFilter) {
|
||||
searchFilters.push(registration.searchFilter);
|
||||
}
|
||||
|
||||
tableRowRenderers.push(registration.renderCell);
|
||||
renderTableHeader.push(registration.titleProps);
|
||||
}
|
||||
|
||||
return {
|
||||
sortingCallbacks,
|
||||
renderTableHeader,
|
||||
renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)),
|
||||
searchFilters,
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default getCategoryColumnsInjectable;
|
||||
+10
-12
@@ -3,15 +3,15 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import styles from "./catalog.module.scss";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { Avatar } from "../avatar";
|
||||
import { Icon } from "../icon";
|
||||
import styles from "../catalog.module.scss";
|
||||
import type { CatalogEntity } from "../../../../common/catalog";
|
||||
import { prevDefault } from "../../../utils";
|
||||
import { Avatar } from "../../avatar";
|
||||
import { Icon } from "../../icon";
|
||||
import React from "react";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
|
||||
import type { HotbarStore } from "../../../../common/hotbars/store";
|
||||
|
||||
const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) => {
|
||||
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
|
||||
@@ -37,7 +37,6 @@ const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) =
|
||||
<Icon
|
||||
small
|
||||
className={styles.pinIcon}
|
||||
material={!isItemInHotbar && "push_pin"}
|
||||
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||
onClick={onClick}
|
||||
@@ -46,8 +45,7 @@ const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) =
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const nameCategoryColumnInjectable = getInjectable({
|
||||
const namedCategoryColumnInjectable = getInjectable({
|
||||
id: "name-category-column",
|
||||
instantiate: (di): RegisteredAdditionalCategoryColumn => ({
|
||||
id: "name",
|
||||
@@ -64,4 +62,4 @@ const nameCategoryColumnInjectable = getInjectable({
|
||||
}),
|
||||
});
|
||||
|
||||
export default nameCategoryColumnInjectable;
|
||||
export default namedCategoryColumnInjectable;
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { CatalogCategory } from "../../../api/catalog-entity";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
|
||||
import categoryColumnsInjectable from "../custom-category-columns.injectable";
|
||||
import defaultCategoryColumnsInjectable from "./default-category.injectable";
|
||||
import namedCategoryColumnInjectable from "./named-category.injectable";
|
||||
|
||||
export type GetColumnsForCategory = (activeCategory: CatalogCategory) => RegisteredAdditionalCategoryColumn[];
|
||||
|
||||
const getColumnsForCategoryInjectable = getInjectable({
|
||||
id: "get-columns-for-category",
|
||||
instantiate: (di): GetColumnsForCategory => {
|
||||
const extensionColumns = di.inject(categoryColumnsInjectable);
|
||||
const defaultCategoryColumns = di.inject(defaultCategoryColumnsInjectable);
|
||||
const nameCategoryColumn = di.inject(namedCategoryColumnInjectable);
|
||||
|
||||
return (activeCategory) => {
|
||||
const fromExtensions = (
|
||||
extensionColumns
|
||||
.get()
|
||||
.get(activeCategory.spec.group)
|
||||
?.get(activeCategory.spec.names.kind)
|
||||
?? []
|
||||
);
|
||||
const fromCategory = activeCategory.spec.displayColumns?.map(({ priority = 50, ...column }) => ({
|
||||
priority,
|
||||
...column,
|
||||
})) ?? defaultCategoryColumns;
|
||||
|
||||
return [
|
||||
nameCategoryColumn,
|
||||
...fromExtensions,
|
||||
...fromCategory,
|
||||
];
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default getColumnsForCategoryInjectable;
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { orderBy } from "lodash";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
||||
import type { ItemListLayoutProps } from "../item-object-list";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import categoryColumnsInjectable from "./custom-category-columns.injectable";
|
||||
import { defaultCategoryColumns, browseAllColumns } from "./internal-category-columns";
|
||||
import nameCategoryColumnInjectable from "./name-category-column.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>;
|
||||
nameCategoryColumn: RegisteredAdditionalCategoryColumn;
|
||||
}
|
||||
|
||||
export interface GetCategoryColumnsParams {
|
||||
activeCategory: CatalogCategory | null | undefined;
|
||||
}
|
||||
|
||||
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
|
||||
|
||||
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>, nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
|
||||
const fromExtensions = (
|
||||
extensionColumns
|
||||
.get()
|
||||
.get(activeCategory.spec.group)
|
||||
?.get(activeCategory.spec.names.kind)
|
||||
?? []
|
||||
);
|
||||
const fromCategory = activeCategory.spec.displayColumns?.map(({ priority = 50, ...column }) => ({
|
||||
priority,
|
||||
...column,
|
||||
})) ?? defaultCategoryColumns;
|
||||
|
||||
return [
|
||||
nameCategoryColumn,
|
||||
...fromExtensions,
|
||||
...fromCategory,
|
||||
];
|
||||
}
|
||||
|
||||
function getBrowseAllColumns(nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
|
||||
return [
|
||||
...browseAllColumns,
|
||||
nameCategoryColumn,
|
||||
...defaultCategoryColumns,
|
||||
];
|
||||
}
|
||||
|
||||
const getCategoryColumns = ({ extensionColumns, nameCategoryColumn }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
|
||||
const allRegistrations = orderBy(
|
||||
activeCategory
|
||||
? getSpecificCategoryColumns(activeCategory, extensionColumns, nameCategoryColumn)
|
||||
: getBrowseAllColumns(nameCategoryColumn),
|
||||
"priority",
|
||||
"asc",
|
||||
);
|
||||
|
||||
const sortingCallbacks: CategoryColumns["sortingCallbacks"] = {};
|
||||
const searchFilters: CategoryColumns["searchFilters"] = [];
|
||||
const renderTableHeader: CategoryColumns["renderTableHeader"] = [];
|
||||
const tableRowRenderers: ((entity: CatalogEntity) => React.ReactNode)[] = [];
|
||||
|
||||
for (const registration of allRegistrations) {
|
||||
if (registration.sortCallback) {
|
||||
sortingCallbacks[registration.id] = registration.sortCallback;
|
||||
}
|
||||
|
||||
if (registration.searchFilter) {
|
||||
searchFilters.push(registration.searchFilter);
|
||||
}
|
||||
|
||||
tableRowRenderers.push(registration.renderCell);
|
||||
renderTableHeader.push(registration.titleProps);
|
||||
}
|
||||
|
||||
return {
|
||||
sortingCallbacks,
|
||||
renderTableHeader,
|
||||
renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)),
|
||||
searchFilters,
|
||||
};
|
||||
};
|
||||
|
||||
const getCategoryColumnsInjectable = getInjectable({
|
||||
id: "get-category-columns",
|
||||
|
||||
instantiate: (di) => getCategoryColumns({
|
||||
extensionColumns: di.inject(categoryColumnsInjectable),
|
||||
nameCategoryColumn: di.inject(nameCategoryColumnInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default getCategoryColumnsInjectable;
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import styles from "./catalog.module.scss";
|
||||
import React from "react";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import searchUrlPageParamInjectable from "../input/search-url-page-param.injectable";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { Badge } from "../badge";
|
||||
|
||||
export type GetLabelBadges = (entity: CatalogEntity, onClick?: (evt: React.MouseEvent<any, MouseEvent>) => void) => JSX.Element[];
|
||||
|
||||
const getLabelBadgesInjectable = getInjectable({
|
||||
id: "get-label-badges",
|
||||
instantiate: (di): GetLabelBadges => {
|
||||
const searchUrlParam = di.inject(searchUrlPageParamInjectable);
|
||||
|
||||
return (entity, onClick) => (
|
||||
KubeObject.stringifyLabels(entity.metadata.labels)
|
||||
.map(label => (
|
||||
<Badge
|
||||
scrollable
|
||||
className={styles.badge}
|
||||
key={label}
|
||||
label={label}
|
||||
title={label}
|
||||
onClick={(event) => {
|
||||
searchUrlParam.set(label);
|
||||
onClick?.(event);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
expandable={false}
|
||||
/>
|
||||
))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default getLabelBadgesInjectable;
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import styles from "./catalog.module.scss";
|
||||
import React from "react";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { Badge } from "../badge";
|
||||
import { searchUrlParam } from "../input";
|
||||
|
||||
/**
|
||||
* @param entity The entity to render badge labels for
|
||||
*/
|
||||
export function getLabelBadges(entity: CatalogEntity, onClick?: (evt: React.MouseEvent<any, MouseEvent>) => void) {
|
||||
return KubeObject.stringifyLabels(entity.metadata.labels)
|
||||
.map(label => (
|
||||
<Badge
|
||||
scrollable
|
||||
className={styles.badge}
|
||||
key={label}
|
||||
label={label}
|
||||
title={label}
|
||||
onClick={(event) => {
|
||||
searchUrlParam.set(label);
|
||||
onClick?.(event);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
expandable={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
@@ -9,17 +9,17 @@ import { MenuItem } from "../menu";
|
||||
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbars/store";
|
||||
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
interface HotbarToggleMenuItemProps {
|
||||
entity: CatalogEntity;
|
||||
addContent: ReactNode;
|
||||
removeContent: ReactNode;
|
||||
entity: CatalogEntity;
|
||||
addContent: ReactNode;
|
||||
removeContent: ReactNode;
|
||||
}
|
||||
|
||||
function NonInjectedHotbarToggleMenuItem({
|
||||
@@ -31,15 +31,17 @@ function NonInjectedHotbarToggleMenuItem({
|
||||
const [itemInHotbar, setItemInHotbar] = useState(hotbarStore.isAddedToActive(entity));
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => {
|
||||
if (itemInHotbar) {
|
||||
hotbarStore.removeFromHotbar(entity.getId());
|
||||
setItemInHotbar(false);
|
||||
} else {
|
||||
hotbarStore.addToHotbar(entity);
|
||||
setItemInHotbar(true);
|
||||
}
|
||||
}}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (itemInHotbar) {
|
||||
hotbarStore.removeFromHotbar(entity.getId());
|
||||
setItemInHotbar(false);
|
||||
} else {
|
||||
hotbarStore.addToHotbar(entity);
|
||||
setItemInHotbar(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{itemInHotbar ? removeContent : addContent }
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import styles from "./catalog.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
|
||||
export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "kind",
|
||||
priority: 5,
|
||||
renderCell: entity => entity.kind,
|
||||
titleProps: {
|
||||
id: "kind",
|
||||
sortBy: "kind",
|
||||
title: "Kind",
|
||||
},
|
||||
sortCallback: entity => entity.kind,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultCategoryColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "source",
|
||||
priority: 10,
|
||||
renderCell: entity => entity.getSource(),
|
||||
titleProps: {
|
||||
title: "Source",
|
||||
className: styles.sourceCell,
|
||||
id: "source",
|
||||
sortBy: "source",
|
||||
},
|
||||
sortCallback: entity => entity.getSource(),
|
||||
searchFilter: entity => `source=${entity.getSource()}`,
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
priority: 20,
|
||||
renderCell: getLabelBadges,
|
||||
titleProps: {
|
||||
id: "labels",
|
||||
title: "Labels",
|
||||
className: `${styles.labelsCell} scrollable`,
|
||||
},
|
||||
searchFilter: entity => KubeObject.stringifyLabels(entity.metadata.labels),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
priority: 30,
|
||||
renderCell: entity => (
|
||||
<span key="phase" className={entity.status.phase}>
|
||||
{entity.status.phase}
|
||||
</span>
|
||||
),
|
||||
titleProps: {
|
||||
title: "Status",
|
||||
className: styles.statusCell,
|
||||
id: "status",
|
||||
sortBy: "status",
|
||||
},
|
||||
searchFilter: entity => entity.status.phase,
|
||||
sortCallback: entity => entity.status.phase,
|
||||
},
|
||||
];
|
||||
@@ -11,23 +11,31 @@ import { computed, makeObservable } from "mobx";
|
||||
import { Icon } from "../icon";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import type { ItemObject } from "../../../common/item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { NodeStore } from "../+nodes/store";
|
||||
import type { EventStore } from "../+events/store";
|
||||
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import eventStoreInjectable from "../+events/store.injectable";
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
import type { PageParam } from "../../navigation";
|
||||
import type { ToggleKubeDetailsPane } from "../kube-detail-params/toggle-details.injectable";
|
||||
import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable";
|
||||
import toggleKubeDetailsPaneInjectable from "../kube-detail-params/toggle-details.injectable";
|
||||
|
||||
export interface ClusterIssuesProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IWarning extends ItemObject {
|
||||
interface Warning extends ItemObject {
|
||||
kind: string;
|
||||
message: string;
|
||||
message: string | undefined;
|
||||
selfLink: string;
|
||||
renderAge: () => React.ReactElement;
|
||||
ageMs: number;
|
||||
@@ -39,16 +47,25 @@ enum sortBy {
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
nodeStore: NodeStore;
|
||||
eventStore: EventStore;
|
||||
apiManager: ApiManager;
|
||||
kubeSelectedUrlParam: PageParam<string>;
|
||||
toggleKubeDetailsPane: ToggleKubeDetailsPane;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
constructor(props: ClusterIssuesProps) {
|
||||
class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Dependencies> {
|
||||
constructor(props: ClusterIssuesProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get warnings(): IWarning[] {
|
||||
@computed get warnings(): Warning[] {
|
||||
return [
|
||||
...nodesStore.items.flatMap(node => (
|
||||
...this.props.nodeStore.items.flatMap(node => (
|
||||
node.getWarningConditions()
|
||||
.map(({ message }) => ({
|
||||
selfLink: node.selfLink,
|
||||
@@ -60,21 +77,27 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
ageMs: -node.getCreationTimestamp(),
|
||||
}))
|
||||
)),
|
||||
...eventStore.getWarnings().map(warning => ({
|
||||
...this.props.eventStore.getWarnings().map(warning => ({
|
||||
getId: () => warning.involvedObject.uid,
|
||||
getName: () => warning.involvedObject.name,
|
||||
renderAge: () => <KubeObjectAge key="age" object={warning} />,
|
||||
ageMs: -warning.getCreationTimestamp(),
|
||||
message: warning.message,
|
||||
kind: warning.kind,
|
||||
selfLink: apiManager.lookupApiLink(warning.involvedObject, warning),
|
||||
selfLink: this.props.apiManager.lookupApiLink(warning.involvedObject, warning),
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
getTableRow = (uid: string) => {
|
||||
const { warnings } = this;
|
||||
const { kubeSelectedUrlParam, toggleKubeDetailsPane: toggleDetails } = this.props;
|
||||
const warning = warnings.find(warn => warn.getId() == uid);
|
||||
|
||||
if (!warning) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { getId, getName, message, kind, selfLink, renderAge } = warning;
|
||||
|
||||
return (
|
||||
@@ -85,7 +108,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
onClick={prevDefault(() => toggleDetails(selfLink))}
|
||||
>
|
||||
<TableCell className={styles.message}>
|
||||
{message}
|
||||
{message ?? "<unknown>"}
|
||||
</TableCell>
|
||||
<TableCell className={styles.object}>
|
||||
{getName()}
|
||||
@@ -103,7 +126,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
renderContent() {
|
||||
const { warnings } = this;
|
||||
|
||||
if (!eventStore.isLoaded) {
|
||||
if (!this.props.eventStore.isLoaded) {
|
||||
return (
|
||||
<Spinner center/>
|
||||
);
|
||||
@@ -112,7 +135,12 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
if (!warnings.length) {
|
||||
return (
|
||||
<div className={cssNames(styles.noIssues, "flex column box grow gaps align-center justify-center")}>
|
||||
<Icon className={styles.Icon} material="check" big sticker/>
|
||||
<Icon
|
||||
className={styles.Icon}
|
||||
material="check"
|
||||
big
|
||||
sticker
|
||||
/>
|
||||
<p className={styles.title}>No issues found</p>
|
||||
<p>Everything is fine in the Cluster</p>
|
||||
</div>
|
||||
@@ -122,7 +150,8 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
return (
|
||||
<>
|
||||
<SubHeader className={styles.SubHeader}>
|
||||
<Icon material="error_outline"/> Warnings: {warnings.length}
|
||||
<Icon material="error_outline"/>
|
||||
{` Warnings: ${warnings.length}`}
|
||||
</SubHeader>
|
||||
<Table
|
||||
tableId="cluster_issues"
|
||||
@@ -137,7 +166,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
|
||||
sortSyncWithUrl={false}
|
||||
getTableRow={this.getTableRow}
|
||||
className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)}
|
||||
className={cssNames("box grow", this.props.themeStore.activeTheme.type)}
|
||||
>
|
||||
<TableHead nowrap>
|
||||
<TableCell className="message">Message</TableCell>
|
||||
@@ -158,3 +187,15 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ClusterIssues = withInjectables<Dependencies, ClusterIssuesProps>(NonInjectedClusterIssues, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
eventStore: di.inject(eventStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
kubeSelectedUrlParam: di.inject(kubeSelectedUrlParamInjectable),
|
||||
toggleKubeDetailsPane: di.inject(toggleKubeDetailsPaneInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -5,20 +5,25 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import type { NodeStore } from "../+nodes/store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Radio, RadioGroup } from "../radio";
|
||||
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { MetricNodeRole, MetricType } from "./cluster-overview-store/cluster-overview-store";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore;
|
||||
nodeStore: NodeStore;
|
||||
}
|
||||
|
||||
const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore }: Dependencies) => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const NonInjectedClusterMetricSwitchers = observer(({
|
||||
clusterOverviewStore,
|
||||
nodeStore,
|
||||
}: Dependencies) => {
|
||||
const { masterNodes, workerNodes } = nodeStore;
|
||||
const metricsValues = clusterOverviewStore.getMetricsValues(clusterOverviewStore.metrics);
|
||||
const disableRoles = !masterNodes.length || !workerNodes.length;
|
||||
const disableMetrics = !metricsValues.length;
|
||||
@@ -30,7 +35,7 @@ const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore }: De
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
||||
value={clusterOverviewStore.metricNodeRole}
|
||||
onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric}
|
||||
onChange={metric => clusterOverviewStore.metricNodeRole = metric}
|
||||
>
|
||||
<Radio label="Master" value={MetricNodeRole.MASTER}/>
|
||||
<Radio label="Worker" value={MetricNodeRole.WORKER}/>
|
||||
@@ -41,7 +46,7 @@ const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore }: De
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
||||
value={clusterOverviewStore.metricType}
|
||||
onChange={(value: MetricType) => clusterOverviewStore.metricType = value}
|
||||
onChange={value => clusterOverviewStore.metricType = value}
|
||||
>
|
||||
<Radio label="CPU" value={MetricType.CPU}/>
|
||||
<Radio label="Memory" value={MetricType.MEMORY}/>
|
||||
@@ -51,13 +56,10 @@ const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore }: De
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterMetricSwitchers = withInjectables<Dependencies>(
|
||||
NonInjectedClusterMetricSwitchers,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const ClusterMetricSwitchers = withInjectables<Dependencies>(NonInjectedClusterMetricSwitchers, {
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import styles from "./cluster-metrics.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ChartOptions, ChartPoint } from "chart.js";
|
||||
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
||||
@@ -13,19 +13,19 @@ import { MetricType } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { BarChart } from "../chart";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ZebraStripes } from "../chart/zebra-stripes.plugin";
|
||||
import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
|
||||
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterOverviewStoreInjectable
|
||||
from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore;
|
||||
}
|
||||
|
||||
const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => {
|
||||
const [plugins] = useState([new ZebraStripesPlugin()]);
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics);
|
||||
const metricValues = getMetricsValues(metrics);
|
||||
const colors = { cpu: "#3D90CE", memory: "#C93DCE" };
|
||||
@@ -52,9 +52,13 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
if (!index) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
return value.y.toString();
|
||||
const value = data.datasets?.[0].data?.[index] as ChartPoint;
|
||||
|
||||
return value.y?.toString() ?? "<unknown>";
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -71,7 +75,11 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
if (!index) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
const value = data.datasets?.[0].data?.[index] as ChartPoint;
|
||||
|
||||
return bytesToUnits(parseInt(value.y as string), { precision: 3 });
|
||||
},
|
||||
@@ -96,7 +104,7 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType
|
||||
data={{ datasets }}
|
||||
timeLabelStep={5}
|
||||
showLegend={false}
|
||||
plugins={[ZebraStripes]}
|
||||
plugins={plugins}
|
||||
className={styles.chart}
|
||||
/>
|
||||
);
|
||||
|
||||
+21
-25
@@ -3,40 +3,36 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type {
|
||||
ClusterOverviewStorageState } from "./cluster-overview-store";
|
||||
import {
|
||||
ClusterOverviewStore,
|
||||
MetricNodeRole,
|
||||
MetricType,
|
||||
} from "./cluster-overview-store";
|
||||
|
||||
import type { ClusterOverviewStorageState } from "./cluster-overview-store";
|
||||
import { ClusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview-store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
import apiManagerInjectable from "../../kube-object-menu/dependencies/api-manager.injectable";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import clusterApiInjectable from "../../../../common/k8s-api/endpoints/cluster.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
|
||||
import assert from "assert";
|
||||
import nodeStoreInjectable from "../../+nodes/store.injectable";
|
||||
|
||||
const clusterOverviewStoreInjectable = getInjectable({
|
||||
id: "cluster-overview-store",
|
||||
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "clusterOverviewStore is only available in certain environments");
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
const clusterApi = di.inject(clusterApiInjectable);
|
||||
|
||||
const storage = createStorage<ClusterOverviewStorageState>(
|
||||
"cluster_overview",
|
||||
{
|
||||
metricType: MetricType.CPU, // setup defaults
|
||||
metricNodeRole: MetricNodeRole.WORKER,
|
||||
},
|
||||
);
|
||||
|
||||
const store = new ClusterOverviewStore({
|
||||
storage,
|
||||
});
|
||||
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
apiManager.registerStore(store);
|
||||
|
||||
return store;
|
||||
return new ClusterOverviewStore({
|
||||
storage: createStorage<ClusterOverviewStorageState>(
|
||||
"cluster_overview",
|
||||
{
|
||||
metricType: MetricType.CPU, // setup defaults
|
||||
metricNodeRole: MetricNodeRole.WORKER,
|
||||
},
|
||||
),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
}, clusterApi);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default clusterOverviewStoreInjectable;
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
import { action, observable, reaction, when, makeObservable } from "mobx";
|
||||
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
import type { Cluster } from "../../../../common/k8s-api/endpoints";
|
||||
import { clusterApi, getMetricsByNodeNames, type IClusterMetrics } from "../../../../common/k8s-api/endpoints";
|
||||
import type { StorageHelper } from "../../../utils";
|
||||
import type { Cluster, ClusterApi } from "../../../../common/k8s-api/endpoints";
|
||||
import { getMetricsByNodeNames, type ClusterMetricData } from "../../../../common/k8s-api/endpoints";
|
||||
import type { StorageLayer } from "../../../utils";
|
||||
import { autoBind } from "../../../utils";
|
||||
import { type IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { nodesStore } from "../../+nodes/nodes.store";
|
||||
import type { NodeStore } from "../../+nodes/store";
|
||||
|
||||
export enum MetricType {
|
||||
MEMORY = "memory",
|
||||
@@ -27,14 +27,13 @@ export interface ClusterOverviewStorageState {
|
||||
metricNodeRole: MetricNodeRole;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
storage: StorageHelper<ClusterOverviewStorageState>;
|
||||
interface ClusterOverviewStoreDependencies {
|
||||
readonly storage: StorageLayer<ClusterOverviewStorageState>;
|
||||
readonly nodeStore: NodeStore;
|
||||
}
|
||||
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState {
|
||||
api = clusterApi;
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> implements ClusterOverviewStorageState {
|
||||
@observable metrics: Partial<ClusterMetricData> = {};
|
||||
@observable metricsLoaded = false;
|
||||
|
||||
get metricType(): MetricType {
|
||||
@@ -53,8 +52,8 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
this.dependencies.storage.merge({ metricNodeRole: value });
|
||||
}
|
||||
|
||||
constructor(private dependencies: Dependencies ) {
|
||||
super();
|
||||
constructor(protected readonly dependencies: ClusterOverviewStoreDependencies, api: ClusterApi) {
|
||||
super(api);
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
|
||||
@@ -71,8 +70,8 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
});
|
||||
|
||||
// check which node type to select
|
||||
reaction(() => nodesStore.items.length, () => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
reaction(() => this.dependencies.nodeStore.items.length, () => {
|
||||
const { masterNodes, workerNodes } = this.dependencies.nodeStore;
|
||||
|
||||
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
|
||||
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
|
||||
@@ -81,15 +80,15 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
|
||||
@action
|
||||
async loadMetrics(params?: IMetricsReqParams) {
|
||||
await when(() => nodesStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
await when(() => this.dependencies.nodeStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = this.dependencies.nodeStore;
|
||||
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
||||
|
||||
this.metrics = await getMetricsByNodeNames(nodes.map(node => node.getName()), params);
|
||||
this.metricsLoaded = true;
|
||||
}
|
||||
|
||||
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
|
||||
getMetricsValues(source: Partial<ClusterMetricData>): [number, string][] {
|
||||
switch (this.metricType) {
|
||||
case MetricType.CPU:
|
||||
return normalizeMetrics(source.cpuUsage).data.result[0].values;
|
||||
|
||||
@@ -8,29 +8,36 @@ import styles from "./cluster-overview.module.scss";
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import type { Disposer } from "../../utils";
|
||||
import { getHostedClusterId, interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout-2";
|
||||
import type { NodeStore } from "../+nodes/store";
|
||||
import type { PodStore } from "../+workloads-pods/store";
|
||||
import { interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { getActiveClusterEntity } from "../../api/catalog/entity/legacy-globals";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import type { EventStore } from "../+events/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable";
|
||||
import assert from "assert";
|
||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||
import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
import eventStoreInjectable from "../+events/store.injectable";
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer;
|
||||
subscribeStores: SubscribeStores;
|
||||
clusterOverviewStore: ClusterOverviewStore;
|
||||
hostedCluster: Cluster;
|
||||
podStore: PodStore;
|
||||
eventStore: EventStore;
|
||||
nodeStore: NodeStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -38,9 +45,7 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
private metricPoller = interval(60, () => this.loadMetrics());
|
||||
|
||||
loadMetrics() {
|
||||
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
|
||||
if (cluster.available) {
|
||||
if (this.props.hostedCluster.available) {
|
||||
this.props.clusterOverviewStore.loadMetrics();
|
||||
}
|
||||
}
|
||||
@@ -50,9 +55,9 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
eventStore,
|
||||
nodesStore,
|
||||
this.props.podStore,
|
||||
this.props.eventStore,
|
||||
this.props.nodeStore,
|
||||
]),
|
||||
|
||||
reaction(
|
||||
@@ -66,7 +71,7 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
this.metricPoller.stop();
|
||||
}
|
||||
|
||||
renderMetrics(isMetricsHidden: boolean) {
|
||||
renderMetrics(isMetricsHidden?: boolean) {
|
||||
if (isMetricsHidden) {
|
||||
return null;
|
||||
}
|
||||
@@ -79,7 +84,7 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
);
|
||||
}
|
||||
|
||||
renderClusterOverview(isLoaded: boolean, isMetricsHidden: boolean) {
|
||||
renderClusterOverview(isLoaded: boolean, isMetricsHidden?: boolean) {
|
||||
if (!isLoaded) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
@@ -93,7 +98,8 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLoaded = nodesStore.isLoaded && eventStore.isLoaded;
|
||||
const { eventStore, nodeStore } = this.props;
|
||||
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
|
||||
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Cluster);
|
||||
|
||||
return (
|
||||
@@ -106,13 +112,19 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const ClusterOverview = withInjectables<Dependencies>(
|
||||
NonInjectedClusterOverview,
|
||||
export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, {
|
||||
getProps: (di) => {
|
||||
const hostedCluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
assert(hostedCluster, "Only allowed to renderer ClusterOverview within cluster frame");
|
||||
|
||||
return {
|
||||
subscribeStores: di.inject(subscribeStoresInjectable),
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
hostedCluster,
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
eventStore: di.inject(eventStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -11,15 +11,17 @@ import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-over
|
||||
import { MetricNodeRole } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Icon } from "../icon";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import type { NodeStore } from "../+nodes/store";
|
||||
import type { PieChartData } from "../chart";
|
||||
import { PieChart } from "../chart";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
|
||||
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
|
||||
@@ -27,9 +29,15 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore;
|
||||
nodeStore: NodeStore;
|
||||
themeStore: ThemeStore;
|
||||
}
|
||||
|
||||
const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependencies) => {
|
||||
const NonInjectedClusterPieCharts = observer(({
|
||||
clusterOverviewStore,
|
||||
nodeStore,
|
||||
themeStore,
|
||||
}: Dependencies) => {
|
||||
const renderLimitWarning = () => {
|
||||
return (
|
||||
<div className="node-warning flex gaps align-center">
|
||||
@@ -46,7 +54,7 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen
|
||||
const { podUsage, podAllocatableCapacity, podCapacity } = data;
|
||||
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
|
||||
const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity;
|
||||
const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor;
|
||||
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null;
|
||||
const cpuData: PieChartData = {
|
||||
@@ -210,7 +218,7 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen
|
||||
};
|
||||
|
||||
const renderContent = ({ metricNodeRole, metricsLoaded }: ClusterOverviewStore) => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const { masterNodes, workerNodes } = nodeStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
|
||||
if (!nodes.length) {
|
||||
@@ -249,12 +257,10 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterPieCharts = withInjectables<Dependencies>(
|
||||
NonInjectedClusterPieCharts,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedClusterPieCharts, {
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -12,51 +12,96 @@ import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { IHpaMetric } from "../../../common/k8s-api/endpoints/hpa.api";
|
||||
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints/hpa.api";
|
||||
import type { HorizontalPodAutoscalerMetricSpec, HorizontalPodAutoscalerMetricTarget } from "../../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api";
|
||||
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
||||
|
||||
export interface HpaDetailsProps extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
apiManager: ApiManager;
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependencies> {
|
||||
private renderTargetLink(target: HorizontalPodAutoscalerMetricTarget | undefined) {
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { object: hpa, apiManager, getDetailsUrl } = this.props;
|
||||
const { kind, name } = target;
|
||||
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
|
||||
|
||||
return (
|
||||
<>
|
||||
on
|
||||
<Link to={objectUrl}>
|
||||
{`${kind}/${name}`}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderMetrics() {
|
||||
const { object: hpa } = this.props;
|
||||
|
||||
const renderName = (metric: IHpaMetric) => {
|
||||
const renderName = (metric: HorizontalPodAutoscalerMetricSpec) => {
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.ContainerResource:
|
||||
|
||||
// fallthrough
|
||||
case HpaMetricType.Resource: {
|
||||
const addition = metric.resource.targetAverageUtilization
|
||||
const metricSpec = metric.resource ?? metric.containerResource;
|
||||
const addition = metricSpec.targetAverageUtilization
|
||||
? "(as a percentage of request)"
|
||||
: "";
|
||||
|
||||
return <>Resource {metric.resource.name} on Pods {addition}</>;
|
||||
}
|
||||
case HpaMetricType.Pods:
|
||||
return <>{metric.pods.metricName} on Pods</>;
|
||||
|
||||
case HpaMetricType.Object: {
|
||||
const { target } = metric.object;
|
||||
const { kind, name } = target;
|
||||
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
|
||||
|
||||
return (
|
||||
<>
|
||||
{metric.object.metricName} on{" "}
|
||||
<Link to={objectUrl}>{kind}/{name}</Link>
|
||||
Resource
|
||||
{metricSpec.name}
|
||||
{" "}
|
||||
on Pods
|
||||
{addition}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case HpaMetricType.Pods:
|
||||
return (
|
||||
<>
|
||||
{metric.pods.metricName}
|
||||
{" "}
|
||||
on Pods
|
||||
</>
|
||||
);
|
||||
|
||||
case HpaMetricType.Object: {
|
||||
return (
|
||||
<>
|
||||
{metric.object.metricName}
|
||||
{" "}
|
||||
{this.renderTargetLink(metric.object.target)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case HpaMetricType.External:
|
||||
return (
|
||||
<>
|
||||
{metric.external.metricName} on{" "}
|
||||
{JSON.stringify(metric.external.selector)}
|
||||
{metric.external.metricName}
|
||||
{" "}
|
||||
on
|
||||
{" "}
|
||||
{JSON.stringify(metric.external.metricSelector)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -82,7 +127,7 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { object: hpa } = this.props;
|
||||
const { object: hpa, apiManager, getDetailsUrl } = this.props;
|
||||
|
||||
if (!hpa) {
|
||||
return null;
|
||||
@@ -103,7 +148,9 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
<DrawerItem name="Reference">
|
||||
{scaleTargetRef && (
|
||||
<Link to={getDetailsUrl(apiManager.lookupApiLink(scaleTargetRef, hpa))}>
|
||||
{scaleTargetRef.kind}/{scaleTargetRef.name}
|
||||
{scaleTargetRef.kind}
|
||||
/
|
||||
{scaleTargetRef.name}
|
||||
</Link>
|
||||
)}
|
||||
</DrawerItem>
|
||||
@@ -120,19 +167,20 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
{hpa.getReplicas()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Status" className="status" labelsOnly>
|
||||
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
name="Status"
|
||||
className="status"
|
||||
labelsOnly
|
||||
>
|
||||
{hpa.getReadyConditions()
|
||||
.map(({ type, tooltip, isReady }) => (
|
||||
<Badge
|
||||
key={type}
|
||||
label={type}
|
||||
tooltip={tooltip}
|
||||
className={cssNames({ [type.toLowerCase()]: isReady })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerTitle>Metrics</DrawerTitle>
|
||||
@@ -143,3 +191,11 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const HpaDetails = withInjectables<Dependencies, HpaDetailsProps>(NonInjectedHpaDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { HorizontalPodAutoscaler } from "../../../common/k8s-api/endpoints/hpa.api";
|
||||
import { hpaApi } from "../../../common/k8s-api/endpoints/hpa.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
|
||||
export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> {
|
||||
api = hpaApi;
|
||||
}
|
||||
|
||||
export const hpaStore = new HPAStore();
|
||||
apiManager.registerStore(hpaStore);
|
||||
@@ -8,8 +8,8 @@ import "./hpa.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { HorizontalPodAutoscaler } from "../../../common/k8s-api/endpoints/hpa.api";
|
||||
import { hpaStore } from "./hpa.store";
|
||||
import type { HorizontalPodAutoscaler } from "../../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api";
|
||||
import { horizontalPodAutoscalerStore } from "./legacy-store";
|
||||
import { Badge } from "../badge";
|
||||
import { cssNames } from "../../utils";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
@@ -38,7 +38,13 @@ export class HorizontalPodAutoscalers extends React.Component {
|
||||
|
||||
const metricsRemain = metrics.length > 1 ? `+${metrics.length - 1} more...` : "";
|
||||
|
||||
return <p>{hpa.getMetricValues(metrics[0])} {metricsRemain}</p>;
|
||||
return (
|
||||
<p>
|
||||
{hpa.getMetricValues(metrics[0])}
|
||||
{" "}
|
||||
{metricsRemain}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -48,7 +54,7 @@ export class HorizontalPodAutoscalers extends React.Component {
|
||||
isConfigurable
|
||||
tableId="configuration_hpa"
|
||||
className="HorizontalPodAutoscalers"
|
||||
store={hpaStore}
|
||||
store={horizontalPodAutoscalerStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: hpa => hpa.getName(),
|
||||
[columnId.namespace]: hpa => hpa.getNs(),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import horizontalPodAutoscalerStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(horizontalPodAutoscalerStoreInjectable)` instead
|
||||
*/
|
||||
export const horizontalPodAutoscalerStore = asLegacyGlobalForExtensionApi(horizontalPodAutoscalerStoreInjectable);
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import horizontalPodAutoscalerApiInjectable from "../../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { HorizontalPodAutoscalerStore } from "./store";
|
||||
|
||||
const horizontalPodAutoscalerStoreInjectable = getInjectable({
|
||||
id: "horizontal-pod-autoscaler-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "horizontalPodAutoscalerStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(horizontalPodAutoscalerApiInjectable);
|
||||
|
||||
return new HorizontalPodAutoscalerStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default horizontalPodAutoscalerStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { HorizontalPodAutoscaler, HorizontalPodAutoscalerApi } from "../../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api";
|
||||
|
||||
export class HorizontalPodAutoscalerStore extends KubeObjectStore<HorizontalPodAutoscaler, HorizontalPodAutoscalerApi> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import limitRangeStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(limitRangeStoreInjectable)` instead
|
||||
*/
|
||||
export const limitRangeStore = asLegacyGlobalForExtensionApi(limitRangeStoreInjectable);
|
||||
@@ -75,27 +75,27 @@ export class LimitRangeDetails extends React.Component<LimitRangeDetailsProps> {
|
||||
<div className="LimitRangeDetails">
|
||||
<KubeObjectMeta object={limitRange}/>
|
||||
|
||||
{containerLimits.length > 0 &&
|
||||
<DrawerItem name="Container Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{podLimits.length > 0 &&
|
||||
<DrawerItem name="Pod Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{pvcLimits.length > 0 &&
|
||||
<DrawerItem name="Persistent Volume Claim Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(pvcLimits, [Resource.STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{containerLimits.length > 0 && (
|
||||
<DrawerItem name="Container Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
)}
|
||||
{podLimits.length > 0 && (
|
||||
<DrawerItem name="Pod Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
)}
|
||||
{pvcLimits.length > 0 && (
|
||||
<DrawerItem name="Persistent Volume Claim Limits" labelsOnly>
|
||||
{
|
||||
renderLimitDetails(pvcLimits, [Resource.STORAGE])
|
||||
}
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import type { LimitRange } from "../../../common/k8s-api/endpoints/limit-range.api";
|
||||
import { limitRangeApi } from "../../../common/k8s-api/endpoints/limit-range.api";
|
||||
|
||||
export class LimitRangesStore extends KubeObjectStore<LimitRange> {
|
||||
api = limitRangeApi;
|
||||
}
|
||||
|
||||
export const limitRangeStore = new LimitRangesStore();
|
||||
apiManager.registerStore(limitRangeStore);
|
||||
@@ -7,7 +7,7 @@ import "./limit-ranges.scss";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { limitRangeStore } from "./limit-ranges.store";
|
||||
import { limitRangeStore } from "./legacy-store";
|
||||
import React from "react";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import limitRangeApiInjectable from "../../../common/k8s-api/endpoints/limit-range.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { LimitRangeStore } from "./store";
|
||||
|
||||
const limitRangeStoreInjectable = getInjectable({
|
||||
id: "limit-range-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "limitRangeStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(limitRangeApiInjectable);
|
||||
|
||||
return new LimitRangeStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default limitRangeStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { LimitRange, LimitRangeApi } from "../../../common/k8s-api/endpoints/limit-range.api";
|
||||
|
||||
export class LimitRangeStore extends KubeObjectStore<LimitRange, LimitRangeApi> {
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { DrawerTitle } from "../drawer";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Input } from "../input";
|
||||
import { Button } from "../button";
|
||||
import { configMapsStore } from "./config-maps.store";
|
||||
import { configMapStore } from "./legacy-store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { ConfigMap } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
@@ -24,7 +24,7 @@ export interface ConfigMapDetailsProps extends KubeObjectDetailsProps<ConfigMap>
|
||||
@observer
|
||||
export class ConfigMapDetails extends React.Component<ConfigMapDetailsProps> {
|
||||
@observable isSaving = false;
|
||||
@observable data = observable.map<string, string>();
|
||||
@observable data = observable.map<string, string | undefined>();
|
||||
|
||||
constructor(props: ConfigMapDetailsProps) {
|
||||
super(props);
|
||||
@@ -48,15 +48,17 @@ export class ConfigMapDetails extends React.Component<ConfigMapDetailsProps> {
|
||||
|
||||
try {
|
||||
this.isSaving = true;
|
||||
await configMapsStore.update(configMap, {
|
||||
await configMapStore.update(configMap, {
|
||||
...configMap,
|
||||
data: Object.fromEntries(this.data),
|
||||
});
|
||||
Notifications.ok(
|
||||
Notifications.ok((
|
||||
<p>
|
||||
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>
|
||||
</p>,
|
||||
);
|
||||
{"ConfigMap "}
|
||||
<b>{configMap.getName()}</b>
|
||||
{" successfully updated."}
|
||||
</p>
|
||||
));
|
||||
} catch (error) {
|
||||
Notifications.error(`Failed to save config map: ${error}`);
|
||||
} finally {
|
||||
@@ -104,7 +106,8 @@ export class ConfigMapDetails extends React.Component<ConfigMapDetailsProps> {
|
||||
}
|
||||
<Button
|
||||
primary
|
||||
label="Save" waiting={this.isSaving}
|
||||
label="Save"
|
||||
waiting={this.isSaving}
|
||||
className="save-btn"
|
||||
onClick={this.save}
|
||||
/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { ConfigMap } from "../../../common/k8s-api/endpoints/configmap.api";
|
||||
import { configMapApi } from "../../../common/k8s-api/endpoints/configmap.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
|
||||
export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
|
||||
api = configMapApi;
|
||||
}
|
||||
|
||||
export const configMapsStore = new ConfigMapsStore();
|
||||
apiManager.registerStore(configMapsStore);
|
||||
@@ -7,7 +7,7 @@ import "./config-maps.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { configMapsStore } from "./config-maps.store";
|
||||
import { configMapStore } from "./legacy-store";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
@@ -29,7 +29,7 @@ export class ConfigMaps extends React.Component {
|
||||
isConfigurable
|
||||
tableId="configuration_configmaps"
|
||||
className="ConfigMaps"
|
||||
store={configMapsStore}
|
||||
store={configMapStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: configMap => configMap.getName(),
|
||||
[columnId.namespace]: configMap => configMap.getNs(),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import configMapStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(configMapStoreInjectable)` instead
|
||||
*/
|
||||
export const configMapStore = asLegacyGlobalForExtensionApi(configMapStoreInjectable);
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import configMapApiInjectable from "../../../common/k8s-api/endpoints/config-map.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { ConfigMapStore } from "./store";
|
||||
|
||||
const configMapStoreInjectable = getInjectable({
|
||||
id: "config-map-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "configMapStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(configMapApiInjectable);
|
||||
|
||||
return new ConfigMapStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default configMapStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { ConfigMap, ConfigMapApi, ConfigMapData } from "../../../common/k8s-api/endpoints/config-map.api";
|
||||
|
||||
export class ConfigMapStore extends KubeObjectStore<ConfigMap, ConfigMapApi, ConfigMapData> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import podDisruptionBudgetStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(podDisruptionBudgetStoreInjectable)` instead
|
||||
*/
|
||||
export const podDisruptionBudgetStore = asLegacyGlobalForExtensionApi(podDisruptionBudgetStoreInjectable);
|
||||
+2
-2
@@ -39,13 +39,13 @@ export class PodDisruptionBudgetDetails extends React.Component<PodDisruptionBud
|
||||
<div className="PdbDetails">
|
||||
<KubeObjectMeta object={pdb}/>
|
||||
|
||||
{selectors.length > 0 &&
|
||||
{selectors.length > 0 && (
|
||||
<DrawerItem name="Selector" labelsOnly>
|
||||
{
|
||||
selectors.map(label => <Badge key={label} label={label}/>)
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
)}
|
||||
|
||||
<DrawerItem name="Min Available">
|
||||
{pdb.getMinAvailable()}
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints/poddisruptionbudget.api";
|
||||
import { pdbApi } from "../../../common/k8s-api/endpoints/poddisruptionbudget.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
|
||||
export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudget> {
|
||||
api = pdbApi;
|
||||
}
|
||||
|
||||
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
|
||||
apiManager.registerStore(podDisruptionBudgetsStore);
|
||||
@@ -7,8 +7,8 @@ import "./pod-disruption-budgets.scss";
|
||||
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { podDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
|
||||
import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints/poddisruptionbudget.api";
|
||||
import { podDisruptionBudgetStore } from "./legacy-store";
|
||||
import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints/pod-disruption-budget.api";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
@@ -37,7 +37,7 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
|
||||
isConfigurable
|
||||
tableId="configuration_distribution_budgets"
|
||||
className="PodDisruptionBudgets"
|
||||
store={podDisruptionBudgetsStore}
|
||||
store={podDisruptionBudgetStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: pdb => pdb.getName(),
|
||||
[columnId.namespace]: pdb => pdb.getNs(),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import podDisruptionBudgetApiInjectable from "../../../common/k8s-api/endpoints/pod-disruption-budget.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { PodDisruptionBudgetStore } from "./store";
|
||||
|
||||
const podDisruptionBudgetStoreInjectable = getInjectable({
|
||||
id: "pod-disruption-budget-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "podDisruptionBudgetStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(podDisruptionBudgetApiInjectable);
|
||||
|
||||
return new PodDisruptionBudgetStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default podDisruptionBudgetStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { PodDisruptionBudget, PodDisruptionBudgetApi } from "../../../common/k8s-api/endpoints/pod-disruption-budget.api";
|
||||
|
||||
export class PodDisruptionBudgetStore extends KubeObjectStore<PodDisruptionBudget, PodDisruptionBudgetApi> {
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import { Dialog } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import type { IResourceQuotaValues } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { resourceQuotaApi } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import type { IResourceQuotaValues } from "../../../common/k8s-api/endpoints";
|
||||
import { resourceQuotaApi } from "../../../common/k8s-api/endpoints";
|
||||
import { Select } from "../select";
|
||||
import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
@@ -55,9 +55,9 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
public defaultNamespace = "default";
|
||||
|
||||
@observable quotaName = "";
|
||||
@observable quotaSelectValue = "";
|
||||
@observable quotaSelectValue: string | null = null;
|
||||
@observable quotaInputValue = "";
|
||||
@observable namespace = this.defaultNamespace;
|
||||
@observable namespace: string | null = this.defaultNamespace;
|
||||
@observable quotas = AddQuotaDialog.defaultQuotas;
|
||||
|
||||
constructor(props: AddQuotaDialogProps) {
|
||||
@@ -75,21 +75,23 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
|
||||
@computed get quotaEntries() {
|
||||
return Object.entries(this.quotas)
|
||||
.filter(([, value]) => !!value.trim());
|
||||
.filter(([, value]) => !!value?.trim());
|
||||
}
|
||||
|
||||
@computed get quotaOptions() {
|
||||
return Object.keys(this.quotas).map(quota => {
|
||||
const isCompute = quota.endsWith(".cpu") || quota.endsWith(".memory");
|
||||
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
|
||||
const isCount = quota.startsWith("count/");
|
||||
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
|
||||
private getQuotaOptionLabelIconMaterial(quota: string) {
|
||||
if (quota.endsWith(".cpu") || quota.endsWith(".memory")) {
|
||||
return "memory";
|
||||
}
|
||||
|
||||
return {
|
||||
label: icon ? <span className="nobr"><Icon material={icon} /> {quota}</span> : quota,
|
||||
value: quota,
|
||||
};
|
||||
});
|
||||
if (quota.endsWith(".storage") || quota === "persistentvolumeclaims") {
|
||||
return "storage";
|
||||
}
|
||||
|
||||
if (quota.startsWith("count/")) {
|
||||
return "looks_one";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setQuota = () => {
|
||||
@@ -111,13 +113,14 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
};
|
||||
|
||||
addQuota = async () => {
|
||||
try {
|
||||
const { quotaName, namespace } = this;
|
||||
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
|
||||
quotas[name] = value;
|
||||
const { quotaName, namespace } = this;
|
||||
|
||||
return quotas;
|
||||
}, {});
|
||||
if (!quotaName || !namespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const quotas = Object.fromEntries(this.quotaEntries);
|
||||
|
||||
await resourceQuotaApi.create({ namespace, name: quotaName }, {
|
||||
spec: {
|
||||
@@ -126,7 +129,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
});
|
||||
this.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
Notifications.checkedError(err, "Unknown error occured while creating ResourceQuota");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,17 +156,19 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep
|
||||
contentClass="flex gaps column"
|
||||
disabledNext={!this.namespace}
|
||||
disabledNext={!this.namespace || !this.quotaName}
|
||||
nextLabel="Create"
|
||||
next={this.addQuota}
|
||||
>
|
||||
<div className="flex gaps">
|
||||
<Input
|
||||
required autoFocus
|
||||
required
|
||||
autoFocus
|
||||
placeholder="ResourceQuota name"
|
||||
trim
|
||||
validators={systemName}
|
||||
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
|
||||
value={this.quotaName}
|
||||
onChange={v => this.quotaName = v.toLowerCase()}
|
||||
className="box grow"
|
||||
/>
|
||||
</div>
|
||||
@@ -175,7 +180,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
placeholder="Namespace"
|
||||
themeName="light"
|
||||
className="box grow"
|
||||
onChange={({ value }) => this.namespace = value}
|
||||
onChange={option => this.namespace = option?.value ?? null}
|
||||
/>
|
||||
|
||||
<SubTitle title="Values" />
|
||||
@@ -185,9 +190,25 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
className="quota-select"
|
||||
themeName="light"
|
||||
placeholder="Select a quota.."
|
||||
options={this.quotaOptions}
|
||||
options={Object.keys(this.quotas).map(quota => ({
|
||||
value: quota,
|
||||
label: quota,
|
||||
}))}
|
||||
value={this.quotaSelectValue}
|
||||
onChange={({ value }) => this.quotaSelectValue = value}
|
||||
onChange={option => this.quotaSelectValue = option?.value ?? null}
|
||||
formatOptionLabel={({ value }) => {
|
||||
const iconMaterial = this.getQuotaOptionLabelIconMaterial(value);
|
||||
|
||||
return iconMaterial
|
||||
? (
|
||||
<span className="nobr">
|
||||
<Icon material={iconMaterial} />
|
||||
{" "}
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
: value;
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
maxLength={10}
|
||||
@@ -197,23 +218,25 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
onKeyDown={this.onInputQuota}
|
||||
className="box grow"
|
||||
/>
|
||||
<Button round primary onClick={this.setQuota}>
|
||||
<Button
|
||||
round
|
||||
primary
|
||||
onClick={this.setQuota}
|
||||
>
|
||||
<Icon
|
||||
material={this.quotas[this.quotaSelectValue] ? "edit" : "add"}
|
||||
material={this.quotaSelectValue && this.quotas[this.quotaSelectValue] ? "edit" : "add"}
|
||||
tooltip="Set quota"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="quota-entries">
|
||||
{this.quotaEntries.map(([quota, value]) => {
|
||||
return (
|
||||
<div key={quota} className="quota gaps inline align-center">
|
||||
<div className="name">{quota}</div>
|
||||
<div className="value">{value}</div>
|
||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{this.quotaEntries.map(([quota, value]) => (
|
||||
<div key={quota} className="quota gaps inline align-center">
|
||||
<div className="name">{quota}</div>
|
||||
<div className="value">{value}</div>
|
||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import resourceQuotaStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(resourceQuotaStoreInjectable)` instead
|
||||
*/
|
||||
export const resourceQuotaStore = asLegacyGlobalForExtensionApi(resourceQuotaStoreInjectable);
|
||||
@@ -8,7 +8,7 @@ import React from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { cpuUnitsToNumber, cssNames, unitsToBytes, metricUnitsToNumber } from "../../utils";
|
||||
import { cpuUnitsToNumber, cssNames, unitsToBytes, metricUnitsToNumber, object, hasDefinedTupleValue } from "../../utils";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { ResourceQuota } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { LineProgress } from "../line-progress";
|
||||
@@ -32,25 +32,29 @@ function transformUnit(name: string, value: string): number {
|
||||
}
|
||||
|
||||
function renderQuotas(quota: ResourceQuota): JSX.Element[] {
|
||||
const { hard = {}, used = {}} = quota.status;
|
||||
const { hard = {}, used = {}} = quota.status ?? {};
|
||||
|
||||
return Object.entries(hard)
|
||||
.filter(([name]) => used[name])
|
||||
return object.entries(hard)
|
||||
.filter(hasDefinedTupleValue)
|
||||
.map(([name, value]) => {
|
||||
const current = transformUnit(name, used[name]);
|
||||
const current = transformUnit(name, value);
|
||||
const max = transformUnit(name, value);
|
||||
const usage = max === 0 ? 100 : Math.ceil(current / max * 100); // special case 0 max as always 100% usage
|
||||
|
||||
return (
|
||||
<div key={name} className={cssNames("param", kebabCase(name))}>
|
||||
<span className="title">{name}</span>
|
||||
<span className="value">{used[name]} / {value}</span>
|
||||
<span className="value">
|
||||
{`${used[name]} / ${value}`}
|
||||
</span>
|
||||
<LineProgress
|
||||
max={max}
|
||||
value={current}
|
||||
tooltip={
|
||||
<p>Set: {value}. Usage: {`${usage}%`}</p>
|
||||
}
|
||||
tooltip={(
|
||||
<p>
|
||||
{`Set: ${value}. Usage: ${usage}%`}
|
||||
</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { ResourceQuota } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { resourceQuotaApi } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
|
||||
export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
|
||||
api = resourceQuotaApi;
|
||||
}
|
||||
|
||||
export const resourceQuotaStore = new ResourceQuotasStore();
|
||||
apiManager.registerStore(resourceQuotaStore);
|
||||
@@ -9,7 +9,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { AddQuotaDialog } from "./add-quota-dialog";
|
||||
import { resourceQuotaStore } from "./resource-quotas.store";
|
||||
import { resourceQuotaStore } from "./legacy-store";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import resourceQuotaApiInjectable from "../../../common/k8s-api/endpoints/resource-quota.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { ResourceQuotaStore } from "./store";
|
||||
|
||||
const resourceQuotaStoreInjectable = getInjectable({
|
||||
id: "resource-quota-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "resourceQuotaStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(resourceQuotaApiInjectable);
|
||||
|
||||
return new ResourceQuotaStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default resourceQuotaStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { ResourceQuota, ResourceQuotaApi } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
|
||||
export class ResourceQuotaStore extends KubeObjectStore<ResourceQuota, ResourceQuotaApi> {
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import { render } from "@testing-library/react";
|
||||
import { SecretDetails } from "../secret-details";
|
||||
import { Secret, SecretType } from "../../../../common/k8s-api/endpoints";
|
||||
|
||||
jest.mock("../../kube-object-meta/kube-object-meta");
|
||||
jest.mock("../../kube-object-meta/kube-object-meta", () => ({
|
||||
KubeObjectMeta: () => null,
|
||||
}));
|
||||
|
||||
|
||||
describe("SecretDetails tests", () => {
|
||||
@@ -20,6 +22,8 @@ describe("SecretDetails tests", () => {
|
||||
name: "test",
|
||||
resourceVersion: "1",
|
||||
uid: "uid",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/secrets/default/test",
|
||||
},
|
||||
data: {
|
||||
foobar: "",
|
||||
|
||||
@@ -13,36 +13,34 @@ import { Dialog } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import type { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { secretsApi, SecretType } from "../../../common/k8s-api/endpoints";
|
||||
import { reverseSecretTypeMap, secretApi, SecretType } from "../../../common/k8s-api/endpoints";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import type { SelectOption } from "../select";
|
||||
import { Select } from "../select";
|
||||
import { Icon } from "../icon";
|
||||
import type { KubeObjectMetadata } from "../../../common/k8s-api/kube-object";
|
||||
import { base64 } from "../../utils";
|
||||
import { base64, iter } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { showDetails } from "../kube-detail-params";
|
||||
import { fromEntries } from "../../../common/utils/objects";
|
||||
|
||||
export interface AddSecretDialogProps extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
interface ISecretTemplateField {
|
||||
interface SecretTemplateField {
|
||||
key: string;
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface ISecretTemplate {
|
||||
[field: string]: ISecretTemplateField[];
|
||||
annotations?: ISecretTemplateField[];
|
||||
labels?: ISecretTemplateField[];
|
||||
data?: ISecretTemplateField[];
|
||||
interface SecretTemplate {
|
||||
[field: string]: SecretTemplateField[] | undefined;
|
||||
annotations?: SecretTemplateField[];
|
||||
labels?: SecretTemplateField[];
|
||||
data?: SecretTemplateField[];
|
||||
}
|
||||
|
||||
type ISecretField = keyof ISecretTemplate;
|
||||
type ISecretField = keyof SecretTemplate;
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
@@ -63,7 +61,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
private secretTemplate: { [p: string]: ISecretTemplate } = {
|
||||
private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
|
||||
[SecretType.Opaque]: {},
|
||||
[SecretType.ServiceAccountToken]: {
|
||||
annotations: [
|
||||
@@ -73,10 +71,6 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
},
|
||||
};
|
||||
|
||||
get types() {
|
||||
return Object.keys(this.secretTemplate) as SecretType[];
|
||||
}
|
||||
|
||||
@observable secret = this.secretTemplate;
|
||||
@observable name = "";
|
||||
@observable namespace = "default";
|
||||
@@ -91,61 +85,59 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
AddSecretDialog.close();
|
||||
};
|
||||
|
||||
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => {
|
||||
return fields.reduce<any>((data, field) => {
|
||||
const { key, value } = field;
|
||||
|
||||
if (key) {
|
||||
data[key] = processValue ? processValue(value) : value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, {});
|
||||
private getDataFromFields = (fields: SecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => {
|
||||
return iter.pipeline(fields.values())
|
||||
.filterMap(({ key, value }) => (
|
||||
value
|
||||
? [key, processValue(value)] as const
|
||||
: undefined
|
||||
))
|
||||
.collect(fromEntries);
|
||||
};
|
||||
|
||||
createSecret = async () => {
|
||||
const { name, namespace, type } = this;
|
||||
const { data = [], labels = [], annotations = [] } = this.secret[type];
|
||||
const secret: Partial<Secret> = {
|
||||
type,
|
||||
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
annotations: this.getDataFromFields(annotations),
|
||||
labels: this.getDataFromFields(labels),
|
||||
} as KubeObjectMetadata,
|
||||
};
|
||||
const { data = [], labels = [], annotations = [] } = this.secret[type] ?? {};
|
||||
|
||||
try {
|
||||
const newSecret = await secretsApi.create({ namespace, name }, secret);
|
||||
const newSecret = await secretApi.create({ namespace, name }, {
|
||||
type,
|
||||
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
annotations: this.getDataFromFields(annotations),
|
||||
labels: this.getDataFromFields(labels),
|
||||
},
|
||||
});
|
||||
|
||||
showDetails(newSecret.selfLink);
|
||||
showDetails(newSecret?.selfLink);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
Notifications.checkedError(err, "Unknown error occured while creating a Secret");
|
||||
}
|
||||
};
|
||||
|
||||
addField = (field: ISecretField) => {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
private getFields(field: ISecretField) {
|
||||
return (this.secret[this.type] ??= {})[field] ??= [];
|
||||
}
|
||||
|
||||
fields.push({ key: "", value: "" });
|
||||
this.secret[this.type][field] = fields;
|
||||
addField = (field: ISecretField) => {
|
||||
this.getFields(field).push({ key: "", value: "" });
|
||||
};
|
||||
|
||||
removeField = (field: ISecretField, index: number) => {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
|
||||
fields.splice(index, 1);
|
||||
this.getFields(field).splice(index, 1);
|
||||
};
|
||||
|
||||
renderFields(field: ISecretField) {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle compact className="fields-title" title={upperFirst(field.toString())}>
|
||||
<SubTitle
|
||||
compact
|
||||
className="fields-title"
|
||||
title={upperFirst(field.toString())}
|
||||
>
|
||||
<Icon
|
||||
small
|
||||
tooltip="Add field"
|
||||
@@ -154,37 +146,37 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
/>
|
||||
</SubTitle>
|
||||
<div className="secret-fields">
|
||||
{fields.map((item, index) => {
|
||||
const { key = "", value = "", required } = item;
|
||||
|
||||
return (
|
||||
{this.getFields(field)
|
||||
.map((item, index) => (
|
||||
<div key={index} className="secret-field flex gaps auto align-center">
|
||||
<Input
|
||||
className="key"
|
||||
placeholder="Name"
|
||||
title={key}
|
||||
tabIndex={required ? -1 : 0}
|
||||
readOnly={required}
|
||||
value={key} onChange={v => item.key = v}
|
||||
title={item.key}
|
||||
tabIndex={item.required ? -1 : 0}
|
||||
readOnly={item.required}
|
||||
value={item.key}
|
||||
onChange={v => item.key = v}
|
||||
/>
|
||||
<Input
|
||||
multiLine maxRows={5}
|
||||
required={required}
|
||||
multiLine
|
||||
maxRows={5}
|
||||
required={item.required}
|
||||
className="value"
|
||||
placeholder="Value"
|
||||
value={value} onChange={v => item.value = v}
|
||||
value={item.value}
|
||||
onChange={v => item.value = v}
|
||||
/>
|
||||
<Icon
|
||||
small
|
||||
disabled={required}
|
||||
tooltip={required ? "Required field" : "Remove field"}
|
||||
disabled={item.required}
|
||||
tooltip={item.required ? "Required field" : "Remove field"}
|
||||
className="remove-icon"
|
||||
material="remove_circle_outline"
|
||||
onClick={() => this.removeField(field, index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -204,15 +196,21 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep contentClass="flow column" nextLabel="Create" next={this.createSecret}>
|
||||
<WizardStep
|
||||
contentClass="flow column"
|
||||
nextLabel="Create"
|
||||
next={this.createSecret}
|
||||
>
|
||||
<div className="secret-name">
|
||||
<SubTitle title="Secret name" />
|
||||
<Input
|
||||
autoFocus required
|
||||
autoFocus
|
||||
required
|
||||
placeholder="Name"
|
||||
trim
|
||||
validators={systemName}
|
||||
value={name} onChange={v => this.name = v}
|
||||
value={name}
|
||||
onChange={v => this.name = v}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex auto gaps">
|
||||
@@ -222,7 +220,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
id="secret-namespace-input"
|
||||
themeName="light"
|
||||
value={namespace}
|
||||
onChange={({ value }) => this.namespace = value}
|
||||
onChange={option => this.namespace = option?.value ?? "default"}
|
||||
/>
|
||||
</div>
|
||||
<div className="secret-type">
|
||||
@@ -230,8 +228,10 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
<Select
|
||||
id="secret-input"
|
||||
themeName="light"
|
||||
options={this.types}
|
||||
value={type} onChange={({ value }: SelectOption) => this.type = value}
|
||||
options={Object.keys(this.secretTemplate) as SecretType[]}
|
||||
value={type}
|
||||
onChange={option => this.type = option?.value ?? SecretType.Opaque}
|
||||
getOptionLabel={option => reverseSecretTypeMap[option.value]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import secretStoreInjectable from "./store.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(secretStoreInjectable)` instead
|
||||
*/
|
||||
export const secretStore = asLegacyGlobalForExtensionApi(secretStoreInjectable);
|
||||
@@ -14,7 +14,7 @@ import { Button } from "../button";
|
||||
import { Notifications } from "../notifications";
|
||||
import { base64, toggle } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { secretsStore } from "./secrets.store";
|
||||
import { secretStore } from "./legacy-store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
@@ -26,7 +26,7 @@ export interface SecretDetailsProps extends KubeObjectDetailsProps<Secret> {
|
||||
@observer
|
||||
export class SecretDetails extends React.Component<SecretDetailsProps> {
|
||||
@observable isSaving = false;
|
||||
@observable data: { [name: string]: string } = {};
|
||||
@observable data: Partial<Record<string, string>> = {};
|
||||
@observable revealSecret = observable.set<string>();
|
||||
|
||||
constructor(props: SecretDetailsProps) {
|
||||
@@ -53,10 +53,10 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
await secretsStore.update(secret, { ...secret, data: this.data });
|
||||
await secretStore.update(secret, { ...secret, data: this.data });
|
||||
Notifications.ok("Secret successfully updated.");
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
Notifications.checkedError(err, "Unknown error occured while updating the secret");
|
||||
}
|
||||
this.isSaving = false;
|
||||
};
|
||||
@@ -65,11 +65,11 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
|
||||
this.data[name] = encoded ? value : base64.encode(value);
|
||||
};
|
||||
|
||||
renderSecret = ([name, value]: [string, string]) => {
|
||||
renderSecret = ([name, value]: [string, string | undefined]) => {
|
||||
let decodedVal: string | undefined;
|
||||
|
||||
try {
|
||||
decodedVal = base64.decode(value);
|
||||
decodedVal = value ? base64.decode(value) : undefined;
|
||||
} catch {
|
||||
/**
|
||||
* The value failed to be decoded, so don't show the visibility
|
||||
@@ -85,7 +85,11 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={name} className="data" data-testid={`${name}-secret-entry`}>
|
||||
<div
|
||||
key={name}
|
||||
className="data"
|
||||
data-testid={`${name}-secret-entry`}
|
||||
>
|
||||
<div className="name">{name}</div>
|
||||
<div className="flex gaps align-center">
|
||||
<Input
|
||||
@@ -120,7 +124,8 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
|
||||
{secrets.map(this.renderSecret)}
|
||||
<Button
|
||||
primary
|
||||
label="Save" waiting={this.isSaving}
|
||||
label="Save"
|
||||
waiting={this.isSaving}
|
||||
className="save-btn"
|
||||
onClick={this.saveSecret}
|
||||
/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { secretsApi } from "../../../common/k8s-api/endpoints";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
|
||||
export class SecretsStore extends KubeObjectStore<Secret> {
|
||||
api = secretsApi;
|
||||
}
|
||||
|
||||
export const secretsStore = new SecretsStore();
|
||||
apiManager.registerStore(secretsStore);
|
||||
@@ -10,7 +10,7 @@ import { observer } from "mobx-react";
|
||||
import { AddSecretDialog } from "./add-secret-dialog";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { Badge } from "../badge";
|
||||
import { secretsStore } from "./secrets.store";
|
||||
import { secretStore } from "./legacy-store";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
@@ -33,7 +33,7 @@ export class Secrets extends React.Component {
|
||||
isConfigurable
|
||||
tableId="configuration_secrets"
|
||||
className="Secrets"
|
||||
store={secretsStore}
|
||||
store={secretStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: secret => secret.getName(),
|
||||
[columnId.namespace]: secret => secret.getNs(),
|
||||
@@ -60,7 +60,14 @@ export class Secrets extends React.Component {
|
||||
secret.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={secret} />,
|
||||
secret.getNs(),
|
||||
secret.getLabels().map(label => <Badge scrollable key={label} label={label} expandable={false}/>),
|
||||
secret.getLabels().map(label => (
|
||||
<Badge
|
||||
scrollable
|
||||
key={label}
|
||||
label={label}
|
||||
expandable={false}
|
||||
/>
|
||||
)),
|
||||
secret.getKeys().join(", "),
|
||||
secret.type,
|
||||
<KubeObjectAge key="age" object={secret} />,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import secretApiInjectable from "../../../common/k8s-api/endpoints/secret.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { SecretStore } from "./store";
|
||||
|
||||
const secretStoreInjectable = getInjectable({
|
||||
id: "secret-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "secretStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(secretApiInjectable);
|
||||
|
||||
return new SecretStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default secretStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { Secret, SecretApi, SecretData } from "../../../common/k8s-api/endpoints";
|
||||
|
||||
export class SecretStore extends KubeObjectStore<Secret, SecretApi, SecretData> {
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import "./crd-details.scss";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
|
||||
import { Badge } from "../badge";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
@@ -70,7 +70,11 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
|
||||
readOnly
|
||||
/>
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Conditions" className="conditions" labelsOnly>
|
||||
<DrawerItem
|
||||
name="Conditions"
|
||||
className="conditions"
|
||||
labelsOnly
|
||||
>
|
||||
{
|
||||
crd.getConditions().map(condition => {
|
||||
const { type, message, lastTransitionTime, status } = condition;
|
||||
@@ -84,7 +88,10 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
|
||||
tooltip={(
|
||||
<>
|
||||
<p>{message}</p>
|
||||
<p>Last transition time: {lastTransitionTime}</p>
|
||||
<p>
|
||||
Last transition time:
|
||||
{lastTransitionTime}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
@@ -107,7 +114,7 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
|
||||
<TableCell>{listKind}</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
{printerColumns.length > 0 &&
|
||||
{printerColumns.length > 0 && (
|
||||
<>
|
||||
<DrawerTitle>Additional Printer Columns</DrawerTitle>
|
||||
<Table selectable className="printer-columns box grow">
|
||||
@@ -133,8 +140,8 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
|
||||
}
|
||||
</Table>
|
||||
</>
|
||||
}
|
||||
{validation &&
|
||||
)}
|
||||
{validation && (
|
||||
<>
|
||||
<DrawerTitle>Validation</DrawerTitle>
|
||||
<MonacoEditor
|
||||
@@ -143,7 +150,7 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
|
||||
style={{ height: 400 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import createPageParamInjectable from "../../navigation/create-page-param.injectable";
|
||||
|
||||
const crdGroupsUrlParamInjectable = getInjectable({
|
||||
id: "crd-groups-url-param",
|
||||
instantiate: (di) => {
|
||||
const createPageParam = di.inject(createPageParamInjectable);
|
||||
|
||||
return createPageParam<string[]>({
|
||||
name: "groups",
|
||||
defaultValue: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default crdGroupsUrlParamInjectable;
|
||||
@@ -6,23 +6,21 @@
|
||||
import "./crd-list.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { stopPropagation } from "../../utils";
|
||||
import { iter, stopPropagation } from "../../utils";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { crdStore } from "./crd.store";
|
||||
import type { SelectOption } from "../select";
|
||||
import { customResourceDefinitionStore } from "./legacy-store";
|
||||
import { Select } from "../select";
|
||||
import { createPageParam } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import { TabLayout } from "../layout/tab-layout-2";
|
||||
|
||||
export const crdGroupsUrlParam = createPageParam<string[]>({
|
||||
name: "groups",
|
||||
defaultValue: [],
|
||||
});
|
||||
import type { PageParam } from "../../navigation";
|
||||
import type { CustomResourceDefinitionStore } from "./definition.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import crdGroupsUrlParamInjectable from "./crd-groups-url-param.injectable";
|
||||
import customResourceDefinitionStoreInjectable from "./definition.store.injectable";
|
||||
|
||||
enum columnId {
|
||||
kind = "kind",
|
||||
@@ -32,49 +30,67 @@ enum columnId {
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
crdGroupsUrlParam: PageParam<string[]>;
|
||||
customResourceDefinitionStore: CustomResourceDefinitionStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CustomResourceDefinitions extends React.Component {
|
||||
constructor(props: {}) {
|
||||
class NonInjectedCustomResourceDefinitions extends React.Component<Dependencies> {
|
||||
private readonly selectedGroups = observable.set(this.props.crdGroupsUrlParam.get());
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get selectedGroups(): string[] {
|
||||
return crdGroupsUrlParam.get();
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
if (this.selectedGroups.length) {
|
||||
return crdStore.items.filter(item => this.selectedGroups.includes(item.getGroup()));
|
||||
if (this.selectedGroups.size) {
|
||||
return this.props.customResourceDefinitionStore.items.filter(item => this.selectedGroups.has(item.getGroup()));
|
||||
}
|
||||
|
||||
return crdStore.items; // show all by default
|
||||
return this.props.customResourceDefinitionStore.items; // show all by default
|
||||
}
|
||||
|
||||
toggleSelection(group: string) {
|
||||
const groups = new Set(crdGroupsUrlParam.get());
|
||||
@computed get groupSelectOptions() {
|
||||
return Object.keys(this.props.customResourceDefinitionStore.groups)
|
||||
.map(group => ({
|
||||
value: group,
|
||||
label: group,
|
||||
isSelected: this.selectedGroups.has(group),
|
||||
}));
|
||||
}
|
||||
|
||||
if (groups.has(group)) {
|
||||
groups.delete(group);
|
||||
} else {
|
||||
groups.add(group);
|
||||
toggleSelection = (options: readonly ({ value: string })[]) => {
|
||||
const groups = options.map(({ value }) => value);
|
||||
|
||||
this.selectedGroups.replace(groups);
|
||||
this.props.crdGroupsUrlParam.set(groups);
|
||||
};
|
||||
|
||||
private getPlaceholder() {
|
||||
if (this.selectedGroups.size === 0) {
|
||||
return "All groups";
|
||||
}
|
||||
crdGroupsUrlParam.set([...groups]);
|
||||
|
||||
const prefix = this.selectedGroups.size === 1
|
||||
? "Group"
|
||||
: "Groups";
|
||||
|
||||
return `${prefix}: ${iter.join(this.selectedGroups.values(), ", ")}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, selectedGroups } = this;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="crd"
|
||||
className="CrdList"
|
||||
store={crdStore}
|
||||
// Don't subscribe the `crdStore` because <Sidebar> already has and is always mounted
|
||||
store={customResourceDefinitionStore}
|
||||
// Don't subscribe the `customResourceDefinitionStore` because <Sidebar> already has and is always mounted
|
||||
subscribeStores={false}
|
||||
items={items}
|
||||
items={this.items}
|
||||
sortingCallbacks={{
|
||||
[columnId.kind]: crd => crd.getResourceKind(),
|
||||
[columnId.group]: crd => crd.getGroup(),
|
||||
@@ -90,42 +106,38 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
crd => -crd.getCreationTimestamp(),
|
||||
]}
|
||||
renderHeaderTitle="Custom Resources"
|
||||
customizeHeader={({ filters, ...headerPlaceholders }) => {
|
||||
let placeholder = <>All groups</>;
|
||||
|
||||
if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}</>;
|
||||
if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")}</>;
|
||||
|
||||
return {
|
||||
// todo: move to global filters
|
||||
filters: (
|
||||
<>
|
||||
{filters}
|
||||
<Select
|
||||
id="crd-input"
|
||||
className="group-select"
|
||||
placeholder={placeholder}
|
||||
options={Object.keys(crdStore.groups)}
|
||||
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
formatOptionLabel={({ value: group }: SelectOption) => {
|
||||
const isSelected = selectedGroups.includes(group);
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Icon small material="folder"/>
|
||||
<span>{group}</span>
|
||||
{isSelected && <Icon small material="check" className="box right"/>}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
...headerPlaceholders,
|
||||
};
|
||||
}}
|
||||
customizeHeader={({ filters, ...headerPlaceholders }) => ({
|
||||
// todo: move to global filters
|
||||
filters: (
|
||||
<>
|
||||
{filters}
|
||||
<Select
|
||||
id="crd-input"
|
||||
className="group-select"
|
||||
placeholder={this.getPlaceholder()}
|
||||
options={this.groupSelectOptions}
|
||||
onChange={this.toggleSelection}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
isMulti={true}
|
||||
formatOptionLabel={({ value, isSelected }) => (
|
||||
<div className="flex gaps align-center">
|
||||
<Icon small material="folder" />
|
||||
<span>{value}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
small
|
||||
material="check"
|
||||
className="box right"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
...headerPlaceholders,
|
||||
})}
|
||||
renderTableHeader={[
|
||||
{ title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind },
|
||||
{ title: "Group", className: "group", sortBy: columnId.group, id: columnId.group },
|
||||
@@ -134,7 +146,11 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={crd => [
|
||||
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
||||
<Link
|
||||
key="link"
|
||||
to={crd.getResourceUrl()}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{crd.getResourceKind()}
|
||||
</Link>,
|
||||
crd.getGroup(),
|
||||
@@ -147,3 +163,11 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomResourceDefinitions = withInjectables<Dependencies>(NonInjectedCustomResourceDefinitions, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
crdGroupsUrlParam: di.inject(crdGroupsUrlParamInjectable),
|
||||
customResourceDefinitionStore: di.inject(customResourceDefinitionStoreInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -13,10 +13,10 @@ import { DrawerItem } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { Input } from "../input";
|
||||
import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
|
||||
import { convertKubectlJsonPathToNodeJsonPath } from "../../utils/jsonPath";
|
||||
import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object";
|
||||
import type { KubeObjectStatus } from "../../../common/k8s-api/kube-object";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import logger from "../../../common/logger";
|
||||
import { JSONPath } from "@astronautlabs/jsonpath";
|
||||
@@ -57,25 +57,34 @@ function convertSpecValue(value: any): any {
|
||||
export class CustomResourceDetails extends React.Component<CustomResourceDetailsProps> {
|
||||
renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) {
|
||||
return columns.map(({ name, jsonPath }) => (
|
||||
<DrawerItem key={name} name={name} renderBoolean>
|
||||
<DrawerItem
|
||||
key={name}
|
||||
name={name}
|
||||
renderBoolean
|
||||
>
|
||||
{convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))}
|
||||
</DrawerItem>
|
||||
));
|
||||
}
|
||||
|
||||
renderStatus(customResource: KubeObject<KubeObjectMetadata, KubeObjectStatus, any>, columns: AdditionalPrinterColumnsV1[]) {
|
||||
renderStatus(customResource: KubeObject<KubeObjectStatus, unknown>, columns: AdditionalPrinterColumnsV1[]) {
|
||||
const showStatus = !columns.find(column => column.name == "Status") && Array.isArray(customResource.status?.conditions);
|
||||
|
||||
if (!showStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditions = customResource.status.conditions
|
||||
.filter(({ type, reason }) => type || reason)
|
||||
.map(({ type, reason, message, status }) => ({ kind: type || reason, message, status }))
|
||||
const conditions = customResource.status?.conditions
|
||||
?.filter(({ type, reason }) => type || reason)
|
||||
.map(({ type, reason, message, status }) => ({
|
||||
kind: type || reason || "<unknown>",
|
||||
message,
|
||||
status,
|
||||
}))
|
||||
.map(({ kind, message, status }, index) => (
|
||||
<Badge
|
||||
key={kind + index} label={kind}
|
||||
key={kind + index}
|
||||
label={kind}
|
||||
disabled={status === "False"}
|
||||
className={kind.toLowerCase()}
|
||||
tooltip={message}
|
||||
@@ -83,7 +92,11 @@ export class CustomResourceDetails extends React.Component<CustomResourceDetails
|
||||
));
|
||||
|
||||
return (
|
||||
<DrawerItem name="Status" className="status" labelsOnly>
|
||||
<DrawerItem
|
||||
name="Status"
|
||||
className="status"
|
||||
labelsOnly
|
||||
>
|
||||
{conditions}
|
||||
</DrawerItem>
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeApi } from "../../../common/k8s-api/kube-api";
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
|
||||
export class CRDResourceStore<K extends KubeObject> extends KubeObjectStore<K> {
|
||||
constructor(api: KubeApi<K>) {
|
||||
super(api);
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,15 @@ import { observer } from "mobx-react";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { crdStore } from "./crd.store";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { safeJSONPathValue } from "../../utils/jsonPath";
|
||||
import { TabLayout } from "../layout/tab-layout-2";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import type { CustomResourceDefinitionStore } from "./definition.store";
|
||||
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import customResourceDefinitionStoreInjectable from "./definition.store.injectable";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -27,27 +29,29 @@ enum columnId {
|
||||
interface Dependencies {
|
||||
group: IComputedValue<string>;
|
||||
name: IComputedValue<string>;
|
||||
apiManager: ApiManager;
|
||||
customResourceDefinitionStore: CustomResourceDefinitionStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCrdResources extends React.Component<Dependencies> {
|
||||
class NonInjectedCustomResources extends React.Component<Dependencies> {
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get crd() {
|
||||
return crdStore.getByGroup(this.props.group.get(), this.props.name.get());
|
||||
return this.props.customResourceDefinitionStore.getByGroup(this.props.group.get(), this.props.name.get());
|
||||
}
|
||||
|
||||
@computed get store() {
|
||||
return apiManager.getStore(this.crd?.getResourceApiBase());
|
||||
return this.props.apiManager.getStore(this.crd?.getResourceApiBase());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { crd, store } = this;
|
||||
|
||||
if (!crd) {
|
||||
if (!crd || !store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,7 +89,9 @@ class NonInjectedCrdResources extends React.Component<Dependencies> {
|
||||
})}
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||
isNamespaced
|
||||
? { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }
|
||||
: undefined,
|
||||
...extraColumns.map(({ name }) => ({
|
||||
title: name,
|
||||
className: name.toLowerCase(),
|
||||
@@ -102,9 +108,13 @@ class NonInjectedCrdResources extends React.Component<Dependencies> {
|
||||
]}
|
||||
failedToLoadMessage={(
|
||||
<>
|
||||
<p>Failed to load {crd.getPluralName()}</p>
|
||||
<p>
|
||||
{`Failed to load ${crd.getPluralName()}`}
|
||||
</p>
|
||||
{!version.served && (
|
||||
<p>Prefered version ({crd.getGroup()}/{version.name}) is not served</p>
|
||||
<p>
|
||||
{`Prefered version (${crd.getGroup()}/${version.name}) is not served`}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -114,18 +124,11 @@ class NonInjectedCrdResources extends React.Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const CrdResources = withInjectables<Dependencies>(
|
||||
NonInjectedCrdResources,
|
||||
|
||||
{
|
||||
getProps: (di) => {
|
||||
const routeParameters = di.inject(customResourcesRouteParametersInjectable);
|
||||
|
||||
return {
|
||||
group: routeParameters.group,
|
||||
name: routeParameters.name,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
export const CustomResources = withInjectables<Dependencies>(NonInjectedCustomResources, {
|
||||
getProps: (di) => ({
|
||||
...di.inject(customResourcesRouteParametersInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
customResourceDefinitionStore: di.inject(customResourceDefinitionStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computed, reaction, makeObservable } from "mobx";
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { crdApi } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeApi } from "../../../common/k8s-api/kube-api";
|
||||
import { CRDResourceStore } from "./crd-resource.store";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
|
||||
function initStore(crd: CustomResourceDefinition) {
|
||||
const objectConstructor = class extends KubeObject {
|
||||
static readonly kind = crd.getResourceKind();
|
||||
static readonly namespaced = crd.isNamespaced();
|
||||
static readonly apiBase = crd.getResourceApiBase();
|
||||
};
|
||||
|
||||
const api = apiManager.getApi(objectConstructor.apiBase)
|
||||
?? new KubeApi({ objectConstructor });
|
||||
|
||||
if (!apiManager.getStore(api)) {
|
||||
apiManager.registerStore(new CRDResourceStore(api));
|
||||
}
|
||||
}
|
||||
|
||||
export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
||||
api = crdApi;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
|
||||
// auto-init stores for crd-s
|
||||
reaction(() => this.getItems(), items => items.forEach(initStore));
|
||||
}
|
||||
|
||||
protected sortItems(items: CustomResourceDefinition[]) {
|
||||
return super.sortItems(items, [
|
||||
crd => crd.getGroup(),
|
||||
crd => crd.getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
@computed get groups() {
|
||||
const groups: Record<string, CustomResourceDefinition[]> = {};
|
||||
|
||||
for (const crd of this.items) {
|
||||
(groups[crd.getGroup()] ??= []).push(crd);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
getByGroup(group: string, pluralName: string) {
|
||||
return this.groups[group]?.find(crd => crd.getPluralName() === pluralName);
|
||||
}
|
||||
|
||||
getByObject(obj: KubeObject) {
|
||||
if (!obj) return null;
|
||||
const { kind, apiVersion } = obj;
|
||||
|
||||
return this.items.find(crd => (
|
||||
kind === crd.getResourceKind() && apiVersion === `${crd.getGroup()}/${crd.getVersion()}`
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export const crdStore = new CRDStore();
|
||||
|
||||
apiManager.registerStore(crdStore);
|
||||
+9
-19
@@ -3,17 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { noop, some } from "lodash/fp";
|
||||
import { noop } from "lodash/fp";
|
||||
import { computed } from "mobx";
|
||||
|
||||
import type {
|
||||
SidebarItemRegistration } from "../layout/sidebar-items.injectable";
|
||||
import {
|
||||
sidebarItemsInjectionToken,
|
||||
} from "../layout/sidebar-items.injectable";
|
||||
import type { SidebarItemRegistration } from "../layout/sidebar-items.injectable";
|
||||
import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable";
|
||||
import { Icon } from "../icon";
|
||||
import React from "react";
|
||||
|
||||
import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable";
|
||||
import sidebarItemsForDefinitionGroupsInjectable from "./sidebar-items-for-definition-groups.injectable";
|
||||
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
|
||||
@@ -26,10 +21,7 @@ const customResourceSidebarItemsInjectable = getInjectable({
|
||||
const navigateToCrdList = di.inject(navigateToCrdListInjectable);
|
||||
const crdListRoute = di.inject(crdListRouteInjectable);
|
||||
const crdListRouteIsActive = di.inject(routeIsActiveInjectable, crdListRoute);
|
||||
|
||||
const definitionGroupSidebarItems = di.inject(
|
||||
sidebarItemsForDefinitionGroupsInjectable,
|
||||
);
|
||||
const definitionGroupSidebarItems = di.inject(sidebarItemsForDefinitionGroupsInjectable);
|
||||
|
||||
return computed((): SidebarItemRegistration[] => {
|
||||
const definitionsItem = {
|
||||
@@ -42,12 +34,10 @@ const customResourceSidebarItemsInjectable = getInjectable({
|
||||
orderNumber: 10,
|
||||
};
|
||||
|
||||
const definitionGroupItems = definitionGroupSidebarItems.get();
|
||||
|
||||
const childrenAndGrandChildren = [
|
||||
const childrenAndGrandChildren = computed(() => [
|
||||
definitionsItem,
|
||||
...definitionGroupItems,
|
||||
];
|
||||
...definitionGroupSidebarItems.get(),
|
||||
]);
|
||||
|
||||
const parentItem: SidebarItemRegistration = {
|
||||
id: "custom-resources",
|
||||
@@ -55,11 +45,11 @@ const customResourceSidebarItemsInjectable = getInjectable({
|
||||
title: "Custom Resources",
|
||||
getIcon: () => <Icon material="extension" />,
|
||||
onClick: noop,
|
||||
isVisible: computed(() => some(item => item.isVisible.get(), childrenAndGrandChildren)),
|
||||
isVisible: computed(() => childrenAndGrandChildren.get().some(item => item.isVisible?.get())),
|
||||
orderNumber: 110,
|
||||
};
|
||||
|
||||
return [parentItem, definitionsItem, ...definitionGroupItems];
|
||||
return [parentItem, definitionsItem, ...definitionGroupSidebarItems.get()];
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { CrdResources } from "./crd-resources";
|
||||
import { CustomResources } from "./crd-resources";
|
||||
import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
|
||||
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
||||
|
||||
@@ -12,7 +12,7 @@ const customResourcesRouteComponentInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => ({
|
||||
route: di.inject(customResourcesRouteInjectable),
|
||||
Component: CrdResources,
|
||||
Component: CustomResources,
|
||||
}),
|
||||
|
||||
injectionToken: routeSpecificComponentInjectionToken,
|
||||
|
||||
Loaded 100 of 1062 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user