diff --git a/.eslintrc.js b/.eslintrc.js
index a80fcdc805..3fda195286 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -16,6 +16,12 @@ module.exports = {
react: {
version: packageJson.devDependencies.react || "detect",
},
+ // the package eslint-import-resolver-typescript is required for this line which fixes errors when using .d.ts files
+ "import/resolver": {
+ "typescript": {
+ "alwaysTryTypes": true,
+ },
+ },
},
overrides: [
{
diff --git a/Makefile b/Makefile
index 6763e7e42a..3f77546354 100644
--- a/Makefile
+++ b/Makefile
@@ -103,8 +103,12 @@ publish-npm: node_modules build-npm
cd src/extensions/npm/extensions && npm publish --access=public --tag=$(NPM_RELEASE_TAG)
git restore src/extensions/npm/extensions/package.json
+.PHONY: build-docs
+build-docs:
+ yarn typedocs-extensions-api
+
.PHONY: docs
-docs:
+docs: build-docs
yarn mkdocs-serve-local
.PHONY: clean-extensions
diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md
index 434aec7444..90368645f3 100644
--- a/docs/extensions/guides/README.md
+++ b/docs/extensions/guides/README.md
@@ -14,25 +14,27 @@ Each guide or code sample includes the following:
## Guides
-| Guide | APIs |
-| ----- | ----- |
-| [Generate new extension project](generator.md) ||
-| [Main process extension](main-extension.md) | Main.LensExtension |
-| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
-| [Resource stack (cluster feature)](resource-stack.md) | |
-| [Stores](stores.md) | |
-| [Components](components.md) | |
-| [KubeObjectListLayout](kube-object-list-layout.md) | |
-| [Working with mobx](working-with-mobx.md) | |
-| [Protocol Handlers](protocol-handlers.md) | |
-| [Sending Data between main and renderer](ipc.md) | |
+| Guide | APIs |
+| --------------------------------------------------------------- | ---------------------- |
+| [Generate new extension project](generator.md) | |
+| [Main process extension](main-extension.md) | Main.LensExtension |
+| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
+| [Resource stack (cluster feature)](resource-stack.md) | |
+| [Extending KubernetesCluster)](extending-kubernetes-cluster.md) | |
+| [Stores](stores.md) | |
+| [Components](components.md) | |
+| [KubeObjectListLayout](kube-object-list-layout.md) | |
+| [Working with mobx](working-with-mobx.md) | |
+| [Protocol Handlers](protocol-handlers.md) | |
+| [Sending Data between main and renderer](ipc.md) | |
+| [Catalog Entities and Categories](catalog.md) | |
## Samples
-| Sample | APIs |
-| ----- | ----- |
-[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
-[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
-[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
-[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
-[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension Renderer.K8sApi.KubeApi Renderer.K8sApi.KubeObjectStore Renderer.Component.KubeObjectListLayout Renderer.Component.KubeObjectDetailsProps Renderer.Component.IconProps |
+| Sample | APIs |
+| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
+| [styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
+| [styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
+| [styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension LensRendererExtension Renderer.Component.Icon Renderer.Component.IconProps |
+| [custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension Renderer.K8sApi.KubeApi Renderer.K8sApi.KubeObjectStore Renderer.Component.KubeObjectListLayout Renderer.Component.KubeObjectDetailsProps Renderer.Component.IconProps |
diff --git a/docs/extensions/guides/catalog.md b/docs/extensions/guides/catalog.md
index 5425382638..24746df0ae 100644
--- a/docs/extensions/guides/catalog.md
+++ b/docs/extensions/guides/catalog.md
@@ -1,5 +1,27 @@
# Catalog (WIP)
-## CatalogCategoryRegistry
+This guide is a brief overview about how the catalog works within Lens.
+The catalog should be thought of as the single source of truth about data within Lens.
-## CatalogEntityRegistry
\ No newline at end of file
+The data flow is unidirectional, it only flows from the main side to the renderer side.
+All data is public within the catalog.
+
+## Categories
+
+A category is the declaration to the catalog of a specific kind of entity.
+It declares the currently supported versions of that kind of entity but providing the constructors for the entity classes.
+
+To declare a new category class you must create a new class that extends [Common.Catalog.CatalogCategory](../api/classes/Common.Catalog.CatalogCategory.md) and implement all of the abstract fields.
+
+The categories provided by Lens itself have the following names:
+
+- `KubernetesClusters`
+- `WebLinks`
+- `General`
+
+To register a category, call the `Main.Catalog.catalogCategories.add()` and `Renderer.Catalog.catalogCategories.add()` with instances of your class.
+
+## Entities
+
+An entity is the data within the catalog.
+All entities are typed and the class instances will be recreated on the renderer side by the catalog and the category registrations.
diff --git a/docs/extensions/guides/extending-kubernetes-cluster.md b/docs/extensions/guides/extending-kubernetes-cluster.md
new file mode 100644
index 0000000000..5c8170a2fe
--- /dev/null
+++ b/docs/extensions/guides/extending-kubernetes-cluster.md
@@ -0,0 +1,69 @@
+# Extending KubernetesCluster
+
+Extension can specify it's own subclass of Common.Catalog.KubernetesCluster. Extension can also specify a new Category for it in the Catalog.
+
+## Extending Common.Catalog.KubernetesCluster
+
+``` typescript
+import { Common } from "@k8slens/extensions";
+
+// The kind must be different from KubernetesCluster's kind
+export const kind = "ManagedDevCluster";
+
+export class ManagedDevCluster extends Common.Catalog.KubernetesCluster {
+ public static readonly kind = kind;
+
+ public readonly kind = kind;
+}
+```
+
+## Extending Common.Catalog.CatalogCategory
+
+These custom Catalog entities can be added a new Category in the Catalog.
+
+``` typescript
+import { Common } from "@k8slens/extensions";
+import { kind, ManagedDevCluster } from "../entities/ManagedDevCluster";
+
+class ManagedDevClusterCategory extends Common.Catalog.CatalogCategory {
+ public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
+ public readonly kind = "CatalogCategory";
+ public metadata = {
+ name: "Managed Dev Clusters",
+ icon: ""
+ };
+ public spec: Common.Catalog.CatalogCategorySpec = {
+ group: "entity.k8slens.dev",
+ versions: [
+ {
+ name: "v1alpha1",
+ entityClass: ManagedDevCluster as any,
+ },
+ ],
+ names: {
+ kind
+ },
+ };
+}
+
+export { ManagedDevClusterCategory };
+export type { ManagedDevClusterCategory as ManagedDevClusterCategoryType };
+```
+
+The category needs to be registered in the `onActivate()` method both in main and renderer
+
+``` typescript
+// in main's on onActivate
+Main.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
+```
+
+``` typescript
+// in renderer's on onActivate
+Renderer.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
+```
+
+You can then add the entities to the Catalog as a new source:
+
+``` typescript
+this.addCatalogSource("managedDevClusters", this.managedDevClusters);
+```
diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md
index a0e20880bf..d05368529c 100644
--- a/docs/extensions/guides/main-extension.md
+++ b/docs/extensions/guides/main-extension.md
@@ -16,11 +16,11 @@ import { Main } from "@k8slens/extensions";
export default class ExampleExtensionMain extends Main.LensExtension {
onActivate() {
- console.log('custom main process extension code started');
+ console.log("custom main process extension code started");
}
onDeactivate() {
- console.log('custom main process extension de-activated');
+ console.log("custom main process extension de-activated");
}
}
```
@@ -33,21 +33,21 @@ Implementing `onDeactivate()` gives you the opportunity to clean up after your e
Disable extensions from the Lens Extensions page:
1. Navigate to **File** > **Extensions** in the top menu bar.
-(On Mac, it is **Lens** > **Extensions**.)
+ (On Mac, it is **Lens** > **Extensions**.)
2. Click **Disable** on the extension you want to disable.
The example above logs messages when the extension is enabled and disabled.
To see standard output from the main process there must be a console connected to it.
Achieve this by starting Lens from the command prompt.
-For more details on accessing Lens state data, please see the [Stores](../stores) guide.
+For more details on accessing Lens state data, please see the [Stores](stores.md) guide.
### `appMenus`
The Main Extension API allows you to customize the UI application menu.
The following example demonstrates adding an item to the **Help** menu.
-``` typescript
+```typescript
import { Main } from "@k8slens/extensions";
export default class SamplePageMainExtension extends Main.LensExtension {
@@ -57,9 +57,9 @@ export default class SamplePageMainExtension extends Main.LensExtension {
label: "Sample",
click() {
console.log("Sample clicked");
- }
- }
- ]
+ },
+ },
+ ];
}
```
@@ -67,18 +67,18 @@ export default class SamplePageMainExtension extends Main.LensExtension {
`MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface.
The properties of the appMenus array objects are defined as follows:
-* `parentId` is the name of the menu where your new menu item will be listed.
-Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
-`"lens"` is valid on Mac only.
-* `label` is the name of your menu item.
-* `click()` is called when the menu item is selected.
-In this example, we simply log a message.
-However, you would typically have this navigate to a specific page or perform another operation.
-Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
+- `parentId` is the name of the menu where your new menu item will be listed.
+ Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
+ `"lens"` is valid on Mac only.
+- `label` is the name of your menu item.
+- `click()` is called when the menu item is selected.
+ In this example, we simply log a message.
+ However, you would typically have this navigate to a specific page or perform another operation.
+ Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
The following example demonstrates how an application menu can be used to navigate to such a page:
-``` typescript
+```typescript
import { Main } from "@k8slens/extensions";
export default class SamplePageMainExtension extends Main.LensExtension {
@@ -86,9 +86,9 @@ export default class SamplePageMainExtension extends Main.LensExtension {
{
parentId: "help",
label: "Sample",
- click: () => this.navigate("myGlobalPage")
- }
- ]
+ click: () => this.navigate("myGlobalPage"),
+ },
+ ];
}
```
@@ -99,32 +99,36 @@ This page would be defined in your extension's `Renderer.LensExtension` implemen
`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`.
-``` typescript
+```typescript
interface TrayMenuRegistration {
label?: string;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
- type?: "normal" | "separator" | "submenu"
+ type?: "normal" | "separator" | "submenu";
toolTip?: string;
enabled?: boolean;
- submenu?: TrayMenuRegistration[]
+ submenu?: TrayMenuRegistration[];
}
```
The following example demonstrates how tray menus can be added from extension:
-``` typescript
+```typescript
import { Main } from "@k8slens/extensions";
export default class SampleTrayMenuMainExtension extends Main.LensExtension {
- trayMenus = [{
- label: "menu from the extension",
- click: () => { console.log("tray menu clicked!") }
- }]
+ trayMenus = [
+ {
+ label: "menu from the extension",
+ click: () => {
+ console.log("tray menu clicked!");
+ },
+ },
+ ];
}
```
### `addCatalogSource()` and `removeCatalogSource()` Methods
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
-See the [`Catalog`](catalog.md) documentation for full details about the catalog.
\ No newline at end of file
+See the [`Catalog`](catalog.md) documentation for full details about the catalog.
diff --git a/mkdocs.yml b/mkdocs.yml
index 4ed763e6d3..b869a63ee9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -24,6 +24,7 @@ nav:
- Renderer Extension: extensions/guides/renderer-extension.md
- Catalog: extensions/guides/catalog.md
- Resource Stack: extensions/guides/resource-stack.md
+ - Extending KubernetesCluster: extensions/guides/extending-kubernetes-cluster.md
- Stores: extensions/guides/stores.md
- Working with MobX: extensions/guides/working-with-mobx.md
- Protocol Handlers: extensions/guides/protocol-handlers.md
diff --git a/package.json b/package.json
index 0443b60eea..2667dea25b 100644
--- a/package.json
+++ b/package.json
@@ -340,6 +340,7 @@
"esbuild": "^0.13.15",
"esbuild-loader": "^2.16.0",
"eslint": "^7.32.0",
+ "eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts
index ee93106adf..f11bd2d593 100644
--- a/src/common/__tests__/cluster-store.test.ts
+++ b/src/common/__tests__/cluster-store.test.ts
@@ -5,7 +5,6 @@
import fs from "fs";
import mockFs from "mock-fs";
-import yaml from "js-yaml";
import path from "path";
import fse from "fs-extra";
import type { Cluster } from "../cluster/cluster";
@@ -334,159 +333,6 @@ users:
});
});
- describe("pre 2.0 config with an existing cluster", () => {
- beforeEach(() => {
- ClusterStore.resetInstance();
-
- const mockOpts = {
- "some-directory-for-user-data": {
- "lens-cluster-store.json": JSON.stringify({
- __internal__: {
- migrations: {
- version: "1.0.0",
- },
- },
- cluster1: minimalValidKubeConfig,
- }),
- },
- };
-
- mockFs(mockOpts);
-
- clusterStore = mainDi.inject(clusterStoreInjectable);
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
- it("migrates to modern format with kubeconfig in a file", async () => {
- const config = clusterStore.clustersList[0].kubeConfigPath;
-
- expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`);
- });
- });
-
- describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
- beforeEach(() => {
- ClusterStore.resetInstance();
- const mockOpts = {
- "some-directory-for-user-data": {
- "lens-cluster-store.json": JSON.stringify({
- __internal__: {
- migrations: {
- version: "2.4.1",
- },
- },
- cluster1: {
- kubeConfig: JSON.stringify({
- apiVersion: "v1",
- clusters: [
- {
- cluster: {
- server: "https://10.211.55.6:8443",
- },
- name: "minikube",
- },
- ],
- contexts: [
- {
- context: {
- cluster: "minikube",
- user: "minikube",
- name: "minikube",
- },
- name: "minikube",
- },
- ],
- "current-context": "minikube",
- kind: "Config",
- preferences: {},
- users: [
- {
- name: "minikube",
- user: {
- "client-certificate": "/Users/foo/.minikube/client.crt",
- "client-key": "/Users/foo/.minikube/client.key",
- "auth-provider": {
- config: {
- "access-token": ["should be string"],
- expiry: ["should be string"],
- },
- },
- },
- },
- ],
- }),
- },
- }),
- },
- };
-
- mockFs(mockOpts);
-
- clusterStore = mainDi.inject(clusterStoreInjectable);
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
- it("replaces array format access token and expiry into string", async () => {
- const file = clusterStore.clustersList[0].kubeConfigPath;
- const config = fs.readFileSync(file, "utf8");
- const kc = yaml.load(config) as Record;
-
- expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe(
- "should be string",
- );
- expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe(
- "should be string",
- );
- });
- });
-
- describe("pre 2.6.0 config with a cluster icon", () => {
- beforeEach(() => {
- ClusterStore.resetInstance();
- const mockOpts = {
- "some-directory-for-user-data": {
- "lens-cluster-store.json": JSON.stringify({
- __internal__: {
- migrations: {
- version: "2.4.1",
- },
- },
- cluster1: {
- kubeConfig: minimalValidKubeConfig,
- icon: "icon_path",
- preferences: {
- terminalCWD: "/some-directory-for-user-data",
- },
- },
- }),
- icon_path: testDataIcon,
- },
- };
-
- mockFs(mockOpts);
-
- clusterStore = mainDi.inject(clusterStoreInjectable);
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
- it("moves the icon into preferences", async () => {
- const storedClusterData = clusterStore.clustersList[0];
-
- expect(Object.prototype.hasOwnProperty.call(storedClusterData, "icon")).toBe(false);
- expect(Object.prototype.hasOwnProperty.call(storedClusterData.preferences, "icon")).toBe(true);
- expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
- });
- });
-
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts
index 2dfb3750ff..813d5d3866 100644
--- a/src/common/catalog-entities/kubernetes-cluster.ts
+++ b/src/common/catalog-entities/kubernetes-cluster.ts
@@ -59,8 +59,8 @@ export interface KubernetesClusterStatus extends CatalogEntityStatus {
}
export class KubernetesCluster extends CatalogEntity {
- public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
- public static readonly kind = "KubernetesCluster";
+ public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
+ public static readonly kind: string = "KubernetesCluster";
public readonly apiVersion = KubernetesCluster.apiVersion;
public readonly kind = KubernetesCluster.kind;
diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts
index 70ebd1aa6b..46ce8228ff 100644
--- a/src/common/catalog/catalog-entity.ts
+++ b/src/common/catalog/catalog-entity.ts
@@ -8,6 +8,7 @@ import type TypedEmitter from "typed-emitter";
import { observable, makeObservable } from "mobx";
import { once } from "lodash";
import { iter, Disposer } from "../utils";
+import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
type ExtractEntityMetadataType = Entity extends CatalogEntity ? Metadata : never;
type ExtractEntityStatusType = Entity extends CatalogEntity ? Status : never;
@@ -46,6 +47,7 @@ export interface CatalogCategorySpec {
* The grouping for for the category. This MUST be a DNS label.
*/
group: string;
+
/**
* The specific versions of the constructors.
*
@@ -54,6 +56,10 @@ export interface CatalogCategorySpec {
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
*/
versions: CatalogCategoryVersion[];
+
+ /**
+ * This is the concerning the category
+ */
names: {
/**
* The kind of entity that this category is for. This value MUST be a DNS
@@ -62,38 +68,107 @@ export interface CatalogCategorySpec {
*/
kind: string;
};
+
+ /**
+ * These are the columns used for displaying entities when in the catalog.
+ *
+ * If this is not provided then some default columns will be used, similar in
+ * scope to the columns in the "Browse" view.
+ *
+ * Even if you provide columns, a "Name" column will be provided as well with
+ * `priority: 0`.
+ *
+ * These columns will not be used in the "Browse" view.
+ */
+ displayColumns?: CategoryColumnRegistration[];
}
/**
- * If the filter returns true, the menu item is displayed
+ * If the filter return a thruthy value, the menu item is displayed
*/
export type AddMenuFilter = (menu: CatalogEntityAddMenu) => any;
export interface CatalogCategoryEvents {
+ /**
+ * This event will be emitted when the category is loaded in the catalog
+ * view.
+ */
load: () => void;
+
+ /**
+ * This event will be emitted when the catalog add menu is opened and is the
+ * way to added entries to that menu.
+ */
catalogAddMenu: (context: CatalogEntityAddMenuContext) => void;
+
+ /**
+ * This event will be emitted when the context menu for an entity is declared
+ * by this category is opened.
+ */
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
}
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter) {
+ /**
+ * The version of category that you are wanting to declare.
+ *
+ * Currently supported values:
+ *
+ * - `"catalog.k8slens.dev/v1alpha1"`
+ */
abstract readonly apiVersion: string;
+
+ /**
+ * The kind of item you wish to declare.
+ *
+ * Currently supported values:
+ *
+ * - `"CatalogCategory"`
+ */
abstract readonly kind: string;
- abstract metadata: {
+
+ /**
+ * The data about the category itself
+ */
+ abstract readonly metadata: {
+ /**
+ * The name of your category. The category can be searched for by this
+ * value. This will also be used for the catalog menu.
+ */
name: string;
+
+ /**
+ * Either an `