diff --git a/docs/extensions/README.md b/docs/extensions/README.md new file mode 100644 index 0000000000..f8906d75ee --- /dev/null +++ b/docs/extensions/README.md @@ -0,0 +1,46 @@ +# Lens Extension API + +Customize and enhance the Lens experience with the Lens Extension API. +Use the extension API to create menus or page content. +The same extension API was used to create many of Lens's core features. +To install your first extension you should goto the [extension page](lens://app/extensions) in lens. + +This documentation describes: + +* How to build, run, test, and publish an extension. +* How to take full advantage of the Lens Extension API. +* Where to find [guides](guides/README.md) and [code samples](https://github.com/lensapp/lens-extension-samples) to help get you started. + +## What Extensions Can Do + +Here are some examples of what you can achieve with the Extension API: + +* Add custom components & views in the UI - Extending the Lens Workbench + +For an overview of the Lens Extension API, refer to the [Common Capabilities](capabilities/common-capabilities.md) page. [Extension Guides Overview](guides/README.md) also includes a list of code samples and guides that illustrate various ways of using the Lens Extension API. + +## How to Build Extensions + +Here is what each section of the Lens Extension API docs can help you with: + +* **Getting Started** teaches fundamental concepts for building extensions with the Hello World sample. +* **Extension Capabilities** dissects Lens's Extension API into smaller categories and points you to more detailed topics. +* **Extension Guides** includes guides and code samples that explain specific usages of Lens Extension API. +* **Testing and Publishing** includes in-depth guides on various extension development topics, such as testing and publishing extensions. +* **API Reference** contains exhaustive references for the Lens Extension API, Contribution Points, and many other topics. + +## What's New + +Just like Lens itself, the extension API updates on a monthly cadence, rolling out new features with every release. + +Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases). + +## Looking for Help + +If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time. + +To provide feedback on the documentation or issues with the Lens Extension API, create new issues at [lensapp/lens](https://github.com/lensapp/lens/issues). Please use the labels `area/documentation` and/or `area/extension`. + +## Downloading Lens + +[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux. diff --git a/docs/extensions/capabilities/README.md b/docs/extensions/capabilities/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md new file mode 100644 index 0000000000..676603ce21 --- /dev/null +++ b/docs/extensions/capabilities/common-capabilities.md @@ -0,0 +1,283 @@ +# Common Capabilities + +Here we will discuss common and important building blocks for your extensions, and explain how you can use them. +Almost all extensions use some of these functionalities. + +## Main Extension + +The main extension runs in the background. +It adds app menu items to the Lens UI. +In order to see logs from this extension, you need to start Lens from the command line. + +### Activate + +This extension can register a custom callback that is executed when an extension is activated (started). + +``` javascript +import { LensMainExtension } from "@k8slens/extensions" + +export default class ExampleMainExtension extends LensMainExtension { + async onActivate() { + console.log("hello world") + } +} +``` + +### Deactivate + +This extension can register a custom callback that is executed when an extension is deactivated (stopped). + +``` javascript +import { LensMainExtension } from "@k8slens/extensions" + +export default class ExampleMainExtension extends LensMainExtension { + async onDeactivate() { + console.log("bye bye") + } +} +``` + +### App Menus + +This extension can register custom app menus that will be displayed on OS native menus. + +Example: + +```typescript +import { LensMainExtension, windowManager } from "@k8slens/extensions" + +export default class ExampleMainExtension extends LensMainExtension { + appMenus = [ + { + parentId: "help", + label: "Example item", + click() { + windowManager.navigate("https://k8slens.dev"); + } + } + ] +} +``` + +## Renderer Extension + +The renderer extension runs in a browser context, and is visible in Lens's main window. +In order to see logs from this extension you need to check them via **View** > **Toggle Developer Tools** > **Console**. + +### Activate + +This extension can register a custom callback that is executed when an extension is activated (started). + +``` javascript +import { LensRendererExtension } from "@k8slens/extensions" + +export default class ExampleExtension extends LensRendererExtension { + async onActivate() { + console.log("hello world") + } +} +``` + +### Deactivate + +This extension can register a custom callback that is executed when an extension is deactivated (stopped). + +``` javascript +import { LensRendererExtension } from "@k8slens/extensions" + +export default class ExampleMainExtension extends LensRendererExtension { + async onDeactivate() { + console.log("bye bye") + } +} +``` + +### Global Pages + +This extension can register custom global pages (views) to Lens's main window. +The global page is a full-screen page that hides all other content from a window. + +```typescript +import React from "react" +import { Component, LensRendererExtension } from "@k8slens/extensions" +import { ExamplePage } from "./src/example-page" + +export default class ExampleRendererExtension extends LensRendererExtension { + globalPages = [ + { + id: "example", + components: { + Page: ExamplePage, + } + } + ] + + globalPageMenus = [ + { + title: "Example page", // used in icon's tooltip + target: { pageId: "example" } + components: { + Icon: () => , + } + } + ] +} +``` + +### App Preferences + +This extension can register custom app preferences. +It is responsible for storing a state for custom preferences. + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions" +import { myCustomPreferencesStore } from "./src/my-custom-preferences-store" +import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference" + + +export default class ExampleRendererExtension extends LensRendererExtension { + appPreferences = [ + { + title: "My Custom Preference", + components: { + Hint: () => , + Input: () => + } + } + ] +} +``` + +### Cluster Pages + +This extension can register custom cluster pages. +These pages are visible in a cluster menu when a cluster is opened. + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./src/page" + +export default class ExampleExtension extends LensRendererExtension { + clusterPages = [ + { + id: "extension-example", // optional + exact: true, // optional + components: { + Page: () => , + } + } + ] + + clusterPageMenus = [ + { + url: "/extension-example", // optional + title: "Example Extension", + components: { + Icon: ExampleIcon, + } + } + ] +} + +``` + +### Cluster Features + +This extension can register installable features for a cluster. +These features are visible in the "Cluster Settings" page. + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions" +import { MyCustomFeature } from "./src/my-custom-feature" + +export default class ExampleExtension extends LensRendererExtension { + clusterFeatures = [ + { + title: "My Custom Feature", + components: { + Description: () => { + return ( + + Just an example. + + ) + } + }, + feature: new MyCustomFeature() + } + ] +} + +``` + +### Status Bar Items + +This extension can register custom icons and text to a status bar area. + +```typescript +import React from "react"; +import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions"; + +export default class ExampleExtension extends LensRendererExtension { + statusBarItems = [ + { + components: { + Item: ( +
this.navigate("/example-page")} > + +
+ ) + } + } + ] +} + +``` + +### Kubernetes Object Menu Items + +This extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions. + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions"; +import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item" + +export default class ExampleExtension extends LensRendererExtension { + kubeObjectMenuItems = [ + { + kind: "Node", + apiVersions: ["v1"], + components: { + MenuItem: (props: CustomMenuItemProps) => + } + } + ] +} + +``` + +### Kubernetes Object Details + +This extension can register custom details (content) for specified Kubernetes kinds/apiVersions. + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions"; +import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details" + +export default class ExampleExtension extends LensRendererExtension { + kubeObjectDetailItems = [ + { + kind: "CustomKind", + apiVersions: ["custom.acme.org/v1"], + components: { + Details: (props: CustomKindDetailsProps) => + } + } + ] +} +``` diff --git a/docs/extensions/capabilities/images/css-vars-in-devtools.png b/docs/extensions/capabilities/images/css-vars-in-devtools.png new file mode 100644 index 0000000000..a9df97e6bb Binary files /dev/null and b/docs/extensions/capabilities/images/css-vars-in-devtools.png differ diff --git a/docs/extensions/capabilities/images/theme-selector.png b/docs/extensions/capabilities/images/theme-selector.png new file mode 100644 index 0000000000..5c2eba3165 Binary files /dev/null and b/docs/extensions/capabilities/images/theme-selector.png differ diff --git a/docs/extensions/capabilities/styling.md b/docs/extensions/capabilities/styling.md new file mode 100644 index 0000000000..62dbddde1a --- /dev/null +++ b/docs/extensions/capabilities/styling.md @@ -0,0 +1,163 @@ +# Styling an Extension + +Lens provides a set of global styles and UI components that can be used by any extension to preserve the look and feel of the application. + +## Layout + +For layout tasks, Lens uses the [flex.box](https://www.npmjs.com/package/flex.box) library which provides helpful class names to specify some of the [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox) properties. +For example, consider the following HTML and its associated CSS properties: + +```html +
+``` + +```css +div { + display: flex; + flex-direction: column; + align-items: center; +} +``` + +However, you are free to use any styling technique or framework you like, including [Emotion](https://github.com/emotion-js/emotion) or even plain CSS. + +### Layout Variables + +There is a set of CSS variables available for for basic layout needs. +They are located inside `:root` and are defined in [app.scss](https://github.com/lensapp/lens/blob/master/src/renderer/components/app.scss): + +```css +--unit: 8px; +--padding: var(--unit); +--margin: var(--unit); +--border-radius: 3px; +``` + +These variables are intended to set consistent margins and paddings across components. +For example: + +```css +.status { + padding-left: calc(var(--padding) * 2); + border-radius: var(--border-radius); +} +``` + +## Themes + +Lens uses two built-in themes defined in [the themes directory](https://github.com/lensapp/lens/tree/master/src/renderer/themes) – one light and one dark. + +### Theme Variables + +When Lens is loaded, it transforms the selected theme's `json` file into a list of [CSS Custom Properties (CSS Variables)](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). +This list then gets injected into the `:root` element so that any of the down-level components can use them. +![CSS vars listed in devtools](images/css-vars-in-devtools.png) + +When the user changes the theme, the above process is repeated, and new CSS variables appear, replacing the previous ones. + +If you want to preserve Lens's native look and feel, with respect to the lightness or darkness of your extension, you can use the provided variables and built-in Lens components such as `Button`, `Select`, `Table`, and so on. + +There is a set of CSS variables available for extensions to use for theming. +They are all located inside `:root` and are defined in [app.scss](https://github.com/lensapp/lens/blob/master/src/renderer/components/app.scss): + +```css +--font-main: 'Roboto', 'Helvetica', 'Arial', sans-serif; +--font-monospace: Lucida Console, Monaco, Consolas, monospace; +--font-size-small: calc(1.5 * var(--unit)); +--font-size: calc(1.75 * var(--unit)); +--font-size-big: calc(2 * var(--unit)); +--font-weight-thin: 300; +--font-weight-normal: 400; +--font-weight-bold: 500; +``` + +as well as in [the theme modules](https://github.com/lensapp/lens/tree/master/src/renderer/themes): + +``` +--blue: #3d90ce; +--magenta: #c93dce; +--golden: #ffc63d; +--halfGray: #87909c80; +--primary: #3d90ce; +--textColorPrimary: #555555; +--textColorSecondary: #51575d; +--textColorAccent: #333333; +--borderColor: #c9cfd3; +--borderFaintColor: #dfdfdf; +--mainBackground: #f1f1f1; +--contentColor: #ffffff; +--layoutBackground: #e8e8e8; +--layoutTabsBackground: #f8f8f8; +--layoutTabsActiveColor: #333333; +--layoutTabsLineColor: #87909c80; +... +``` + +These variables can be used in the following form: `var(--magenta)`. +For example: + +```css +.status { + font-size: var(--font-size-small); + background-color: var(--colorSuccess); +} +``` + +### Theme Switching + +When the light theme is active, the `` element gets a "theme-light" class, or: ``. +If the class isn't there, the theme defaults to dark. The active theme can be changed in the **Preferences** page: +![Color Theme](images/theme-selector.png) + +There is a way of detect active theme and its changes in JS. [MobX observer function/decorator](https://github.com/mobxjs/mobx-react#observercomponent) can be used for this purpose. + +```js +import React from "react" +import { observer } from "mobx-react" +import { App, Component, Theme } from "@k8slens/extensions"; + +@observer +export class SupportPage extends React.Component { + render() { + return ( +
+

Active theme is {Theme.getActiveTheme().name}

+
+ ); + } +} +``` + +`Theme` entity from `@k8slens/extensions` provides active theme object and `@observer` decorator makes component reactive - so it will rerender each time any of the observables (active theme in our case) will be changed. + +Working example provided in [Styling with Emotion](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) sample extension. + +## Injected Styles + +Every extension is affected by the list of default global styles defined in [app.scss](https://github.com/lensapp/lens/blob/master/src/renderer/components/app.scss). These are basic browser resets and element styles, including: + +- setting the `box-sizing` property for every element +- default text and background colors +- default font sizes +- basic heading (h1, h2, etc) formatting +- custom scrollbar styling + +Extensions may overwrite these defaults if needed. They have low CSS specificity, so overriding them should be fairly easy. + +## CSS-in-JS + +If an extension uses a system like `Emotion` to work with styles, it can use CSS variables as follows: + +```javascript +const Container = styled.div(() => ({ + backgroundColor: 'var(--mainBackground)' +})); +``` + +## Examples + +You can explore samples for each styling technique that you can use for extensions: + +- [Styling with Sass](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) +- [Styling with Emotion](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) +- [Styling with CSS Modules](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) diff --git a/docs/extensions/get-started/anatomy.md b/docs/extensions/get-started/anatomy.md new file mode 100644 index 0000000000..f445e421bf --- /dev/null +++ b/docs/extensions/get-started/anatomy.md @@ -0,0 +1,116 @@ +# Extension Anatomy + +In the [previous section](your-first-extension.md) you learned how to create your first extension. +In this section you will learn how this extension works under the hood. + +The Hello World sample extension does three things: + +- Implements `onActivate()` and outputs a message to the console. +- Implements `onDectivate()` and outputs a message to the console. +- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard. + +Let's take a closer look at our Hello World sample's source code and see how these three things are achieved. + +## Extension File Structure + +``` +. +├── .gitignore // Ignore build output and node_modules +├── Makefile // Config for build tasks that compiles the extension +├── README.md // Readable description of your extension's functionality +├── src +│ └── page.tsx // Extension's additional source code +├── main.ts // Source code for extension's main entrypoint +├── package.json // Extension manifest and dependencies +├── renderer.tsx // Source code for extension's renderer entrypoint +├── tsconfig.json // TypeScript configuration +├── webpack.config.js // Webpack configuration +``` + +The extension directory contains the extension's entry files and a few configuration files. +Three files: `package.json`, `main.ts` and `renderer.tsx` are essential to understanding the Hello World sample extension. +We'll look at those first. + +### Extension Manifest + +Each Lens extension must have a `package.json` file. +It contains a mix of Node.js fields, including scripts and dependencies, and Lens-specific fields such as `publisher` and `contributes`. +Some of the most-important fields include: + +- `name` and `publisher`: Lens uses `@/` as a unique ID for the extension. +For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`. +Lens uses this ID to uniquely identify your extension. +- `main`: the extension's entry point run in `main` process. +- `renderer`: the extension's entry point run in `renderer` process. +- `engines.lens`: the minimum version of Lens API that the extension depends upon. + +``` javascript +{ + "name": "helloworld-sample", + "publisher": "lens-samples", + "version": "0.0.1", + "description": "Lens helloworld-sample", + "license": "MIT", + "homepage": "https://github.com/lensapp/lens-extension-samples", + "engines": { + "lens": "^4.0.0" + }, + "main": "dist/main.js", + "renderer": "dist/renderer.js", + "scripts": { + "build": "webpack --config webpack.config.js", + "dev": "npm run build --watch" + }, + "dependencies": { + "react-open-doodles": "^1.0.5" + }, + "devDependencies": { + "@k8slens/extensions": "^4.0.0-alpha.2", + "ts-loader": "^8.0.4", + "typescript": "^4.0.3", + "@types/react": "^16.9.35", + "@types/node": "^12.0.0", + "webpack": "^4.44.2", + "webpack-cli": "^3.3.11" + } +} +``` + +## Extension Entry Files + +Lens extensions can have two separate entry files. +One file is used in the `main` process of the Lens application and the other is used in the `renderer` process. +The `main` entry file exports the class that extends `LensMainExtension`, and the `renderer` entry file exports the class that extends `LensRendererExtension`. + +Both extension classes have `onActivate` and `onDeactivate` methods. +The `onActivate` method is executed when your extension is activated. +If you need to initialize something in your extension, this is where such an operation should occur. +The `onDeactivate` method gives you a chance to clean up before your extension becomes deactivated. +For extensions where explicit cleanup is not required, you don't need to override this method. +However, if an extension needs to perform an operation when Lens is shutting down (or if the extension is disabled or uninstalled), this is the method where such an operation should occur. + +The Hello World sample extension does not do anything on the `main` process, so we'll focus on the `renderer` process, instead. +On the `renderer` entry point, the Hello World sample extension defines the `Cluster Page` object. +The `Cluster Page` object registers the `/extension-example` path, and this path renders the `ExamplePage` React component. +It also registers the `MenuItem` component that displays the `ExampleIcon` React component and the "Hello World" text in the left-side menu of the cluster dashboard. +These React components are defined in the additional `./src/page.tsx` file. + +``` typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./page" +import React from "react" + +export default class ExampleExtension extends LensRendererExtension { + clusterPages = [ + { + id: "extension-example", + components: { + Page: () => , + } + } + ] +} +``` + +The Hello World sample extension uses the `Cluster Page` capability, which is just one of the Lens extension API's capabilities. +The [Common Capabilities](../capabilities/common-capabilities.md) page will help you home in on the right capabilities to use with your own extensions. diff --git a/docs/extensions/get-started/overview.md b/docs/extensions/get-started/overview.md new file mode 100644 index 0000000000..84777aafa0 --- /dev/null +++ b/docs/extensions/get-started/overview.md @@ -0,0 +1,27 @@ +# Extension Development Overview + +This is a general overview to how the development of an extension will proceed. +For building extensions there will be a few things that you should have installed, and some other things that might be of help. + +### Required: +- [Node.js](https://www.nodejs.org/en/) +- [Git](https://www.git-scm.com/) +- Some sort of text editor – we recommend [VSCode](https://code.visualstudio.com/) +- We use [Webpack](https://www.webpack.js.org/) for compilation. +All extension need to be at least compatible with a webpack system. + +### Recommended: + +All Lens extensions are javascript packages. +We recommend that you program in [Typescript](https://www.typescriptlang.org/) because it catches many common errors. + +Lens is a standard [Electron](https://www.electronjs.org/) application with both main and renderer processes. +An extension is made up of two parts, one for each of Lens's core processes. +When an extension is loaded, each part is first loaded and issues a notification that it has been loaded. +From there, the extension can start doing is work. + +Lens uses [React](https://www.reactjs.org/) as its UI framework and provides some of Lens's own components for reuse with extensions. +An extension is responsible for the lifetime of any resources it spins up. +If an extension's main part starts new processes they all must be stopped and cleaned up when the extension is deactivated or unloaded. + +See [Your First Extension](your-first-extension.md) to get started. diff --git a/docs/extensions/get-started/wrapping-up.md b/docs/extensions/get-started/wrapping-up.md new file mode 100644 index 0000000000..f4aa174476 --- /dev/null +++ b/docs/extensions/get-started/wrapping-up.md @@ -0,0 +1,24 @@ +# Wrapping Up + +In [Your First Extension](your-first-extension.md), you learned how to create and run an extension. +In [Extension Anatomy](anatomy.md), you learned in detail how a basic extension works. +This is just a glimpse into what can be created with Lens extensions. +Below are some suggested routes for learning more. + +## Extension Capabilities + +In this section, you'll find information on common extension capabilities, styling information, and a color reference guide. +Determine whether your idea for an extension is doable and get ideas for new extensions by reading through the [Common Capabilities](../capabilities/common-capabilities.md) page. + +## Guides and Samples + +Here you'll find a collection of sample extensions that you can use as a base to work from. +Some of these samples include a detailed guide that explains the source code. +You can find all samples and guides in the [lens-extension-samples](https://github.com/lensapp/lens-extension-samples) repository. + +## Testing and Publishing + +In this section, you can learn: + +* How to add [integration tests](../testing-and-publishing/testing.md) to your extension +* How to [publish your extension](../testing-and-publishing/publishing.md) diff --git a/docs/extensions/get-started/your-first-extension.md b/docs/extensions/get-started/your-first-extension.md new file mode 100644 index 0000000000..45ed3f885f --- /dev/null +++ b/docs/extensions/get-started/your-first-extension.md @@ -0,0 +1,100 @@ +# Your First Extension + +We recommend to always use [Yeoman generator for Lens Extension](https://github.com/lensapp/generator-lens-ext) to start new extension project. +[Read the generator guide here](../guides/generator.md). + +If you want to setup the project manually, please continue reading. + +## First Extension + +In this topic, you'll learn the basics of building extensions by creating an extension that adds a "Hello World" page to a cluster menu. + +## Install the Extension + +To install the extension, clone the [Lens Extension samples](https://github.com/lensapp/lens-extension-samples) repository to your local machine: + +```sh +git clone https://github.com/lensapp/lens-extension-samples.git +``` + +Next you need to create a symlink. +A symlink connects the directory that Lens will monitor for user-installed extensions to the sample extension. +In this case the sample extension is `helloworld-sample`. + +### Linux & macOS + +```sh +mkdir -p ~/.k8slens/extensions +cd ~/.k8slens/extensions +ln -s lens-extension-samples/helloworld-sample helloworld-sample +``` + +### Windows + +Create the directory that Lens will monitor for user-installed extensions: + +```sh +mkdir C:\Users\\.k8slens\extensions -force +cd C:\Users\\.k8slens\extensions +``` + +If you have administrator rights, you can create symlink to the sample extension – in this case `helloworld-sample`: + +```sh +cmd /c mklink /D helloworld-sample lens-extension-samples\helloworld-sample +``` + +Without administrator rights, you need to copy the extensions sample directory into `C:\Users\\.k8slens\extensions`: + +``` +Copy-Item 'lens-extension-samples\helloworld-sample' 'C:\Users\\.k8slens\extensions\helloworld-sample' +``` + +## Build the Extension + +To build the extension you can use `make` or run the `npm` commands manually: + +```sh +cd /helloworld-sample +make build +``` + +To run the `npm` commands, enter: + +```sh +cd /helloworld-sample +npm install +npm run build +``` + +Optionally, automatically rebuild the extension by watching for changes to the source code. +To do so, enter: + +```sh +cd /helloworld-sample +npm run dev +``` + +You must restart Lens for the extension to load. +After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. + +With Lens running, either connect to an existing cluster or [create a new one](../../clusters/adding-clusters.md). +You will see the "Hello World" page in the left-side cluster menu. + +## Develop the Extension + +Finally, you'll make a change to the message that our Hello World sample extension displays: + +1. Navigate to `/helloworld-sample`. +2. In `page.tsx`, change the message from `HelloWorld!` to `Hello Lens Extensions`. +3. Rebuild the extension. If you used `npm run dev`, the extension will rebuild automatically. +4. Reload the Lens window. +5. Click on the Hello World page. +6. The updated message will appear. + +## Next Steps + +In the [next topic](anatomy.md), we'll take a closer look at the source code of our Hello World sample. + +You can find the source code for this tutorial at: [lensapp/lens-extension-samples](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample). +[Extension Guides](../guides/README.md) contains additional samples. diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md new file mode 100644 index 0000000000..06bbbe9e3c --- /dev/null +++ b/docs/extensions/guides/README.md @@ -0,0 +1,37 @@ +# Extension Guides + +This section explains how to use specific Lens Extension APIs. +It includes detailed guides and code samples. +For introductory information about the Lens Extension API, please see [Your First Extension](../get-started/your-first-extension.md). + +Each guide or code sample includes the following: + +- Clearly commented source code. +- Instructions for running the sample extension. +- An image showing the sample extension's appearance and usage. +- A listing of the Extension API being used. +- An explanation of the concepts relevant to the Extension. + +## Guides + +| Guide | APIs | +| ----- | ----- | +| [Generate new extension project](generator.md) || +| [Main process extension](main-extension.md) | LensMainExtension | +| [Renderer process extension](renderer-extension.md) | LensRendererExtension | +| [Stores](stores.md) | | +| [Components](components.md) | | +| [KubeObjectListLayout](kube-object-list-layout.md) | | +| [Working with mobx](working-with-mobx.md) | | +| [Protocol Handlers](protocol-handlers.md) | | + +## Samples + +| Sample | APIs | +| ----- | ----- | +[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension
Store.ClusterStore
Store.workspaceStore | +[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension
K8sApi.KubeApi
K8sApi.KubeObjectStore
Component.KubeObjectListLayout
Component.KubeObjectDetailsProps
Component.IconProps | diff --git a/docs/extensions/guides/anatomy.md b/docs/extensions/guides/anatomy.md new file mode 100644 index 0000000000..cc7b84e256 --- /dev/null +++ b/docs/extensions/guides/anatomy.md @@ -0,0 +1,3 @@ +--- +WIP +--- diff --git a/docs/extensions/guides/components.md b/docs/extensions/guides/components.md new file mode 100644 index 0000000000..cc7b84e256 --- /dev/null +++ b/docs/extensions/guides/components.md @@ -0,0 +1,3 @@ +--- +WIP +--- diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md new file mode 100644 index 0000000000..6e09117407 --- /dev/null +++ b/docs/extensions/guides/generator.md @@ -0,0 +1,75 @@ +# Lens Extension Generator + +The [Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) creates a directory with the necessary files for developing an extension. + +## Installing and Getting Started with the Generator + +To begin, install Yeoman and the Lens Extension Generator with the following command: + +```bash +npm install -g yo generator-lens-ext +``` + +Run the generator by entering the following command: `yo lens-ext`. + +Answer the following questions: + +```bash +# ? What type of extension do you want to create? New Extension (TypeScript) +# ? What's the name of your extension? my-first-lens-ext +# ? What's the description of your extension? My hello world extension +# ? What's your extension's publisher name? @my-org/my-first-lens-ext +# ? Initialize a git repository? Yes +# ? Install dependencies after initialization? Yes +# ? Which package manager to use? yarn +# ? symlink created extension folder to ~/.k8slens/extensions (mac/linux) or :Users\\.k8slens\extensions (windows)? Yes +``` + +Next, you'll need to have webpack watch the `my-first-lens-ext` folder. +Start webpack by entering: + +```bash +cd my-first-lens-ext +npm start # start the webpack server in watch mode +``` + +Open Lens and you will see a **Hello World** item in the left-side menu under **Custom Resources**: + +![Hello World](images/hello-world.png) + +## Developing the Extension + +Next, you'll try changing the way the new menu item appears in the UI. +You'll change it from "Hello World" to "Hello Lens". + +Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`: + +```typescript +clusterPageMenus = [ + { + target: { pageId: "hello" }, + title: "Hello Lens", + components: { + Icon: ExampleIcon, + } + } +] +``` + +Reload Lens and you will see that the menu item text has changed to "Hello Lens". +To reload Lens, enter `CMD+R` on Mac and `Ctrl+R` on Windows/Linux. + +![Hello World](images/hello-lens.png) + +## Debugging the Extension + +To debug your extension, please see our instructions on [Testing Extensions](../testing-and-publishing/testing.md). + +## Next Steps + +To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). + +If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). +You can find the Lens contribution guidelines [here](../../contributing/README.md). + +The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext). diff --git a/docs/extensions/guides/images/certificates-crd-list.png b/docs/extensions/guides/images/certificates-crd-list.png new file mode 100644 index 0000000000..19c9558f71 Binary files /dev/null and b/docs/extensions/guides/images/certificates-crd-list.png differ diff --git a/docs/extensions/guides/images/clusterpagemenus.png b/docs/extensions/guides/images/clusterpagemenus.png new file mode 100644 index 0000000000..3ed1c79e5b Binary files /dev/null and b/docs/extensions/guides/images/clusterpagemenus.png differ diff --git a/docs/extensions/guides/images/globalpagemenus.png b/docs/extensions/guides/images/globalpagemenus.png new file mode 100644 index 0000000000..e986cc32e9 Binary files /dev/null and b/docs/extensions/guides/images/globalpagemenus.png differ diff --git a/docs/extensions/guides/images/hello-lens.png b/docs/extensions/guides/images/hello-lens.png new file mode 100644 index 0000000000..5e2c0ac0a5 Binary files /dev/null and b/docs/extensions/guides/images/hello-lens.png differ diff --git a/docs/extensions/guides/images/hello-world.png b/docs/extensions/guides/images/hello-world.png new file mode 100644 index 0000000000..1a4a9c73a9 Binary files /dev/null and b/docs/extensions/guides/images/hello-world.png differ diff --git a/docs/extensions/guides/images/kubeobjectdetailitem.png b/docs/extensions/guides/images/kubeobjectdetailitem.png new file mode 100644 index 0000000000..e2d68f0c3b Binary files /dev/null and b/docs/extensions/guides/images/kubeobjectdetailitem.png differ diff --git a/docs/extensions/guides/images/kubeobjectdetailitemwithpods.png b/docs/extensions/guides/images/kubeobjectdetailitemwithpods.png new file mode 100644 index 0000000000..9a91f230f3 Binary files /dev/null and b/docs/extensions/guides/images/kubeobjectdetailitemwithpods.png differ diff --git a/docs/extensions/guides/images/kubeobjectmenuitem.png b/docs/extensions/guides/images/kubeobjectmenuitem.png new file mode 100644 index 0000000000..f9f91675de Binary files /dev/null and b/docs/extensions/guides/images/kubeobjectmenuitem.png differ diff --git a/docs/extensions/guides/images/kubeobjectmenuitemdetail.png b/docs/extensions/guides/images/kubeobjectmenuitemdetail.png new file mode 100644 index 0000000000..ab5f9ac0f0 Binary files /dev/null and b/docs/extensions/guides/images/kubeobjectmenuitemdetail.png differ diff --git a/docs/extensions/guides/images/routing-diag.png b/docs/extensions/guides/images/routing-diag.png new file mode 100644 index 0000000000..9185ce94d8 Binary files /dev/null and b/docs/extensions/guides/images/routing-diag.png differ diff --git a/docs/extensions/guides/kube-object-list-layout.md b/docs/extensions/guides/kube-object-list-layout.md new file mode 100644 index 0000000000..99f6796c91 --- /dev/null +++ b/docs/extensions/guides/kube-object-list-layout.md @@ -0,0 +1,287 @@ +# KubeObjectListLayout Sample + +In this guide we will learn how to list Kubernetes CRD objects on the cluster dashboard. +You can see the complete source code for this guide [here](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page). + +![](./images/certificates-crd-list.png) + +Next, we will go the implementation through in steps. +To achieve our goal, we need to: + +1. [Register ClusterPage and ClusterPageMenu objects](#register-objects-for-clustepages-and-clusterpagemenus) +2. [List Certificate Objects on the Cluster Page](#list-certificate-objects-on-the-cluster-page) +3. [Customize Details Panel](#customize-details-panel) + +## Register `clusterPage` and `clusterPageMenu` Objects + +First thing we need to do with our extension is to register new menu item in the cluster menu and create a cluster page that is opened when clicking the menu item. +We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class: + +```typescript +export default class CrdSampleExtension extends LensRendererExtension { +} +``` + +To register menu item in the cluster menu we need to register `PageMenuRegistration` object. +This object will register a menu item with "Certificates" text. +It will also use `CertificateIcon` component to render an icon and navigate to cluster page that is having `certificates` page id. + +```typescript +export function CertificateIcon(props: Component.IconProps) { + return +} + +export default class CrdSampleExtension extends LensRendererExtension { + + clusterPageMenus = [ + { + target: { pageId: "certificates" }, + title: "Certificates", + components: { + Icon: CertificateIcon, + } + }, + ] +} +``` + +Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates. + +```typescript +export default class CrdSampleExtension extends LensRendererExtension { + ... + + clusterPages = [{ + id: "certificates", + components: { + Page: () => , + MenuIcon: CertificateIcon, + } + }] +} +``` + +## List Certificate Objects on the Cluster Page + +In the previous step we defined `CertificatePage` component to render certificates. +In this step we will actually implement that. +`CertificatePage` is a React component that will render `Component.KubeObjectListLayout` component to list `Certificate` CRD objects. + +### Get CRD objects + +In order to list CRD objects, we need first fetch those from Kubernetes API. +Lens Extensions API provides easy mechanism to do this. +We just need to define `Certificate` class derived from `K8sApi.KubeObject`, `CertificatesApi`derived from `K8sApi.KubeApi` and `CertificatesStore` derived from `K8sApi.KubeObjectStore`. + +`Certificate` class defines properties found in the CRD object: + +```typescript +export class Certificate extends K8sApi.KubeObject { + static kind = "Certificate" + static namespaced = true + static apiBase = "/apis/cert-manager.io/v1alpha2/certificates" + + kind: string + apiVersion: string + metadata: { + name: string; + namespace: string; + selfLink: string; + uid: string; + resourceVersion: string; + creationTimestamp: string; + labels: { + [key: string]: string; + }; + annotations: { + [key: string]: string; + }; + } + spec: { + dnsNames: string[]; + issuerRef: { + group: string; + kind: string; + name: string; + } + secretName: string + } + status: { + conditions: { + lastTransitionTime: string; + message: string; + reason: string; + status: string; + type?: string; + }[]; + } +} +``` + +With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API: + +```typescript +export class CertificatesApi extends K8sApi.KubeApi { +} +export const certificatesApi = new CertificatesApi({ + objectConstructor: Certificate +}); +``` + +`CertificateStore` defines storage for `Certificate` objects + +```typescript +export class CertificatesStore extends K8sApi.KubeObjectStore { + api = certificatesApi +} + +export const certificatesStore = new CertificatesStore(); +``` + +And, finally, we register this store to Lens's API manager. + +```typescript +K8sApi.apiManager.registerStore(certificatesStore); +``` + + +### Create CertificatePage component + +Now we have created mechanism to manage `Certificate` objects in Kubernetes API. +Then we need to fetch those and render them in the UI. + +First we define `CertificatePage` class that extends `React.Component`. + +```typescript +import { Component, LensRendererExtension } from "@k8slens/extensions"; +import React from "react"; +import { certificatesStore } from "../certificate-store"; +import { Certificate } from "../certificate" + +export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> { + +} +``` + +Next we will implement `render` method that will display certificates in a list. +To do that, we just need to add `Component.KubeObjectListLayout` component inside `Component.TabLayout` component in render method. +To define which objects the list is showing, we need to pass `certificateStore` object to `Component.KubeObjectListLayout` in `store` property. +`Component.KubeObjectListLayout` will fetch automatically items from the given store when component is mounted. +Also, we can define needed sorting callbacks and search filters for the list: + +```typescript +enum sortBy { + name = "name", + namespace = "namespace", + issuer = "issuer" +} + +export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> { + // ... + + render() { + return ( + + certificate.getName(), + [sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace, + [sortBy.issuer]: (certificate: Certificate) => certificate.spec.issuerRef.name + }} + searchFilters={[ + (certificate: Certificate) => certificate.getSearchFields() + ]} + renderHeaderTitle="Certificates" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: sortBy.name }, + { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, + { title: "Issuer", className: "issuer", sortBy: sortBy.namespace }, + ]} + renderTableContents={(certificate: Certificate) => [ + certificate.getName(), + certificate.metadata.namespace, + certificate.spec.issuerRef.name + ]} + /> + + ) + } +} +``` + +### Customize Details panel + +We have learned now, how to list CRD objects in a list view. +Next, we will learn how to customize details panel that will be opened when the object is clicked in the list. + +First, we need to register our custom component to render details for the specific Kubernetes custom resource, in our case `Certificate`. +We will do this again in `CrdSampleExtension` class: + +```typescript +export default class CrdSampleExtension extends LensRendererExtension { + //... + + kubeObjectDetailItems = [{ + kind: Certificate.kind, + apiVersions: ["cert-manager.io/v1alpha2"], + components: { + Details: (props: CertificateDetailsProps) => + } + }] +} +``` + +Here we defined that `CertificateDetails` component will render the resource details. +So, next we need to implement that component. +Lens will inject `Certificate` object into our component so we just need to render some information out of it. +We can use `Component.DrawerItem` component from Lens Extensions API to give the same look and feel as Lens is using elsewhere: + +```typescript +import { Component, K8sApi } from "@k8slens/extensions"; +import React from "react"; +import { Certificate } from "../certificate"; + +export interface CertificateDetailsProps extends Component.KubeObjectDetailsProps{ +} + +export class CertificateDetails extends React.Component { + + render() { + const { object: certificate } = this.props; + if (!certificate) return null; + return ( +
+ + {certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp }) + + + {certificate.spec.dnsNames.join(",")} + + + {certificate.spec.secretName} + + + {certificate.status.conditions.map((condition, index) => { + const { type, reason, message, status } = condition; + const kind = type || reason; + if (!kind) return null; + return ( + + ); + })} + +
+ ) + } +} +``` + +## Summary + +Like we can see above, it's very easy to add custom pages and fetch Kubernetes resources by using Extensions API. +Please see the [complete source code](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) to test it out. diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md new file mode 100644 index 0000000000..f1212c0d37 --- /dev/null +++ b/docs/extensions/guides/main-extension.md @@ -0,0 +1,105 @@ +# Main Extension + +The Main Extension API is the interface to Lens's main process. +Lens runs in both main and renderer processes. +The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items, and run custom code in Lens's main process. + +## `LensMainExtension` Class + +### `onActivate()` and `onDeactivate()` Methods + +To create a main extension simply extend the `LensMainExtension` class: + +```typescript +import { LensMainExtension } from "@k8slens/extensions"; + +export default class ExampleExtensionMain extends LensMainExtension { + onActivate() { + console.log('custom main process extension code started'); + } + + onDeactivate() { + console.log('custom main process extension de-activated'); + } +} +``` + +Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. +Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. +You can initiate custom code by implementing `onActivate()`. +Implementing `onDeactivate()` gives you the opportunity to clean up after your extension. + +Disable extensions from the Lens Extensions page: + +1. Navigate to **File** > **Extensions** in the top menu bar. +(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. + +The following example is a little more interesting. +It accesses some Lens state data, and it periodically logs the name of the cluster that is currently active in Lens. + +```typescript +import { LensMainExtension, Store } from "@k8slens/extensions"; + +export default class ActiveClusterExtensionMain extends LensMainExtension { + + timer: NodeJS.Timeout + + onActivate() { + console.log("Cluster logger activated"); + this.timer = setInterval(() => { + if (!Store.ClusterStore.getInstance().active) { + console.log("No active cluster"); + return; + } + console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName) + }, 5000) + } + + onDeactivate() { + clearInterval(this.timer) + console.log("Cluster logger deactivated"); + } +} +``` + +For more details on accessing Lens state data, please see the [Stores](../stores) guide. + +### `appMenus` + +The Main Extension API allows you to customize the UI application menu. +Note that this is the only UI feature that the Main Extension API allows you to customize. +The following example demonstrates adding an item to the **Help** menu. + +``` typescript +import { LensMainExtension } from "@k8slens/extensions"; + +export default class SamplePageMainExtension extends LensMainExtension { + appMenus = [ + { + parentId: "help", + label: "Sample", + click() { + console.log("Sample clicked"); + } + } + ] +} +``` + +`appMenus` is an array of objects that satisfy the `MenuRegistration` interface. +`MenuRegistration` extends React'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 [`LensRendererExtension`](renderer-extension.md) class and can be defined in the process of extending it. diff --git a/docs/extensions/guides/protocol-handlers.md b/docs/extensions/guides/protocol-handlers.md new file mode 100644 index 0000000000..8e13c8436a --- /dev/null +++ b/docs/extensions/guides/protocol-handlers.md @@ -0,0 +1,83 @@ +# Lens Protocol Handlers + +Lens has a file association with the `lens://` protocol. +This means that Lens can be opened by external programs by providing a link that has `lens` as its protocol. +Lens provides a routing mechanism that extensions can use to register custom handlers. + +## Registering A Protocol Handler + +The field `protocolHandlers` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#protocolhandlers) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#protocolhandlers). +This field will be iterated through every time a `lens://` request gets sent to the application. +The `pathSchema` argument must comply with the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package's `compileToRegex` function. + +Once you have registered a handler it will be called when a user opens a link on their computer. +Handlers will be run in both `main` and `renderer` in parallel with no synchronization between the two processes. +Furthermore, both `main` and `renderer` are routed separately. +In other words, which handler is selected in either process is independent from the list of possible handlers in the other. + +Example of registering a handler: + +```typescript +import { LensMainExtension, Interface } from "@k8slens/extensions"; + +function rootHandler(params: Iterface.ProtocolRouteParams) { + console.log("routed to ExampleExtension", params); +} + +export default class ExampleExtensionMain extends LensMainExtension { + protocolHandlers = [ + pathSchema: "/", + handler: rootHandler, + ] +} +``` + +For testing the routing of URIs the `open` (on macOS) or `xdg-open` (on most linux) CLI utilities can be used. +For the above handler, the following URI would be always routed to it: + +``` +open lens://extension/example-extension/ +``` + +## Deregistering A Protocol Handler + +All that is needed to deregister a handler is to remove it from the array of handlers. + +## Routing Algorithm + +The routing mechanism for extensions is quite straight forward. +For example consider an extension `example-extension` which is published by the `@mirantis` org. +If it were to register a handler with `"/display/:type"` as its corresponding link then we would match the following URI like this: + +![Lens Protocol Link Resolution](images/routing-diag.png) + +Once matched, the handler would be called with the following argument (note both `"search"` and `"pathname"` will always be defined): + +```json +{ + "search": { + "text": "Hello" + }, + "pathname": { + "type": "notification" + } +} +``` + +As the diagram above shows, the search (or query) params are not considered as part of the handler resolution. +If the URI had instead been `lens://extension/@mirantis/example-extension/display/notification/green` then a third (and optional) field will have the rest of the path. +The `tail` field would be filled with `"/green"`. +If multiple `pathSchema`'s match a given URI then the most specific handler will be called. + +For example consider the following `pathSchema`'s: + +1. `"/"` +1. `"/display"` +1. `"/display/:type"` +1. `"/show/:id"` + +The URI sub-path `"/display"` would be routed to #2 since it is an exact match. +On the other hand, the subpath `"/display/notification"` would be routed to #3. + +The URI is routed to the most specific matching `pathSchema`. +This way the `"/"` (root) `pathSchema` acts as a sort of catch all or default route if no other route matches. diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md new file mode 100644 index 0000000000..1c2512023a --- /dev/null +++ b/docs/extensions/guides/renderer-extension.md @@ -0,0 +1,918 @@ +# Renderer Extension + +The Renderer Extension API is the interface to Lens's renderer process. +Lens runs in both the main and renderer processes. +The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, and run custom code in Lens's renderer process. + +The custom Lens UI elements that you can add include: + +* [Cluster pages](#clusterpages) +* [Cluster page menus](#clusterpagemenus) +* [Global pages](#globalpages) +* [Global page menus](#globalpagemenus) +* [Cluster features](#clusterfeatures) +* [App preferences](#apppreferences) +* [Status bar items](#statusbaritems) +* [KubeObject menu items](#kubeobjectmenuitems) +* [KubeObject detail items](#kubeobjectdetailitems) + +All UI elements are based on React components. + +## `LensRendererExtension` Class + +### `onActivate()` and `onDeactivate()` Methods + +To create a renderer extension, extend the `LensRendererExtension` class: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; + +export default class ExampleExtensionMain extends LensRendererExtension { + onActivate() { + console.log('custom renderer process extension code started'); + } + + onDeactivate() { + console.log('custom renderer process extension de-activated'); + } +} +``` + +Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. +Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. +You can initiate custom code by implementing `onActivate()`. +Implementing `onDeactivate()` gives you the opportunity to clean up after your extension. + +!!! info + Disable extensions from the Lens Extensions page: + + 1. Navigate to **File** > **Extensions** in the top menu bar. + (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. + +### `clusterPages` + +Cluster pages appear in the cluster dashboard. +Use cluster pages to display information about or add functionality to the active cluster. +It is also possible to include custom details from other clusters. +Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore). + +Add a cluster page definition to a `LensRendererExtension` subclass with the following example: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./page" +import React from "react" + +export default class ExampleExtension extends LensRendererExtension { + clusterPages = [ + { + id: "hello", + components: { + Page: () => , + } + } + ]; +} +``` + +`clusterPages` is an array of objects that satisfy the `PageRegistration` interface. +The properties of the `clusterPages` array objects are defined as follows: + +* `id` is a string that identifies the page. +* `components` matches the `PageComponents` interface for which there is one field, `Page`. +* `Page` is of type ` React.ComponentType`. +It offers flexibility in defining the appearance and behavior of your page. + +`ExamplePage` in the example above can be defined in `page.tsx`: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import React from "react" + +export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { + render() { + return ( +
+

Hello world!

+
+ ) + } +} +``` + +Note that the `ExamplePage` class defines the `extension` property. +This allows the `ExampleExtension` object to be passed in the cluster page definition in the React style. +This way, `ExamplePage` can access all `ExampleExtension` subclass data. + +The above example shows how to create a cluster page, but not how to make that page available to the Lens user. +Use `clusterPageMenus`, covered in the next section, to add cluster pages to the Lens UI. + +### `clusterPageMenus` + +`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav. + +By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./page" +import React from "react" + +export default class ExampleExtension extends LensRendererExtension { + clusterPages = [ + { + id: "hello", + components: { + Page: () => , + } + } + ]; + + clusterPageMenus = [ + { + target: { pageId: "hello" }, + title: "Hello World", + components: { + Icon: ExampleIcon, + } + }, + ]; +} +``` + +`clusterPageMenus` is an array of objects that satisfy the `ClusterPageMenuRegistration` interface. +This element defines how the cluster page menu item will appear and what it will do when you click it. +The properties of the `clusterPageMenus` array objects are defined as follows: + +* `target` links to the relevant cluster page using `pageId`. +* `pageId` takes the value of the relevant cluster page's `id` property. +* `title` sets the name of the cluster page menu item that will appear in the left side menu. +* `components` is used to set an icon that appears to the left of the `title` text in the left side menu. + +The above example creates a menu item that reads **Hello World**. +When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`. + +This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows: + +```typescript +import { LensRendererExtension, Component } from "@k8slens/extensions"; +import React from "react" + +export function ExampleIcon(props: Component.IconProps) { + return +} + +export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { + render() { + return ( +
+

Hello world!

+
+ ) + } +} +``` + +Lens includes various built-in components available for extension developers to use. +One of these is the `Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). +The properties that `Component.Icon` uses are defined as follows: + +* `material` takes the name of the icon you want to use. +* `tooltip` sets the text you want to appear when a user hovers over the icon. + +`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. +The following example groups two sub menu items under one parent menu item: + + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExampleIcon, ExamplePage } from "./page" +import React from "react" + +export default class ExampleExtension extends LensRendererExtension { + clusterPages = [ + { + id: "hello", + components: { + Page: () => , + } + }, + { + id: "bonjour", + components: { + Page: () => , + } + } + ]; + + clusterPageMenus = [ + { + id: "example", + title: "Greetings", + components: { + Icon: ExampleIcon, + } + }, + { + parentId: "example", + target: { pageId: "hello" }, + title: "Hello World", + components: { + Icon: ExampleIcon, + } + }, + { + parentId: "example", + target: { pageId: "bonjour" }, + title: "Bonjour le monde", + components: { + Icon: ExempleIcon, + } + } + ]; +} +``` + +The above defines two cluster pages and three cluster page menu objects. +The cluster page definitions are straightforward. +The three cluster page menu objects include one parent menu item and two sub menu items. +The first cluster page menu object defines the parent of a foldout submenu. +Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu. +Also note that the `target` field is not specified (it is ignored if the `id` field is specified). +This cluster page menu object specifies the `title` and `components` fields, which are used in displaying the menu item in the cluster dashboard sidebar. +Initially the submenu is hidden. +Activating this menu item toggles on and off the appearance of the submenu below it. +The remaining two cluster page menu objects define the contents of the submenu. +A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case. + +This is what the example will look like, including how the menu item will appear in the secondary left nav: + +### `globalPages` + +Global pages are independent of the cluster dashboard and can fill the entire Lens UI. +Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension. + +Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page. +You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). +Unlike cluster pages, users can trigger global pages even when there is no active cluster. + +The following example defines a `LensRendererExtension` subclass with a single global page definition: + +```typescript +import { LensRendererExtension } from '@k8slens/extensions'; +import { HelpPage } from './page'; +import React from 'react'; + +export default class HelpExtension extends LensRendererExtension { + globalPages = [ + { + id: "help", + components: { + Page: () => , + } + } + ]; +} +``` + +`globalPages` is an array of objects that satisfy the `PageRegistration` interface. +The properties of the `globalPages` array objects are defined as follows: + +* `id` is a string that identifies the page. +* `components` matches the `PageComponents` interface for which there is one field, `Page`. +* `Page` is of type `React.ComponentType`. +It offers flexibility in defining the appearance and behavior of your page. + +`HelpPage` in the example above can be defined in `page.tsx`: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import React from "react" + +export class HelpPage extends React.Component<{ extension: LensRendererExtension }> { + render() { + return ( +
+

Help yourself

+
+ ) + } +} +``` + +Note that the `HelpPage` class defines the `extension` property. +This allows the `HelpExtension` object to be passed in the global page definition in the React-style. +This way, `HelpPage` can access all `HelpExtension` subclass data. + +This example code shows how to create a global page, but not how to make that page available to the Lens user. +Global pages can be made available in the following ways: + +* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide. +* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems). +* To add global pages to the left side menu, see [`globalPageMenus`](#globalpagemenus). + +### `globalPageMenus` + +`globalPageMenus` allows you to add global page menu items to the left nav. + +By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { HelpIcon, HelpPage } from "./page" +import React from "react" + +export default class HelpExtension extends LensRendererExtension { + globalPages = [ + { + id: "help", + components: { + Page: () => , + } + } + ]; + + globalPageMenus = [ + { + target: { pageId: "help" }, + title: "Help", + components: { + Icon: HelpIcon, + } + }, + ]; +} +``` + +`globalPageMenus` is an array of objects that satisfy the `PageMenuRegistration` interface. +This element defines how the global page menu item will appear and what it will do when you click it. +The properties of the `globalPageMenus` array objects are defined as follows: + +* `target` links to the relevant global page using `pageId`. +* `pageId` takes the value of the relevant global page's `id` property. +* `title` sets the name of the global page menu item that will display as a tooltip in the left nav. +* `components` is used to set an icon that appears in the left nav. + +The above example creates a "Help" icon menu item. +When users click the icon, the Lens UI will display the contents of `ExamplePage`. + +This example requires the definition of another React-based component, `HelpIcon`. +Update `page.tsx` from the example above with the `HelpIcon` definition, as follows: + +```typescript +import { LensRendererExtension, Component } from "@k8slens/extensions"; +import React from "react" + +export function HelpIcon(props: Component.IconProps) { + return +} + +export class HelpPage extends React.Component<{ extension: LensRendererExtension }> { + render() { + return ( +
+

Help

+
+ ) + } +} +``` + +Lens includes various built-in components available for extension developers to use. +One of these is the `Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). +The property that `Component.Icon` uses is defined as follows: + +* `material` takes the name of the icon you want to use. + +This is what the example will look like, including how the menu item will appear in the left nav: + +![globalPageMenus](images/globalpagemenus.png) + +### `clusterFeatures` + +Cluster features are Kubernetes resources that can be applied to and managed within the active cluster. +They can be installed and uninstalled by the Lens user from the cluster **Settings** page. + +!!! info + To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**. + +The following example shows how to add a cluster feature as part of a `LensRendererExtension`: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions" +import { ExampleFeature } from "./src/example-feature" +import React from "react" + +export default class ExampleFeatureExtension extends LensRendererExtension { + clusterFeatures = [ + { + title: "Example Feature", + components: { + Description: () => { + return ( + + Enable an example feature. + + ) + } + }, + feature: new ExampleFeature() + } + ]; +} +``` + +The properties of the `clusterFeatures` array objects are defined as follows: + +* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section. +* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods: + +```typescript + abstract install(cluster: Cluster): Promise; + abstract upgrade(cluster: Cluster): Promise; + abstract uninstall(cluster: Cluster): Promise; + abstract updateStatus(cluster: Cluster): Promise; +``` + +The four methods listed above are defined as follows: + +* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../../api/lens/extensions/README.md). +This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page). + +* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature. +This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page). + +* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../../api/lens/extensions/README.md). +This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page). + +* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class. +Lens periodically calls this method to determine details about the feature's current status. +The implementation of this method should uninstall Kubernetes resources using the Kubernetes api (`K8sApi`) +Consider using the following properties with `updateStatus()`: + + * `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description. + + * `status.installed` should be set to `true` if the feature is installed, and `false` otherwise. + + * `status.canUpgrade` is set according to a rule meant to determine whether the feature can be upgraded. + This rule can involve `status.currentVersion` and `status.latestVersion`, if desired. + +The following shows a very simple implementation of a `ClusterFeature`: + +```typescript +import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions"; +import * as path from "path"; + +export class ExampleFeature extends ClusterFeature.Feature { + + async install(cluster: Store.Cluster): Promise { + + super.applyResources(cluster, path.join(__dirname, "../resources/")); + } + + async upgrade(cluster: Store.Cluster): Promise { + return this.install(cluster); + } + + async updateStatus(cluster: Store.Cluster): Promise { + try { + const pod = K8sApi.forCluster(cluster, K8sApi.Pod); + const examplePod = await pod.get({name: "example-pod", namespace: "default"}); + if (examplePod?.kind) { + this.status.installed = true; + this.status.currentVersion = examplePod.spec.containers[0].image.split(":")[1]; + this.status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature + } else { + this.status.installed = false; + this.status.canUpgrade = false; + } + } catch(e) { + if (e?.error?.code === 404) { + this.status.installed = false; + this.status.canUpgrade = false; + } + } + + return this.status; + } + + async uninstall(cluster: Store.Cluster): Promise { + const podApi = K8sApi.forCluster(cluster, K8sApi.Pod); + await podApi.delete({name: "example-pod", namespace: "default"}); + } +} +``` + +This example implements the `install()` method by invoking the helper `applyResources()` method. +`applyResources()` tries to apply all resources read from all files found in the folder path provided. +In this case the folder path is the `../resources` subfolder relative to the current source code's folder. +The file `../resources/example-pod.yml` could contain: + +``` yaml +apiVersion: v1 +kind: Pod +metadata: + name: example-pod +spec: + containers: + - name: example-pod + image: nginx +``` + +The example above implements the four methods as follows: + +* It implements `upgrade()` by invoking the `install()` method. +Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps. + +* It implements `uninstall()` by utilizing the [Kubernetes API](../../api/lens/extensions/README.md) which Lens provides to delete the `example-pod` applied by the `install()` method. + +* It implements `updateStatus()` by using the [Kubernetes API](../../api/lens/extensions/README.md) which Lens provides to determine whether the `example-pod` is installed, what version is associated with it, and whether it can be upgraded. +The implementation determines what the status is for a specific cluster feature. + +### `appPreferences` + +The Lens **Preferences** page is a built-in global page. +You can use Lens extensions to add custom preferences to the Preferences page, providing a single location for users to configure global options. + +The following example demonstrates adding a custom preference: + +```typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference"; +import { observable } from "mobx"; +import React from "react"; + +export default class ExampleRendererExtension extends LensRendererExtension { + + @observable preference = { enabled: false }; + + appPreferences = [ + { + title: "Example Preferences", + components: { + Input: () => , + Hint: () => + } + } + ]; +} +``` + +`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface. +The properties of the `appPreferences` array objects are defined as follows: + +* `title` sets the heading text displayed on the Preferences page. +* `components` specifies two `React.Component` objects that define the interface for the preference. + * `Input` specifies an interactive input element for the preference. + * `Hint` provides descriptive information for the preference, shown below the `Input` element. + +!!! note + Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension. + +`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance. +This is how `ExampleRendererExtension` handles the state of the preference input. +`ExampleRendererExtension` has a `preference` field, which you will add to `ExamplePreferenceInput`. + +In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows: + +```typescript +import { Component } from "@k8slens/extensions"; +import { observer } from "mobx-react"; +import React from "react"; + +export class ExamplePreferenceProps { + preference: { + enabled: boolean; + } +} + +@observer +export class ExamplePreferenceInput extends React.Component { + + render() { + const { preference } = this.props; + return ( + { preference.enabled = v; }} + /> + ); + } +} + +export class ExamplePreferenceHint extends React.Component { + render() { + return ( + This is an example of an appPreference for extensions. + ); + } +} +``` + +`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties: + +* `label` sets the text that displays next to the checkbox. +* `value` is initially set to `preference.enabled`. +* `onChange` is a function that responds when the state of the checkbox changes. + +`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props. +This is an object with the single `enabled` property. +It is used to indicate the state of the preference, and it is bound to the checkbox state in `onChange`. + +`ExamplePreferenceHint` is a simple text span. + +The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. +`mobx` simplifies state management. +Without it, this example would not visually update the checkbox properly when the user activates it. +[Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements. +We recommend that extensions rely on it, as well. +Alternatively, you can use React's state management, though `mobx` is typically simpler to use. + +Note that you can manage an extension's state data using an `ExtensionStore` object, which conveniently handles persistence and synchronization. +To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state. +However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore). + +### `statusBarItems` + +The status bar is the blue strip along the bottom of the Lens UI. +`statusBarItems` are `React.ReactNode` types. +They can be used to display status information, or act as links to global pages as well as external pages. + +The following example adds a `statusBarItems` definition and a `globalPages` definition to a `LensRendererExtension` subclass. +It configures the status bar item to navigate to the global page upon activation (normally a mouse click): + +```typescript +import { LensRendererExtension } from '@k8slens/extensions'; +import { HelpIcon, HelpPage } from "./page" +import React from 'react'; + +export default class HelpExtension extends LensRendererExtension { + globalPages = [ + { + id: "help", + components: { + Page: () => , + } + } + ]; + + statusBarItems = [ + { + components: { + Item: ( +
this.navigate("help")} + > + + My Status Bar Item +
+ ) + }, + }, + ]; +} +``` + +The properties of the `statusBarItems` array objects are defined as follows: + +* `Item` specifies the `React.Component` that will be shown on the status bar. +By default, items are added starting from the right side of the status bar. +Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text. +The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus). +* `onClick` determines what the `statusBarItem` does when it is clicked. +In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. +`navigate` takes the `id` of the associated global page as a parameter. +Thus, clicking the status bar item activates the associated global pages. + +### `kubeObjectMenuItems` + +An extension can add custom menu items (`kubeObjectMenuItems`) for specific Kubernetes resource kinds and apiVersions. +`kubeObjectMenuItems` appear under the vertical ellipsis for each listed resource in the cluster dashboard: + +![List](images/kubeobjectmenuitem.png) + +They also appear on the title bar of the details page for specific resources: + +![Details](images/kubeobjectmenuitemdetail.png) + +The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action: + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions"; +import { NamespaceMenuItem } from "./src/namespace-menu-item" + +export default class ExampleExtension extends LensRendererExtension { + kubeObjectMenuItems = [ + { + kind: "Namespace", + apiVersions: ["v1"], + components: { + MenuItem: (props: Component.KubeObjectMenuProps) => + } + } + ]; +} + +``` + +`kubeObjectMenuItems` is an array of objects matching the `KubeObjectMenuRegistration` interface. +The example above adds a menu item for namespaces in the cluster dashboard. +The properties of the `kubeObjectMenuItems` array objects are defined as follows: + +* `kind` specifies the Kubernetes resource type the menu item will apply to. +* `apiVersion` specifies the Kubernetes API version number to use with the resource type. +* `components` defines the menu item's appearance and behavior. +* `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties. +In this example a `NamespaceMenuItem` object is returned. + +`NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`: + +```typescript +import React from "react"; +import { Component, K8sApi, Navigation} from "@k8slens/extensions"; + +export function NamespaceMenuItem(props: Component.KubeObjectMenuProps) { + const { object: namespace, toolbar } = props; + if (!namespace) return null; + + const namespaceName = namespace.getName(); + + const sendToTerminal = (command: string) => { + Component.terminalStore.sendCommand(command, { + enter: true, + newTab: true, + }); + Navigation.hideDetails(); + }; + + const getPods = () => { + sendToTerminal(`kubectl get pods -n ${namespaceName}`); + }; + + return ( + + + Get Pods + + ); +} + +``` + +`NamespaceMenuItem` returns a `Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property. +In the example, `getPods()` opens a terminal tab and runs `kubectl` to get a list of pods running in the current namespace. + +The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`. +`namespace` is the `props.object`, which is of type `K8sApi.Namespace`. +`K8sApi.Namespace` is the API for accessing namespaces. +The current namespace in this example is simply given by `namespace.getName()`. +Thus, `kubeObjectMenuItems` afford convenient access to the specific resource selected by the user. + +### `kubeObjectDetailItems` + +An extension can add custom details (`kubeObjectDetailItems`) for specified Kubernetes resource kinds and apiVersions. +These custom details appear on the details page for a specific resource, such as a Namespace as shown here: + +![Details](images/kubeobjectdetailitem.png) + +The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page: + +```typescript +import React from "react" +import { LensRendererExtension } from "@k8slens/extensions"; +import { NamespaceDetailsItem } from "./src/namespace-details-item" + +export default class ExampleExtension extends LensRendererExtension { + kubeObjectDetailItems = [ + { + kind: "Namespace", + apiVersions: ["v1"], + priority: 10, + components: { + Details: (props: Component.KubeObjectDetailsProps) => + } + } + ]; +} +``` + +`kubeObjectDetailItems` is an array of objects matching the `KubeObjectDetailRegistration` interface. +This example above adds a detail item for namespaces in the cluster dashboard. +The properties of the `kubeObjectDetailItems` array objects are defined as follows: + +* `kind` specifies the Kubernetes resource type the detail item will apply to. +* `apiVersion` specifies the Kubernetes API version number to use with the resource type. +* `components` defines the detail item's appearance and behavior. +* `Details` provides a function that returns a `React.Component` given a set of detail item properties. +In this example a `NamespaceDetailsItem` object is returned. + +`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`: + +``` typescript +import { Component, K8sApi } from "@k8slens/extensions"; +import { PodsDetailsList } from "./pods-details-list"; +import React from "react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; + +@observer +export class NamespaceDetailsItem extends React.Component> { + + @observable private pods: K8sApi.Pod[]; + + async componentDidMount() { + this.pods = await K8sApi.podsApi.list({namespace: this.props.object.getName()}); + } + + render() { + return ( +
+ + +
+ ) + } +} +``` + +Since `NamespaceDetailsItem` extends `React.Component>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`. +You can query this object for many details about the current namespace. +In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method. +Use the namespace's name to limit the list of pods only to those in the relevant namespace. +To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method. +The `K8sApi.podsApi` is automatically configured for the active cluster. + +Note that `K8sApi.podsApi.list()` is an asynchronous method. +Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`. +It is a common technique in React development to await async calls in `componentDidMount()`. +However, `componentDidMount()` is called right after the first call to `render()`. +In order to effect a subsequent `render()` call, React must be made aware of a state change. +Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates. +This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`. + +Finally, the `NamespaceDetailsItem` renders using the `render()` method. +Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one. +Multiple details in a drawer can be placed in `` elements for further separation, if desired. +The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`: + +``` typescript +import React from "react"; +import { Component, K8sApi } from "@k8slens/extensions"; + +interface Props { + pods: K8sApi.Pod[]; +} + +export class PodsDetailsList extends React.Component { + + getTableRow(index: number) { + const {pods} = this.props; + return ( + + {pods[index].getName()} + {pods[index].getAge()} + {pods[index].getStatus()} + + ) + } + + render() { + const {pods} = this.props + if (!pods?.length) { + return null; + } + + return ( +
+ + + Name + Age + Status + + { + pods.map((pod, index) => this.getTableRow(index)) + } + +
+ ) + } +} +``` + +`PodsDetailsList` produces a simple table showing a list of the pods found in this namespace: + +![DetailsWithPods](images/kubeobjectdetailitemwithpods.png) + +Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods. +Construct the table using the `Component.Table` and related elements. + +For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods. +The table is constructed using the `Component.Table` and related elements. +See [`Component` documentation](https://docs.k8slens.dev/master/extensions/api/modules/_renderer_api_components_/) for further details. diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md new file mode 100644 index 0000000000..c8a5ec270d --- /dev/null +++ b/docs/extensions/guides/stores.md @@ -0,0 +1,165 @@ +# Stores + +Stores are components that persist and synchronize state data. Lens uses a number of stores to maintain various kinds of state information, including: + +* The `ClusterStore` manages cluster state data (such as cluster details), and it tracks which cluster is active. +* The `WorkspaceStore` manages workspace state data (such as the workspace name), and and it tracks which clusters belong to a given workspace. +* The `ExtensionStore` manages custom extension state data. + +This guide focuses on the `ExtensionStore`. + +## ExtensionStore + +Extension developers can create their own store for managing state data by extending the `ExtensionStore` class. +This guide shows how to create a store for the [`appPreferences`](../renderer-extension#apppreferences) guide example, which demonstrates how to add a custom preference to the **Preferences** page. +The preference is a simple boolean that indicates whether or not something is enabled. +However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted. + +`Store.ExtensionStore`'s child class will need to be created before being used. +It is recommended to call the inherited static method `getInstanceOrCreate()` only in one place, generally within you extension's `onActivate()` method. +It is also recommenced to delete the instance, using the inherited static method `resetInstance()`, in your extension's `onDeactivate()` method. +Everywhere else in your code you should use the `getInstance()` static method. +This is so that your data is kept up to date and not persisted longer than expected. + +The following example code creates a store for the `appPreferences` guide example: + +``` typescript +import { Store } from "@k8slens/extensions"; +import { observable, toJS } from "mobx"; + +export type ExamplePreferencesModel = { + enabled: boolean; +}; + +export class ExamplePreferencesStore extends Store.ExtensionStore { + + @observable enabled = false; + + private constructor() { + super({ + configName: "example-preferences-store", + defaults: { + enabled: false + } + }); + } + + protected fromStore({ enabled }: ExamplePreferencesModel): void { + this.enabled = enabled; + } + + toJSON(): ExamplePreferencesModel { + return toJS({ + enabled: this.enabled + }, { + recurseEverything: true + }); + } +} +``` + +First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. +This has a single field, `enabled`, which represents the preference's state. +`ExamplePreferencesStore` extends `Store.ExtensionStore`, which is based on the `ExamplePreferencesModel`. +The `enabled` field is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference. +Note the use of the `observable` decorator on the `enabled` field. +The [`appPreferences`](../renderer-extension#apppreferences) guide example uses [MobX](https://mobx.js.org/README.html) for the UI state management, ensuring the checkbox updates when it's activated by the user. + +Next, our example implements the constructor and two abstract methods. +The constructor specifies the name of the store (`"example-preferences-store"`) and the default (initial) value for the preference state (`enabled: false`). +Lens internals call the `fromStore()` method when the store loads. +It gives the extension the opportunity to retrieve the stored state data values based on the defined data model. +The `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked. +The `toJSON()` method is complementary to `fromStore()`. +It is called when the store is being saved. +`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. +The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here. + +Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension. +Note that `ExamplePreferencesStore` is a singleton. +Calling this function will create an instance if one has not been made before. +Through normal use you should call `ExamplePreferencesStore.getInstance()` as that will throw an error if an instance does not exist. +This provides some logical safety in that it limits where a new instance can be created. +Thus it prevents an instance from being created when the constructor args are not present at the call site. + +If you are doing some cleanup it is recommended to call `ExamplePreferencesStore.getInstance(false)` which returns `undefined` instead of throwing when there is no instance. + +The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store. +`ExamplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. +This can be done in `./main.ts`: + +``` typescript +import { LensMainExtension } from "@k8slens/extensions"; +import { ExamplePreferencesStore } from "./src/example-preference-store"; + +export default class ExampleMainExtension extends LensMainExtension { + async onActivate() { + await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this); + } +} +``` + +Here, `ExamplePreferencesStore` loads with `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`. +Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. +This can be done in `./renderer.ts`: + +``` typescript +import { LensRendererExtension } from "@k8slens/extensions"; +import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference"; +import { ExamplePreferencesStore } from "./src/example-preference-store"; +import React from "react"; + +export default class ExampleRendererExtension extends LensRendererExtension { + + async onActivate() { + await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this); + } + + appPreferences = [ + { + title: "Example Preferences", + components: { + Input: () => , + Hint: () => + } + } + ]; +} +``` + +Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is called to load `ExamplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. + +`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`: + +``` typescript +import { Component } from "@k8slens/extensions"; +import { observer } from "mobx-react"; +import React from "react"; +import { ExamplePreferencesStore } from "./example-preference-store"; + +@observer +export class ExamplePreferenceInput extends React.Component { + + render() { + return ( + { ExamplePreferencesStore.getInstace().enabled = v; }} + /> + ); + } +} + +export class ExamplePreferenceHint extends React.Component { + render() { + return ( + This is an example of an appPreference for extensions. + ); + } +} +``` + +The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type. +Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the +`ExamplePreferencesStore`. diff --git a/docs/extensions/guides/working-with-mobx.md b/docs/extensions/guides/working-with-mobx.md new file mode 100644 index 0000000000..41ddc487a6 --- /dev/null +++ b/docs/extensions/guides/working-with-mobx.md @@ -0,0 +1,26 @@ +# Working with MobX + +## Introduction + +Lens uses MobX on top of React's state management system. +The result is a more declarative state management style, rather than React's native `setState` mechanism. + +You can review how React handles state management [here](https://reactjs.org/docs/faq-state.html). + +The following is a quick overview: + +* `React.Component` is generic with respect to both `props` and `state` (which default to the empty object type). +* `props` should be considered read-only from the point of view of the component, and it is the mechanism for passing in arguments to a component. +* `state` is a component's internal state, and can be read by accessing the super-class field `state`. +* `state` **must** be updated using the `setState` parent method which merges the new data with the old state. +* React does some optimizations around re-rendering components after quick successions of `setState` calls. + +## How MobX Works: + +MobX is a package that provides an abstraction over React's state management system. The three main concepts are: + +* `observable` is a marker for data stored in the component's `state`. +* `action` is a function that modifies any `observable` data. +* `computed` is a marker for data that is derived from `observable` data, but that is not actually stored. Think of this as computing `isEmpty` rather than an observable field called `count`. + +Further reading is available on the [MobX website](https://mobx.js.org/the-gist-of-mobx.html). diff --git a/docs/extensions/testing-and-publishing/bundling.md b/docs/extensions/testing-and-publishing/bundling.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/extensions/testing-and-publishing/publishing.md b/docs/extensions/testing-and-publishing/publishing.md new file mode 100644 index 0000000000..d8e7b8efad --- /dev/null +++ b/docs/extensions/testing-and-publishing/publishing.md @@ -0,0 +1,46 @@ +# Publishing Extensions + +To be able to easily share extensions with users they need to be published somewhere. +Lens currently only supports installing extensions from NPM tarballs. +All hosted extensions must, therefore, be retrievable in a NPM tarball. + +## Places To Host Your Extension + +We recommend to host your extension somewhere on the web so that it is easy for people to search for and download it. +We recommend either hosting it as an NPM package on https://www.npmjs.com or through GitHub releases. +We recommend against using GitHub packages (https://github.com/features/packages) as it requires a GitHub token to access the package. + +### Publishing via NPM + +This is the easiest method of publishing as NPM comes built in with mechanism to get a link to download the package as a tarball. +Once you have set up an account with NPM (https://www.npmjs.com/signup) and logged in with their CLI (`npm login`) you will be ready to publish. + +* Run `npm version ` to bump the version of your extension by the appropriate amount. +* Run `npm publish` to publish your extension to NPM +* Run `git push && git push --tags` to push the commit that NPM creates to your git remote. + +It is probably a good idea to put into your README.md the following instructions for your users to get the tarball download link. + +```bash +npm view dist.tarball +``` + +This will output the link that they will need to give to Lens to install your extension. + +### Publish via GitHub Releases + +Another method of publishing your extensions is to do so with the releases mechanism built into GitHub. +We recommend reading [GitHub's Releases Documentation](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository) for how to actually do the steps of a release. +The following will be a quick walk through on how to make the tarball which will be the released file. + +### Making a NPM Tarball of Your Extension + +While this is necessary for hosting on GitHub releases, this is also the means for creating a tarball if you plan on hosting on a different file hosting platform. + +Say you have your project folder at `~/my-extension/` and you want to create an NPM package we need to do the following within your git repo: + +``` +npm pack +``` + +This will create a NPM tarball that can be hosted on Github Releases or any other publicly available file hosting service. diff --git a/docs/extensions/testing-and-publishing/testing.md b/docs/extensions/testing-and-publishing/testing.md new file mode 100644 index 0000000000..af178efeb7 --- /dev/null +++ b/docs/extensions/testing-and-publishing/testing.md @@ -0,0 +1,95 @@ +# Testing Extensions + +## Renderer Process Unit Testing + +UI components in the extension's renderer process are based on React/ReactDOM. +These components can be tested by popular React testing tools like [React Testing Library](https://github.com/testing-library/react-testing-library). + +If you are using the [Yeoman Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) to scaffold extension project then the testing environment for render process is already set up for you. +Just use `npm start` or `yarn test` to run the tests. + +For example, I have a component `GlobalPageMenuIcon` and want to test if `props.navigate` is called when user clicks the icon. + +My component `GlobalPageMenuIcon` + +```typescript +import React from "react" +import { Component: { Icon } } from "@k8slens/extensions"; + +const GlobalPageMenuIcon = ({ navigate }: { navigate?: () => void }): JSX.Element => ( + navigate()} + data-testid="global-page-menu-icon" + /> +) +``` + +The test + +```js +import React from "react" +import { render, screen, fireEvent } from "@testing-library/react"; + +import GlobalPageMenuIcon from "./GlobalPageMenuIcon"; + +test("click called navigate()", () => { + const navigate = jest.fn(); + render(); + fireEvent.click(screen.getByTestId("global-page-menu-icon")); + expect(navigate).toHaveBeenCalled(); + }); +``` + +In the example we used [React Testing Library](https://github.com/testing-library/react-testing-library) but any React testing framework can be used to test renderer process UI components. + +There are more example tests in the generator's [template](https://github.com/lensapp/generator-lens-ext/tree/main/generators/app/templates/ext-ts/components). +Extend your tests based on the examples. + +## Main Process Unit Testing + +Code in the extension's main process consists of normal JavaScript files that have access to extension api, you can write unit tests using any testing framework. + +If you are using the [Yeoman Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) to scaffold your extension project then the [Jest](https://jestjs.io/) testing environment is set up for you. +Just use `npm start` or `yarn test` to run the tests. + +## Tips + +### Console.log + +Extension developers might find `console.log()` useful for printing out information and errors from extensions. +To use `console.log()`, note that Lens is based on Electron, and that Electron has two types of processes: [Main and Renderer](https://www.electronjs.org/docs/tutorial/quick-start#main-and-renderer-processes). + +### Renderer Process Logs + +In the Renderer process, `console.log()` is printed in the Console in Developer Tools (**View** > **Toggle Developer Tools**). + +### Main Process Logs + +Viewing the logs from the Main process is a little trickier, since they cannot be printed using Developer Tools. + +#### macOS + +On macOS, view the Main process logs by running Lens from the terminal: + +```bash +/Applications/Lens.app/Contents/MacOS/Lens +``` + +You can also use [Console.app](https://support.apple.com/en-gb/guide/console/welcome/mac) to view the Main process logs. + +#### Linux + +On Linux, you can access the Main process logs using the Lens PID. +First get the PID: + +```bash +ps aux | grep Lens | grep -v grep +``` + +Then get the Main process logs using the PID: + +```bash +tail -f /proc/[pid]/fd/1 # stdout (console.log) +tail -f /proc/[pid]/fd/2 # stdout (console.error) +``` diff --git a/docs/extensions/usage/README.md b/docs/extensions/usage/README.md new file mode 100644 index 0000000000..02b2db223a --- /dev/null +++ b/docs/extensions/usage/README.md @@ -0,0 +1,26 @@ +# Using Extensions + +The features that Lens includes out-of-the-box are just the start. +Lens extensions let you add new features to your installation to support your workflow. +Rich extensibility model lets extension authors plug directly into the Lens UI and contribute functionality through the same APIs used by Lens itself. +The start using Lens Extensions go to **File** (or **Lens** on macOS) > **Extensions** in the application menu. +This is the `Extensions` management page where all the management of the extensions you want to use is done. + +![Extensions](images/extensions.png) + +## Installing an Extension + +There are three ways to install extensions. +If you have the extension as a `.tgz` file then dragging and dropping it in the extension management page will install it for you. +If it is hosted on the web, you can paste the URL and click `Install` and Lens will download and install it. +The third way is to move the extension into your `~/.k8slens/extensions` (or `C:\Users\\.k8slens\extensions`) folder and Lens will automatically detect it and install the extension. + +## Enabling or Disabling an Extension + +Go to the extension management page and click either the `Enable` or `Disable` buttons. +Extensions will be enabled by default when you first install them. +A disabled extension is not loaded by Lens and is not run. + +## Uninstalling an Extension + +If, for whatever reason, you wish to remove the installation of an extension simple click the `Uninstall` button. This will remove all the files that Lens would need to run the extension. diff --git a/docs/extensions/usage/images/extensions.png b/docs/extensions/usage/images/extensions.png new file mode 100644 index 0000000000..5deb8e4bd2 Binary files /dev/null and b/docs/extensions/usage/images/extensions.png differ