1
0
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:
Alex Andreev 2021-06-07 10:11:24 +03:00
commit 4fbfdc485b
81 changed files with 1379 additions and 702 deletions

View File

@ -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`);
});
}

View File

@ -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",

View File

@ -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>
); );
} }

View File

@ -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",

View File

@ -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 |

View File

@ -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`.

View File

@ -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>
) )
} }

View File

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

View File

@ -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,

View File

@ -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;
} }
async upgrade(cluster: Store.Cluster): Promise<void> { export class ExampleFeature {
return this.install(cluster); protected stack: ResourceStack;
constructor(protected cluster: KubernetesCluster) {
this.stack = new ResourceStack(cluster, this.name);
} }
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> { install(): Promise<string> {
return this.stack.kubectlApplyFolder(path.join(__dirname, "../resources/"));
}
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) {
const {pods} = this.props;
return ( return (
<Component.TableRow key={index} nowrap> <TableRow key={index} nowrap>
<Component.TableCell className="podName">{pods[index].getName()}</Component.TableCell> <TableCell className="podName">{pods[index].getName()}</TableCell>
<Component.TableCell className="podAge">{pods[index].getAge()}</Component.TableCell> <TableCell className="podAge">{pods[index].getAge()}</TableCell>
<Component.TableCell className="podStatus">{pods[index].getStatus()}</Component.TableCell> <TableCell className="podStatus">{pods[index].getStatus()}</TableCell>
</Component.TableRow> </TableRow>
) )
} };
render() { render() {
const { pods } = this.props const { pods } = this.props
if (!pods?.length) { if (!pods?.length) {
return null; 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>
}
</Component.Table>
</div> </div>
) );
} }
} }
``` ```
@ -909,9 +992,9 @@ export class PodsDetailsList extends React.Component<Props> {
![DetailsWithPods](images/kubeobjectdetailitemwithpods.png) ![DetailsWithPods](images/kubeobjectdetailitemwithpods.png)
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.

View File

@ -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; }}

View File

@ -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

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -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: {

View File

@ -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)

View File

@ -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";
@ -36,6 +36,7 @@ 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;

View File

@ -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 });
};
}

View File

@ -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);

View File

@ -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,

View File

@ -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),
]; ];

View File

@ -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[] = [];

View File

@ -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,
}; };

View 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);
}

View File

@ -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";

View File

@ -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();

View File

@ -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";

View File

@ -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,
}); });

View File

@ -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);

View File

@ -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
};
} }
} }
} }

View File

@ -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;
@ -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 [];
} }
} }

View File

@ -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) {

View File

@ -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 });
}
} }
} }
@ -255,6 +261,7 @@ app.on("will-quit", (event) => {
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 });
}
}); });
/** /**

View File

@ -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");
}); });
}); });

View File

@ -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;
} }
/** /**

View File

@ -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) {

View 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";

View 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;
}
}

View File

@ -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);
});
}); });

View File

@ -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[] {

View File

@ -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 };
interface EndpointQuery {
all?: boolean;
} }
) => string;
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 {
@ -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) {

View File

@ -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);
} }

View File

@ -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(),

View File

@ -58,21 +58,19 @@ 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, [
reaction(() => this.props.release, release => {
if (!release) return; if (!release) return;
this.loadDetails(); this.loadDetails();
this.loadValues(); this.loadValues();
this.releaseSecret = null; this.releaseSecret = null;
} }),
); reaction(() => secretsStore.getItems(), () => {
@disposeOnUnmount
secretWatcher = reaction(() => secretsStore.getItems(), () => {
if (!this.props.release) return; if (!this.props.release) return;
const { getReleaseSecret } = releaseStore; const { getReleaseSecret } = releaseStore;
const { release } = this.props; const { release } = this.props;
@ -83,7 +81,12 @@ export class ReleaseDetails extends Component<Props> {
this.loadDetails(); this.loadDetails();
} }
this.releaseSecret = secret; this.releaseSecret = secret;
}); }),
reaction(() => this.showOnlyUserSuppliedValues, () => {
this.loadValues();
}),
]);
}
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -100,11 +103,16 @@ 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)) ?? "";
} catch (error) {
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
this.values = "";
} finally {
this.valuesLoading = false; this.valuesLoading = false;
} }
}
updateValues = async () => { updateValues = async () => {
const { release } = this.props; const { release } = this.props;
@ -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 />
: <AceEditor
mode="yaml" mode="yaml"
value={values} value={values}
onChange={values => this.values = values} onChange={text => this.values = text}
/> className={cssNames({ loading: valuesLoading })}
} readOnly={valuesLoading || this.showOnlyUserSuppliedValues}
>
{valuesLoading && <Spinner center />}
</AceEditor>
<Button <Button
primary primary
label="Save" label="Save"

View File

@ -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];

View File

@ -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 })
]); ]);

View File

@ -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);
} }
} }

View File

@ -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;
} }

View File

@ -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>

View File

@ -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: [

View File

@ -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

View File

@ -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}

View File

@ -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={[

View File

@ -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;

View File

@ -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}

View File

@ -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}

View File

@ -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(),

View File

@ -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(),

View File

@ -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(),

View File

@ -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"/>
);
}
}
}
]);

View File

@ -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;

View File

@ -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>
); );
} }

View File

@ -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 }) => {

View File

@ -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;

View File

@ -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,9 +58,14 @@ 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;
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); await autoCleanOnRemove(clusterId, iframe);
} }
}
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
await when(() => { await when(() => {
@ -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}`);
});
}); });
} }

View File

@ -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;

View File

@ -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));
} }
@ -397,19 +390,25 @@ 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/>} <>
{showNamespaceSelectFilter && <NamespaceSelectFilter />}
<PageFiltersSelect allowEmpty disableFilters={{ <PageFiltersSelect allowEmpty disableFilters={{
[FilterType.NAMESPACE]: true, // namespace-select used instead [FilterType.NAMESPACE]: true, // namespace-select used instead
}} /> }} />
</>, </>
),
search: <SearchInputUrl />, search: <SearchInputUrl />,
}; };
let header = this.renderHeaderContent(placeholders); let header = this.renderHeaderContent(placeholders);

View File

@ -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,

View File

@ -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"

View File

@ -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);

View File

@ -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", () => {

View File

@ -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);
}
}

View 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
View File

@ -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"