Add back in extension documents
Signed-off-by: Steve Richards <srichards@mirantis.com>
46
docs/extensions/README.md
Normal file
@ -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.
|
||||
0
docs/extensions/capabilities/README.md
Normal file
283
docs/extensions/capabilities/common-capabilities.md
Normal file
@ -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: () => <Component.Icon material="arrow"/>,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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: () => <MyCustomPreferenceHint/>,
|
||||
Input: () => <MyCustomPreferenceInput store={myCustomPreferencesStore}/>
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
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 (
|
||||
<span>
|
||||
Just an example.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
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: (
|
||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||
<Component.Icon material="favorite" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 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) => <CustomMenuItem {...props} />
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 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) => <CustomKindDetails {...props} />
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
BIN
docs/extensions/capabilities/images/css-vars-in-devtools.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/extensions/capabilities/images/theme-selector.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
163
docs/extensions/capabilities/styling.md
Normal file
@ -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
|
||||
<div className="flex column align-center"></div>
|
||||
```
|
||||
|
||||
```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.
|
||||

|
||||
|
||||
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 `<body>` element gets a "theme-light" class, or: `<body class="theme-light">`.
|
||||
If the class isn't there, the theme defaults to dark. The active theme can be changed in the **Preferences** page:
|
||||

|
||||
|
||||
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 (
|
||||
<div className="SupportPage">
|
||||
<h1>Active theme is {Theme.getActiveTheme().name}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`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)
|
||||
116
docs/extensions/get-started/anatomy.md
Normal file
@ -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 `@<publisher>/<name>` 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: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
27
docs/extensions/get-started/overview.md
Normal file
@ -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.
|
||||
24
docs/extensions/get-started/wrapping-up.md
Normal file
@ -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)
|
||||
100
docs/extensions/get-started/your-first-extension.md
Normal file
@ -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\<user>\.k8slens\extensions -force
|
||||
cd C:\Users\<user>\.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\<user>\.k8slens\extensions`:
|
||||
|
||||
```
|
||||
Copy-Item 'lens-extension-samples\helloworld-sample' 'C:\Users\<user>\.k8slens\extensions\helloworld-sample'
|
||||
```
|
||||
|
||||
## Build the Extension
|
||||
|
||||
To build the extension you can use `make` or run the `npm` commands manually:
|
||||
|
||||
```sh
|
||||
cd <lens-extension-samples directory>/helloworld-sample
|
||||
make build
|
||||
```
|
||||
|
||||
To run the `npm` commands, enter:
|
||||
|
||||
```sh
|
||||
cd <lens-extension-samples directory>/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 <lens-extension-samples directory>/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 `<lens-extension-samples directory>/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.
|
||||
37
docs/extensions/guides/README.md
Normal file
@ -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 <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.ClusterStore <br> Store.workspaceStore |
|
||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension <br> K8sApi.KubeApi <br> K8sApi.KubeObjectStore <br> Component.KubeObjectListLayout <br> Component.KubeObjectDetailsProps <br> Component.IconProps |
|
||||
3
docs/extensions/guides/anatomy.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
WIP
|
||||
---
|
||||
3
docs/extensions/guides/components.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
WIP
|
||||
---
|
||||
75
docs/extensions/guides/generator.md
Normal file
@ -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\<user>\.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**:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
BIN
docs/extensions/guides/images/certificates-crd-list.png
Normal file
|
After Width: | Height: | Size: 792 KiB |
BIN
docs/extensions/guides/images/clusterpagemenus.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
docs/extensions/guides/images/globalpagemenus.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
docs/extensions/guides/images/hello-lens.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/extensions/guides/images/hello-world.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/extensions/guides/images/kubeobjectdetailitem.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
docs/extensions/guides/images/kubeobjectdetailitemwithpods.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
docs/extensions/guides/images/kubeobjectmenuitem.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
docs/extensions/guides/images/kubeobjectmenuitemdetail.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
docs/extensions/guides/images/routing-diag.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
287
docs/extensions/guides/kube-object-list-layout.md
Normal file
@ -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).
|
||||
|
||||

|
||||
|
||||
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 <Component.Icon {...props} material="security" tooltip="Certificates"/>
|
||||
}
|
||||
|
||||
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: () => <CertificatePage extension={this} />,
|
||||
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<Certificate> {
|
||||
}
|
||||
export const certificatesApi = new CertificatesApi({
|
||||
objectConstructor: Certificate
|
||||
});
|
||||
```
|
||||
|
||||
`CertificateStore` defines storage for `Certificate` objects
|
||||
|
||||
```typescript
|
||||
export class CertificatesStore extends K8sApi.KubeObjectStore<Certificate> {
|
||||
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 (
|
||||
<Component.TabLayout>
|
||||
<Component.KubeObjectListLayout
|
||||
className="Certicates" store={certificatesStore}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (certificate: Certificate) => 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
|
||||
]}
|
||||
/>
|
||||
</Component.TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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) => <CertificateDetails {...props} />
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
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<Certificate>{
|
||||
}
|
||||
|
||||
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
||||
|
||||
render() {
|
||||
const { object: certificate } = this.props;
|
||||
if (!certificate) return null;
|
||||
return (
|
||||
<div className="Certificate">
|
||||
<Component.DrawerItem name="Created">
|
||||
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
||||
</Component.DrawerItem>
|
||||
<Component.DrawerItem name="DNS Names">
|
||||
{certificate.spec.dnsNames.join(",")}
|
||||
</Component.DrawerItem>
|
||||
<Component.DrawerItem name="Secret">
|
||||
{certificate.spec.secretName}
|
||||
</Component.DrawerItem>
|
||||
<Component.DrawerItem name="Status" className="status" labelsOnly>
|
||||
{certificate.status.conditions.map((condition, index) => {
|
||||
const { type, reason, message, status } = condition;
|
||||
const kind = type || reason;
|
||||
if (!kind) return null;
|
||||
return (
|
||||
<Component.Badge
|
||||
key={kind + index} label={kind}
|
||||
className={"success "+kind.toLowerCase()}
|
||||
tooltip={message}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Component.DrawerItem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
105
docs/extensions/guides/main-extension.md
Normal file
@ -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.
|
||||
83
docs/extensions/guides/protocol-handlers.md
Normal file
@ -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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
918
docs/extensions/guides/renderer-extension.md
Normal file
@ -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: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`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<any>`.
|
||||
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 (
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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 <Component.Icon {...props} material="pages" tooltip={"Hi!"}/>
|
||||
}
|
||||
|
||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "bonjour",
|
||||
components: {
|
||||
Page: () => <ExemplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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: () => <HelpPage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`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<any>`.
|
||||
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 (
|
||||
<div>
|
||||
<p>Help yourself</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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: () => <HelpPage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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 <Component.Icon {...props} material="help"/>
|
||||
}
|
||||
|
||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Help</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
### `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 (
|
||||
<span>
|
||||
Enable an example feature.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
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<void>;
|
||||
abstract upgrade(cluster: Cluster): Promise<void>;
|
||||
abstract uninstall(cluster: Cluster): Promise<void>;
|
||||
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||
```
|
||||
|
||||
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<void> {
|
||||
|
||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||
}
|
||||
|
||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
||||
return this.install(cluster);
|
||||
}
|
||||
|
||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
||||
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<void> {
|
||||
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: () => <ExamplePreferenceInput preference={this.preference}/>,
|
||||
Hint: () => <ExamplePreferenceHint/>
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`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<ExamplePreferenceProps> {
|
||||
|
||||
render() {
|
||||
const { preference } = this.props;
|
||||
return (
|
||||
<Component.Checkbox
|
||||
label="I understand appPreferences"
|
||||
value={preference.enabled}
|
||||
onChange={v => { preference.enabled = v; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExamplePreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>This is an example of an appPreference for extensions.</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`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: () => <HelpPage extension={this}/>,
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
statusBarItems = [
|
||||
{
|
||||
components: {
|
||||
Item: (
|
||||
<div
|
||||
className="flex align-center gaps"
|
||||
onClick={() => this.navigate("help")}
|
||||
>
|
||||
<HelpIcon />
|
||||
My Status Bar Item
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
They also appear on the title bar of the details page for specific resources:
|
||||
|
||||

|
||||
|
||||
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<K8sApi.Namespace>) => <NamespaceMenuItem {...props} />
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`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<K8sApi.Namespace>) {
|
||||
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 (
|
||||
<Component.MenuItem onClick={getPods}>
|
||||
<Component.Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
||||
<span className="title">Get Pods</span>
|
||||
</Component.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`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:
|
||||
|
||||

|
||||
|
||||
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<K8sApi.Namespace>) => <NamespaceDetailsItem {...props} />
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
`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<Component.KubeObjectDetailsProps<K8sApi.Namespace>> {
|
||||
|
||||
@observable private pods: K8sApi.Pod[];
|
||||
|
||||
async componentDidMount() {
|
||||
this.pods = await K8sApi.podsApi.list({namespace: this.props.object.getName()});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Component.DrawerTitle title="Pods" />
|
||||
<PodsDetailsList pods={this.pods}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, 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 `<Component.DrawerItem>` 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<Props> {
|
||||
|
||||
getTableRow(index: number) {
|
||||
const {pods} = this.props;
|
||||
return (
|
||||
<Component.TableRow key={index} nowrap>
|
||||
<Component.TableCell className="podName">{pods[index].getName()}</Component.TableCell>
|
||||
<Component.TableCell className="podAge">{pods[index].getAge()}</Component.TableCell>
|
||||
<Component.TableCell className="podStatus">{pods[index].getStatus()}</Component.TableCell>
|
||||
</Component.TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {pods} = this.props
|
||||
if (!pods?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div >
|
||||
<Component.Table>
|
||||
<Component.TableHead>
|
||||
<Component.TableCell className="podName">Name</Component.TableCell>
|
||||
<Component.TableCell className="podAge">Age</Component.TableCell>
|
||||
<Component.TableCell className="podStatus">Status</Component.TableCell>
|
||||
</Component.TableHead>
|
||||
{
|
||||
pods.map((pod, index) => this.getTableRow(index))
|
||||
}
|
||||
</Component.Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`PodsDetailsList` produces a simple table showing a list of the pods found in this namespace:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
165
docs/extensions/guides/stores.md
Normal file
@ -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<ExamplePreferencesModel> {
|
||||
|
||||
@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: () => <ExamplePreferenceInput />,
|
||||
Hint: () => <ExamplePreferenceHint/>
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<Component.Checkbox
|
||||
label="I understand appPreferences"
|
||||
value={ExamplePreferencesStore.getInstace().enabled}
|
||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExamplePreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>This is an example of an appPreference for extensions.</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
26
docs/extensions/guides/working-with-mobx.md
Normal file
@ -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).
|
||||
0
docs/extensions/testing-and-publishing/bundling.md
Normal file
46
docs/extensions/testing-and-publishing/publishing.md
Normal file
@ -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 <major|minor|patch>` 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 <extension-name> 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.
|
||||
95
docs/extensions/testing-and-publishing/testing.md
Normal file
@ -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 => (
|
||||
<Icon
|
||||
material="trip_origin"
|
||||
onClick={() => 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(<GlobalPageMenuIcon navigate={navigate} />);
|
||||
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)
|
||||
```
|
||||
26
docs/extensions/usage/README.md
Normal file
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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\<user>\.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.
|
||||
BIN
docs/extensions/usage/images/extensions.png
Normal file
|
After Width: | Height: | Size: 589 KiB |