mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into back-to-catalog-button
This commit is contained in:
commit
4fbfdc485b
@ -22,6 +22,7 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import appInfo from "../package.json";
|
import appInfo from "../package.json";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
import fastGlob from "fast-glob";
|
||||||
|
|
||||||
const packagePath = path.join(__dirname, "../package.json");
|
const packagePath = path.join(__dirname, "../package.json");
|
||||||
const versionInfo = semver.parse(appInfo.version);
|
const versionInfo = semver.parse(appInfo.version);
|
||||||
@ -41,3 +42,14 @@ if (versionInfo.prerelease) {
|
|||||||
|
|
||||||
|
|
||||||
fs.writeFileSync(packagePath, `${JSON.stringify(appInfo, null, 2)}\n`);
|
fs.writeFileSync(packagePath, `${JSON.stringify(appInfo, null, 2)}\n`);
|
||||||
|
|
||||||
|
const extensionManifests = fastGlob.sync(["extensions/*/package.json"]);
|
||||||
|
|
||||||
|
for (const manifestPath of extensionManifests) {
|
||||||
|
const packagePath = path.join(__dirname, "..", manifestPath);
|
||||||
|
|
||||||
|
import(packagePath).then((packageInfo) => {
|
||||||
|
packageInfo.default.version = `${versionInfo.raw}.${Date.now()}`;
|
||||||
|
fs.writeFileSync(packagePath, `${JSON.stringify(packageInfo.default, null, 2)}\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -14,9 +14,9 @@ In order to see logs from this extension, you need to start Lens from the comman
|
|||||||
This extension can register a custom callback that is executed when an extension is activated (started).
|
This extension can register a custom callback that is executed when an extension is activated (started).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensMainExtension } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
console.log("hello world")
|
console.log("hello world")
|
||||||
}
|
}
|
||||||
@ -28,9 +28,9 @@ export default class ExampleMainExtension extends LensMainExtension {
|
|||||||
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensMainExtension } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onDeactivate() {
|
async onDeactivate() {
|
||||||
console.log("bye bye")
|
console.log("bye bye")
|
||||||
}
|
}
|
||||||
@ -44,15 +44,15 @@ This extension can register custom app menus that will be displayed on OS native
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension, windowManager } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
appMenus = [
|
appMenus = [
|
||||||
{
|
{
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
label: "Example item",
|
label: "Example item",
|
||||||
click() {
|
click() {
|
||||||
windowManager.navigate("https://k8slens.dev");
|
Main.Navigation.navigate("https://k8slens.dev");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -69,9 +69,9 @@ In order to see logs from this extension you need to check them via **View** > *
|
|||||||
This extension can register a custom callback that is executed when an extension is activated (started).
|
This extension can register a custom callback that is executed when an extension is activated (started).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
console.log("hello world")
|
console.log("hello world")
|
||||||
}
|
}
|
||||||
@ -83,9 +83,9 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensRendererExtension {
|
export default class ExampleMainExtension extends Renderer.LensExtension {
|
||||||
async onDeactivate() {
|
async onDeactivate() {
|
||||||
console.log("bye bye")
|
console.log("bye bye")
|
||||||
}
|
}
|
||||||
@ -99,10 +99,16 @@ The global page is a full-screen page that hides all other content from a window
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component, LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { ExamplePage } from "./src/example-page"
|
import { ExamplePage } from "./src/example-page"
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "example",
|
id: "example",
|
||||||
@ -117,7 +123,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
title: "Example page", // used in icon's tooltip
|
title: "Example page", // used in icon's tooltip
|
||||||
target: { pageId: "example" }
|
target: { pageId: "example" }
|
||||||
components: {
|
components: {
|
||||||
Icon: () => <Component.Icon material="arrow"/>,
|
Icon: () => <Icon material="arrow"/>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -131,12 +137,12 @@ It is responsible for storing a state for custom preferences.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
||||||
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
|
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
|
||||||
|
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
appPreferences = [
|
appPreferences = [
|
||||||
{
|
{
|
||||||
title: "My Custom Preference",
|
title: "My Custom Preference",
|
||||||
@ -156,10 +162,10 @@ These pages are visible in a cluster menu when a cluster is opened.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./src/page"
|
import { ExampleIcon, ExamplePage } from "./src/page"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "extension-example", // optional
|
id: "extension-example", // optional
|
||||||
@ -190,10 +196,10 @@ These features are visible in the "Cluster Settings" page.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { MyCustomFeature } from "./src/my-custom-feature"
|
import { MyCustomFeature } from "./src/my-custom-feature"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterFeatures = [
|
clusterFeatures = [
|
||||||
{
|
{
|
||||||
title: "My Custom Feature",
|
title: "My Custom Feature",
|
||||||
@ -219,15 +225,21 @@ This extension can register custom icons and text to a status bar area.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
components: {
|
components: {
|
||||||
Item: (
|
Item: (
|
||||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||||
<Component.Icon material="favorite" />
|
<Icon material="favorite" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -243,10 +255,10 @@ This extension can register custom menu items (actions) for specified Kubernetes
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectMenuItems = [
|
kubeObjectMenuItems = [
|
||||||
{
|
{
|
||||||
kind: "Node",
|
kind: "Node",
|
||||||
@ -266,10 +278,10 @@ This extension can register custom details (content) for specified Kubernetes ki
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectDetailItems = [
|
kubeObjectDetailItems = [
|
||||||
{
|
{
|
||||||
kind: "CustomKind",
|
kind: "CustomKind",
|
||||||
|
|||||||
@ -114,14 +114,14 @@ There is a way of detect active theme and its changes in JS. [MobX observer func
|
|||||||
```js
|
```js
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { observer } from "mobx-react"
|
import { observer } from "mobx-react"
|
||||||
import { App, Component, Theme } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SupportPage extends React.Component {
|
export class SupportPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="SupportPage">
|
<div className="SupportPage">
|
||||||
<h1>Active theme is {Theme.getActiveTheme().name}</h1>
|
<h1>Active theme is {Renderer.Theme.getActiveTheme().name}</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,11 +96,11 @@ It also registers the `MenuItem` component that displays the `ExampleIcon` React
|
|||||||
These React components are defined in the additional `./src/page.tsx` file.
|
These React components are defined in the additional `./src/page.tsx` file.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "extension-example",
|
id: "extension-example",
|
||||||
|
|||||||
@ -30,9 +30,8 @@ Each guide or code sample includes the following:
|
|||||||
|
|
||||||
| Sample | APIs |
|
| Sample | APIs |
|
||||||
| ----- | ----- |
|
| ----- | ----- |
|
||||||
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.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> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||||
[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> Renderer.Component.Icon <br> Renderer.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> Renderer.Component.Icon <br> Renderer.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> Renderer.K8sApi.KubeApi <br> Renderer.K8sApi.KubeObjectStore <br> Renderer.Component.KubeObjectListLayout <br> Renderer.Component.KubeObjectDetailsProps <br> Renderer.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 |
|
|
||||||
|
|||||||
@ -43,10 +43,10 @@ To register either a handler or a listener, you should do something like the fol
|
|||||||
|
|
||||||
`main.ts`:
|
`main.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
import { IpcMain } from "./helpers/main";
|
import { IpcMain } from "./helpers/main";
|
||||||
|
|
||||||
export class ExampleExtensionMain extends LensMainExtension {
|
export class ExampleExtensionMain extends Main.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
IpcMain.createInstance(this);
|
IpcMain.createInstance(this);
|
||||||
}
|
}
|
||||||
@ -60,10 +60,10 @@ Lens will automatically clean up that store and all the handlers on deactivation
|
|||||||
|
|
||||||
`helpers/main.ts`:
|
`helpers/main.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { Ipc, Types } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export class IpcMain extends Ipc.Main {
|
export class IpcMain extends Main.Ipc {
|
||||||
constructor(extension: LensMainExtension) {
|
constructor(extension: Main.LensExtension) {
|
||||||
super(extension);
|
super(extension);
|
||||||
|
|
||||||
this.listen("initialize", onInitialize);
|
this.listen("initialize", onInitialize);
|
||||||
@ -82,10 +82,10 @@ You should be able to just call `IpcMain.getInstance()` anywhere it is needed in
|
|||||||
|
|
||||||
`renderer.ts`:
|
`renderer.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { IpcRenderer } from "./helpers/renderer";
|
import { IpcRenderer } from "./helpers/renderer";
|
||||||
|
|
||||||
export class ExampleExtensionRenderer extends LensRendererExtension {
|
export class ExampleExtensionRenderer extends Renderer.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
const ipc = IpcRenderer.createInstance(this);
|
const ipc = IpcRenderer.createInstance(this);
|
||||||
|
|
||||||
@ -100,9 +100,9 @@ It is also needed to create an instance to broadcast messages too.
|
|||||||
|
|
||||||
`helpers/renderer.ts`:
|
`helpers/renderer.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { Ipc } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export class IpcRenderer extends Ipc.Renderer {}
|
export class IpcRenderer extends Renderer.Ipc {}
|
||||||
```
|
```
|
||||||
|
|
||||||
It is necessary to create child classes of these `abstract class`'s in your extension before you can use them.
|
It is necessary to create child classes of these `abstract class`'s in your extension before you can use them.
|
||||||
@ -116,7 +116,7 @@ There is no behind the scenes transfer of these functions.
|
|||||||
To register a "handler" call `IpcMain.getInstance().handle(...)`.
|
To register a "handler" call `IpcMain.getInstance().handle(...)`.
|
||||||
The cleanup of these handlers is handled by Lens itself.
|
The cleanup of these handlers is handled by Lens itself.
|
||||||
|
|
||||||
The `listen()` methods on `Ipc.Main` and `Ipc.Renderer` return a `Disposer`, or more specifically, a `() => void`.
|
The `listen()` methods on `Main.Ipc` and `Renderer.Ipc` return a `Disposer`, or more specifically, a `() => void`.
|
||||||
This can be optionally called to remove the listener early.
|
This can be optionally called to remove the listener early.
|
||||||
|
|
||||||
Calling either `IpcRenderer.getInstance().broadcast(...)` or `IpcMain.getInstance().broadcast(...)` sends an event to all `renderer` frames and to `main`.
|
Calling either `IpcRenderer.getInstance().broadcast(...)` or `IpcMain.getInstance().broadcast(...)` sends an event to all `renderer` frames and to `main`.
|
||||||
|
|||||||
@ -18,7 +18,7 @@ First thing we need to do with our extension is to register new menu item in the
|
|||||||
We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class:
|
We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -27,11 +27,21 @@ 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.
|
It will also use `CertificateIcon` component to render an icon and navigate to cluster page that is having `certificates` page id.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export function CertificateIcon(props: Component.IconProps) {
|
import { Renderer } from "@k8slens/extensions";
|
||||||
return <Component.Icon {...props} material="security" tooltip="Certificates"/>
|
|
||||||
|
type IconProps = Renderer.Component.IconProps;
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function CertificateIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="security" tooltip="Certificates"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
clusterPageMenus = [
|
clusterPageMenus = [
|
||||||
{
|
{
|
||||||
@ -48,7 +58,7 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates.
|
Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
...
|
...
|
||||||
|
|
||||||
clusterPages = [{
|
clusterPages = [{
|
||||||
@ -65,18 +75,29 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
In the previous step we defined `CertificatePage` component to render certificates.
|
In the previous step we defined `CertificatePage` component to render certificates.
|
||||||
In this step we will actually implement that.
|
In this step we will actually implement that.
|
||||||
`CertificatePage` is a React component that will render `Component.KubeObjectListLayout` component to list `Certificate` CRD objects.
|
`CertificatePage` is a React component that will render `Renderer.Component.KubeObjectListLayout` component to list `Certificate` CRD objects.
|
||||||
|
|
||||||
### Get CRD objects
|
### Get CRD objects
|
||||||
|
|
||||||
In order to list CRD objects, we need first fetch those from Kubernetes API.
|
In order to list CRD objects, we need first fetch those from Kubernetes API.
|
||||||
Lens Extensions API provides easy mechanism to do this.
|
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`.
|
We just need to define `Certificate` class derived from `Renderer.K8sApi.KubeObject`, `CertificatesApi`derived from `Renderer.K8sApi.KubeApi` and `CertificatesStore` derived from `Renderer.K8sApi.KubeObjectStore`.
|
||||||
|
|
||||||
`Certificate` class defines properties found in the CRD object:
|
`Certificate` class defines properties found in the CRD object:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class Certificate extends K8sApi.KubeObject {
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
K8sApi: {
|
||||||
|
KubeObject,
|
||||||
|
KubeObjectStore,
|
||||||
|
KubeApi,
|
||||||
|
apiManager,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export class Certificate extends KubeObject {
|
||||||
static kind = "Certificate"
|
static kind = "Certificate"
|
||||||
static namespaced = true
|
static namespaced = true
|
||||||
static apiBase = "/apis/cert-manager.io/v1alpha2/certificates"
|
static apiBase = "/apis/cert-manager.io/v1alpha2/certificates"
|
||||||
@ -121,8 +142,8 @@ export class Certificate extends K8sApi.KubeObject {
|
|||||||
With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API:
|
With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CertificatesApi extends K8sApi.KubeApi<Certificate> {
|
export class CertificatesApi extends KubeApi<Certificate> {}
|
||||||
}
|
|
||||||
export const certificatesApi = new CertificatesApi({
|
export const certificatesApi = new CertificatesApi({
|
||||||
objectConstructor: Certificate
|
objectConstructor: Certificate
|
||||||
});
|
});
|
||||||
@ -131,7 +152,7 @@ export const certificatesApi = new CertificatesApi({
|
|||||||
`CertificateStore` defines storage for `Certificate` objects
|
`CertificateStore` defines storage for `Certificate` objects
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CertificatesStore extends K8sApi.KubeObjectStore<Certificate> {
|
export class CertificatesStore extends KubeObjectStore<Certificate> {
|
||||||
api = certificatesApi
|
api = certificatesApi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +162,7 @@ export const certificatesStore = new CertificatesStore();
|
|||||||
And, finally, we register this store to Lens's API manager.
|
And, finally, we register this store to Lens's API manager.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
K8sApi.apiManager.registerStore(certificatesStore);
|
apiManager.registerStore(certificatesStore);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -153,23 +174,32 @@ Then we need to fetch those and render them in the UI.
|
|||||||
First we define `CertificatePage` class that extends `React.Component`.
|
First we define `CertificatePage` class that extends `React.Component`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { certificatesStore } from "../certificate-store";
|
import { certificatesStore } from "../certificate-store";
|
||||||
import { Certificate } from "../certificate"
|
import { Certificate } from "../certificate"
|
||||||
|
|
||||||
export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class CertificatePage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Next we will implement `render` method that will display certificates in a list.
|
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 do that, we just need to add `Renderer.Component.KubeObjectListLayout` component inside `Renderer.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.
|
To define which objects the list is showing, we need to pass `certificateStore` object to `Renderer.Component.KubeObjectListLayout` in `store` property.
|
||||||
`Component.KubeObjectListLayout` will fetch automatically items from the given store when component is mounted.
|
`Renderer.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:
|
Also, we can define needed sorting callbacks and search filters for the list:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
TabLayout,
|
||||||
|
KubeObjectListLayout,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
@ -181,8 +211,8 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Component.TabLayout>
|
<TabLayout>
|
||||||
<Component.KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Certicates" store={certificatesStore}
|
className="Certicates" store={certificatesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||||
@ -204,7 +234,7 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
certificate.spec.issuerRef.name
|
certificate.spec.issuerRef.name
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Component.TabLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,7 +249,7 @@ First, we need to register our custom component to render details for the specif
|
|||||||
We will do this again in `CrdSampleExtension` class:
|
We will do this again in `CrdSampleExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
//...
|
//...
|
||||||
|
|
||||||
kubeObjectDetailItems = [{
|
kubeObjectDetailItems = [{
|
||||||
@ -235,14 +265,22 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
Here we defined that `CertificateDetails` component will render the resource details.
|
Here we defined that `CertificateDetails` component will render the resource details.
|
||||||
So, next we need to implement that component.
|
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.
|
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:
|
We can use `Renderer.Component.DrawerItem` component from Lens Extensions API to give the same look and feel as Lens is using elsewhere:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Certificate } from "../certificate";
|
import { Certificate } from "../certificate";
|
||||||
|
|
||||||
export interface CertificateDetailsProps extends Component.KubeObjectDetailsProps<Certificate>{
|
const {
|
||||||
|
Component: {
|
||||||
|
KubeObjectDetailsProps,
|
||||||
|
DrawerItem,
|
||||||
|
Badge,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export interface CertificateDetailsProps extends KubeObjectDetailsProps<Certificate>{
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
||||||
@ -252,29 +290,29 @@ export class CertificateDetails extends React.Component<CertificateDetailsProps>
|
|||||||
if (!certificate) return null;
|
if (!certificate) return null;
|
||||||
return (
|
return (
|
||||||
<div className="Certificate">
|
<div className="Certificate">
|
||||||
<Component.DrawerItem name="Created">
|
<DrawerItem name="Created">
|
||||||
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="DNS Names">
|
<DrawerItem name="DNS Names">
|
||||||
{certificate.spec.dnsNames.join(",")}
|
{certificate.spec.dnsNames.join(",")}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="Secret">
|
<DrawerItem name="Secret">
|
||||||
{certificate.spec.secretName}
|
{certificate.spec.secretName}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="Status" className="status" labelsOnly>
|
<DrawerItem name="Status" className="status" labelsOnly>
|
||||||
{certificate.status.conditions.map((condition, index) => {
|
{certificate.status.conditions.map((condition, index) => {
|
||||||
const { type, reason, message, status } = condition;
|
const { type, reason, message, status } = condition;
|
||||||
const kind = type || reason;
|
const kind = type || reason;
|
||||||
if (!kind) return null;
|
if (!kind) return null;
|
||||||
return (
|
return (
|
||||||
<Component.Badge
|
<Badge
|
||||||
key={kind + index} label={kind}
|
key={kind + index} label={kind}
|
||||||
className={"success "+kind.toLowerCase()}
|
className={"success "+kind.toLowerCase()}
|
||||||
tooltip={message}
|
tooltip={message}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ The Main Extension API allows you to access, configure, and customize Lens data,
|
|||||||
To create a main extension simply extend the `LensMainExtension` class:
|
To create a main extension simply extend the `LensMainExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensMainExtension {
|
export default class ExampleExtensionMain extends Main.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
console.log('custom main process extension code started');
|
console.log('custom main process extension code started');
|
||||||
}
|
}
|
||||||
@ -39,34 +39,6 @@ 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.
|
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.
|
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.
|
For more details on accessing Lens state data, please see the [Stores](../stores) guide.
|
||||||
|
|
||||||
### `appMenus`
|
### `appMenus`
|
||||||
@ -76,9 +48,9 @@ Note that this is the only UI feature that the Main Extension API allows you to
|
|||||||
The following example demonstrates adding an item to the **Help** menu.
|
The following example demonstrates adding an item to the **Help** menu.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class SamplePageMainExtension extends LensMainExtension {
|
export default class SamplePageMainExtension extends Main.LensExtension {
|
||||||
appMenus = [
|
appMenus = [
|
||||||
{
|
{
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
@ -102,4 +74,4 @@ Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
|
|||||||
* `click()` is called when the menu item is selected.
|
* `click()` is called when the menu item is selected.
|
||||||
In this example, we simply log a message.
|
In this example, we simply log a message.
|
||||||
However, you would typically have this navigate to a specific page or perform another operation.
|
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.
|
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
||||||
|
|||||||
@ -18,13 +18,13 @@ In other words, which handler is selected in either process is independent from
|
|||||||
Example of registering a handler:
|
Example of registering a handler:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension, Interface } from "@k8slens/extensions";
|
import { Main, Common } from "@k8slens/extensions";
|
||||||
|
|
||||||
function rootHandler(params: Iterface.ProtocolRouteParams) {
|
function rootHandler(params: Common.Types.ProtocolRouteParams) {
|
||||||
console.log("routed to ExampleExtension", params);
|
console.log("routed to ExampleExtension", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensMainExtension {
|
export default class ExampleExtensionMain extends Main.LensExtension {
|
||||||
protocolHandlers = [
|
protocolHandlers = [
|
||||||
pathSchema: "/",
|
pathSchema: "/",
|
||||||
handler: rootHandler,
|
handler: rootHandler,
|
||||||
|
|||||||
@ -24,9 +24,9 @@ All UI elements are based on React components.
|
|||||||
To create a renderer extension, extend the `LensRendererExtension` class:
|
To create a renderer extension, extend the `LensRendererExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensRendererExtension {
|
export default class ExampleExtensionMain extends Renderer.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
console.log('custom renderer process extension code started');
|
console.log('custom renderer process extension code started');
|
||||||
}
|
}
|
||||||
@ -61,11 +61,11 @@ Use your extension to access Kubernetes resources in the active cluster with [`C
|
|||||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -88,7 +88,7 @@ It offers flexibility in defining the appearance and behavior of your page.
|
|||||||
`ExamplePage` in the example above can be defined in `page.tsx`:
|
`ExamplePage` in the example above can be defined in `page.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
@ -116,11 +116,11 @@ Use `clusterPageMenus`, covered in the next section, to add cluster pages to the
|
|||||||
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -157,14 +157,20 @@ When users click **Hello World**, the cluster dashboard will show the contents o
|
|||||||
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export function ExampleIcon(props: Component.IconProps) {
|
type IconProps = Renderer.Component.IconProps;
|
||||||
return <Component.Icon {...props} material="pages" tooltip={"Hi!"}/>
|
|
||||||
|
const {
|
||||||
|
Component: { Icon },
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function ExampleIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="pages" tooltip={"Hi!"}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -176,8 +182,8 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
```
|
```
|
||||||
|
|
||||||
Lens includes various built-in components available for extension developers to use.
|
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).
|
One of these is the `Renderer.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:
|
The properties that `Renderer.Component.Icon` uses are defined as follows:
|
||||||
|
|
||||||
* `material` takes the name of the icon you want to use.
|
* `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.
|
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||||
@ -187,11 +193,11 @@ The following example groups two sub menu items under one parent menu item:
|
|||||||
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -261,11 +267,11 @@ Unlike cluster pages, users can trigger global pages even when there is no activ
|
|||||||
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
import { HelpPage } from './page';
|
import { HelpPage } from './page';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -288,7 +294,7 @@ It offers flexibility in defining the appearance and behavior of your page.
|
|||||||
`HelpPage` in the example above can be defined in `page.tsx`:
|
`HelpPage` in the example above can be defined in `page.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
@ -320,11 +326,11 @@ Global pages can be made available in the following ways:
|
|||||||
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -362,14 +368,20 @@ This example requires the definition of another React-based component, `HelpIcon
|
|||||||
Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export function HelpIcon(props: Component.IconProps) {
|
type IconProps = Renderer.Component.IconProps;
|
||||||
return <Component.Icon {...props} material="help"/>
|
|
||||||
|
const {
|
||||||
|
Component: { Icon },
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function HelpIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="help"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
export class HelpPage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -381,8 +393,8 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
|||||||
```
|
```
|
||||||
|
|
||||||
Lens includes various built-in components available for extension developers to use.
|
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).
|
One of these is the `Renderer.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:
|
The property that `Renderer.Component.Icon` uses is defined as follows:
|
||||||
|
|
||||||
* `material` takes the name of the icon you want to use.
|
* `material` takes the name of the icon you want to use.
|
||||||
|
|
||||||
@ -401,11 +413,11 @@ They can be installed and uninstalled by the Lens user from the cluster **Settin
|
|||||||
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { ExampleFeature } from "./src/example-feature"
|
import { ExampleFeature } from "./src/example-feature"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleFeatureExtension extends LensRendererExtension {
|
export default class ExampleFeatureExtension extends Renderer.LensExtension {
|
||||||
clusterFeatures = [
|
clusterFeatures = [
|
||||||
{
|
{
|
||||||
title: "Example Feature",
|
title: "Example Feature",
|
||||||
@ -462,45 +474,69 @@ Consider using the following properties with `updateStatus()`:
|
|||||||
The following shows a very simple implementation of a `ClusterFeature`:
|
The following shows a very simple implementation of a `ClusterFeature`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
|
import { Renderer, Common } from "@k8slens/extensions";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export class ExampleFeature extends ClusterFeature.Feature {
|
const {
|
||||||
|
K8sApi: {
|
||||||
|
ResourceStack,
|
||||||
|
forCluster,
|
||||||
|
StorageClass,
|
||||||
|
Namespace,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
async install(cluster: Store.Cluster): Promise<void> {
|
type ResourceStack = Renderer.K8sApi.ResourceStack;
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
type KubernetesCluster = Common.Catalog.KubernetesCluster;
|
||||||
|
|
||||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
export interface MetricsStatus {
|
||||||
|
installed: boolean;
|
||||||
|
canUpgrade: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExampleFeature {
|
||||||
|
protected stack: ResourceStack;
|
||||||
|
|
||||||
|
constructor(protected cluster: KubernetesCluster) {
|
||||||
|
this.stack = new ResourceStack(cluster, this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
install(): Promise<string> {
|
||||||
return this.install(cluster);
|
return this.stack.kubectlApplyFolder(path.join(__dirname, "../resources/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
upgrade(): Promise<string> {
|
||||||
|
return this.install(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(): Promise<MetricsStatus> {
|
||||||
|
const status: MetricsStatus = { installed: false, canUpgrade: false};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pod = K8sApi.forCluster(cluster, K8sApi.Pod);
|
const pod = forCluster(cluster, Pod);
|
||||||
const examplePod = await pod.get({name: "example-pod", namespace: "default"});
|
const examplePod = await pod.get({name: "example-pod", namespace: "default"});
|
||||||
|
|
||||||
if (examplePod?.kind) {
|
if (examplePod?.kind) {
|
||||||
this.status.installed = true;
|
status.installed = true;
|
||||||
this.status.currentVersion = examplePod.spec.containers[0].image.split(":")[1];
|
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
|
status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature
|
||||||
} else {
|
} else {
|
||||||
this.status.installed = false;
|
status.installed = false;
|
||||||
this.status.canUpgrade = false;
|
status.canUpgrade = false;
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e?.error?.code === 404) {
|
if (e?.error?.code === 404) {
|
||||||
this.status.installed = false;
|
status.installed = false;
|
||||||
this.status.canUpgrade = false;
|
status.canUpgrade = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall(cluster: Store.Cluster): Promise<void> {
|
async uninstall(): Promise<string> {
|
||||||
const podApi = K8sApi.forCluster(cluster, K8sApi.Pod);
|
return this.stack.kubectlDeleteFolder(this.resourceFolder);
|
||||||
await podApi.delete({name: "example-pod", namespace: "default"});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -539,12 +575,12 @@ You can use Lens extensions to add custom preferences to the Preferences page, p
|
|||||||
The following example demonstrates adding a custom preference:
|
The following example demonstrates adding a custom preference:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
@observable preference = { enabled: false };
|
@observable preference = { enabled: false };
|
||||||
|
|
||||||
@ -578,10 +614,16 @@ This is how `ExampleRendererExtension` handles the state of the preference input
|
|||||||
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
export class ExamplePreferenceProps {
|
export class ExamplePreferenceProps {
|
||||||
preference: {
|
preference: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -594,7 +636,7 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
|
|||||||
render() {
|
render() {
|
||||||
const { preference } = this.props;
|
const { preference } = this.props;
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={preference.enabled}
|
value={preference.enabled}
|
||||||
onChange={v => { preference.enabled = v; }}
|
onChange={v => { preference.enabled = v; }}
|
||||||
@ -612,7 +654,7 @@ export class ExamplePreferenceHint extends React.Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties:
|
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Renderer.Component.Checkbox` using the following properties:
|
||||||
|
|
||||||
* `label` sets the text that displays next to the checkbox.
|
* `label` sets the text that displays next to the checkbox.
|
||||||
* `value` is initially set to `preference.enabled`.
|
* `value` is initially set to `preference.enabled`.
|
||||||
@ -645,11 +687,11 @@ The following example adds a `statusBarItems` definition and a `globalPages` def
|
|||||||
It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -703,16 +745,19 @@ The following example shows how to add a `kubeObjectMenuItems` for namespace res
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectMenuItems = [
|
kubeObjectMenuItems = [
|
||||||
{
|
{
|
||||||
kind: "Namespace",
|
kind: "Namespace",
|
||||||
apiVersions: ["v1"],
|
apiVersions: ["v1"],
|
||||||
components: {
|
components: {
|
||||||
MenuItem: (props: Component.KubeObjectMenuProps<K8sApi.Namespace>) => <NamespaceMenuItem {...props} />
|
MenuItem: (props: KubeObjectMenuProps<Namespace>) => <NamespaceMenuItem {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -734,16 +779,28 @@ In this example a `NamespaceMenuItem` object is returned.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, K8sApi, Navigation} from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export function NamespaceMenuItem(props: Component.KubeObjectMenuProps<K8sApi.Namespace>) {
|
const {
|
||||||
|
Component: {
|
||||||
|
terminalStore,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
Navigation,
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export function NamespaceMenuItem(props: KubeObjectMenuProps<Namespace>) {
|
||||||
const { object: namespace, toolbar } = props;
|
const { object: namespace, toolbar } = props;
|
||||||
if (!namespace) return null;
|
if (!namespace) return null;
|
||||||
|
|
||||||
const namespaceName = namespace.getName();
|
const namespaceName = namespace.getName();
|
||||||
|
|
||||||
const sendToTerminal = (command: string) => {
|
const sendToTerminal = (command: string) => {
|
||||||
Component.terminalStore.sendCommand(command, {
|
terminalStore.sendCommand(command, {
|
||||||
enter: true,
|
enter: true,
|
||||||
newTab: true,
|
newTab: true,
|
||||||
});
|
});
|
||||||
@ -755,21 +812,21 @@ export function NamespaceMenuItem(props: Component.KubeObjectMenuProps<K8sApi.Na
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={getPods}>
|
<MenuItem onClick={getPods}>
|
||||||
<Component.Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
<Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
||||||
<span className="title">Get Pods</span>
|
<span className="title">Get Pods</span>
|
||||||
</Component.MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`NamespaceMenuItem` returns a `Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property.
|
`NamespaceMenuItem` returns a `Renderer.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.
|
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()`.
|
The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`.
|
||||||
`namespace` is the `props.object`, which is of type `K8sApi.Namespace`.
|
`namespace` is the `props.object`, which is of type `Renderer.K8sApi.Namespace`.
|
||||||
`K8sApi.Namespace` is the API for accessing namespaces.
|
`Renderer.K8sApi.Namespace` is the API for accessing namespaces.
|
||||||
The current namespace in this example is simply given by `namespace.getName()`.
|
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.
|
Thus, `kubeObjectMenuItems` afford convenient access to the specific resource selected by the user.
|
||||||
|
|
||||||
@ -783,18 +840,22 @@ These custom details appear on the details page for a specific resource, such as
|
|||||||
The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page:
|
The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react";
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { NamespaceDetailsItem } from "./src/namespace-details-item"
|
import { NamespaceDetailsItem } from "./src/namespace-details-item";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type KubeObjectDetailsProps = Renderer.Component.KubeObjectDetailsProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectDetailItems = [
|
kubeObjectDetailItems = [
|
||||||
{
|
{
|
||||||
kind: "Namespace",
|
kind: "Namespace",
|
||||||
apiVersions: ["v1"],
|
apiVersions: ["v1"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
components: {
|
components: {
|
||||||
Details: (props: Component.KubeObjectDetailsProps<K8sApi.Namespace>) => <NamespaceDetailsItem {...props} />
|
Details: (props: KubeObjectDetailsProps<Namespace>) => <NamespaceDetailsItem {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -814,25 +875,39 @@ In this example a `NamespaceDetailsItem` object is returned.
|
|||||||
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { PodsDetailsList } from "./pods-details-list";
|
import { PodsDetailsList } from "./pods-details-list";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
@observer
|
const {
|
||||||
export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>> {
|
K8sApi: {
|
||||||
|
podsApi,
|
||||||
|
},
|
||||||
|
Component: {
|
||||||
|
DrawerTitle,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
@observable private pods: K8sApi.Pod[];
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps<Namespace>> {
|
||||||
|
@observable private pods: Pod[];
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.pods = await K8sApi.podsApi.list({namespace: this.props.object.getName()});
|
const namespace = this.props.object.getName();
|
||||||
|
|
||||||
|
this.pods = await podsApi.list({ namespace });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Component.DrawerTitle title="Pods" />
|
<DrawerTitle title="Pods" />
|
||||||
<PodsDetailsList pods={this.pods}/>
|
<PodsDetailsList pods={this.pods}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -840,14 +915,14 @@ export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDe
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
Since `NamespaceDetailsItem` extends `React.Component<KubeObjectDetailsProps<Namespace>>`, it can access the current namespace object (type `Namespace`) through `this.props.object`.
|
||||||
You can query this object for many details about the current namespace.
|
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.
|
In the example above, `componentDidMount()` gets the namespace's name using the `Namespace` `getName()` method.
|
||||||
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
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.
|
To get this list of pods, this example uses the Kubernetes pods API `podsApi.list()` method.
|
||||||
The `K8sApi.podsApi` is automatically configured for the active cluster.
|
The `podsApi` is automatically configured for the active cluster.
|
||||||
|
|
||||||
Note that `K8sApi.podsApi.list()` is an asynchronous method.
|
Note that `podsApi.list()` is an asynchronous method.
|
||||||
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
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()`.
|
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()`.
|
However, `componentDidMount()` is called right after the first call to `render()`.
|
||||||
@ -856,51 +931,59 @@ Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.
|
|||||||
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
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.
|
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.
|
Details are placed in drawers, and using `Renderer.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.
|
Multiple details in a drawer can be placed in `<Renderer.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`:
|
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Table,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pods: K8sApi.Pod[];
|
pods?: Pod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PodsDetailsList extends React.Component<Props> {
|
export class PodsDetailsList extends React.Component<Props> {
|
||||||
|
getTableRow = (pod: Pod) => {
|
||||||
getTableRow(index: number) {
|
return (
|
||||||
const {pods} = this.props;
|
<TableRow key={index} nowrap>
|
||||||
return (
|
<TableCell className="podName">{pods[index].getName()}</TableCell>
|
||||||
<Component.TableRow key={index} nowrap>
|
<TableCell className="podAge">{pods[index].getAge()}</TableCell>
|
||||||
<Component.TableCell className="podName">{pods[index].getName()}</Component.TableCell>
|
<TableCell className="podStatus">{pods[index].getStatus()}</TableCell>
|
||||||
<Component.TableCell className="podAge">{pods[index].getAge()}</Component.TableCell>
|
</TableRow>
|
||||||
<Component.TableCell className="podStatus">{pods[index].getStatus()}</Component.TableCell>
|
)
|
||||||
</Component.TableRow>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {pods} = this.props
|
const { pods } = this.props
|
||||||
if (!pods?.length) {
|
|
||||||
return null;
|
if (!pods?.length) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div >
|
<div>
|
||||||
<Component.Table>
|
<Table>
|
||||||
<Component.TableHead>
|
<TableHead>
|
||||||
<Component.TableCell className="podName">Name</Component.TableCell>
|
<TableCell className="podName">Name</TableCell>
|
||||||
<Component.TableCell className="podAge">Age</Component.TableCell>
|
<TableCell className="podAge">Age</TableCell>
|
||||||
<Component.TableCell className="podStatus">Status</Component.TableCell>
|
<TableCell className="podStatus">Status</TableCell>
|
||||||
</Component.TableHead>
|
</TableHead>
|
||||||
{
|
{ pods.map(this.getTableRow) }
|
||||||
pods.map((pod, index) => this.getTableRow(index))
|
</Table>
|
||||||
}
|
</div>
|
||||||
</Component.Table>
|
);
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -909,9 +992,9 @@ export class PodsDetailsList extends React.Component<Props> {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods.
|
Obtain the name, age, and status for each pod using the `Renderer.K8sApi.Pod` methods.
|
||||||
Construct the table using the `Component.Table` and related elements.
|
Construct the table using the `Renderer.Component.Table` and related elements.
|
||||||
|
|
||||||
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods.
|
||||||
The table is constructed using the `Component.Table` and related elements.
|
The table is constructed using the `Renderer.Component.Table` and related elements.
|
||||||
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
||||||
|
|||||||
@ -24,14 +24,14 @@ This is so that your data is kept up to date and not persisted longer than expec
|
|||||||
The following example code creates a store for the `appPreferences` guide example:
|
The following example code creates a store for the `appPreferences` guide example:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Store } from "@k8slens/extensions";
|
import { Common } from "@k8slens/extensions";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
|
|
||||||
export type ExamplePreferencesModel = {
|
export type ExamplePreferencesModel = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePreferencesModel> {
|
export class ExamplePreferencesStore extends Common.Store.ExtensionStore<ExamplePreferencesModel> {
|
||||||
|
|
||||||
@observable enabled = false;
|
@observable enabled = false;
|
||||||
|
|
||||||
@ -87,10 +87,10 @@ The following example code, modified from the [`appPreferences`](../renderer-ext
|
|||||||
This can be done in `./main.ts`:
|
This can be done in `./main.ts`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||||
}
|
}
|
||||||
@ -102,12 +102,12 @@ Similarly, `ExamplePreferencesStore` must load in the renderer process where the
|
|||||||
This can be done in `./renderer.ts`:
|
This can be done in `./renderer.ts`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||||
@ -130,17 +130,23 @@ Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is ca
|
|||||||
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ExamplePreferencesStore } from "./example-preference-store";
|
import { ExamplePreferencesStore } from "./example-preference-store";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ExamplePreferenceInput extends React.Component {
|
export class ExamplePreferenceInput extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={ExamplePreferencesStore.getInstace().enabled}
|
value={ExamplePreferencesStore.getInstace().enabled}
|
||||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
||||||
|
|||||||
@ -14,7 +14,13 @@ My component `GlobalPageMenuIcon`
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component: { Icon } } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
const GlobalPageMenuIcon = ({ navigate }: { navigate?: () => void }): JSX.Element => (
|
const GlobalPageMenuIcon = ({ navigate }: { navigate?: () => void }): JSX.Element => (
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kube-object-event-status",
|
"name": "kube-object-event-status",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Adds kube object status from events",
|
"description": "Adds kube object status from events",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
@ -7551,9 +7551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.5",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-metrics-cluster-feature",
|
"name": "lens-metrics-cluster-feature",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens metrics cluster feature",
|
"description": "Lens metrics cluster feature",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
6
extensions/node-menu/package-lock.json
generated
6
extensions/node-menu/package-lock.json
generated
@ -7534,9 +7534,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-node-menu",
|
"name": "lens-node-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens node menu",
|
"description": "Lens node menu",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
6
extensions/pod-menu/package-lock.json
generated
6
extensions/pod-menu/package-lock.json
generated
@ -7487,9 +7487,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-pod-menu",
|
"name": "lens-pod-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens pod menu",
|
"description": "Lens pod menu",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
18
package.json
18
package.json
@ -2,6 +2,7 @@
|
|||||||
"name": "open-lens",
|
"name": "open-lens",
|
||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.0-beta.6",
|
"version": "5.0.0-beta.6",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
@ -178,7 +179,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/call": "^8.0.0",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.12.0",
|
"@kubernetes/client-node": "^0.12.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
@ -239,7 +240,7 @@
|
|||||||
"win-ca": "^3.2.0",
|
"win-ca": "^3.2.0",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.2.1",
|
||||||
"winston-transport-browserconsole": "^1.0.5",
|
"winston-transport-browserconsole": "^1.0.5",
|
||||||
"ws": "^7.3.0"
|
"ws": "^7.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||||
@ -267,7 +268,7 @@
|
|||||||
"@types/jsdom": "^16.2.4",
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash": "^4.14.155",
|
"@types/lodash": "^4.14.155",
|
||||||
"@types/marked": "^0.7.4",
|
"@types/marked": "^2.0.3",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.10.0",
|
"@types/mock-fs": "^4.10.0",
|
||||||
@ -281,7 +282,7 @@
|
|||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/react-select": "^3.0.13",
|
"@types/react-select": "3.1.2",
|
||||||
"@types/react-table": "^7.7.0",
|
"@types/react-table": "^7.7.0",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@types/readable-stream": "^2.3.9",
|
||||||
@ -321,7 +322,7 @@
|
|||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.21.5",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-plugin-unused-imports": "^1.0.1",
|
"eslint-plugin-unused-imports": "^1.0.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.2.0",
|
||||||
"flex.box": "^3.4.4",
|
"flex.box": "^3.4.4",
|
||||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
@ -333,7 +334,7 @@
|
|||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
@ -348,19 +349,20 @@
|
|||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "3.1.1",
|
||||||
"react-select-event": "^5.1.0",
|
"react-select-event": "^5.1.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.26.1",
|
"sharp": "^0.26.1",
|
||||||
"spectron": "11.0.0",
|
"spectron": "11.0.0",
|
||||||
"style-loader": "^1.2.1",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.1.2",
|
"tailwindcss": "^2.1.2",
|
||||||
"ts-jest": "26.3.0",
|
"ts-jest": "26.3.0",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
|
"typed-emitter": "^1.3.1",
|
||||||
"typedoc": "0.17.0-3",
|
"typedoc": "0.17.0-3",
|
||||||
"typedoc-plugin-markdown": "^2.4.0",
|
"typedoc-plugin-markdown": "^2.4.0",
|
||||||
"typeface-roboto": "^0.0.75",
|
"typeface-roboto": "^0.0.75",
|
||||||
|
|||||||
@ -165,7 +165,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.on("onCatalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
||||||
ctx.menuItems.push({
|
ctx.menuItems.push({
|
||||||
icon: "text_snippet",
|
icon: "text_snippet",
|
||||||
title: "Add from kubeconfig",
|
title: "Add from kubeconfig",
|
||||||
|
|||||||
@ -19,7 +19,8 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import EventEmitter from "events";
|
||||||
|
import type TypedEmitter from "typed-emitter";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
|
|
||||||
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
||||||
@ -47,7 +48,13 @@ export interface CatalogCategorySpec {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class CatalogCategory extends EventEmitter {
|
export interface CatalogCategoryEvents {
|
||||||
|
load: () => void;
|
||||||
|
catalogAddMenu: (context: CatalogEntityAddMenuContext) => void;
|
||||||
|
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
|
||||||
abstract readonly apiVersion: string;
|
abstract readonly apiVersion: string;
|
||||||
abstract readonly kind: string;
|
abstract readonly kind: string;
|
||||||
abstract metadata: {
|
abstract metadata: {
|
||||||
|
|||||||
@ -25,9 +25,12 @@ import { appEventBus } from "./event-bus";
|
|||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||||
import { clusterFrameMap } from "./cluster-frames";
|
import { clusterFrameMap } from "./cluster-frames";
|
||||||
|
import { catalogEntityRegistry } from "../main/catalog";
|
||||||
|
import type { KubernetesCluster } from "./catalog-entities";
|
||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate";
|
export const clusterActivateHandler = "cluster:activate";
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||||
|
export const clusterVisibilityHandler = "cluster:visibility";
|
||||||
export const clusterRefreshHandler = "cluster:refresh";
|
export const clusterRefreshHandler = "cluster:refresh";
|
||||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||||
@ -49,6 +52,18 @@ if (ipcMain) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handleRequest(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
||||||
|
const entity = catalogEntityRegistry.getById<KubernetesCluster>(clusterId);
|
||||||
|
|
||||||
|
for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) {
|
||||||
|
kubeEntity.status.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
entity.status.active = visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
return ClusterStore.getInstance()
|
return ClusterStore.getInstance()
|
||||||
.getById(clusterId)
|
.getById(clusterId)
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { match, matchPath } from "react-router";
|
import { match, matchPath } from "react-router";
|
||||||
import { countBy } from "lodash";
|
import { countBy } from "lodash";
|
||||||
import { Singleton } from "../utils";
|
import { iter, Singleton } from "../utils";
|
||||||
import { pathToRegexp } from "path-to-regexp";
|
import { pathToRegexp } from "path-to-regexp";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type Url from "url-parse";
|
import type Url from "url-parse";
|
||||||
@ -35,7 +35,8 @@ import type { RouteHandler, RouteParams } from "../../extensions/registries/prot
|
|||||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
|
|
||||||
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
||||||
export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
export const ProtocolHandlerExtension = `${ProtocolHandlerIpcPrefix}:extension`;
|
||||||
|
export const ProtocolHandlerInvalid = `${ProtocolHandlerIpcPrefix}:invalid`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These two names are long and cumbersome by design so as to decrease the chances
|
* These two names are long and cumbersome by design so as to decrease the chances
|
||||||
@ -47,6 +48,34 @@ export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
|||||||
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
||||||
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned from routing attempts
|
||||||
|
*/
|
||||||
|
export enum RouteAttempt {
|
||||||
|
/**
|
||||||
|
* A handler was found in the set of registered routes
|
||||||
|
*/
|
||||||
|
MATCHED = "matched",
|
||||||
|
/**
|
||||||
|
* A handler was not found within the set of registered routes
|
||||||
|
*/
|
||||||
|
MISSING = "missing",
|
||||||
|
/**
|
||||||
|
* The extension that was matched in the route was not activated
|
||||||
|
*/
|
||||||
|
MISSING_EXTENSION = "no-extension",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: RouteAttempt): RouteAttempt {
|
||||||
|
switch (mainAttempt) {
|
||||||
|
case RouteAttempt.MATCHED:
|
||||||
|
return RouteAttempt.MATCHED;
|
||||||
|
case RouteAttempt.MISSING:
|
||||||
|
case RouteAttempt.MISSING_EXTENSION:
|
||||||
|
return rendererAttempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class LensProtocolRouter extends Singleton {
|
export abstract class LensProtocolRouter extends Singleton {
|
||||||
// Map between path schemas and the handlers
|
// Map between path schemas and the handlers
|
||||||
protected internalRoutes = new Map<string, RouteHandler>();
|
protected internalRoutes = new Map<string, RouteHandler>();
|
||||||
@ -56,11 +85,12 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Attempts to route the given URL to all internal routes that have been registered
|
||||||
* @param url the parsed URL that initiated the `lens://` protocol
|
* @param url the parsed URL that initiated the `lens://` protocol
|
||||||
|
* @returns true if a route has been found
|
||||||
*/
|
*/
|
||||||
protected _routeToInternal(url: Url): void {
|
protected _routeToInternal(url: Url): RouteAttempt {
|
||||||
this._route(Array.from(this.internalRoutes.entries()), url);
|
return this._route(this.internalRoutes.entries(), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +99,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* @param routes the array of path schemas, handler pairs to match against
|
* @param routes the array of path schemas, handler pairs to match against
|
||||||
* @param url the url (in its current state)
|
* @param url the url (in its current state)
|
||||||
*/
|
*/
|
||||||
protected _findMatchingRoute(routes: [string, RouteHandler][], url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
protected _findMatchingRoute(routes: Iterable<[string, RouteHandler]>, url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
||||||
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
||||||
|
|
||||||
for (const [schema, handler] of routes) {
|
for (const [schema, handler] of routes) {
|
||||||
@ -96,7 +126,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* @param routes the array of (path schemas, handler) pairs to match against
|
* @param routes the array of (path schemas, handler) pairs to match against
|
||||||
* @param url the url (in its current state)
|
* @param url the url (in its current state)
|
||||||
*/
|
*/
|
||||||
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
protected _route(routes: Iterable<[string, RouteHandler]>, url: Url, extensionName?: string): RouteAttempt {
|
||||||
const route = this._findMatchingRoute(routes, url);
|
const route = this._findMatchingRoute(routes, url);
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
@ -106,7 +136,9 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
data.extensionName = extensionName;
|
data.extensionName = extensionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return void logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||||
|
|
||||||
|
return RouteAttempt.MISSING;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [match, handler] = route;
|
const [match, handler] = route;
|
||||||
@ -121,6 +153,8 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler(params);
|
handler(params);
|
||||||
|
|
||||||
|
return RouteAttempt.MATCHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,23 +208,22 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* Note: this function modifies its argument, do not reuse
|
* Note: this function modifies its argument, do not reuse
|
||||||
* @param url the protocol request URI that was "open"-ed
|
* @param url the protocol request URI that was "open"-ed
|
||||||
*/
|
*/
|
||||||
protected async _routeToExtension(url: Url): Promise<void> {
|
protected async _routeToExtension(url: Url): Promise<RouteAttempt> {
|
||||||
const extension = await this._findMatchingExtensionByName(url);
|
const extension = await this._findMatchingExtensionByName(url);
|
||||||
|
|
||||||
if (typeof extension === "string") {
|
if (typeof extension === "string") {
|
||||||
// failed to find an extension, it returned its name
|
// failed to find an extension, it returned its name
|
||||||
return;
|
return RouteAttempt.MISSING_EXTENSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the extension name from the path name so we don't need to match on it anymore
|
// remove the extension name from the path name so we don't need to match on it anymore
|
||||||
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||||
|
|
||||||
const handlers = extension
|
|
||||||
.protocolHandlers
|
|
||||||
.map<[string, RouteHandler]>(({ pathSchema, handler }) => [pathSchema, handler]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._route(handlers, url, extension.name);
|
const handlers = iter.map(extension.protocolHandlers, ({ pathSchema, handler }) => [pathSchema, handler] as [string, RouteHandler]);
|
||||||
|
|
||||||
|
return this._route(handlers, url, extension.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RoutingError) {
|
if (error instanceof RoutingError) {
|
||||||
error.extensionName = extension.name;
|
error.extensionName = extension.name;
|
||||||
|
|||||||
@ -41,3 +41,11 @@ export function buildURL<P extends object = {}, Q extends object = {}>(path: str
|
|||||||
return parts.filter(Boolean).join("");
|
return parts.filter(Boolean).join("");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildURLPositional<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||||
|
const builder = buildURL(path);
|
||||||
|
|
||||||
|
return function(params?: P, query?: Q, fragment?: string): string {
|
||||||
|
return builder({ params, query, fragment });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const isPublishConfigured = Object.keys(packageInfo.build).includes("publ
|
|||||||
|
|
||||||
export const productName = packageInfo.productName;
|
export const productName = packageInfo.productName;
|
||||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||||
export const publicPath = "/build/";
|
export const publicPath = "/build/" as string;
|
||||||
|
|
||||||
// Webpack build paths
|
// Webpack build paths
|
||||||
export const contextDir = process.cwd();
|
export const contextDir = process.cwd();
|
||||||
@ -60,13 +60,13 @@ defineGlobal("__static", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Apis
|
// Apis
|
||||||
export const apiPrefix = "/api"; // local router apis
|
export const apiPrefix = "/api" as string; // local router apis
|
||||||
export const apiKubePrefix = "/api-kube"; // k8s cluster apis
|
export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
|
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
|
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/";
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
// This explicitly ignores the prerelease info on the package version
|
// This explicitly ignores the prerelease info on the package version
|
||||||
const { major, minor, patch } = new SemVer(packageInfo.version);
|
const { major, minor, patch } = new SemVer(packageInfo.version);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { extensionInstaller, PackageJson } from "./extension-installer";
|
|||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
|
import { isProduction } from "../common/vars";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
id: LensExtensionId;
|
id: LensExtensionId;
|
||||||
@ -353,7 +354,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: installedManifestPath,
|
id: installedManifestPath,
|
||||||
|
|||||||
@ -279,6 +279,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
||||||
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
||||||
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
||||||
|
registries.workloadsOverviewDetailRegistry.add(extension.kubeWorkloadsOverviewItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(extension.commands),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
||||||
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration,
|
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
|
||||||
} from "./registries";
|
} from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
@ -40,6 +40,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||||
|
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: CommandRegistration[] = [];
|
commands: CommandRegistration[] = [];
|
||||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -20,11 +20,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Catalog from "./catalog";
|
import * as Catalog from "./catalog";
|
||||||
|
import * as Navigation from "./navigation";
|
||||||
import { IpcMain as Ipc } from "../ipc/ipc-main";
|
import { IpcMain as Ipc } from "../ipc/ipc-main";
|
||||||
import { LensMainExtension as LensExtension } from "../lens-main-extension";
|
import { LensMainExtension as LensExtension } from "../lens-main-extension";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Catalog,
|
Catalog,
|
||||||
|
Navigation,
|
||||||
Ipc,
|
Ipc,
|
||||||
LensExtension,
|
LensExtension,
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/extensions/main-api/navigation.ts
Normal file
26
src/extensions/main-api/navigation.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WindowManager } from "../../main/window-manager";
|
||||||
|
|
||||||
|
export function navigate(url: string) {
|
||||||
|
return WindowManager.getInstance().navigate(url);
|
||||||
|
}
|
||||||
@ -33,3 +33,4 @@ export * from "./command-registry";
|
|||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
export * from "./welcome-menu-registry";
|
||||||
export * from "./protocol-handler-registry";
|
export * from "./protocol-handler-registry";
|
||||||
|
export * from "./workloads-overview-detail-registry";
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface WorkloadsOverviewDetailComponents {
|
||||||
|
Details: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkloadsOverviewDetailRegistration {
|
||||||
|
components: WorkloadsOverviewDetailComponents;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkloadsOverviewDetailRegistry extends BaseRegistry<WorkloadsOverviewDetailRegistration> {
|
||||||
|
getItems() {
|
||||||
|
const items = super.getItems();
|
||||||
|
|
||||||
|
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workloadsOverviewDetailRegistry = new WorkloadsOverviewDetailRegistry();
|
||||||
@ -53,6 +53,7 @@ export * from "../../renderer/components/stepper";
|
|||||||
export * from "../../renderer/components/wizard";
|
export * from "../../renderer/components/wizard";
|
||||||
export * from "../../renderer/components/+workloads-pods/pod-details-list";
|
export * from "../../renderer/components/+workloads-pods/pod-details-list";
|
||||||
export * from "../../renderer/components/+namespaces/namespace-select";
|
export * from "../../renderer/components/+namespaces/namespace-select";
|
||||||
|
export * from "../../renderer/components/+namespaces/namespace-select-filter";
|
||||||
export * from "../../renderer/components/layout/sub-title";
|
export * from "../../renderer/components/layout/sub-title";
|
||||||
export * from "../../renderer/components/input/search-input";
|
export * from "../../renderer/components/input/search-input";
|
||||||
export * from "../../renderer/components/chart/bar-chart";
|
export * from "../../renderer/components/chart/bar-chart";
|
||||||
|
|||||||
@ -24,10 +24,17 @@ import { broadcastMessage } from "../common/ipc";
|
|||||||
import type { CatalogEntityRegistry } from "./catalog";
|
import type { CatalogEntityRegistry } from "./catalog";
|
||||||
import "../common/catalog-entities/kubernetes-cluster";
|
import "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
|
|
||||||
|
|
||||||
|
const broadcaster = debounce((items: CatalogEntity[]) => {
|
||||||
|
broadcastMessage("catalog:items", items);
|
||||||
|
}, 1_000, { trailing: true });
|
||||||
|
|
||||||
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
||||||
return reaction(() => toJS(catalog.items), (items) => {
|
return reaction(() => toJS(catalog.items), (items) => {
|
||||||
broadcastMessage("catalog:items", items);
|
broadcaster(items);
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -48,6 +48,15 @@ export class CatalogEntityRegistry {
|
|||||||
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
|
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getById<T extends CatalogEntity>(id: string): T | undefined {
|
||||||
|
const item = this.items.find((entity) => entity.metadata.uid === id);
|
||||||
|
|
||||||
|
if (item) return item as T;
|
||||||
|
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||||
|
|
||||||
|
|||||||
@ -79,8 +79,7 @@ export class ClusterManager extends Singleton {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||||
|
|
||||||
entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
|
this.updateEntityStatus(entity, cluster);
|
||||||
entity.status.active = !cluster.disconnected;
|
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
if (cluster.preferences?.clusterName) {
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
entity.metadata.name = cluster.preferences.clusterName;
|
||||||
@ -101,6 +100,10 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
||||||
|
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const cluster = this.store.getById(entity.metadata.uid);
|
const cluster = this.store.getById(entity.metadata.uid);
|
||||||
@ -118,10 +121,7 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
|
|
||||||
entity.status = {
|
this.updateEntityStatus(entity, cluster);
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
|
||||||
active: !cluster.disconnected
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { promiseExec} from "../promise-exec";
|
import { promiseExec } from "../promise-exec";
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
@ -39,7 +39,7 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
output.forEach((release: any, index: number) => {
|
output.forEach((release: any, index: number) => {
|
||||||
output[index] = toCamelCase(release);
|
output[index] = toCamelCase(release);
|
||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
@ -49,9 +49,9 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string){
|
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export async function installChart(chart: string, values: any, name: string | un
|
|||||||
|
|
||||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export async function getRelease(name: string, namespace: string, cluster: Clust
|
|||||||
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
try {
|
try {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch ({ stderr }) {
|
||||||
@ -127,10 +127,16 @@ export async function deleteRelease(name: string, namespace: string, pathToKubec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) {
|
interface GetValuesOptions {
|
||||||
|
namespace: string;
|
||||||
|
all?: boolean;
|
||||||
|
pathToKubeconfig: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) {
|
||||||
try {
|
try {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch ({ stderr }) {
|
||||||
@ -167,8 +173,8 @@ async function getResources(name: string, namespace: string, cluster: Cluster) {
|
|||||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||||
|
|
||||||
return stdout;
|
return JSON.parse(stdout).items;
|
||||||
} catch {
|
} catch {
|
||||||
return { stdout: JSON.stringify({ items: [] }) };
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,12 @@ import { HelmChartManager } from "./helm-chart-manager";
|
|||||||
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||||
|
|
||||||
|
interface GetReleaseValuesArgs {
|
||||||
|
cluster: Cluster;
|
||||||
|
namespace: string;
|
||||||
|
all: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class HelmService {
|
class HelmService {
|
||||||
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
@ -86,12 +92,12 @@ class HelmService {
|
|||||||
return getRelease(releaseName, namespace, cluster);
|
return getRelease(releaseName, namespace, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
|
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
logger.debug("Fetch release values");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return getValues(releaseName, namespace, all, proxyKubeconfig);
|
return getValues(releaseName, { namespace, all, pathToKubeconfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
|
|||||||
@ -93,8 +93,11 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
for (const arg of process.argv) {
|
for (const arg of process.argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg)
|
try {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
lprm.route(arg);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,8 +107,11 @@ app.on("second-instance", (event, argv) => {
|
|||||||
|
|
||||||
for (const arg of argv) {
|
for (const arg of argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg)
|
try {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
lprm.route(arg);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,9 +258,10 @@ autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
|||||||
app.on("will-quit", (event) => {
|
app.on("will-quit", (event) => {
|
||||||
// Quit app on Cmd+Q (MacOS)
|
// Quit app on Cmd+Q (MacOS)
|
||||||
logger.info("APP:QUIT");
|
logger.info("APP:QUIT");
|
||||||
appEventBus.emit({name: "app", action: "close"});
|
appEventBus.emit({ name: "app", action: "close" });
|
||||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||||
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||||
|
LensProtocolRouterMain.getInstance(false)?.cleanup();
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
if (blockQuit) {
|
if (blockQuit) {
|
||||||
@ -268,10 +275,11 @@ app.on("open-url", (event, rawUrl) => {
|
|||||||
// lens:// protocol handler
|
// lens:// protocol handler
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
LensProtocolRouterMain
|
try {
|
||||||
.getInstance()
|
LensProtocolRouterMain.getInstance().route(rawUrl);
|
||||||
.route(rawUrl)
|
} catch (error) {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import * as uuid from "uuid";
|
|||||||
|
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||||
import { noop } from "../../../common/utils";
|
import { delay, noop } from "../../../common/utils";
|
||||||
import { LensExtension } from "../../../extensions/main-api";
|
import { LensExtension } from "../../../extensions/main-api";
|
||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
||||||
@ -56,27 +56,27 @@ describe("protocol router tests", () => {
|
|||||||
LensProtocolRouterMain.resetInstance();
|
LensProtocolRouterMain.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on non-lens URLS", async () => {
|
it("should throw on non-lens URLS", () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
expect(lpr.route("https://google.ca")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when host not internal or extension", async () => {
|
it("should throw when host not internal or extension", () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
expect(lpr.route("lens://foobar")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("should not throw when has valid host", async () => {
|
it("should not throw when has valid host", async () => {
|
||||||
const extId = uuid.v4();
|
const extId = uuid.v4();
|
||||||
const ext = new LensExtension({
|
const ext = new LensExtension({
|
||||||
id: extId,
|
id: extId,
|
||||||
@ -102,38 +102,39 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app")).toBeUndefined();
|
expect(lpr.route("lens://app")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app/");
|
await delay(50);
|
||||||
expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, "lens://extension/@mirantis/minikube");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app/", "matched");
|
||||||
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call handler if matches", async () => {
|
it("should call handler if matches", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = true; });
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page")).toBeUndefined();
|
expect(lpr.route("lens://app/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(true);
|
expect(called).toBe(true);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler", async () => {
|
it("should call most exact handler", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -141,13 +142,13 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe("foo");
|
expect(called).toBe("foo");
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler for an extension", async () => {
|
it("should call most exact handler for an extension", async () => {
|
||||||
@ -180,13 +181,14 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await delay(50);
|
||||||
expect(called).toBe("foob");
|
expect(called).toBe("foob");
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work with non-org extensions", async () => {
|
it("should work with non-org extensions", async () => {
|
||||||
@ -245,13 +247,15 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await delay(50);
|
||||||
|
|
||||||
expect(called).toBe(1);
|
expect(called).toBe(1);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if urlSchema is invalid", () => {
|
it("should throw if urlSchema is invalid", () => {
|
||||||
@ -260,7 +264,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 3 found handlers", async () => {
|
it("should call most exact handler with 3 found handlers", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -270,16 +274,16 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(3);
|
expect(called).toBe(3);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 2 found handlers", async () => {
|
it("should call most exact handler with 2 found handlers", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -288,12 +292,12 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(1);
|
expect(called).toBe(1);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import Url from "url-parse";
|
|||||||
import type { LensExtension } from "../../extensions/lens-extension";
|
import type { LensExtension } from "../../extensions/lens-extension";
|
||||||
import { broadcastMessage } from "../../common/ipc";
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
import { observable, when, makeObservable } from "mobx";
|
import { observable, when, makeObservable } from "mobx";
|
||||||
|
import { ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
|
||||||
|
import { disposer } from "../../common/utils";
|
||||||
|
|
||||||
export interface FallbackHandler {
|
export interface FallbackHandler {
|
||||||
(name: string): Promise<boolean>;
|
(name: string): Promise<boolean>;
|
||||||
@ -36,19 +38,25 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
@observable rendererLoaded = false;
|
@observable rendererLoaded = false;
|
||||||
@observable extensionsLoaded = false;
|
@observable extensionsLoaded = false;
|
||||||
|
|
||||||
|
protected disposers = disposer();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
this.disposers();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the most specific registered handler, if it exists, and invoke it.
|
* Find the most specific registered handler, if it exists, and invoke it.
|
||||||
*
|
*
|
||||||
* This will send an IPC message to the renderer router to do the same
|
* This will send an IPC message to the renderer router to do the same
|
||||||
* in the renderer.
|
* in the renderer.
|
||||||
*/
|
*/
|
||||||
public async route(rawUrl: string): Promise<void> {
|
public route(rawUrl: string) {
|
||||||
try {
|
try {
|
||||||
const url = new Url(rawUrl, true);
|
const url = new Url(rawUrl, true);
|
||||||
|
|
||||||
@ -60,16 +68,18 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
|
|
||||||
switch (url.host) {
|
switch (url.host) {
|
||||||
case "app":
|
case "app":
|
||||||
return this._routeToInternal(url);
|
this._routeToInternal(url);
|
||||||
|
break;
|
||||||
case "extension":
|
case "extension":
|
||||||
await when(() => this.extensionsLoaded);
|
this.disposers.push(when(() => this.extensionsLoaded, () => this._routeToExtension(url)));
|
||||||
|
break;
|
||||||
return this._routeToExtension(url);
|
|
||||||
default:
|
default:
|
||||||
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url);
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||||
|
|
||||||
if (error instanceof proto.RoutingError) {
|
if (error instanceof proto.RoutingError) {
|
||||||
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
||||||
} else {
|
} else {
|
||||||
@ -102,17 +112,16 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _routeToInternal(url: Url): Promise<void> {
|
protected _routeToInternal(url: Url): RouteAttempt {
|
||||||
const rawUrl = url.toString(); // for sending to renderer
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
const attempt = super._routeToInternal(url);
|
||||||
|
|
||||||
super._routeToInternal(url);
|
this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt)));
|
||||||
|
|
||||||
await when(() => this.rendererLoaded);
|
return attempt;
|
||||||
|
|
||||||
return broadcastMessage(proto.ProtocolHandlerInternal, rawUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _routeToExtension(url: Url): Promise<void> {
|
protected async _routeToExtension(url: Url): Promise<RouteAttempt> {
|
||||||
const rawUrl = url.toString(); // for sending to renderer
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,10 +131,11 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
* Note: this needs to clone the url because _routeToExtension modifies its
|
* Note: this needs to clone the url because _routeToExtension modifies its
|
||||||
* argument.
|
* argument.
|
||||||
*/
|
*/
|
||||||
await super._routeToExtension(new Url(url.toString(), true));
|
const attempt = await super._routeToExtension(new Url(url.toString(), true));
|
||||||
await when(() => this.rendererLoaded);
|
|
||||||
|
|
||||||
return broadcastMessage(proto.ProtocolHandlerExtension, rawUrl);
|
this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt)));
|
||||||
|
|
||||||
|
return attempt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,8 +21,9 @@
|
|||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
import { helmService } from "../helm/helm-service";
|
import { helmService } from "../helm/helm-service";
|
||||||
import { respondJson, respondText } from "../utils/http-responses";
|
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
import { respondJson, respondText } from "../utils/http-responses";
|
||||||
|
import { getBoolean } from "./utils/parse-query";
|
||||||
|
|
||||||
export class HelmApiRoute {
|
export class HelmApiRoute {
|
||||||
static async listCharts(request: LensApiRequest) {
|
static async listCharts(request: LensApiRequest) {
|
||||||
@ -122,10 +123,11 @@ export class HelmApiRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getReleaseValues(request: LensApiRequest) {
|
static async getReleaseValues(request: LensApiRequest) {
|
||||||
const { cluster, params, response, query } = request;
|
const { cluster, params: { namespace, release }, response, query } = request;
|
||||||
|
const all = getBoolean(query, "all");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
|
const result = await helmService.getReleaseValues(release, { cluster, namespace, all });
|
||||||
|
|
||||||
respondText(response, result);
|
respondText(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
21
src/main/routes/utils/index.ts
Normal file
21
src/main/routes/utils/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export * from "./parse-query";
|
||||||
34
src/main/routes/utils/parse-query.ts
Normal file
34
src/main/routes/utils/parse-query.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export function getBoolean(query: URLSearchParams, key: string): boolean {
|
||||||
|
const value = query.get(key);
|
||||||
|
|
||||||
|
switch (value?.toLowerCase()) {
|
||||||
|
case "false":
|
||||||
|
case "f":
|
||||||
|
case "0":
|
||||||
|
case null:
|
||||||
|
case undefined:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,14 +22,36 @@
|
|||||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
import "../../../common/catalog-entities";
|
import "../../../common/catalog-entities";
|
||||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||||
import type { CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
||||||
|
import { WebLink } from "../../../common/catalog-entities";
|
||||||
|
|
||||||
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
|
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
|
||||||
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
|
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
|
||||||
this.rawItems.replace(items);
|
this.updateItems(items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FooBarCategory extends CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "FooBars",
|
||||||
|
icon: "broken"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: WebLink
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "FooBar"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("CatalogEntityRegistry", () => {
|
describe("CatalogEntityRegistry", () => {
|
||||||
describe("updateItems", () => {
|
describe("updateItems", () => {
|
||||||
it("adds new catalog item", () => {
|
it("adds new catalog item", () => {
|
||||||
@ -99,6 +121,32 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
expect(catalog.items[0].status.phase).toEqual("connected");
|
expect(catalog.items[0].status.phase).toEqual("connected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates activeEntity", () => {
|
||||||
|
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.replaceItems(items);
|
||||||
|
catalog.activeEntity = catalog.items[0];
|
||||||
|
expect(catalog.activeEntity.status.phase).toEqual("disconnected");
|
||||||
|
|
||||||
|
items[0].status.phase = "connected";
|
||||||
|
catalog.replaceItems(items);
|
||||||
|
expect(catalog.activeEntity.status.phase).toEqual("connected");
|
||||||
|
});
|
||||||
|
|
||||||
it("removes deleted items", () => {
|
it("removes deleted items", () => {
|
||||||
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
const items = [
|
const items = [
|
||||||
@ -175,8 +223,31 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
catalog.replaceItems(items);
|
catalog.replaceItems(items);
|
||||||
|
|
||||||
expect(catalog.items.length).toBe(1);
|
expect(catalog.items.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does return items after matching category is added", () => {
|
||||||
|
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "FooBar",
|
||||||
|
metadata: {
|
||||||
|
uid: "456",
|
||||||
|
name: "barbaz",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
catalog.replaceItems(items);
|
||||||
|
catalogCategoryRegistry.add(new FooBarCategory());
|
||||||
|
expect(catalog.items.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,17 +19,21 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, makeObservable, observable } from "mobx";
|
import { computed, observable, makeObservable, action } from "mobx";
|
||||||
import { subscribeToBroadcast } from "../../common/ipc";
|
import { subscribeToBroadcast } from "../../common/ipc";
|
||||||
import { CatalogCategory, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../../common/catalog";
|
import { CatalogCategory, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import "../../common/catalog-entities";
|
import "../../common/catalog-entities";
|
||||||
import { iter } from "../utils";
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
protected rawItems = observable.array<CatalogEntityData & CatalogEntityKindData>();
|
@observable.ref activeEntity: CatalogEntity;
|
||||||
@observable.ref activeEntity?: CatalogEntity;
|
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
||||||
|
*/
|
||||||
|
protected rawEntities: (CatalogEntityData & CatalogEntityKindData)[] = [];
|
||||||
|
|
||||||
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
constructor(private categoryRegistry: CatalogCategoryRegistry) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
@ -37,20 +41,68 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||||
this.rawItems.replace(items);
|
this.updateItems(items);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||||
|
this.rawEntities.length = 0;
|
||||||
|
|
||||||
|
const newIds = new Set(items.map((item) => item.metadata.uid));
|
||||||
|
|
||||||
|
for (const uid of this._entities.keys()) {
|
||||||
|
if (!newIds.has(uid)) {
|
||||||
|
this._entities.delete(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
this.updateItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action protected updateItem(item: (CatalogEntityData & CatalogEntityKindData)) {
|
||||||
|
const existing = this._entities.get(item.metadata.uid);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const entity = this.categoryRegistry.getEntityForData(item);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
this._entities.set(entity.metadata.uid, entity);
|
||||||
|
} else {
|
||||||
|
this.rawEntities.push(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
existing.metadata = item.metadata;
|
||||||
|
existing.spec = item.spec;
|
||||||
|
existing.status = item.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processRawEntities() {
|
||||||
|
const items = [...this.rawEntities];
|
||||||
|
|
||||||
|
this.rawEntities.length = 0;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
this.updateItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@computed get items() {
|
@computed get items() {
|
||||||
return Array.from(iter.filterMap(this.rawItems, rawItem => this.categoryRegistry.getEntityForData(rawItem)));
|
this.processRawEntities();
|
||||||
|
|
||||||
|
return Array.from(this._entities.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get entities(): Map<string, CatalogEntity> {
|
@computed get entities(): Map<string, CatalogEntity> {
|
||||||
return new Map(this.items.map(item => [item.metadata.uid, item]));
|
this.processRawEntities();
|
||||||
|
|
||||||
|
return this._entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(id: string) {
|
getById<T extends CatalogEntity>(id: string) {
|
||||||
return this.entities.get(id);
|
return this.entities.get(id) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import { compile } from "path-to-regexp";
|
|
||||||
import { autoBind, formatDuration } from "../../utils";
|
import { autoBind, formatDuration } from "../../utils";
|
||||||
import capitalize from "lodash/capitalize";
|
import capitalize from "lodash/capitalize";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
@ -28,6 +27,8 @@ import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.st
|
|||||||
import type { ItemObject } from "../../item.store";
|
import type { ItemObject } from "../../item.store";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import type { JsonApiData } from "../json-api";
|
import type { JsonApiData } from "../json-api";
|
||||||
|
import { buildURLPositional } from "../../../common/utils/buildUrl";
|
||||||
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
interface IReleasePayload {
|
interface IReleasePayload {
|
||||||
name: string;
|
name: string;
|
||||||
@ -46,7 +47,7 @@ interface IReleasePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IReleaseRawDetails extends IReleasePayload {
|
interface IReleaseRawDetails extends IReleasePayload {
|
||||||
resources: string;
|
resources: KubeJsonApiData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseDetails extends IReleasePayload {
|
export interface IReleaseDetails extends IReleasePayload {
|
||||||
@ -83,12 +84,16 @@ export interface IReleaseRevision {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
|
type EndpointParams = {}
|
||||||
params?: {
|
| { namespace: string }
|
||||||
namespace?: string;
|
| { namespace: string, name: string }
|
||||||
name?: string;
|
| { namespace: string, name: string, route: string };
|
||||||
}
|
|
||||||
) => string;
|
interface EndpointQuery {
|
||||||
|
all?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||||
|
|
||||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
||||||
@ -98,10 +103,8 @@ export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
|||||||
|
|
||||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||||
const path = endpoint({ name, namespace });
|
const path = endpoint({ name, namespace });
|
||||||
|
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
|
||||||
const details = await apiBase.get<IReleaseRawDetails>(path);
|
const resources = rawResources.map(KubeObject.create);
|
||||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
|
||||||
const resources = items.map(item => KubeObject.create(item));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...details,
|
...details,
|
||||||
@ -134,25 +137,25 @@ export async function deleteRelease(name: string, namespace: string): Promise<Js
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
||||||
const path = `${endpoint({ name, namespace })}/values${all? "?all": ""}`;
|
const route = "values";
|
||||||
|
const path = endpoint({ name, namespace, route }, { all });
|
||||||
|
|
||||||
return apiBase.get<string>(path);
|
return apiBase.get<string>(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||||
const path = `${endpoint({ name, namespace })}/history`;
|
const route = "history";
|
||||||
|
const path = endpoint({ name, namespace, route });
|
||||||
|
|
||||||
return apiBase.get(path);
|
return apiBase.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
||||||
const path = `${endpoint({ name, namespace })}/rollback`;
|
const route = "rollback";
|
||||||
|
const path = endpoint({ name, namespace, route });
|
||||||
|
const data = { revision };
|
||||||
|
|
||||||
return apiBase.put(path, {
|
return apiBase.put(path, { data });
|
||||||
data: {
|
|
||||||
revision
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HelmRelease {
|
export interface HelmRelease {
|
||||||
@ -190,7 +193,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
getChart(withVersion = false) {
|
getChart(withVersion = false) {
|
||||||
let chart = this.chart;
|
let chart = this.chart;
|
||||||
|
|
||||||
if(!withVersion && this.getVersion() != "" ) {
|
if (!withVersion && this.getVersion() != "") {
|
||||||
const search = new RegExp(`-${this.getVersion()}`);
|
const search = new RegExp(`-${this.getVersion()}`);
|
||||||
|
|
||||||
chart = chart.replace(search, "");
|
chart = chart.replace(search, "");
|
||||||
@ -210,12 +213,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
getVersion() {
|
getVersion() {
|
||||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||||
|
|
||||||
if (versions) {
|
return versions?.[0] ?? "";
|
||||||
return versions[0];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUpdated(humanize = true, compact = true) {
|
getUpdated(humanize = true, compact = true) {
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
|||||||
status?: Status;
|
status?: Status;
|
||||||
spec?: Spec;
|
spec?: Spec;
|
||||||
|
|
||||||
static create(data: any) {
|
static create(data: KubeJsonApiData) {
|
||||||
return new KubeObject(data);
|
return new KubeObject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isJsonApiDataList<T>(object: unknown, verifyItem:(val: unknown) => val is T): object is KubeJsonApiDataList<T> {
|
static isJsonApiDataList<T>(object: unknown, verifyItem: (val: unknown) => val is T): object is KubeJsonApiDataList<T> {
|
||||||
return (
|
return (
|
||||||
isObject(object)
|
isObject(object)
|
||||||
&& hasTypedProperty(object, "kind", isString)
|
&& hasTypedProperty(object, "kind", isString)
|
||||||
|
|||||||
@ -81,7 +81,6 @@ export class HelmCharts extends Component<Props> {
|
|||||||
tableId="helm_charts"
|
tableId="helm_charts"
|
||||||
className="HelmCharts"
|
className="HelmCharts"
|
||||||
store={helmChartStore}
|
store={helmChartStore}
|
||||||
isClusterScoped={true}
|
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (chart: HelmChart) => chart.getName(),
|
[columnId.name]: (chart: HelmChart) => chart.getName(),
|
||||||
|
|||||||
@ -58,32 +58,35 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
@observable details: IReleaseDetails;
|
@observable details: IReleaseDetails;
|
||||||
@observable values = "";
|
@observable values = "";
|
||||||
@observable valuesLoading = false;
|
@observable valuesLoading = false;
|
||||||
@observable userSuppliedOnly = false;
|
@observable showOnlyUserSuppliedValues = false;
|
||||||
@observable saving = false;
|
@observable saving = false;
|
||||||
@observable releaseSecret: Secret;
|
@observable releaseSecret: Secret;
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
releaseSelector = reaction(() => this.props.release, release => {
|
disposeOnUnmount(this, [
|
||||||
if (!release) return;
|
reaction(() => this.props.release, release => {
|
||||||
this.loadDetails();
|
if (!release) return;
|
||||||
this.loadValues();
|
this.loadDetails();
|
||||||
this.releaseSecret = null;
|
this.loadValues();
|
||||||
|
this.releaseSecret = null;
|
||||||
|
}),
|
||||||
|
reaction(() => secretsStore.getItems(), () => {
|
||||||
|
if (!this.props.release) return;
|
||||||
|
const { getReleaseSecret } = releaseStore;
|
||||||
|
const { release } = this.props;
|
||||||
|
const secret = getReleaseSecret(release);
|
||||||
|
|
||||||
|
if (this.releaseSecret) {
|
||||||
|
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
||||||
|
this.loadDetails();
|
||||||
|
}
|
||||||
|
this.releaseSecret = secret;
|
||||||
|
}),
|
||||||
|
reaction(() => this.showOnlyUserSuppliedValues, () => {
|
||||||
|
this.loadValues();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
secretWatcher = reaction(() => secretsStore.getItems(), () => {
|
|
||||||
if (!this.props.release) return;
|
|
||||||
const { getReleaseSecret } = releaseStore;
|
|
||||||
const { release } = this.props;
|
|
||||||
const secret = getReleaseSecret(release);
|
|
||||||
|
|
||||||
if (this.releaseSecret) {
|
|
||||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
|
||||||
this.loadDetails();
|
|
||||||
}
|
|
||||||
this.releaseSecret = secret;
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -100,10 +103,15 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
async loadValues() {
|
async loadValues() {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
|
|
||||||
this.values = "";
|
try {
|
||||||
this.valuesLoading = true;
|
this.valuesLoading = true;
|
||||||
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.userSuppliedOnly)) ?? "";
|
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.showOnlyUserSuppliedValues)) ?? "";
|
||||||
this.valuesLoading = false;
|
} catch (error) {
|
||||||
|
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
|
||||||
|
this.values = "";
|
||||||
|
} finally {
|
||||||
|
this.valuesLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValues = async () => {
|
updateValues = async () => {
|
||||||
@ -146,21 +154,19 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="User-supplied values only"
|
label="User-supplied values only"
|
||||||
value={this.userSuppliedOnly}
|
value={this.showOnlyUserSuppliedValues}
|
||||||
onChange={values => {
|
onChange={value => this.showOnlyUserSuppliedValues = value}
|
||||||
this.userSuppliedOnly = values;
|
|
||||||
this.loadValues();
|
|
||||||
}}
|
|
||||||
disabled={valuesLoading}
|
disabled={valuesLoading}
|
||||||
/>
|
/>
|
||||||
{valuesLoading
|
<AceEditor
|
||||||
? <Spinner />
|
mode="yaml"
|
||||||
: <AceEditor
|
value={values}
|
||||||
mode="yaml"
|
onChange={text => this.values = text}
|
||||||
value={values}
|
className={cssNames({ loading: valuesLoading })}
|
||||||
onChange={values => this.values = values}
|
readOnly={valuesLoading || this.showOnlyUserSuppliedValues}
|
||||||
/>
|
>
|
||||||
}
|
{valuesLoading && <Spinner center />}
|
||||||
|
</AceEditor>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
label="Save"
|
label="Save"
|
||||||
|
|||||||
@ -66,10 +66,8 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
onOpen = async () => {
|
onOpen = async () => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const currentRevision = this.release.getRevision();
|
|
||||||
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
|
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
|
||||||
|
|
||||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
|
||||||
releases = orderBy(releases, "revision", "desc"); // sort
|
releases = orderBy(releases, "revision", "desc"); // sort
|
||||||
this.revisions.replace(releases);
|
this.revisions.replace(releases);
|
||||||
this.revision = this.revisions[0];
|
this.revision = this.revisions[0];
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
|||||||
menuItems: this.menuItems
|
menuItems: this.menuItems
|
||||||
};
|
};
|
||||||
|
|
||||||
category.emit("onCatalogAddMenu", context);
|
category.emit("catalogAddMenu", context);
|
||||||
}
|
}
|
||||||
}, { fireImmediately: true })
|
}, { fireImmediately: true })
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -23,12 +23,15 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
|
|||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
|
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
|
||||||
import { ItemObject, ItemStore } from "../../item.store";
|
import { ItemObject, ItemStore } from "../../item.store";
|
||||||
import { CatalogCategory } from "../../../common/catalog";
|
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
|
||||||
import { autoBind } from "../../../common/utils";
|
import { autoBind } from "../../../common/utils";
|
||||||
|
|
||||||
export class CatalogEntityItem implements ItemObject {
|
export class CatalogEntityItem implements ItemObject {
|
||||||
constructor(public entity: CatalogEntity) {}
|
constructor(public entity: CatalogEntity) {}
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return this.entity.kind;
|
||||||
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this.entity.metadata.name;
|
return this.entity.metadata.name;
|
||||||
}
|
}
|
||||||
@ -111,6 +114,14 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadAll() {
|
loadAll() {
|
||||||
|
if (this.activeCategory) {
|
||||||
|
this.activeCategory.emit("load");
|
||||||
|
} else {
|
||||||
|
for (const category of catalogCategoryRegistry.items) {
|
||||||
|
category.emit("load");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this.loadItems(() => this.entities);
|
return this.loadItems(() => this.entities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableCell.kind {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.TableCell.source {
|
.TableCell.source {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { ItemListLayout } from "../item-object-list";
|
|||||||
import { action, makeObservable, observable, reaction, when } from "mobx";
|
import { action, makeObservable, observable, reaction, when } from "mobx";
|
||||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
|
import { kebabCase } from "lodash";
|
||||||
import { MenuItem, MenuActions } from "../menu";
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
@ -48,6 +49,7 @@ import { MaterialTooltip } from "../material-tooltip/material-tooltip";
|
|||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
|
kind = "kind",
|
||||||
source = "source",
|
source = "source",
|
||||||
status = "status"
|
status = "status"
|
||||||
}
|
}
|
||||||
@ -203,6 +205,83 @@ export class Catalog extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSingleCategoryList() {
|
||||||
|
return (
|
||||||
|
<ItemListLayout
|
||||||
|
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
tableId="catalog-items"
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(entity: CatalogEntityItem) => entity.searchFields,
|
||||||
|
]}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "", className: "icon" },
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Source", className: "source", sortBy: sortBy.source },
|
||||||
|
{ title: "Labels", className: "labels" },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
this.renderIcon(item),
|
||||||
|
item.name,
|
||||||
|
item.source,
|
||||||
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
|
renderItemMenu={this.renderItemMenu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAllCategoriesList() {
|
||||||
|
return (
|
||||||
|
<ItemListLayout
|
||||||
|
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
tableId="catalog-items"
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.kind]: (item: CatalogEntityItem) => item.kind,
|
||||||
|
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(entity: CatalogEntityItem) => entity.searchFields,
|
||||||
|
]}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "", className: "icon" },
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Kind", className: "kind", sortBy: sortBy.kind },
|
||||||
|
{ title: "Source", className: "source", sortBy: sortBy.source },
|
||||||
|
{ title: "Labels", className: "labels" },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
this.renderIcon(item),
|
||||||
|
item.name,
|
||||||
|
item.kind,
|
||||||
|
item.source,
|
||||||
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
|
renderItemMenu={this.renderItemMenu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.catalogEntityStore) {
|
if (!this.catalogEntityStore) {
|
||||||
return null;
|
return null;
|
||||||
@ -219,39 +298,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
</TopBar>
|
</TopBar>
|
||||||
<MainLayout sidebar={this.renderNavigation()}>
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
<div className="p-6 h-full">
|
<div className="p-6 h-full">
|
||||||
<ItemListLayout
|
{ this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
|
||||||
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
|
||||||
isClusterScoped
|
|
||||||
isSearchable={true}
|
|
||||||
isSelectable={false}
|
|
||||||
className="CatalogItemList"
|
|
||||||
store={this.catalogEntityStore}
|
|
||||||
tableId="catalog-items"
|
|
||||||
sortingCallbacks={{
|
|
||||||
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
|
||||||
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
|
||||||
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
|
||||||
}}
|
|
||||||
searchFilters={[
|
|
||||||
(entity: CatalogEntityItem) => entity.searchFields,
|
|
||||||
]}
|
|
||||||
renderTableHeader={[
|
|
||||||
{ title: "", className: styles.iconCell },
|
|
||||||
{ title: "Name", className: styles.nameCell, sortBy: sortBy.name },
|
|
||||||
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source },
|
|
||||||
{ title: "Labels", className: styles.labelsCell },
|
|
||||||
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status },
|
|
||||||
]}
|
|
||||||
renderTableContents={(item: CatalogEntityItem) => [
|
|
||||||
this.renderIcon(item),
|
|
||||||
item.name,
|
|
||||||
item.source,
|
|
||||||
item.labels.map((label) => <Badge className={styles.badge} key={label} label={label} title={label} />),
|
|
||||||
{ title: item.phase, className: cssNames({ [styles.connected]: item.phase == "connected" }) }
|
|
||||||
]}
|
|
||||||
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
|
||||||
renderItemMenu={this.renderItemMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@ -64,7 +64,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#c93dce",
|
"#c93dce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuUsage"
|
id: "cpuUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -75,7 +76,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuRequests"
|
id: "cpuRequests",
|
||||||
|
label: "Requests"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -86,7 +88,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#3d90ce",
|
"#3d90ce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuLimits"
|
id: "cpuLimits",
|
||||||
|
label: "Limits"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
@ -107,7 +110,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#c93dce",
|
"#c93dce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryUsage"
|
id: "memoryUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -118,7 +122,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryRequests"
|
id: "memoryRequests",
|
||||||
|
label: "Requests"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -129,7 +134,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#3d90ce",
|
"#3d90ce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryLimits"
|
id: "memoryLimits",
|
||||||
|
label: "Limits"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
@ -150,7 +156,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "podUsage"
|
id: "podUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export class CRDDetails extends React.Component<Props> {
|
|||||||
<>
|
<>
|
||||||
<DrawerTitle title="Validation"/>
|
<DrawerTitle title="Validation"/>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode="json"
|
mode="yaml"
|
||||||
className="validation"
|
className="validation"
|
||||||
value={validation}
|
value={validation}
|
||||||
readOnly
|
readOnly
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export class CrdList extends React.Component {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="crd"
|
tableId="crd"
|
||||||
className="CrdList"
|
className="CrdList"
|
||||||
isClusterScoped={true}
|
|
||||||
store={crdStore}
|
store={crdStore}
|
||||||
items={items}
|
items={items}
|
||||||
sortingCallbacks={sortingCallbacks}
|
sortingCallbacks={sortingCallbacks}
|
||||||
|
|||||||
@ -95,7 +95,6 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="crd_resources"
|
tableId="crd_resources"
|
||||||
className="CrdResources"
|
className="CrdResources"
|
||||||
isClusterScoped={!isNamespaced}
|
|
||||||
store={store}
|
store={store}
|
||||||
sortingCallbacks={sortingCallbacks}
|
sortingCallbacks={sortingCallbacks}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { namespaceStore } from "./namespace.store";
|
|||||||
|
|
||||||
import type { SelectOption, SelectProps } from "../select";
|
import type { SelectOption, SelectProps } from "../select";
|
||||||
|
|
||||||
const Placeholder = observer((props: PlaceholderProps<any>) => {
|
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
||||||
const getPlaceholder = (): React.ReactNode => {
|
const getPlaceholder = (): React.ReactNode => {
|
||||||
const namespaces = namespaceStore.contextNamespaces;
|
const namespaces = namespaceStore.contextNamespaces;
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export class Namespaces extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<TabLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
isClusterScoped
|
|
||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="namespaces"
|
tableId="namespaces"
|
||||||
className="Namespaces" store={namespaceStore}
|
className="Namespaces" store={namespaceStore}
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="nodes"
|
tableId="nodes"
|
||||||
className="Nodes"
|
className="Nodes"
|
||||||
store={nodesStore} isClusterScoped
|
store={nodesStore}
|
||||||
isReady={nodesStore.isLoaded}
|
isReady={nodesStore.isLoaded}
|
||||||
dependentStores={[podsStore]}
|
dependentStores={[podsStore]}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export class PodSecurityPolicies extends React.Component {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="access_pod_security_policies"
|
tableId="access_pod_security_policies"
|
||||||
className="PodSecurityPolicies"
|
className="PodSecurityPolicies"
|
||||||
isClusterScoped={true}
|
|
||||||
store={podSecurityPoliciesStore}
|
store={podSecurityPoliciesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: PodSecurityPolicy) => item.getName(),
|
[columnId.name]: (item: PodSecurityPolicy) => item.getName(),
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class StorageClasses extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="storage_classes"
|
tableId="storage_classes"
|
||||||
className="StorageClasses"
|
className="StorageClasses"
|
||||||
store={storageClassStore} isClusterScoped
|
store={storageClassStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: StorageClass) => item.getName(),
|
[columnId.name]: (item: StorageClass) => item.getName(),
|
||||||
[columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(),
|
[columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(),
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class PersistentVolumes extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="storage_volumes"
|
tableId="storage_volumes"
|
||||||
className="PersistentVolumes"
|
className="PersistentVolumes"
|
||||||
store={volumesStore} isClusterScoped
|
store={volumesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: PersistentVolume) => item.getName(),
|
[columnId.name]: (item: PersistentVolume) => item.getName(),
|
||||||
[columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(),
|
[columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(),
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import { Events } from "../+events";
|
|||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
import { kubeWatchApi } from "../../api/kube-watch-api";
|
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||||
import { clusterContext } from "../context";
|
import { clusterContext } from "../context";
|
||||||
|
import { workloadsOverviewDetailRegistry } from "../../../extensions/registries";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -57,11 +58,34 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const items = workloadsOverviewDetailRegistry.getItems().map((item, index) => {
|
||||||
|
return (
|
||||||
|
<item.components.Details key={`workload-overview-${index}`}/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="WorkloadsOverview flex column gaps">
|
<div className="WorkloadsOverview flex column gaps">
|
||||||
<OverviewStatuses/>
|
{items}
|
||||||
{isAllowedResource("events") && <Events compact hideFilters className="box grow"/>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workloadsOverviewDetailRegistry.add([
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Details: (props: any) => <OverviewStatuses {...props} />,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 5,
|
||||||
|
components: {
|
||||||
|
Details: () => {
|
||||||
|
return (
|
||||||
|
isAllowedResource("events") && <Events compact hideFilters className="box grow"/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|||||||
@ -30,6 +30,19 @@
|
|||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparentize(white, .85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .editor {
|
> .editor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: inherit;
|
width: inherit;
|
||||||
@ -76,4 +89,4 @@
|
|||||||
.ace_comment {
|
.ace_comment {
|
||||||
color: var(--dockEditorComment);
|
color: var(--dockEditorComment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,11 +169,12 @@ export class AceEditor extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, hidden } = this.props;
|
const { className, hidden, children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("AceEditor", className, { hidden })}>
|
<div className={cssNames("AceEditor", className, { hidden })}>
|
||||||
<div className="editor" ref={e => this.elem = e}/>
|
<div className="editor" ref={e => this.elem = e}/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,10 +46,11 @@ export class PieChart extends React.Component<Props> {
|
|||||||
const dataset: any = data["datasets"][tooltipItem.datasetIndex];
|
const dataset: any = data["datasets"][tooltipItem.datasetIndex];
|
||||||
const metaData = Object.values<{ total: number }>(dataset["_meta"])[0];
|
const metaData = Object.values<{ total: number }>(dataset["_meta"])[0];
|
||||||
const percent = Math.round((dataset["data"][tooltipItem["index"]] / metaData.total) * 100);
|
const percent = Math.round((dataset["data"][tooltipItem["index"]] / metaData.total) * 100);
|
||||||
|
const label = dataset["label"];
|
||||||
|
|
||||||
if (isNaN(percent)) return "N/A";
|
if (isNaN(percent)) return `${label}: N/A`;
|
||||||
|
|
||||||
return `${percent}%`;
|
return `${label}: ${percent}%`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filter: ({ datasetIndex, index }, { datasets }) => {
|
filter: ({ datasetIndex, index }, { datasets }) => {
|
||||||
|
|||||||
@ -30,21 +30,26 @@ import { ClusterStore } from "../../../common/cluster-store";
|
|||||||
import { requestMain } from "../../../common/ipc";
|
import { requestMain } from "../../../common/ipc";
|
||||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { getMatchedClusterId, navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { catalogURL } from "../+catalog/catalog.route";
|
import { catalogURL } from "../+catalog/catalog.route";
|
||||||
import { ClusterTopbar } from "./cluster-topbar";
|
import { ClusterTopbar } from "./cluster-topbar";
|
||||||
|
import type { RouteComponentProps } from "react-router-dom";
|
||||||
|
import type { IClusterViewRouteParams } from "./cluster-view.route";
|
||||||
|
|
||||||
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterView extends React.Component {
|
export class ClusterView extends React.Component<Props> {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get clusterId() {
|
@computed get clusterId() {
|
||||||
return getMatchedClusterId();
|
return this.props.match.params.clusterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get cluster(): Cluster | undefined {
|
@computed get cluster(): Cluster | undefined {
|
||||||
@ -61,6 +66,10 @@ export class ClusterView extends React.Component {
|
|||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
refreshViews();
|
||||||
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, async (clusterId) => {
|
reaction(() => this.clusterId, async (clusterId) => {
|
||||||
@ -72,14 +81,12 @@ export class ClusterView extends React.Component {
|
|||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reaction(() => this.isReady, (ready) => {
|
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], (values) => {
|
||||||
if (ready) {
|
const disconnected = values[1];
|
||||||
refreshViews(this.clusterId); // show cluster's view (iframe)
|
|
||||||
} else if (hasLoadedView(this.clusterId)) {
|
if (hasLoadedView(this.clusterId) && disconnected) {
|
||||||
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -88,7 +95,7 @@ export class ClusterView extends React.Component {
|
|||||||
const { clusterId, cluster, isReady } = this;
|
const { clusterId, cluster, isReady } = this;
|
||||||
|
|
||||||
if (cluster && !isReady) {
|
if (cluster && !isReady) {
|
||||||
return <ClusterStatus clusterId={clusterId} className="box center"/>;
|
return <ClusterStatus key={clusterId} clusterId={clusterId} className="box center"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
|
import { requestMain } from "../../../common/ipc";
|
||||||
|
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
||||||
|
|
||||||
export interface LensView {
|
export interface LensView {
|
||||||
isLoaded?: boolean
|
isLoaded?: boolean
|
||||||
@ -56,8 +58,13 @@ export async function initView(clusterId: ClusterId) {
|
|||||||
parentElem.appendChild(iframe);
|
parentElem.appendChild(iframe);
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
||||||
await cluster.whenReady;
|
|
||||||
await autoCleanOnRemove(clusterId, iframe);
|
try {
|
||||||
|
await when(() => cluster.ready, { timeout: 5_000 }); // we cannot wait forever because cleanup would be blocked for broken cluster connections
|
||||||
|
logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`);
|
||||||
|
} finally {
|
||||||
|
await autoCleanOnRemove(clusterId, iframe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
||||||
@ -88,5 +95,9 @@ export function refreshViews(visibleClusterId?: string) {
|
|||||||
const isVisible = isCurrent && isLoaded && isReady;
|
const isVisible = isCurrent && isLoaded && isReady;
|
||||||
|
|
||||||
view.style.display = isVisible ? "flex" : "none";
|
view.style.display = isVisible ? "flex" : "none";
|
||||||
|
|
||||||
|
requestMain(clusterVisibilityHandler, clusterId, isVisible).catch(() => {
|
||||||
|
logger.error(`[LENS-VIEW]: failed to set cluster visibility, clusterId=${clusterId}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export class LogList extends React.Component<Props> {
|
|||||||
*/
|
*/
|
||||||
@computed
|
@computed
|
||||||
get logs() {
|
get logs() {
|
||||||
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
|
const showTimestamps = logTabStore.getData(this.props.id)?.showTimestamps;
|
||||||
|
|
||||||
if (!showTimestamps) {
|
if (!showTimestamps) {
|
||||||
return logStore.logsWithoutTimestamps;
|
return logStore.logsWithoutTimestamps;
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import groupBy from "lodash/groupBy";
|
|||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { computed, makeObservable } from "mobx";
|
import { computed, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
||||||
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
|
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
|
||||||
import { boundMethod, createStorage, cssNames, IClassName, isReactNode, noop, ObservableToggleSet, prevDefault, stopPropagation } from "../../utils";
|
import { boundMethod, createStorage, cssNames, IClassName, isReactNode, noop, ObservableToggleSet, prevDefault, stopPropagation } from "../../utils";
|
||||||
@ -36,13 +36,14 @@ import { SearchInputUrl } from "../input";
|
|||||||
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
||||||
import { PageFiltersList } from "./page-filters-list";
|
import { PageFiltersList } from "./page-filters-list";
|
||||||
import { PageFiltersSelect } from "./page-filters-select";
|
import { PageFiltersSelect } from "./page-filters-select";
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { MenuActions } from "../menu/menu-actions";
|
import { MenuActions } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
|
|
||||||
// todo: refactor, split to small re-usable components
|
// todo: refactor, split to small re-usable components
|
||||||
|
|
||||||
@ -63,7 +64,6 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
|
|||||||
store: ItemStore<T>;
|
store: ItemStore<T>;
|
||||||
dependentStores?: ItemStore[];
|
dependentStores?: ItemStore[];
|
||||||
preloadStores?: boolean;
|
preloadStores?: boolean;
|
||||||
isClusterScoped?: boolean;
|
|
||||||
hideFilters?: boolean;
|
hideFilters?: boolean;
|
||||||
searchFilters?: SearchFilter<T>[];
|
searchFilters?: SearchFilter<T>[];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
@ -137,7 +137,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props;
|
const { isConfigurable, tableId, preloadStores } = this.props;
|
||||||
|
|
||||||
if (isConfigurable && !tableId) {
|
if (isConfigurable && !tableId) {
|
||||||
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
|
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
|
||||||
@ -149,12 +149,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
|
|
||||||
if (preloadStores) {
|
if (preloadStores) {
|
||||||
this.loadStores();
|
this.loadStores();
|
||||||
|
|
||||||
if (!isClusterScoped) {
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
namespaceStore.onContextChange(() => this.loadStores())
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +156,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
const { store, dependentStores } = this.props;
|
const { store, dependentStores } = this.props;
|
||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
|
|
||||||
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
|
|
||||||
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
|
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +217,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get items() {
|
@computed get items() {
|
||||||
const {filters, filterCallbacks } = this;
|
const { filters, filterCallbacks } = this;
|
||||||
const filterGroups = groupBy<Filter>(filters, ({ type }) => type);
|
const filterGroups = groupBy<Filter>(filters, ({ type }) => type);
|
||||||
|
|
||||||
const filterItems: ItemsFilter[] = [];
|
const filterItems: ItemsFilter[] = [];
|
||||||
@ -330,7 +323,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageFiltersList filters={filters}/>;
|
return <PageFiltersList filters={filters} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNoItems() {
|
renderNoItems() {
|
||||||
@ -355,7 +348,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NoItems/>;
|
return <NoItems />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItems() {
|
renderItems() {
|
||||||
@ -397,20 +390,26 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
const { showHeader, customizeHeader, renderHeaderTitle, headerClassName, isClusterScoped } = this.props;
|
const { showHeader, customizeHeader, renderHeaderTitle, headerClassName } = this.props;
|
||||||
|
|
||||||
if (!showHeader) return null;
|
if (!showHeader) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNamespaceSelectFilter = this.props.store instanceof KubeObjectStore && this.props.store.api.isNamespaced;
|
||||||
const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle;
|
const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle;
|
||||||
const placeholders: IHeaderPlaceholders = {
|
const placeholders: IHeaderPlaceholders = {
|
||||||
title: <h5 className="title">{title}</h5>,
|
title: <h5 className="title">{title}</h5>,
|
||||||
info: this.renderInfo(),
|
info: this.renderInfo(),
|
||||||
filters: <>
|
filters: (
|
||||||
{!isClusterScoped && <NamespaceSelectFilter/>}
|
<>
|
||||||
<PageFiltersSelect allowEmpty disableFilters={{
|
{showNamespaceSelectFilter && <NamespaceSelectFilter />}
|
||||||
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
<PageFiltersSelect allowEmpty disableFilters={{
|
||||||
}}/>
|
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
||||||
</>,
|
}} />
|
||||||
search: <SearchInputUrl/>,
|
</>
|
||||||
|
),
|
||||||
|
search: <SearchInputUrl />,
|
||||||
};
|
};
|
||||||
let header = this.renderHeaderContent(placeholders);
|
let header = this.renderHeaderContent(placeholders);
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export class Notifications extends React.Component {
|
|||||||
static ok(message: NotificationMessage) {
|
static ok(message: NotificationMessage) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message,
|
message,
|
||||||
timeout: 2500,
|
timeout: 2_500,
|
||||||
status: NotificationStatus.OK
|
status: NotificationStatus.OK
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -45,12 +45,19 @@ export class Notifications extends React.Component {
|
|||||||
static error(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
static error(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message,
|
message,
|
||||||
timeout: 10000,
|
timeout: 10_000,
|
||||||
status: NotificationStatus.ERROR,
|
status: NotificationStatus.ERROR,
|
||||||
...customOpts
|
...customOpts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static shortInfo(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
|
this.info(message, {
|
||||||
|
timeout: 5_000,
|
||||||
|
...customOpts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static info(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
static info(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
return notificationsStore.add({
|
return notificationsStore.add({
|
||||||
status: NotificationStatus.INFO,
|
status: NotificationStatus.INFO,
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import React, { ReactNode } from "react";
|
|||||||
import { computed, makeObservable } from "mobx";
|
import { computed, makeObservable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { boundMethod, cssNames } from "../../utils";
|
import { boundMethod, cssNames } from "../../utils";
|
||||||
import ReactSelect, { ActionMeta, components, Props as ReactSelectProps, Styles } from "react-select";
|
import ReactSelect, { ActionMeta, components, OptionTypeBase, Props as ReactSelectProps, Styles } from "react-select";
|
||||||
import Creatable, { CreatableProps } from "react-select/creatable";
|
import Creatable, { CreatableProps } from "react-select/creatable";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ export interface SelectOption<T = any> {
|
|||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps<T = any> extends ReactSelectProps<T>, CreatableProps<T> {
|
export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, CreatableProps<T, boolean> {
|
||||||
value?: T;
|
value?: T;
|
||||||
themeName?: "dark" | "light" | "outlined" | "lens";
|
themeName?: "dark" | "light" | "outlined" | "lens";
|
||||||
menuClass?: string;
|
menuClass?: string;
|
||||||
@ -69,7 +69,7 @@ export class Select extends React.Component<SelectProps> {
|
|||||||
return this.props.themeName || ThemeStore.getInstance().activeTheme.type;
|
return this.props.themeName || ThemeStore.getInstance().activeTheme.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private styles: Styles = {
|
private styles: Styles<OptionTypeBase, boolean> = {
|
||||||
menuPortal: styles => ({
|
menuPortal: styles => ({
|
||||||
...styles,
|
...styles,
|
||||||
zIndex: "auto"
|
zIndex: "auto"
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { KubeObject, KubeStatus } from "./api/kube-object";
|
|||||||
import type { IKubeWatchEvent } from "./api/kube-watch-api";
|
import type { IKubeWatchEvent } from "./api/kube-watch-api";
|
||||||
import { ItemStore } from "./item.store";
|
import { ItemStore } from "./item.store";
|
||||||
import { apiManager } from "./api/api-manager";
|
import { apiManager } from "./api/api-manager";
|
||||||
import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
|
import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
|
||||||
import type { KubeJsonApiData } from "./api/kube-json-api";
|
import type { KubeJsonApiData } from "./api/kube-json-api";
|
||||||
import { Notifications } from "./components/notifications";
|
import { Notifications } from "./components/notifications";
|
||||||
|
|
||||||
@ -280,6 +280,9 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
async update(item: T, data: Partial<T>): Promise<T> {
|
async update(item: T, data: Partial<T>): Promise<T> {
|
||||||
const newItem = await item.update<T>(data);
|
const newItem = await item.update<T>(data);
|
||||||
|
|
||||||
|
ensureObjectSelfLink(this.api, newItem);
|
||||||
|
|
||||||
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
||||||
|
|
||||||
this.items.splice(index, 1, newItem);
|
this.items.splice(index, 1, newItem);
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { addClusterURL } from "../components/+add-cluster";
|
import { addClusterURL } from "../components/+add-cluster";
|
||||||
import { catalogURL } from "../components/+catalog";
|
import { catalogURL } from "../components/+catalog";
|
||||||
import { attemptInstallByInfo, extensionsURL } from "../components/+extensions";
|
import { attemptInstallByInfo, extensionsURL } from "../components/+extensions";
|
||||||
@ -30,6 +31,7 @@ import { entitySettingsURL } from "../components/+entity-settings";
|
|||||||
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
|
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
|
||||||
|
import { Notifications } from "../components/notifications";
|
||||||
|
|
||||||
export function bindProtocolAddRouteHandlers() {
|
export function bindProtocolAddRouteHandlers() {
|
||||||
LensProtocolRouterRenderer
|
LensProtocolRouterRenderer
|
||||||
@ -37,7 +39,16 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
.addInternalHandler("/preferences", ({ search: { highlight }}) => {
|
.addInternalHandler("/preferences", ({ search: { highlight }}) => {
|
||||||
navigate(preferencesURL({ fragment: highlight }));
|
navigate(preferencesURL({ fragment: highlight }));
|
||||||
})
|
})
|
||||||
.addInternalHandler("/", () => {
|
.addInternalHandler("/", ({ tail }) => {
|
||||||
|
if (tail) {
|
||||||
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown Action for <code>lens://app/{tail}</code>.{" "}
|
||||||
|
Are you on the latest version?
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
navigate(catalogURL());
|
navigate(catalogURL());
|
||||||
})
|
})
|
||||||
.addInternalHandler("/landing", () => {
|
.addInternalHandler("/landing", () => {
|
||||||
@ -59,7 +70,11 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
if (entity) {
|
if (entity) {
|
||||||
navigate(entitySettingsURL({ params: { entityId } }));
|
navigate(entitySettingsURL({ params: { entityId } }));
|
||||||
} else {
|
} else {
|
||||||
console.log("[APP-HANDLER]: catalog entity with given ID does not exist", { entityId });
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown catalog entity <code>{entityId}</code>.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Handlers below are deprecated and only kept for backward compact purposes
|
// Handlers below are deprecated and only kept for backward compact purposes
|
||||||
@ -69,7 +84,11 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
if (cluster) {
|
if (cluster) {
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
navigate(clusterViewURL({ params: { clusterId } }));
|
||||||
} else {
|
} else {
|
||||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown catalog entity <code>{clusterId}</code>.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addInternalHandler("/cluster/:clusterId/settings", ({ pathname: { clusterId } }) => {
|
.addInternalHandler("/cluster/:clusterId/settings", ({ pathname: { clusterId } }) => {
|
||||||
@ -78,7 +97,11 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
if (cluster) {
|
if (cluster) {
|
||||||
navigate(entitySettingsURL({ params: { entityId: clusterId } }));
|
navigate(entitySettingsURL({ params: { entityId: clusterId } }));
|
||||||
} else {
|
} else {
|
||||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown catalog entity <code>{clusterId}</code>.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addInternalHandler("/extensions", () => {
|
.addInternalHandler("/extensions", () => {
|
||||||
@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ipcRenderer } from "electron";
|
|
||||||
import * as proto from "../../common/protocol-handler";
|
|
||||||
import logger from "../../main/logger";
|
|
||||||
import Url from "url-parse";
|
|
||||||
import { boundMethod } from "../utils";
|
|
||||||
|
|
||||||
export class LensProtocolRouterRenderer extends proto.LensProtocolRouter {
|
|
||||||
/**
|
|
||||||
* This function is needed to be called early on in the renderers lifetime.
|
|
||||||
*/
|
|
||||||
public init(): void {
|
|
||||||
ipcRenderer
|
|
||||||
.on(proto.ProtocolHandlerInternal, this.ipcInternalHandler)
|
|
||||||
.on(proto.ProtocolHandlerExtension, this.ipcExtensionHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
|
||||||
private ipcInternalHandler(event: Electron.IpcRendererEvent, ...args: any[]): void {
|
|
||||||
if (args.length !== 1) {
|
|
||||||
return void logger.warn(`${proto.LensProtocolRouter.LoggingPrefix}: unexpected number of args`, { args });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [rawUrl] = args;
|
|
||||||
const url = new Url(rawUrl, true);
|
|
||||||
|
|
||||||
this._routeToInternal(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
|
||||||
private ipcExtensionHandler(event: Electron.IpcRendererEvent, ...args: any[]): void {
|
|
||||||
if (args.length !== 1) {
|
|
||||||
return void logger.warn(`${proto.LensProtocolRouter.LoggingPrefix}: unexpected number of args`, { args });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [rawUrl] = args;
|
|
||||||
const url = new Url(rawUrl, true);
|
|
||||||
|
|
||||||
this._routeToExtension(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
119
src/renderer/protocol-handler/router.tsx
Normal file
119
src/renderer/protocol-handler/router.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import * as proto from "../../common/protocol-handler";
|
||||||
|
import Url from "url-parse";
|
||||||
|
import { onCorrect } from "../../common/ipc";
|
||||||
|
import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
|
||||||
|
import { Notifications } from "../components/notifications";
|
||||||
|
|
||||||
|
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
||||||
|
if (args.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[0] !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[1]) {
|
||||||
|
case RouteAttempt.MATCHED:
|
||||||
|
case RouteAttempt.MISSING:
|
||||||
|
case RouteAttempt.MISSING_EXTENSION:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LensProtocolRouterRenderer extends proto.LensProtocolRouter {
|
||||||
|
/**
|
||||||
|
* This function is needed to be called early on in the renderers lifetime.
|
||||||
|
*/
|
||||||
|
public init(): void {
|
||||||
|
onCorrect({
|
||||||
|
channel: proto.ProtocolHandlerInternal,
|
||||||
|
source: ipcRenderer,
|
||||||
|
verifier: verifyIpcArgs,
|
||||||
|
listener: (event, rawUrl, mainAttemptResult) => {
|
||||||
|
const rendererAttempt = this._routeToInternal(new Url(rawUrl, true));
|
||||||
|
|
||||||
|
if (foldAttemptResults(mainAttemptResult, rendererAttempt) === RouteAttempt.MISSING) {
|
||||||
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown action <code>{rawUrl}</code>.{" "}
|
||||||
|
Are you on the latest version?
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onCorrect({
|
||||||
|
channel: proto.ProtocolHandlerExtension,
|
||||||
|
source: ipcRenderer,
|
||||||
|
verifier: verifyIpcArgs,
|
||||||
|
listener: async (event, rawUrl, mainAttemptResult) => {
|
||||||
|
const rendererAttempt = await this._routeToExtension(new Url(rawUrl, true));
|
||||||
|
|
||||||
|
switch (foldAttemptResults(mainAttemptResult, rendererAttempt)) {
|
||||||
|
case RouteAttempt.MISSING:
|
||||||
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Unknown action <code>{rawUrl}</code>.{" "}
|
||||||
|
Are you on the latest version of the extension?
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RouteAttempt.MISSING_EXTENSION:
|
||||||
|
Notifications.shortInfo(
|
||||||
|
<p>
|
||||||
|
Missing extension for action <code>{rawUrl}</code>.{" "}
|
||||||
|
Not able to find extension in our known list.{" "}
|
||||||
|
Try installing it manually.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onCorrect({
|
||||||
|
channel: ProtocolHandlerInvalid,
|
||||||
|
source: ipcRenderer,
|
||||||
|
listener: (event, error, rawUrl) => {
|
||||||
|
Notifications.error((
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Failed to route <code>{rawUrl}</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>Error:</b> {error}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
},
|
||||||
|
verifier: (args): args is [string, string] => {
|
||||||
|
return args.length === 2 && typeof args[0] === "string";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
144
yarn.lock
144
yarn.lock
@ -456,13 +456,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
purgecss "^3.1.3"
|
purgecss "^3.1.3"
|
||||||
|
|
||||||
"@hapi/address@4.x.x":
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.1.tgz#267301ddf7bc453718377a6fb3832a2f04a721dd"
|
|
||||||
integrity sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==
|
|
||||||
dependencies:
|
|
||||||
"@hapi/hoek" "^9.0.0"
|
|
||||||
|
|
||||||
"@hapi/b64@5.x.x":
|
"@hapi/b64@5.x.x":
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d"
|
resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d"
|
||||||
@ -482,12 +475,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
|
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
|
||||||
integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==
|
integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==
|
||||||
|
|
||||||
"@hapi/call@^8.0.0":
|
"@hapi/call@^8.0.1":
|
||||||
version "8.0.0"
|
version "8.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.0.tgz#a5e456b611d848cf5a12662ef120da886041c54d"
|
resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0"
|
||||||
integrity sha512-4xHIWWqaIDQlVU88XAnomACSoC7iWUfaLfdu2T7I0y+HFFwZUrKKGfwn6ik4kwKsJRMnOliG3UXsF8V/94+Lkg==
|
integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/address" "4.x.x"
|
|
||||||
"@hapi/boom" "9.x.x"
|
"@hapi/boom" "9.x.x"
|
||||||
"@hapi/hoek" "9.x.x"
|
"@hapi/hoek" "9.x.x"
|
||||||
|
|
||||||
@ -1401,12 +1393,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
|
||||||
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
|
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.4":
|
"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
|
||||||
version "7.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
|
||||||
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
|
|
||||||
|
|
||||||
"@types/json-schema@^7.0.5":
|
|
||||||
version "7.0.7"
|
version "7.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
|
||||||
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
|
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
|
||||||
@ -1426,10 +1413,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
|
||||||
integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg==
|
integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg==
|
||||||
|
|
||||||
"@types/marked@^0.7.4":
|
"@types/marked@^2.0.3":
|
||||||
version "0.7.4"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.7.4.tgz#607685669bb1bbde2300bc58ba43486cbbee1f0a"
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-2.0.3.tgz#c8ea93684e530cc3b667d3e7226556dd0844ad1f"
|
||||||
integrity sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw==
|
integrity sha512-lbhSN1rht/tQ+dSWxawCzGgTfxe9DB31iLgiT1ZVT5lshpam/nyOA1m3tKHRoNPctB2ukSL22JZI5Fr+WI/zYg==
|
||||||
|
|
||||||
"@types/md5-file@^4.0.2":
|
"@types/md5-file@^4.0.2":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
@ -1629,10 +1616,10 @@
|
|||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-select@^3.0.13":
|
"@types/react-select@3.1.2":
|
||||||
version "3.0.13"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.0.13.tgz#b1a05eae0f65fb4f899b4db1f89b8420cb9f3656"
|
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.1.2.tgz#38627df4b49be9b28f800ed72b35d830369a624b"
|
||||||
integrity sha512-JxmSArGgzAOtb37+Jz2+3av8rVmp/3s3DGwlcP+g59/a3owkiuuU4/Jajd+qA32beDPHy4gJR2kkxagPY3j9kg==
|
integrity sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
"@types/react-dom" "*"
|
"@types/react-dom" "*"
|
||||||
@ -2333,7 +2320,7 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.5.5:
|
|||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ajv@^6.12.4:
|
ajv@^6.12.4, ajv@^6.12.5:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||||
@ -4900,9 +4887,9 @@ dns-equal@^1.0.0:
|
|||||||
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
|
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
|
||||||
|
|
||||||
dns-packet@^1.3.1:
|
dns-packet@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
|
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f"
|
||||||
integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
|
integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==
|
||||||
dependencies:
|
dependencies:
|
||||||
ip "^1.1.0"
|
ip "^1.1.0"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
@ -6044,13 +6031,13 @@ file-js@0.3.0:
|
|||||||
minimatch "^3.0.3"
|
minimatch "^3.0.3"
|
||||||
proper-lockfile "^1.2.0"
|
proper-lockfile "^1.2.0"
|
||||||
|
|
||||||
file-loader@^6.0.0:
|
file-loader@^6.2.0:
|
||||||
version "6.0.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f"
|
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
|
||||||
integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==
|
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
schema-utils "^2.6.5"
|
schema-utils "^3.0.0"
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@ -9716,14 +9703,13 @@ mini-create-react-context@^0.4.0:
|
|||||||
"@babel/runtime" "^7.5.5"
|
"@babel/runtime" "^7.5.5"
|
||||||
tiny-warning "^1.0.3"
|
tiny-warning "^1.0.3"
|
||||||
|
|
||||||
mini-css-extract-plugin@^0.9.0:
|
mini-css-extract-plugin@^1.6.0:
|
||||||
version "0.9.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
|
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.0.tgz#b4db2525af2624899ed64a23b0016e0036411893"
|
||||||
integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==
|
integrity sha512-nPFKI7NSy6uONUo9yn2hIfb9vyYvkFu95qki0e21DQ9uaqNKDP15DGpK0KnV6wDroWxPHtExrdEwx/yDQ8nVRw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loader-utils "^1.1.0"
|
loader-utils "^2.0.0"
|
||||||
normalize-url "1.9.1"
|
schema-utils "^3.0.0"
|
||||||
schema-utils "^1.0.0"
|
|
||||||
webpack-sources "^1.1.0"
|
webpack-sources "^1.1.0"
|
||||||
|
|
||||||
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
||||||
@ -10271,16 +10257,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||||
|
|
||||||
normalize-url@1.9.1:
|
|
||||||
version "1.9.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
|
|
||||||
integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
|
|
||||||
dependencies:
|
|
||||||
object-assign "^4.0.1"
|
|
||||||
prepend-http "^1.0.0"
|
|
||||||
query-string "^4.1.0"
|
|
||||||
sort-keys "^1.0.0"
|
|
||||||
|
|
||||||
normalize-url@2.0.1:
|
normalize-url@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
|
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
|
||||||
@ -11598,7 +11574,7 @@ prelude-ls@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
|
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
|
||||||
|
|
||||||
prepend-http@^1.0.0, prepend-http@^1.0.1:
|
prepend-http@^1.0.1:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||||
@ -11881,14 +11857,6 @@ qs@~6.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||||
|
|
||||||
query-string@^4.1.0:
|
|
||||||
version "4.3.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
|
||||||
integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
|
|
||||||
dependencies:
|
|
||||||
object-assign "^4.1.0"
|
|
||||||
strict-uri-encode "^1.0.0"
|
|
||||||
|
|
||||||
query-string@^5.0.1:
|
query-string@^5.0.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
|
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
|
||||||
@ -12083,10 +12051,10 @@ react-select-event@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@testing-library/dom" ">=7"
|
"@testing-library/dom" ">=7"
|
||||||
|
|
||||||
react-select@^3.1.0:
|
react-select@3.1.1:
|
||||||
version "3.1.0"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27"
|
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.1.tgz#156a5b4a6c22b1e3d62a919cb1fd827adb4060bc"
|
||||||
integrity sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==
|
integrity sha512-HjC6jT2BhUxbIbxMZWqVcDibrEpdUJCfGicN0MMV+BQyKtCaPTgFekKWiOizSCy4jdsLMGjLqcFGJMhVGWB0Dg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
"@emotion/cache" "^10.0.9"
|
"@emotion/cache" "^10.0.9"
|
||||||
@ -12834,7 +12802,7 @@ schema-utils@1.0.0, schema-utils@^1.0.0:
|
|||||||
ajv-errors "^1.0.0"
|
ajv-errors "^1.0.0"
|
||||||
ajv-keywords "^3.1.0"
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
schema-utils@^2.6.1, schema-utils@^2.7.0:
|
schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.7.0:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||||
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||||
@ -12843,14 +12811,14 @@ schema-utils@^2.6.1, schema-utils@^2.7.0:
|
|||||||
ajv "^6.12.4"
|
ajv "^6.12.4"
|
||||||
ajv-keywords "^3.5.2"
|
ajv-keywords "^3.5.2"
|
||||||
|
|
||||||
schema-utils@^2.6.5:
|
schema-utils@^3.0.0:
|
||||||
version "2.7.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
|
||||||
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
|
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.4"
|
"@types/json-schema" "^7.0.6"
|
||||||
ajv "^6.12.2"
|
ajv "^6.12.5"
|
||||||
ajv-keywords "^3.4.1"
|
ajv-keywords "^3.5.2"
|
||||||
|
|
||||||
scss-tokenizer@^0.2.3:
|
scss-tokenizer@^0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
@ -13249,13 +13217,6 @@ socks@~2.3.2:
|
|||||||
ip "1.1.5"
|
ip "1.1.5"
|
||||||
smart-buffer "^4.1.0"
|
smart-buffer "^4.1.0"
|
||||||
|
|
||||||
sort-keys@^1.0.0:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
|
||||||
integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
|
|
||||||
dependencies:
|
|
||||||
is-plain-obj "^1.0.0"
|
|
||||||
|
|
||||||
sort-keys@^2.0.0:
|
sort-keys@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
|
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
|
||||||
@ -13777,13 +13738,13 @@ strip-outer@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp "^1.0.2"
|
escape-string-regexp "^1.0.2"
|
||||||
|
|
||||||
style-loader@^1.2.1:
|
style-loader@^2.0.0:
|
||||||
version "1.3.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e"
|
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
|
||||||
integrity sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==
|
integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
schema-utils "^2.7.0"
|
schema-utils "^3.0.0"
|
||||||
|
|
||||||
stylus@^0.54.7:
|
stylus@^0.54.7:
|
||||||
version "0.54.8"
|
version "0.54.8"
|
||||||
@ -14413,6 +14374,11 @@ type-is@~1.6.17, type-is@~1.6.18:
|
|||||||
media-typer "0.3.0"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.24"
|
mime-types "~2.1.24"
|
||||||
|
|
||||||
|
typed-emitter@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-1.3.1.tgz#c98d71551a99d5f08ba9085ee44b8fc9b2357502"
|
||||||
|
integrity sha512-2h7utWyXgd2R2u2IuL8B4yu1gqMxbgUj2VS/MGVbFhEVQNJKXoQQoS5CBMh+eW31zFeSmDfEQ3qQf4xy5SlPVQ==
|
||||||
|
|
||||||
typedarray-to-buffer@^3.1.5:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
@ -15330,10 +15296,10 @@ ws@^6.1.0, ws@^6.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
async-limiter "~1.0.0"
|
async-limiter "~1.0.0"
|
||||||
|
|
||||||
ws@^7.2.3, ws@^7.3.0:
|
ws@^7.2.3, ws@^7.4.6:
|
||||||
version "7.3.0"
|
version "7.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||||
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
|
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
||||||
|
|
||||||
xdg-basedir@^3.0.0:
|
xdg-basedir@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user