[] = [];
topBarItems: TopBarRegistration[] = [];
+ additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
async navigate(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts
index 3181e7144a..79372c71dd 100644
--- a/src/extensions/renderer-api/components.ts
+++ b/src/extensions/renderer-api/components.ts
@@ -31,6 +31,11 @@ export * from "../../renderer/components/input/input";
// command-overlay
export const CommandOverlay = asLegacyGlobalObjectForExtensionApi(commandOverlayInjectable);
+export type {
+ CategoryColumnRegistration,
+ AdditionalCategoryColumnRegistration,
+} from "../../renderer/components/+catalog/custom-category-columns";
+
// other components
export * from "../../renderer/components/icon";
export * from "../../renderer/components/tooltip";
diff --git a/src/extensions/renderer-extensions.injectable.ts b/src/extensions/renderer-extensions.injectable.ts
index 24e3bd0557..3f06df5ca8 100644
--- a/src/extensions/renderer-extensions.injectable.ts
+++ b/src/extensions/renderer-extensions.injectable.ts
@@ -8,10 +8,8 @@ import extensionsInjectable from "./extensions.injectable";
import type { LensRendererExtension } from "./lens-renderer-extension";
const rendererExtensionsInjectable = getInjectable({
+ instantiate: (di) => di.inject(extensionsInjectable) as IComputedValue,
lifecycle: lifecycleEnum.singleton,
-
- instantiate: (di) =>
- di.inject(extensionsInjectable) as IComputedValue,
});
export default rendererExtensionsInjectable;
diff --git a/src/renderer/components/+catalog/__tests__/custom-columns.test.ts b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts
new file mode 100644
index 0000000000..00fd5e033b
--- /dev/null
+++ b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import type { CatalogCategorySpec } from "../../../../common/catalog";
+import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
+import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
+import { CatalogCategory } from "../../../api/catalog-entity";
+import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
+import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
+import getCategoryColumnsInjectable, { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable";
+
+class TestCategory extends CatalogCategory {
+ apiVersion = "catalog.k8slens.dev/v1alpha1";
+ kind = "CatalogCategory";
+ metadata: {
+ name: "Test";
+ icon: "question_mark";
+ };
+ spec: CatalogCategorySpec = {
+ group: "foo.bar.bat",
+ names: {
+ kind: "Test",
+ },
+ versions: [],
+ };
+
+ constructor(columns?: CategoryColumnRegistration[]) {
+ super();
+ this.spec.displayColumns = columns;
+ }
+}
+
+describe("Custom Category Columns", () => {
+ let di: ConfigurableDependencyInjectionContainer;
+
+ beforeEach(() => {
+ di = getDiForUnitTesting();
+ });
+
+ describe("without extensions", () => {
+ let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
+
+ beforeEach(() => {
+ di.override(rendererExtensionsInjectable, () => computed(() => [] as LensRendererExtension[]));
+ getCategoryColumns = di.inject(getCategoryColumnsInjectable);
+ });
+
+ it("should contain a kind column if activeCategory is falsy", () => {
+ 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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ it("should include the displayColumns from the provided category", () => {
+ const columns: CategoryColumnRegistration[] = [
+ {
+ id: "foo",
+ renderCell: () => null,
+ titleProps: {
+ title: "Foo",
+ },
+ },
+ ];
+
+ expect(getCategoryColumns({ activeCategory: new TestCategory(columns) }).renderTableHeader.find(elem => elem.title === "Foo")).toBeTruthy();
+ });
+ });
+
+ describe("with extensions", () => {
+ let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
+
+ beforeEach(() => {
+ di.override(rendererExtensionsInjectable, () => computed(() => [
+ {
+ name: "test-extension",
+ additionalCategoryColumns: [
+ {
+ group: "foo.bar.bat",
+ id: "high",
+ kind: "Test",
+ renderCell: () => "",
+ titleProps: {
+ title: "High",
+ },
+ } as AdditionalCategoryColumnRegistration,
+ {
+ group: "foo.bar",
+ id: "high",
+ kind: "Test",
+ renderCell: () => "",
+ titleProps: {
+ title: "High2",
+ },
+ } as AdditionalCategoryColumnRegistration,
+ ],
+ } as LensRendererExtension,
+ ]));
+ getCategoryColumns = di.inject(getCategoryColumnsInjectable);
+ });
+
+ it("should include columns from extensions that match", () => {
+ 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();
+ });
+ });
+});
diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx
index ae6ab3795f..107e02db30 100644
--- a/src/renderer/components/+catalog/catalog.tsx
+++ b/src/renderer/components/+catalog/catalog.tsx
@@ -28,25 +28,18 @@ import { RenderDelay } from "../render-delay/render-delay";
import { Icon } from "../icon";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
import { Avatar } from "../avatar";
-import { KubeObject } from "../../../common/k8s-api/kube-object";
-import { getLabelBadges } from "./helpers";
import { withInjectables } from "@ogre-tools/injectable-react";
-import catalogPreviousActiveTabStorageInjectable
- from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
+import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
-
-enum sortBy {
- name = "name",
- kind = "kind",
- source = "source",
- status = "status",
-}
+import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
+import getCategoryColumnsInjectable from "./get-category-columns.injectable";
interface Props extends RouteComponentProps {}
interface Dependencies {
- catalogPreviousActiveTabStorage: { set: (value: string ) => void }
- catalogEntityStore: CatalogEntityStore
+ catalogPreviousActiveTabStorage: { set: (value: string ) => void };
+ catalogEntityStore: CatalogEntityStore;
+ getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
}
@observer
@@ -228,46 +221,23 @@ class NonInjectedCatalog extends React.Component {
return null;
}
+ const { sortingCallbacks, searchFilters, renderTableContents, renderTableHeader } = this.props.getCategoryColumns({ activeCategory });
+
return (
entity.getName(),
- [sortBy.source]: entity => entity.getSource(),
- [sortBy.status]: entity => entity.status.phase,
- [sortBy.kind]: entity => entity.kind,
- }}
- searchFilters={[
- entity => [
- entity.getName(),
- entity.getId(),
- entity.status.phase,
- `source=${entity.getSource()}`,
- ...KubeObject.stringifyLabels(entity.metadata.labels),
- ],
- ]}
- renderTableHeader={[
- { title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" },
- !activeCategory && { title: "Kind", sortBy: sortBy.kind, id: "kind" },
- { title: "Source", className: styles.sourceCell, sortBy: sortBy.source, id: "source" },
- { title: "Labels", className: `${styles.labelsCell} scrollable`, id: "labels" },
- { title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
- ].filter(Boolean)}
+ sortingCallbacks={sortingCallbacks}
+ searchFilters={searchFilters}
+ renderTableHeader={renderTableHeader}
customizeTableRowProps={entity => ({
disabled: !entity.isEnabled(),
})}
- renderTableContents={entity => [
- this.renderName(entity),
- !activeCategory && entity.kind,
- entity.getSource(),
- getLabelBadges(entity),
- {entity.status.phase},
- ].filter(Boolean)}
+ renderTableContents={renderTableContents}
onDetails={this.onDetails}
renderItemMenu={this.renderItemMenu}
/>
@@ -306,17 +276,11 @@ class NonInjectedCatalog extends React.Component {
}
}
-export const Catalog = withInjectables(
- NonInjectedCatalog,
- {
- getProps: (di, props) => ({
- catalogEntityStore: di.inject(catalogEntityStoreInjectable),
-
- catalogPreviousActiveTabStorage: di.inject(
- catalogPreviousActiveTabStorageInjectable,
- ),
-
- ...props,
- }),
- },
-);
+export const Catalog = withInjectables( NonInjectedCatalog, {
+ getProps: (di, props) => ({
+ catalogEntityStore: di.inject(catalogEntityStoreInjectable),
+ catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
+ getCategoryColumns: di.inject(getCategoryColumnsInjectable),
+ ...props,
+ }),
+});
diff --git a/src/renderer/components/+catalog/custom-category-columns.injectable.tsx b/src/renderer/components/+catalog/custom-category-columns.injectable.tsx
new file mode 100644
index 0000000000..26d4a304d7
--- /dev/null
+++ b/src/renderer/components/+catalog/custom-category-columns.injectable.tsx
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
+import { computed, IComputedValue } from "mobx";
+import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
+import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
+import { getOrInsert } from "../../utils";
+import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
+
+interface Dependencies {
+ extensions: IComputedValue;
+}
+
+function getAdditionCategoryColumns({ extensions }: Dependencies): IComputedValue