mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' of github.com:lensapp/lens into toggle-auto-update-4.1
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
commit
e58e18efe6
9
.github/release-drafter.yml
vendored
9
.github/release-drafter.yml
vendored
@ -10,8 +10,9 @@ categories:
|
|||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
labels:
|
labels:
|
||||||
- 'chore'
|
- 'chore'
|
||||||
- 'area/ci
|
- 'area/ci'
|
||||||
- 'area/tests'
|
- 'area/tests'
|
||||||
|
- 'dependencies'
|
||||||
|
|
||||||
template: |
|
template: |
|
||||||
## Changes since $PREVIOUS_TAG
|
## Changes since $PREVIOUS_TAG
|
||||||
@ -20,8 +21,10 @@ template: |
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
- [Lens v$RESOLVED_VERSION - Linux](https://snapcraft.io/kontena-lens)
|
- Lens v$RESOLVED_VERSION - Linux
|
||||||
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.AppImage)
|
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
|
||||||
|
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
|
||||||
|
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
|
||||||
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
||||||
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
|
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
|
||||||
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
||||||
|
|||||||
@ -40,7 +40,7 @@ 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 { LensMainExtension, windowManager } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends LensMainExtension {
|
||||||
@ -92,7 +92,7 @@ export default class ExampleMainExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom global pages (views) to Lens's main window. The global page is a full-screen page that hides all other content from a window.
|
This extension can register custom global pages (views) to Lens's main window. The global page is a full-screen page that hides all other content from a window.
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component, LensRendererExtension } from "@k8slens/extensions"
|
import { Component, LensRendererExtension } from "@k8slens/extensions"
|
||||||
import { ExamplePage } from "./src/example-page"
|
import { ExamplePage } from "./src/example-page"
|
||||||
@ -123,7 +123,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom app preferences. It is responsible for storing a state for custom preferences.
|
This extension can register custom app preferences. 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 { LensRendererExtension } from "@k8slens/extensions"
|
||||||
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
||||||
@ -147,7 +147,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom cluster pages. These pages are visible in a cluster menu when a cluster is opened.
|
This extension can register custom cluster pages. 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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./src/page"
|
import { ExampleIcon, ExamplePage } from "./src/page"
|
||||||
@ -180,7 +180,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register installable features for a cluster. These features are visible in the "Cluster Settings" page.
|
This extension can register installable features for a cluster. 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 { LensRendererExtension } from "@k8slens/extensions"
|
||||||
import { MyCustomFeature } from "./src/my-custom-feature"
|
import { MyCustomFeature } from "./src/my-custom-feature"
|
||||||
@ -209,18 +209,20 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom icons and text to a status bar area.
|
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 { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends LensRendererExtension {
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
item: (
|
components: {
|
||||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
Item: (
|
||||||
<Component.Icon material="favorite" />
|
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||||
</div>
|
<Component.Icon material="favorite" />
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -231,7 +233,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions.
|
This extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions.
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
||||||
@ -254,7 +256,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
This extension can register custom details (content) for specified Kubernetes kinds/apiVersions.
|
This extension can register custom details (content) for specified Kubernetes kinds/apiVersions.
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
||||||
|
|||||||
@ -42,7 +42,7 @@ Next, you'll try changing the way the new menu item appears in the UI. You'll ch
|
|||||||
|
|
||||||
Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`:
|
Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`:
|
||||||
|
|
||||||
```tsx
|
```typescript
|
||||||
clusterPageMenus = [
|
clusterPageMenus = [
|
||||||
{
|
{
|
||||||
target: { pageId: "hello" },
|
target: { pageId: "hello" },
|
||||||
|
|||||||
@ -22,7 +22,7 @@ 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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensRendererExtension {
|
export default class ExampleExtensionMain extends LensRendererExtension {
|
||||||
@ -52,7 +52,7 @@ Cluster pages appear in the cluster dashboard. Use cluster pages to display info
|
|||||||
|
|
||||||
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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
@ -77,7 +77,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
`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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ The above example shows how to create a cluster page, but not how to make that p
|
|||||||
|
|
||||||
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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
@ -140,7 +140,7 @@ The above example creates a menu item that reads **Hello World**. When users cli
|
|||||||
|
|
||||||
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 { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ Lens includes various built-in components available for extension developers to
|
|||||||
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. The following example groups two sub menu items under one parent menu item:
|
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. The following example groups two sub menu items under one parent menu item:
|
||||||
|
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
@ -216,12 +216,20 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above defines two cluster pages and three cluster page menu objects. The three cluster page menu objects include one parent menu item and two sub menu items. Parent items require an `id` value, whereas sub items require a `parentId` value. The value of the sub item `parentId` will match the value of the corresponding parent item `id`. Parent items don't require a `target` value. Assign values to the remaining properties as explained above.
|
The above defines two cluster pages and three cluster page menu objects.
|
||||||
|
The cluster page definitions are straightforward.
|
||||||
|
The three cluster page menu objects include one parent menu item and two sub menu items.
|
||||||
|
The first cluster page menu object defines the parent of a foldout submenu.
|
||||||
|
Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu.
|
||||||
|
Also note that the `target` field is not specified (it is ignored if the `id` field is specified).
|
||||||
|
This cluster page menu object specifies the `title` and `components` fields, which are used in displaying the menu item in the cluster dashboard sidebar.
|
||||||
|
Initially the submenu is hidden.
|
||||||
|
Activating this menu item toggles on and off the appearance of the submenu below it.
|
||||||
|
The remaining two cluster page menu objects define the contents of the submenu.
|
||||||
|
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case.
|
||||||
|
|
||||||
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### `globalPages`
|
### `globalPages`
|
||||||
|
|
||||||
Global pages are independent of the cluster dashboard and can fill the entire Lens UI. Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
|
Global pages are independent of the cluster dashboard and can fill the entire Lens UI. Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
|
||||||
@ -230,7 +238,7 @@ Typically, you would use a [global page menu](#globalpagemenus) located in the l
|
|||||||
|
|
||||||
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 { LensRendererExtension } from '@k8slens/extensions';
|
||||||
import { HelpPage } from './page';
|
import { HelpPage } from './page';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -255,7 +263,7 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
`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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
@ -284,7 +292,7 @@ This example code shows how to create a global page, but not how to make that pa
|
|||||||
|
|
||||||
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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
@ -322,7 +330,7 @@ The above example creates a "Help" icon menu item. When users click the icon, th
|
|||||||
|
|
||||||
This example requires the definition of another React-based component, `HelpIcon`. Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
This example requires the definition of another React-based component, `HelpIcon`. Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
@ -359,7 +367,7 @@ 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 { LensRendererExtension } from "@k8slens/extensions"
|
||||||
import { ExampleFeature } from "./src/example-feature"
|
import { ExampleFeature } from "./src/example-feature"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
@ -388,7 +396,7 @@ The properties of the `clusterFeatures` array objects are defined as follows:
|
|||||||
* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section.
|
* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section.
|
||||||
* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods:
|
* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods:
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
abstract install(cluster: Cluster): Promise<void>;
|
abstract install(cluster: Cluster): Promise<void>;
|
||||||
abstract upgrade(cluster: Cluster): Promise<void>;
|
abstract upgrade(cluster: Cluster): Promise<void>;
|
||||||
abstract uninstall(cluster: Cluster): Promise<void>;
|
abstract uninstall(cluster: Cluster): Promise<void>;
|
||||||
@ -397,13 +405,19 @@ The properties of the `clusterFeatures` array objects are defined as follows:
|
|||||||
|
|
||||||
The four methods listed above are defined as follows:
|
The four methods listed above are defined as follows:
|
||||||
|
|
||||||
* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../api/README.md). This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page).
|
* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../api/README.md).
|
||||||
|
This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature. This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page).
|
* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature.
|
||||||
|
This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../api/README.md). This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page).
|
* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../api/README.md).
|
||||||
|
This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class. Lens periodically calls this method to determine details about the feature's current status. Consider using the following properties with `updateStatus()`:
|
* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class.
|
||||||
|
Lens periodically calls this method to determine details about the feature's current status.
|
||||||
|
The implementation of this method should uninstall Kubernetes resources using the Kubernetes api (`K8sApi`)
|
||||||
|
Consider using the following properties with `updateStatus()`:
|
||||||
|
|
||||||
* `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description.
|
* `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description.
|
||||||
|
|
||||||
@ -413,7 +427,7 @@ The four methods listed above are defined as follows:
|
|||||||
|
|
||||||
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 { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
@ -487,7 +501,7 @@ The Lens **Preferences** page is a built-in global page. You can use Lens extens
|
|||||||
|
|
||||||
The following example demonstrates adding a custom preference:
|
The following example demonstrates adding a custom preference:
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } 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";
|
||||||
@ -524,7 +538,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
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 { Component } from "@k8slens/extensions";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -580,7 +594,7 @@ The status bar is the blue strip along the bottom of the Lens UI. `statusBarItem
|
|||||||
|
|
||||||
The following example adds a `statusBarItems` definition and a `globalPages` definition to a `LensRendererExtension` subclass. It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
The following example adds a `statusBarItems` definition and a `globalPages` definition to a `LensRendererExtension` subclass. It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import { LensRendererExtension } from '@k8slens/extensions';
|
import { LensRendererExtension } from '@k8slens/extensions';
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -597,15 +611,17 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
item: (
|
components: {
|
||||||
<div
|
Item: (
|
||||||
className="flex align-center gaps"
|
<div
|
||||||
onClick={() => this.navigate("help")}
|
className="flex align-center gaps"
|
||||||
>
|
onClick={() => this.navigate("help")}
|
||||||
<HelpIcon />
|
>
|
||||||
My Status Bar Item
|
<HelpIcon />
|
||||||
</div>
|
My Status Bar Item
|
||||||
),
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -613,7 +629,7 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
The properties of the `statusBarItems` array objects are defined as follows:
|
The properties of the `statusBarItems` array objects are defined as follows:
|
||||||
|
|
||||||
* `item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
* `Item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
||||||
* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages.
|
* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages.
|
||||||
|
|
||||||
### `kubeObjectMenuItems`
|
### `kubeObjectMenuItems`
|
||||||
@ -629,7 +645,7 @@ They also appear on the title bar of the details page for specific resources:
|
|||||||
|
|
||||||
The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action:
|
The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action:
|
||||||
|
|
||||||
``` typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
||||||
@ -702,7 +718,7 @@ 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 { LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { NamespaceDetailsItem } from "./src/namespace-details-item"
|
import { NamespaceDetailsItem } from "./src/namespace-details-item"
|
||||||
@ -757,9 +773,20 @@ 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`. You can query this object for many details about the current namespace. In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method. Use the namespace's name to limit the list of pods only to those in the relevant namespace. To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method. The `K8sApi.podsApi` is automatically configured for the active cluster.
|
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
||||||
|
You can query this object for many details about the current namespace.
|
||||||
|
In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method.
|
||||||
|
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
||||||
|
To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method.
|
||||||
|
The `K8sApi.podsApi` is automatically configured for the active cluster.
|
||||||
|
|
||||||
Note that `K8sApi.podsApi.list()` is an asynchronous method. Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`. It is a common technique in React development to await async calls in `componentDidMount()`. However, `componentDidMount()` is called right after the first call to `render()`. In order to effect a subsequent `render()` call, React must be made aware of a state change. Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates. This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
Note that `K8sApi.podsApi.list()` is an asynchronous method.
|
||||||
|
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
||||||
|
It is a common technique in React development to await async calls in `componentDidMount()`.
|
||||||
|
However, `componentDidMount()` is called right after the first call to `render()`.
|
||||||
|
In order to effect a subsequent `render()` call, React must be made aware of a state change.
|
||||||
|
Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates.
|
||||||
|
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
||||||
|
|
||||||
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
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 `Component.DrawerTitle` provides a separator from details above this one.
|
||||||
|
|||||||
@ -10,7 +10,7 @@ For example, I have a component `GlobalPageMenuIcon` and want to test if `props.
|
|||||||
|
|
||||||
My component `GlobalPageMenuIcon`
|
My component `GlobalPageMenuIcon`
|
||||||
|
|
||||||
```tsx
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component: { Icon } } from "@k8slens/extensions";
|
import { Component: { Icon } } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
|||||||
9
extensions/survey/main.ts
Normal file
9
extensions/survey/main.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { LensMainExtension } from "@k8slens/extensions";
|
||||||
|
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||||
|
|
||||||
|
export default class SurveyMainExtension extends LensMainExtension {
|
||||||
|
|
||||||
|
async onActivate() {
|
||||||
|
await surveyPreferencesStore.loadExtension(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
7928
extensions/survey/package-lock.json
generated
Normal file
7928
extensions/survey/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
extensions/survey/package.json
Normal file
28
extensions/survey/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-survey",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Lens survey",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"renderer": "dist/renderer.js",
|
||||||
|
"lens": {
|
||||||
|
"metadata": {},
|
||||||
|
"styles": []
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p",
|
||||||
|
"dev": "webpack --watch",
|
||||||
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
|
"got": "^11.8.1",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"node-machine-id": "^1.1.12",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"refiner-js": "^1.0.1",
|
||||||
|
"ts-loader": "^8.0.4",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"webpack": "^4.44.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
extensions/survey/renderer.tsx
Normal file
21
extensions/survey/renderer.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import { survey } from "./src/survey";
|
||||||
|
import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference";
|
||||||
|
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class SurveyRendererExtension extends LensRendererExtension {
|
||||||
|
appPreferences = [
|
||||||
|
{
|
||||||
|
title: "In-App Surveys",
|
||||||
|
components: {
|
||||||
|
Hint: () => <SurveyPreferenceHint/>,
|
||||||
|
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
async onActivate() {
|
||||||
|
await surveyPreferencesStore.loadExtension(this);
|
||||||
|
survey.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module "refiner-js" {
|
||||||
|
export default function Refiner(key: string, value: string|object|number|Boolean|Array): void;
|
||||||
|
}
|
||||||
27
extensions/survey/src/survey-preference.tsx
Normal file
27
extensions/survey/src/survey-preference.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from "@k8slens/extensions";
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { SurveyPreferencesStore } from "./survey-preferences-store";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> {
|
||||||
|
render() {
|
||||||
|
const { survey } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component.Checkbox
|
||||||
|
label="Allow in-app surveys"
|
||||||
|
value={survey.enabled}
|
||||||
|
onChange={v => survey.enabled = v }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SurveyPreferenceHint extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span>This will allow you to participate in surveys to improve the Lens experience.</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
extensions/survey/src/survey-preferences-store.ts
Normal file
36
extensions/survey/src/survey-preferences-store.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Store } from "@k8slens/extensions";
|
||||||
|
import { observable, toJS, when } from "mobx";
|
||||||
|
|
||||||
|
export type SurveyPreferencesModel = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SurveyPreferencesStore extends Store.ExtensionStore<SurveyPreferencesModel> {
|
||||||
|
|
||||||
|
@observable enabled = true;
|
||||||
|
|
||||||
|
whenEnabled = when(() => this.enabled);
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super({
|
||||||
|
configName: "preferences-store",
|
||||||
|
defaults: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fromStore({ enabled }: SurveyPreferencesModel): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): SurveyPreferencesModel {
|
||||||
|
return toJS({
|
||||||
|
enabled: this.enabled
|
||||||
|
}, {
|
||||||
|
recurseEverything: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const surveyPreferencesStore = SurveyPreferencesStore.getInstance<SurveyPreferencesStore>();
|
||||||
46
extensions/survey/src/survey.ts
Normal file
46
extensions/survey/src/survey.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Util } from "@k8slens/extensions";
|
||||||
|
import { machineId } from "node-machine-id";
|
||||||
|
import Refiner from "refiner-js";
|
||||||
|
import got from "got";
|
||||||
|
import { surveyPreferencesStore } from "./survey-preferences-store";
|
||||||
|
|
||||||
|
type SurveyIdResponse = {
|
||||||
|
surveyId: string;
|
||||||
|
};
|
||||||
|
export class Survey extends Util.Singleton {
|
||||||
|
static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43";
|
||||||
|
protected anonymousId: string;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await surveyPreferencesStore.whenEnabled;
|
||||||
|
|
||||||
|
const surveyId = await this.fetchSurveyId();
|
||||||
|
|
||||||
|
if (surveyId) {
|
||||||
|
Refiner("setProject", Survey.PROJECT_ID);
|
||||||
|
Refiner("identifyUser", {
|
||||||
|
id: surveyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSurveyId() {
|
||||||
|
try {
|
||||||
|
const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev";
|
||||||
|
const anonymousId = await machineId();
|
||||||
|
const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"});
|
||||||
|
|
||||||
|
return (body as SurveyIdResponse).surveyId;
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const survey = Survey.getInstance<Survey>();
|
||||||
29
extensions/survey/tsconfig.json
Normal file
29
extensions/survey/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"sourceMap": false,
|
||||||
|
"declaration": false,
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"node_modules/*",
|
||||||
|
"../../types/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"renderer.ts",
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
67
extensions/survey/webpack.config.js
Normal file
67
extensions/survey/webpack.config.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
entry: "./main.ts",
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-main",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
"@k8slens/extensions": "var global.LensExtensions",
|
||||||
|
"react": "var global.React",
|
||||||
|
"mobx": "var global.Mobx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [ ".tsx", ".ts", ".js" ],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: "main.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: "./renderer.tsx",
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-renderer",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
"@k8slens/extensions": "var global.LensExtensions",
|
||||||
|
"react": "var global.React",
|
||||||
|
"mobx": "var global.Mobx",
|
||||||
|
"mobx-react": "var global.MobxReact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [ ".tsx", ".ts", ".js" ],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: "renderer.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -4,7 +4,7 @@ import { exec } from "child_process";
|
|||||||
|
|
||||||
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||||
"win32": "./dist/win-unpacked/Lens.exe",
|
"win32": "./dist/win-unpacked/Lens.exe",
|
||||||
"linux": "./dist/linux-unpacked/lens",
|
"linux": "./dist/linux-unpacked/kontena-lens",
|
||||||
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
16
package.json
16
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.1.0-alpha.1",
|
"version": "4.1.0-alpha.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -103,7 +103,6 @@
|
|||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"executableName": "lens",
|
|
||||||
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
||||||
"target": [
|
"target": [
|
||||||
"deb",
|
"deb",
|
||||||
@ -162,16 +161,16 @@
|
|||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true
|
"allowToChangeInstallationDirectory": true
|
||||||
},
|
},
|
||||||
|
"snap": {
|
||||||
|
"confinement": "classic"
|
||||||
|
},
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"repo": "lens",
|
"repo": "lens",
|
||||||
"owner": "lensapp"
|
"owner": "lensapp"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"snap": {
|
|
||||||
"confinement": "classic"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"lens": {
|
"lens": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@ -180,7 +179,8 @@
|
|||||||
"node-menu",
|
"node-menu",
|
||||||
"metrics-cluster-feature",
|
"metrics-cluster-feature",
|
||||||
"license-menu-item",
|
"license-menu-item",
|
||||||
"kube-object-event-status"
|
"kube-object-event-status",
|
||||||
|
"survey"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -292,7 +292,7 @@
|
|||||||
"@types/webpack-dev-server": "^3.11.1",
|
"@types/webpack-dev-server": "^3.11.1",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/webpack-node-externals": "^1.7.1",
|
"@types/webpack-node-externals": "^1.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.12.0",
|
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||||
"@typescript-eslint/parser": "^4.0.0",
|
"@typescript-eslint/parser": "^4.0.0",
|
||||||
"ace-builds": "^1.4.11",
|
"ace-builds": "^1.4.11",
|
||||||
"ansi_up": "^4.0.4",
|
"ansi_up": "^4.0.4",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import requestPromise from "request-promise-native";
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
|
|
||||||
export function getAppVersion(): string {
|
export function getAppVersion(): string {
|
||||||
@ -11,3 +12,13 @@ export function getBundledKubectlVersion(): string {
|
|||||||
export function getBundledExtensions(): string[] {
|
export function getBundledExtensions(): string[] {
|
||||||
return packageInfo.lens?.extensions || [];
|
return packageInfo.lens?.extensions || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
|
||||||
|
const response = await requestPromise({
|
||||||
|
method: "GET",
|
||||||
|
uri: `http://localhost:${proxyPort}/version`,
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(response.body).version;
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface StatusBarRegistration {
|
interface StatusBarComponents {
|
||||||
|
Item?: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusBarRegistrationV2 {
|
||||||
|
components: StatusBarComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusBarRegistration extends StatusBarRegistrationV2 {
|
||||||
|
/**
|
||||||
|
* @deprecated use components.Item instead
|
||||||
|
*/
|
||||||
item?: React.ReactNode;
|
item?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,7 @@ describe("create clusters", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
||||||
|
jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true));
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
|
|||||||
@ -64,6 +64,8 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
|||||||
|
|
||||||
export async function checkForUpdates(): Promise<void> {
|
export async function checkForUpdates(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
logger.info(`📡 Checking for app updates`);
|
||||||
|
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
|
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export interface ClusterState {
|
|||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
|
isGlobalWatchEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +92,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable initializing = false;
|
@observable initializing = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initialized
|
* Is cluster object initialized
|
||||||
*
|
*
|
||||||
@ -177,6 +177,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
|
/**
|
||||||
|
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable isGlobalWatchEnabled = false;
|
||||||
/**
|
/**
|
||||||
* Preferences
|
* Preferences
|
||||||
*
|
*
|
||||||
@ -353,9 +359,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAccessibility();
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
|
||||||
this.ready = true;
|
|
||||||
this.ensureKubectl();
|
this.ensureKubectl();
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
@ -410,13 +414,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
await this.refreshAccessibility();
|
||||||
await this.refreshAllowedResources();
|
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
if (opts.refreshMetadata) {
|
||||||
this.refreshMetadata();
|
this.refreshMetadata();
|
||||||
}
|
}
|
||||||
this.ready = true;
|
|
||||||
}
|
}
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
@ -433,6 +435,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private async refreshAccessibility(): Promise<void> {
|
||||||
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
|
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
|
||||||
|
|
||||||
|
await this.refreshAllowedResources();
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -571,6 +585,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
|
||||||
|
return this.canI({
|
||||||
|
verb: "watch",
|
||||||
|
resource: "*",
|
||||||
|
...customizeResource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(): ClusterModel {
|
toJSON(): ClusterModel {
|
||||||
const model: ClusterModel = {
|
const model: ClusterModel = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -604,6 +629,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
|
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs Electron developer tools in the development build.
|
* Installs Electron developer tools in the development build.
|
||||||
* The dependency is not bundled to the production build.
|
* The dependency is not bundled to the production build.
|
||||||
*/
|
*/
|
||||||
export const installDeveloperTools = async () => {
|
export const installDeveloperTools = async () => {
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
logger.info("🤓 Installing developer tools");
|
||||||
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
||||||
|
|
||||||
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension-
|
|||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { filesystemProvisionerStore } from "./extension-filesystem";
|
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||||
import { bindBroadcastHandlers } from "../common/ipc";
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
import { startUpdateChecking } from "./app-updater";
|
import { startUpdateChecking } from "./app-updater";
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ if (process.env.LENS_DISABLE_GPU) {
|
|||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
||||||
|
logger.info("🐚 Syncing shell environment");
|
||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
bindBroadcastHandlers();
|
bindBroadcastHandlers();
|
||||||
@ -74,6 +76,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
await installDeveloperTools();
|
await installDeveloperTools();
|
||||||
|
|
||||||
|
logger.info("💾 Loading stores");
|
||||||
// preload
|
// preload
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
@ -85,6 +88,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
// find free port
|
// find free port
|
||||||
try {
|
try {
|
||||||
|
logger.info("🔑 Getting free port for LensProxy server");
|
||||||
proxyPort = await getFreePort();
|
proxyPort = await getFreePort();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
@ -97,6 +101,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
// run proxy
|
// run proxy
|
||||||
try {
|
try {
|
||||||
|
logger.info("🔌 Starting LensProxy");
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
proxyServer = LensProxy.create(proxyPort, clusterManager);
|
proxyServer = LensProxy.create(proxyPort, clusterManager);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -105,11 +110,28 @@ app.on("ready", async () => {
|
|||||||
app.exit();
|
app.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test proxy connection
|
||||||
|
try {
|
||||||
|
logger.info("🔎 Testing LensProxy connection ...");
|
||||||
|
const versionFromProxy = await getAppVersionFromProxyServer(proxyPort);
|
||||||
|
|
||||||
|
if (getAppVersion() !== versionFromProxy) {
|
||||||
|
logger.error(`Proxy server responded with invalid response`);
|
||||||
|
}
|
||||||
|
logger.info("⚡ LensProxy connection OK");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Checking proxy server connection failed", error);
|
||||||
|
}
|
||||||
|
|
||||||
extensionLoader.init();
|
extensionLoader.init();
|
||||||
extensionDiscovery.init();
|
extensionDiscovery.init();
|
||||||
|
|
||||||
|
logger.info("🖥️ Starting WindowManager");
|
||||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||||
windowManager.whenLoaded.then(() => startUpdateChecking());
|
windowManager.whenLoaded.then(() => startUpdateChecking());
|
||||||
|
|
||||||
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
// call after windowManager to see splash earlier
|
||||||
try {
|
try {
|
||||||
const extensions = await extensionDiscovery.load();
|
const extensions = await extensionDiscovery.load();
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class LensProxy {
|
|||||||
|
|
||||||
listen(port = this.port): this {
|
listen(port = this.port): this {
|
||||||
this.proxyServer = this.buildCustomProxy().listen(port);
|
this.proxyServer = this.buildCustomProxy().listen(port);
|
||||||
logger.info(`LensProxy server has started at ${this.origin}`);
|
logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import path from "path";
|
|||||||
import { readFile } from "fs-extra";
|
import { readFile } from "fs-extra";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute, versionRoute } from "./routes";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
@ -143,6 +143,7 @@ export class Router {
|
|||||||
this.handleStaticFile(params.path, response, req);
|
this.handleStaticFile(params.path, response, req);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
|
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
|
||||||
|
|
||||||
// Watch API
|
// Watch API
|
||||||
|
|||||||
@ -4,3 +4,4 @@ export * from "./port-forward-route";
|
|||||||
export * from "./watch-route";
|
export * from "./watch-route";
|
||||||
export * from "./helm-route";
|
export * from "./helm-route";
|
||||||
export * from "./resource-applier-route";
|
export * from "./resource-applier-route";
|
||||||
|
export * from "./version-route";
|
||||||
|
|||||||
13
src/main/routes/version-route.ts
Normal file
13
src/main/routes/version-route.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { LensApiRequest } from "../router";
|
||||||
|
import { LensApi } from "../lens-api";
|
||||||
|
import { getAppVersion } from "../../common/utils";
|
||||||
|
|
||||||
|
class VersionRoute extends LensApi {
|
||||||
|
public async getVersion(request: LensApiRequest) {
|
||||||
|
const { response } = request;
|
||||||
|
|
||||||
|
this.respondJson(response, { version: getAppVersion()}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versionRoute = new VersionRoute();
|
||||||
@ -62,16 +62,6 @@ function buildTray(icon: string | NativeImage, menu: Menu, windowManager: Window
|
|||||||
|
|
||||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
function createTrayMenu(windowManager: WindowManager): Menu {
|
||||||
return Menu.buildFromTemplate([
|
return Menu.buildFromTemplate([
|
||||||
{
|
|
||||||
label: "About Lens",
|
|
||||||
async click() {
|
|
||||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
|
||||||
const browserWindow = await windowManager.ensureMainWindow();
|
|
||||||
|
|
||||||
showAbout(browserWindow);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
{
|
||||||
label: "Open Lens",
|
label: "Open Lens",
|
||||||
async click() {
|
async click() {
|
||||||
@ -116,6 +106,15 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
await windowManager.ensureMainWindow();
|
await windowManager.ensureMainWindow();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "About Lens",
|
||||||
|
async click() {
|
||||||
|
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||||
|
const browserWindow = await windowManager.ensureMainWindow();
|
||||||
|
|
||||||
|
showAbout(browserWindow);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Quit App",
|
label: "Quit App",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { initMenu } from "./menu";
|
|||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export class WindowManager extends Singleton {
|
export class WindowManager extends Singleton {
|
||||||
protected mainWindow: BrowserWindow;
|
protected mainWindow: BrowserWindow;
|
||||||
@ -84,10 +85,19 @@ export class WindowManager extends Singleton {
|
|||||||
this.splashWindow = null;
|
this.splashWindow = null;
|
||||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => {
|
||||||
|
logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.on("did-finish-load", () => {
|
||||||
|
logger.info("[WINDOW-MANAGER]: Main window loaded");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (showSplash) await this.showSplash();
|
if (showSplash) await this.showSplash();
|
||||||
|
logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainUrl} ...`);
|
||||||
await this.mainWindow.loadURL(this.mainUrl);
|
await this.mainWindow.loadURL(this.mainUrl);
|
||||||
this.mainWindow.show();
|
this.mainWindow.show();
|
||||||
this.splashWindow?.close();
|
this.splashWindow?.close();
|
||||||
|
|||||||
@ -5,12 +5,11 @@ import type { Cluster } from "../../main/cluster";
|
|||||||
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
import type { NamespaceStore } from "../components/+namespaces/namespace.store";
|
|
||||||
|
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { comparer, computed, observable, reaction } from "mobx";
|
import { autorun, comparer, computed, IReactionDisposer, observable, reaction } from "mobx";
|
||||||
import { autobind, EventEmitter } from "../utils";
|
import { autobind, EventEmitter, noop } from "../utils";
|
||||||
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
||||||
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
||||||
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
|
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
|
||||||
@ -19,6 +18,7 @@ import { apiManager } from "./api-manager";
|
|||||||
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
|
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
|
||||||
|
|
||||||
export interface IKubeWatchMessage<T extends KubeObject = any> {
|
export interface IKubeWatchMessage<T extends KubeObject = any> {
|
||||||
|
namespace?: string;
|
||||||
data?: IKubeWatchEvent<KubeJsonApiData>
|
data?: IKubeWatchEvent<KubeJsonApiData>
|
||||||
error?: IKubeWatchEvent<KubeJsonApiError>;
|
error?: IKubeWatchEvent<KubeJsonApiError>;
|
||||||
api?: KubeApi<T>;
|
api?: KubeApi<T>;
|
||||||
@ -28,7 +28,7 @@ export interface IKubeWatchMessage<T extends KubeObject = any> {
|
|||||||
export interface IKubeWatchSubscribeStoreOptions {
|
export interface IKubeWatchSubscribeStoreOptions {
|
||||||
preload?: boolean; // preload store items, default: true
|
preload?: boolean; // preload store items, default: true
|
||||||
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
||||||
cacheLoading?: boolean; // when enabled loading store will be skipped, default: false
|
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchReconnectOptions {
|
export interface IKubeWatchReconnectOptions {
|
||||||
@ -43,50 +43,50 @@ export interface IKubeWatchLog {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
private cluster: Cluster;
|
|
||||||
private namespaceStore: NamespaceStore;
|
|
||||||
|
|
||||||
private requestId = 0;
|
private requestId = 0;
|
||||||
private isConnected = false;
|
|
||||||
private reader: ReadableStreamReader<string>;
|
private reader: ReadableStreamReader<string>;
|
||||||
private subscribers = observable.map<KubeApi, number>();
|
|
||||||
|
|
||||||
// events
|
|
||||||
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
||||||
|
|
||||||
|
@observable.ref private cluster: Cluster;
|
||||||
|
@observable.ref private namespaces: string[] = [];
|
||||||
|
@observable subscribers = observable.map<KubeApi, number>();
|
||||||
|
@observable isConnected = false;
|
||||||
|
|
||||||
|
@computed get isReady(): boolean {
|
||||||
|
return Boolean(this.cluster && this.namespaces);
|
||||||
|
}
|
||||||
|
|
||||||
@computed get isActive(): boolean {
|
@computed get isActive(): boolean {
|
||||||
return this.apis.length > 0;
|
return this.apis.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get apis(): string[] {
|
@computed get apis(): string[] {
|
||||||
const { cluster, namespaceStore } = this;
|
if (!this.isReady) {
|
||||||
const activeApis = Array.from(this.subscribers.keys());
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return activeApis.map(api => {
|
return Array.from(this.subscribers.keys()).map(api => {
|
||||||
if (!cluster.isAllowedResource(api.kind)) {
|
if (!this.isAllowedApi(api)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (api.isNamespaced) {
|
// TODO: optimize - check when all namespaces are selected and then request all in one
|
||||||
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
if (api.isNamespaced && !this.cluster.isGlobalWatchEnabled) {
|
||||||
} else {
|
return this.namespaces.map(namespace => api.getWatchUrl(namespace));
|
||||||
return api.getWatchUrl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return api.getWatchUrl();
|
||||||
}).flat();
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
async init({ getCluster, getNamespaces }: {
|
||||||
this.init();
|
getCluster: () => Cluster,
|
||||||
}
|
getNamespaces: () => string[],
|
||||||
|
}): Promise<void> {
|
||||||
private async init() {
|
autorun(() => {
|
||||||
const { getHostedCluster } = await import("../../common/cluster-store");
|
this.cluster = getCluster();
|
||||||
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
|
this.namespaces = getNamespaces();
|
||||||
|
});
|
||||||
await namespaceStore.whenReady;
|
|
||||||
|
|
||||||
this.cluster = getHostedCluster();
|
|
||||||
this.namespaceStore = namespaceStore;
|
|
||||||
this.bindAutoConnect();
|
this.bindAutoConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAllowedApi(api: KubeApi): boolean {
|
isAllowedApi(api: KubeApi): boolean {
|
||||||
return !!this?.cluster.isAllowedResource(api.kind);
|
return Boolean(this?.cluster.isAllowedResource(api.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
||||||
@ -129,45 +129,66 @@ export class KubeWatchApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
preloadStores(stores: KubeObjectStore[], { loadOnce = false } = {}) {
|
||||||
const { preload = true, waitUntilLoaded = true, cacheLoading = false } = options;
|
|
||||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
const preloading: Promise<any>[] = [];
|
const preloading: Promise<any>[] = [];
|
||||||
|
|
||||||
|
for (const store of stores) {
|
||||||
|
preloading.push(limitRequests(async () => {
|
||||||
|
if (store.isLoaded && loadOnce) return; // skip
|
||||||
|
|
||||||
|
return store.loadAll(this.namespaces);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: Promise.allSettled(preloading),
|
||||||
|
cancelLoading: () => limitRequests.clearQueue(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
||||||
|
const { preload = true, waitUntilLoaded = true, loadOnce = false } = options;
|
||||||
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
|
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
|
||||||
const unsubscribeList: (() => void)[] = [];
|
const unsubscribeList: (() => void)[] = [];
|
||||||
let isUnsubscribed = false;
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
|
const load = () => this.preloadStores(stores, { loadOnce });
|
||||||
|
let preloading = preload && load();
|
||||||
|
let cancelReloading: IReactionDisposer = noop;
|
||||||
|
|
||||||
const subscribe = () => {
|
const subscribe = () => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) return;
|
||||||
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
|
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (preload) {
|
if (preloading) {
|
||||||
for (const store of stores) {
|
if (waitUntilLoaded) {
|
||||||
preloading.push(limitRequests(async () => {
|
preloading.loading.then(subscribe, error => {
|
||||||
if (cacheLoading && store.isLoaded) return; // skip
|
this.log({
|
||||||
|
message: new Error("Loading stores has failed"),
|
||||||
return store.loadAll();
|
meta: { stores, error, options },
|
||||||
}));
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waitUntilLoaded) {
|
|
||||||
Promise.all(preloading).then(subscribe, error => {
|
|
||||||
this.log({
|
|
||||||
message: new Error("Loading stores has failed"),
|
|
||||||
meta: { stores, error, options },
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload when context namespaces changes
|
||||||
|
cancelReloading = reaction(() => this.namespaces, () => {
|
||||||
|
preloading?.cancelLoading();
|
||||||
|
preloading = load();
|
||||||
|
}, {
|
||||||
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
subscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsubscribe
|
// unsubscribe
|
||||||
return () => {
|
return () => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) return;
|
||||||
isUnsubscribed = true;
|
isUnsubscribed = true;
|
||||||
limitRequests.clearQueue();
|
cancelReloading();
|
||||||
|
preloading?.cancelLoading();
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -254,6 +275,10 @@ export class KubeWatchApi {
|
|||||||
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
||||||
const message = this.getMessage(kubeEvent);
|
const message = this.getMessage(kubeEvent);
|
||||||
|
|
||||||
|
if (!this.namespaces.includes(message.namespace)) {
|
||||||
|
continue; // skip updates from non-watching resources context
|
||||||
|
}
|
||||||
|
|
||||||
this.onMessage.emit(message);
|
this.onMessage.emit(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return json;
|
return json;
|
||||||
@ -286,6 +311,7 @@ export class KubeWatchApi {
|
|||||||
|
|
||||||
message.api = api;
|
message.api = api;
|
||||||
message.store = apiManager.getStore(api);
|
message.store = apiManager.getStore(api);
|
||||||
|
message.namespace = namespace;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,11 +58,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll(namespaces = namespaceStore.allowedNamespaces) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = await this.loadItems(namespaceStore.getContextNamespaces());
|
const items = await this.loadItems(namespaces);
|
||||||
|
|
||||||
this.items.replace(this.sortItems(items));
|
this.items.replace(this.sortItems(items));
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
@ -73,6 +73,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSelectedNamespaces(): Promise<void> {
|
||||||
|
return this.loadAll(namespaceStore.getContextNamespaces());
|
||||||
|
}
|
||||||
|
|
||||||
async loadItems(namespaces: string[]) {
|
async loadItems(namespaces: string[]) {
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
@ -82,7 +86,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async create(payload: IReleaseCreatePayload) {
|
async create(payload: IReleaseCreatePayload) {
|
||||||
const response = await helmReleasesApi.create(payload);
|
const response = await helmReleasesApi.create(payload);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -90,7 +94,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -98,7 +102,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async rollback(name: string, namespace: string, revision: number) {
|
async rollback(name: string, namespace: string, revision: number) {
|
||||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
const { store } = this;
|
const { store } = this;
|
||||||
|
|
||||||
if (store && !store.isLoading && !store.isLoaded) {
|
if (store && !store.isLoading && !store.isLoaded) {
|
||||||
store.loadAll();
|
store.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
eventStore.loadAll();
|
eventStore.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
resourceQuotaStore.loadAll();
|
resourceQuotaStore.loadSelectedNamespaces();
|
||||||
limitRangeStore.loadAll();
|
limitRangeStore.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -13,17 +13,14 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
|
|||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
showIcons?: boolean;
|
showIcons?: boolean;
|
||||||
showClusterOption?: boolean; // show cluster option on the top (default: false)
|
showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
|
||||||
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster")
|
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
|
||||||
customizeOptions?(nsOptions: SelectOption[]): SelectOption[];
|
customizeOptions?(options: SelectOption[]): SelectOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<Props> = {
|
||||||
showIcons: true,
|
showIcons: true,
|
||||||
showClusterOption: false,
|
showClusterOption: false,
|
||||||
get clusterOptionLabel() {
|
|
||||||
return `Cluster`;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -39,13 +36,17 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get options(): SelectOption[] {
|
@computed get options(): SelectOption[] {
|
||||||
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
|
const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
|
||||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||||
|
|
||||||
options = customizeOptions ? customizeOptions(options) : options;
|
if (showAllNamespacesOption) {
|
||||||
|
options.unshift({ label: "All Namespaces", value: "" });
|
||||||
|
} else if (showClusterOption) {
|
||||||
|
options.unshift({ label: "Cluster", value: "" });
|
||||||
|
}
|
||||||
|
|
||||||
if (showClusterOption) {
|
if (customizeOptions) {
|
||||||
options.unshift({ value: null, label: clusterOptionLabel });
|
options = customizeOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
@ -64,7 +65,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
|
const { className, showIcons, customizeOptions, ...selectProps } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -80,32 +81,55 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class NamespaceSelectFilter extends React.Component {
|
export class NamespaceSelectFilter extends React.Component {
|
||||||
|
@computed get placeholder(): React.ReactNode {
|
||||||
|
const namespaces = namespaceStore.getContextNamespaces();
|
||||||
|
|
||||||
|
switch (namespaces.length) {
|
||||||
|
case 0:
|
||||||
|
case namespaceStore.allowedNamespaces.length:
|
||||||
|
return <>All namespaces</>;
|
||||||
|
case 1:
|
||||||
|
return <>Namespace: {namespaces[0]}</>;
|
||||||
|
default:
|
||||||
|
return <>Namespaces: {namespaces.join(", ")}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
|
||||||
|
if (namespace) {
|
||||||
|
const isSelected = namespaceStore.hasContext(namespace);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<FilterIcon type={FilterType.NAMESPACE}/>
|
||||||
|
<span>{namespace}</span>
|
||||||
|
{isSelected && <Icon small material="check" className="box right"/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = ([{ value: namespace }]: SelectOption[]) => {
|
||||||
|
if (namespace) {
|
||||||
|
namespaceStore.toggleContext(namespace);
|
||||||
|
} else {
|
||||||
|
namespaceStore.resetContext(); // "All namespaces" clicked, empty list considered as "all"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { contextNs, hasContext, toggleContext } = namespaceStore;
|
|
||||||
let placeholder = <>All namespaces</>;
|
|
||||||
|
|
||||||
if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}</>;
|
|
||||||
if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}</>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NamespaceSelect
|
<NamespaceSelect
|
||||||
placeholder={placeholder}
|
isMulti={true}
|
||||||
|
showAllNamespacesOption={true}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
isOptionSelected={() => false}
|
isOptionSelected={() => false}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
isMulti
|
placeholder={this.placeholder}
|
||||||
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
|
onChange={this.onChange}
|
||||||
formatOptionLabel={({ value: namespace }: SelectOption) => {
|
formatOptionLabel={this.formatOptionLabel}
|
||||||
const isSelected = hasContext(namespace);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gaps align-center">
|
|
||||||
<FilterIcon type={FilterType.NAMESPACE}/>
|
|
||||||
<span>{namespace}</span>
|
|
||||||
{isSelected && <Icon small material="check" className="box right"/>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||||
@ -6,7 +6,7 @@ import { createPageParam } from "../../navigation";
|
|||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
const storage = createStorage<string[]>("context_namespaces");
|
const storage = createStorage<string[]>("context_namespaces", []);
|
||||||
|
|
||||||
export const namespaceUrlParam = createPageParam<string[]>({
|
export const namespaceUrlParam = createPageParam<string[]>({
|
||||||
name: "namespaces",
|
name: "namespaces",
|
||||||
@ -34,7 +34,7 @@ export function getDummyNamespace(name: string) {
|
|||||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||||
api = namespacesApi;
|
api = namespacesApi;
|
||||||
|
|
||||||
@observable contextNs = observable.array<string>();
|
@observable private contextNs = observable.set<string>();
|
||||||
@observable isReady = false;
|
@observable isReady = false;
|
||||||
|
|
||||||
whenReady = when(() => this.isReady);
|
whenReady = when(() => this.isReady);
|
||||||
@ -57,7 +57,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
||||||
return reaction(() => this.contextNs.toJS(), callback, {
|
return reaction(() => Array.from(this.contextNs), callback, {
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
@ -73,41 +73,43 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get allowedNamespaces(): string[] {
|
@computed get allowedNamespaces(): string[] {
|
||||||
return toJS(getHostedCluster().allowedNamespaces);
|
return toJS(getHostedCluster().allowedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
private get initialNamespaces(): string[] {
|
private get initialNamespaces(): string[] {
|
||||||
const allowed = new Set(this.allowedNamespaces);
|
const namespaces = new Set(this.allowedNamespaces);
|
||||||
const prevSelected = storage.get();
|
const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
|
||||||
|
|
||||||
if (Array.isArray(prevSelected)) {
|
// return previously saved namespaces from local-storage
|
||||||
return prevSelected.filter(namespace => allowed.has(namespace));
|
if (prevSelected.length > 0) {
|
||||||
|
return prevSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise select "default" or first allowed namespace
|
// otherwise select "default" or first allowed namespace
|
||||||
if (allowed.has("default")) {
|
if (namespaces.has("default")) {
|
||||||
return ["default"];
|
return ["default"];
|
||||||
} else if (allowed.size) {
|
} else if (namespaces.size) {
|
||||||
return [Array.from(allowed)[0]];
|
return [Array.from(namespaces)[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextNamespaces(): string[] {
|
getContextNamespaces(): string[] {
|
||||||
const namespaces = this.contextNs.toJS();
|
const namespaces = Array.from(this.contextNs);
|
||||||
|
|
||||||
// show all namespaces when nothing selected
|
// show all namespaces when nothing selected
|
||||||
if (!namespaces.length) {
|
if (!namespaces.length) {
|
||||||
|
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
|
||||||
if (this.isLoaded) {
|
if (this.isLoaded) {
|
||||||
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
|
|
||||||
return this.items.map(namespace => namespace.getName());
|
return this.items.map(namespace => namespace.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,26 +145,51 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setContext(namespaces: string[]) {
|
setContext(namespace: string | string[]) {
|
||||||
|
const namespaces = [namespace].flat();
|
||||||
|
|
||||||
this.contextNs.replace(namespaces);
|
this.contextNs.replace(namespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasContext(namespace: string | string[]) {
|
@action
|
||||||
const context = Array.isArray(namespace) ? namespace : [namespace];
|
resetContext() {
|
||||||
|
this.contextNs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
return context.every(namespace => this.contextNs.includes(namespace));
|
hasContext(namespaces: string | string[]) {
|
||||||
|
return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get hasAllContexts(): boolean {
|
||||||
|
return this.contextNs.size === this.allowedNamespaces.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleContext(namespace: string) {
|
toggleContext(namespace: string) {
|
||||||
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
|
if (this.hasContext(namespace)) {
|
||||||
else this.contextNs.push(namespace);
|
this.contextNs.delete(namespace);
|
||||||
|
} else {
|
||||||
|
this.contextNs.add(namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleAll(showAll?: boolean) {
|
||||||
|
if (typeof showAll === "boolean") {
|
||||||
|
if (showAll) {
|
||||||
|
this.setContext(this.allowedNamespaces);
|
||||||
|
} else {
|
||||||
|
this.contextNs.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggleAll(!this.hasAllContexts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async remove(item: Namespace) {
|
async remove(item: Namespace) {
|
||||||
await super.remove(item);
|
await super.remove(item);
|
||||||
this.contextNs.remove(item.getName());
|
this.contextNs.delete(item.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
await Promise.all(stores.map(store => store.loadAll()));
|
await Promise.all(stores.map(store => store.loadSelectedNamespaces()));
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
|
|||||||
@observer
|
@observer
|
||||||
export class CronJobDetails extends React.Component<Props> {
|
export class CronJobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!jobStore.isLoaded) {
|
jobStore.loadSelectedNamespaces();
|
||||||
jobStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -30,9 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -31,9 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -25,9 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
|
|||||||
@observer
|
@observer
|
||||||
export class JobDetails extends React.Component<Props> {
|
export class JobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -30,9 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../navigation";
|
import { history } from "../navigation";
|
||||||
@ -42,7 +43,7 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
|
|||||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
import { eventStore } from "./+events/event.store";
|
import { eventStore } from "./+events/event.store";
|
||||||
import { computed, reaction, observable } from "mobx";
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
import { nodesStore } from "./+nodes/nodes.store";
|
import { nodesStore } from "./+nodes/nodes.store";
|
||||||
import { podsStore } from "./+workloads-pods/pods.store";
|
import { podsStore } from "./+workloads-pods/pods.store";
|
||||||
import { kubeWatchApi } from "../api/kube-watch-api";
|
import { kubeWatchApi } from "../api/kube-watch-api";
|
||||||
@ -74,6 +75,12 @@ export class App extends React.Component {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
whatInput.ask(); // Start to monitor user input device
|
whatInput.ask(); // Start to monitor user input device
|
||||||
|
|
||||||
|
await namespaceStore.whenReady;
|
||||||
|
await kubeWatchApi.init({
|
||||||
|
getCluster: getHostedCluster,
|
||||||
|
getNamespaces: namespaceStore.getContextNamespaces,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
@ -14,14 +14,19 @@ describe("<BottomBar />", () => {
|
|||||||
expect(container).toBeInstanceOf(HTMLElement);
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
// some defensive testing
|
it("renders w/o errors when .getItems() returns unexpected (not type complient) data", async () => {
|
||||||
it("renders w/o errors when .getItems() returns edge cases", async () => {
|
|
||||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => undefined);
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => undefined);
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => "hello");
|
||||||
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => 6);
|
||||||
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => null);
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => null);
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => []);
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => []);
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => [{}]);
|
||||||
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => { return {};});
|
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => { return {};});
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,16 +4,48 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
import { workspaceStore } from "../../../common/workspace-store";
|
||||||
import { statusBarRegistry } from "../../../extensions/registries";
|
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
import { CommandOverlay } from "../command-palette/command-container";
|
||||||
import { ChooseWorkspace } from "../+workspaces";
|
import { ChooseWorkspace } from "../+workspaces";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class BottomBar extends React.Component {
|
export class BottomBar extends React.Component {
|
||||||
|
renderRegisteredItem(registration: StatusBarRegistration) {
|
||||||
|
const { item } = registration;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
return typeof item === "function" ? item() : item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <registration.components.Item />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRegisteredItems() {
|
||||||
|
const items = statusBarRegistry.getItems();
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="extensions box grow flex gaps justify-flex-end">
|
||||||
|
{items.map((registration, index) => {
|
||||||
|
if (!registration?.item && !registration?.components?.Item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex align-center gaps item" key={index}>
|
||||||
|
{this.renderRegisteredItem(registration)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentWorkspace } = workspaceStore;
|
const { currentWorkspace } = workspaceStore;
|
||||||
// in case .getItems() returns undefined
|
|
||||||
const items = statusBarRegistry.getItems() ?? [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="BottomBar flex gaps">
|
<div className="BottomBar flex gaps">
|
||||||
@ -21,20 +53,7 @@ export class BottomBar extends React.Component {
|
|||||||
<Icon smallest material="layers"/>
|
<Icon smallest material="layers"/>
|
||||||
<span className="workspace-name" data-test-id="current-workspace-name">{currentWorkspace.name}</span>
|
<span className="workspace-name" data-test-id="current-workspace-name">{currentWorkspace.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="extensions box grow flex gaps justify-flex-end">
|
{this.renderRegisteredItems()}
|
||||||
{Array.isArray(items) && items.map(({ item }, index) => {
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex align-center gaps item"
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{typeof item === "function" ? item() : item}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
|||||||
const values = this.values.getData(tabId);
|
const values = this.values.getData(tabId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
!releaseStore.isLoaded && releaseStore.loadAll(),
|
!releaseStore.isLoaded && releaseStore.loadSelectedNamespaces(),
|
||||||
!values && this.loadValues(tabId)
|
!values && this.loadValues(tabId)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,7 +138,7 @@ 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]));
|
||||||
|
|
||||||
stores.forEach(store => store.loadAll());
|
stores.forEach(store => store.loadAll(namespaceStore.getContextNamespaces()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class PageFiltersStore {
|
|||||||
protected syncWithContextNamespace() {
|
protected syncWithContextNamespace() {
|
||||||
const disposers = [
|
const disposers = [
|
||||||
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
|
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
|
||||||
if (filteredNs.length !== namespaceStore.contextNs.length) {
|
if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
|
||||||
namespaceStore.setContext(filteredNs);
|
namespaceStore.setContext(filteredNs);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -40,9 +40,7 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class Sidebar extends React.Component<Props> {
|
export class Sidebar extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
|
crdStore.loadSelectedNamespaces();
|
||||||
crdStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCustomResources() {
|
renderCustomResources() {
|
||||||
|
|||||||
@ -106,17 +106,18 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
|
async loadAll(namespaces: string[] = []): Promise<void> {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!contextNamespaces) {
|
if (!namespaces.length) {
|
||||||
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
contextNamespaces = namespaceStore.getContextNamespaces();
|
// load all available namespaces by default
|
||||||
|
namespaces.push(...namespaceStore.allowedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
|
let items = await this.loadItems({ namespaces, api: this.api });
|
||||||
|
|
||||||
items = this.filterItemsOnLoad(items);
|
items = this.filterItemsOnLoad(items);
|
||||||
items = this.sortItems(items);
|
items = this.sortItems(items);
|
||||||
@ -131,6 +132,12 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSelectedNamespaces(): Promise<void> {
|
||||||
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
|
return this.loadAll(namespaceStore.getContextNamespaces());
|
||||||
|
}
|
||||||
|
|
||||||
protected resetOnError(error: any) {
|
protected resetOnError(error: any) {
|
||||||
if (error) this.reset();
|
if (error) this.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||||
|
|
||||||
## 4.1.0-alpha.1 (current version)
|
## 4.1.0-alpha.2 (current version)
|
||||||
|
|
||||||
- Change: list views default to a namespace (insted of listing resources from all namespaces)
|
- Change: list views default to a namespace (insted of listing resources from all namespaces)
|
||||||
|
- Command palette
|
||||||
- Generic logs view with Pod selector
|
- Generic logs view with Pod selector
|
||||||
- Possibility to add custom Helm repository through Lens
|
- Possibility to add custom Helm repository through Lens
|
||||||
- Possibility to change visibility of Pod list columns
|
- Possibility to change visibility of common resource list columns
|
||||||
- Suspend / resume buttons for CronJobs
|
- Suspend / resume buttons for CronJobs
|
||||||
|
- Allow namespace to specified on role creation
|
||||||
|
- Allow for changing installation directory on Windows
|
||||||
- Dock tabs context menu
|
- Dock tabs context menu
|
||||||
- Display node column in Pod list
|
- Display node column in Pod list
|
||||||
- Unify age column output with kubectl
|
- Unify age column output with kubectl
|
||||||
@ -16,6 +19,8 @@ Here you can find description of changes we've built into each release. While we
|
|||||||
- Improve Pod tolerations layout
|
- Improve Pod tolerations layout
|
||||||
- Lens metrics: scrape only lens-metrics namespace
|
- Lens metrics: scrape only lens-metrics namespace
|
||||||
- Lens metrics: Prometheus v2.19.3
|
- Lens metrics: Prometheus v2.19.3
|
||||||
|
- Update bundled kubectl to v1.18.15
|
||||||
|
- Improve how watch requests are handled
|
||||||
- Export PodDetailsList component to extension API
|
- Export PodDetailsList component to extension API
|
||||||
- Export Wizard components to extension API
|
- Export Wizard components to extension API
|
||||||
- Export NamespaceSelect component to extension API
|
- Export NamespaceSelect component to extension API
|
||||||
|
|||||||
76
yarn.lock
76
yarn.lock
@ -1838,28 +1838,29 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^4.12.0", "@typescript-eslint/eslint-plugin@^4.5.0":
|
"@typescript-eslint/eslint-plugin@^4.14.2", "@typescript-eslint/eslint-plugin@^4.5.0":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz#00d1b23b40b58031e6d7c04a5bc6c1a30a2e834a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.2.tgz#47a15803cfab89580b96933d348c2721f3d2f6fe"
|
||||||
integrity sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q==
|
integrity sha512-uMGfG7GFYK/nYutK/iqYJv6K/Xuog/vrRRZX9aEP4Zv1jsYXuvFUMDFLhUnc8WFv3D2R5QhNQL3VYKmvLS5zsQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/experimental-utils" "4.12.0"
|
"@typescript-eslint/experimental-utils" "4.14.2"
|
||||||
"@typescript-eslint/scope-manager" "4.12.0"
|
"@typescript-eslint/scope-manager" "4.14.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
functional-red-black-tree "^1.0.1"
|
functional-red-black-tree "^1.0.1"
|
||||||
|
lodash "^4.17.15"
|
||||||
regexpp "^3.0.0"
|
regexpp "^3.0.0"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
tsutils "^3.17.1"
|
||||||
|
|
||||||
"@typescript-eslint/experimental-utils@4.12.0":
|
"@typescript-eslint/experimental-utils@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.2.tgz#9df35049d1d36b6cbaba534d703648b9e1f05cbb"
|
||||||
integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==
|
integrity sha512-mV9pmET4C2y2WlyHmD+Iun8SAEqkLahHGBkGqDVslHkmoj3VnxnGP4ANlwuxxfq1BsKdl/MPieDbohCEQgKrwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.3"
|
"@types/json-schema" "^7.0.3"
|
||||||
"@typescript-eslint/scope-manager" "4.12.0"
|
"@typescript-eslint/scope-manager" "4.14.2"
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/typescript-estree" "4.12.0"
|
"@typescript-eslint/typescript-estree" "4.14.2"
|
||||||
eslint-scope "^5.0.0"
|
eslint-scope "^5.0.0"
|
||||||
eslint-utils "^2.0.0"
|
eslint-utils "^2.0.0"
|
||||||
|
|
||||||
@ -1873,13 +1874,13 @@
|
|||||||
"@typescript-eslint/typescript-estree" "4.8.2"
|
"@typescript-eslint/typescript-estree" "4.8.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@4.12.0":
|
"@typescript-eslint/scope-manager@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.2.tgz#64cbc9ca64b60069aae0c060b2bf81163243b266"
|
||||||
integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==
|
integrity sha512-cuV9wMrzKm6yIuV48aTPfIeqErt5xceTheAgk70N1V4/2Ecj+fhl34iro/vIssJlb7XtzcaD07hWk7Jk0nKghg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.12.0"
|
"@typescript-eslint/visitor-keys" "4.14.2"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@4.8.2":
|
"@typescript-eslint/scope-manager@4.8.2":
|
||||||
version "4.8.2"
|
version "4.8.2"
|
||||||
@ -1889,23 +1890,23 @@
|
|||||||
"@typescript-eslint/types" "4.8.2"
|
"@typescript-eslint/types" "4.8.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.8.2"
|
"@typescript-eslint/visitor-keys" "4.8.2"
|
||||||
|
|
||||||
"@typescript-eslint/types@4.12.0":
|
"@typescript-eslint/types@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.2.tgz#d96da62be22dc9dc6a06647f3633815350fb3174"
|
||||||
integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==
|
integrity sha512-LltxawRW6wXy4Gck6ZKlBD05tCHQUj4KLn4iR69IyRiDHX3d3NCAhO+ix5OR2Q+q9bjCrHE/HKt+riZkd1At8Q==
|
||||||
|
|
||||||
"@typescript-eslint/types@4.8.2":
|
"@typescript-eslint/types@4.8.2":
|
||||||
version "4.8.2"
|
version "4.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.2.tgz#c862dd0e569d9478eb82d6aee662ea53f5661a36"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.2.tgz#c862dd0e569d9478eb82d6aee662ea53f5661a36"
|
||||||
integrity sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==
|
integrity sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@4.12.0":
|
"@typescript-eslint/typescript-estree@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.2.tgz#9c5ebd8cae4d7b014f890acd81e8e17f309c9df9"
|
||||||
integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==
|
integrity sha512-ESiFl8afXxt1dNj8ENEZT12p+jl9PqRur+Y19m0Z/SPikGL6rqq4e7Me60SU9a2M28uz48/8yct97VQYaGl0Vg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.12.0"
|
"@typescript-eslint/visitor-keys" "4.14.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
globby "^11.0.1"
|
globby "^11.0.1"
|
||||||
is-glob "^4.0.1"
|
is-glob "^4.0.1"
|
||||||
@ -1927,12 +1928,12 @@
|
|||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
tsutils "^3.17.1"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@4.12.0":
|
"@typescript-eslint/visitor-keys@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.2.tgz#997cbe2cb0690e1f384a833f64794e98727c70c6"
|
||||||
integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==
|
integrity sha512-KBB+xLBxnBdTENs/rUgeUKO0UkPBRs2vD09oMRRIkj5BEN8PX1ToXV532desXfpQnZsYTyLLviS7JrPhdL154w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
eslint-visitor-keys "^2.0.0"
|
eslint-visitor-keys "^2.0.0"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@4.8.2":
|
"@typescript-eslint/visitor-keys@4.8.2":
|
||||||
@ -8654,12 +8655,7 @@ lodash.without@~4.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
||||||
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
||||||
|
|
||||||
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
|
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
|
||||||
version "4.17.15"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
|
||||||
|
|
||||||
lodash@^4.17.19:
|
|
||||||
version "4.17.20"
|
version "4.17.20"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user