mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into in-app-survey
This commit is contained in:
commit
1118340e70
@ -57,7 +57,11 @@ jobs:
|
|||||||
displayName: Run tests
|
displayName: Run tests
|
||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
- script: make integration-win
|
- bash: |
|
||||||
|
set -e
|
||||||
|
rm -rf extensions/telemetry
|
||||||
|
make integration-win
|
||||||
|
git checkout extensions/telemetry
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
@ -98,7 +102,11 @@ jobs:
|
|||||||
displayName: Run tests
|
displayName: Run tests
|
||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
- script: make integration-mac
|
- bash: |
|
||||||
|
set -e
|
||||||
|
rm -rf extensions/telemetry
|
||||||
|
make integration-mac
|
||||||
|
git checkout extensions/telemetry
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
@ -152,7 +160,11 @@ jobs:
|
|||||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
||||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||||
displayName: Install integration test dependencies
|
displayName: Install integration test dependencies
|
||||||
- script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
- bash: |
|
||||||
|
set -e
|
||||||
|
rm -rf extensions/telemetry
|
||||||
|
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||||
|
git checkout extensions/telemetry
|
||||||
displayName: Run integration tests
|
displayName: Run integration tests
|
||||||
- bash: |
|
- bash: |
|
||||||
sudo chown root:root /
|
sudo chown root:root /
|
||||||
|
|||||||
@ -46,6 +46,8 @@ module.exports = {
|
|||||||
"avoidEscape": true,
|
"avoidEscape": true,
|
||||||
"allowTemplateLiterals": true,
|
"allowTemplateLiterals": true,
|
||||||
}],
|
}],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
"semi": ["error", "always"],
|
"semi": ["error", "always"],
|
||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
@ -101,6 +103,8 @@ module.exports = {
|
|||||||
}],
|
}],
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
@ -162,6 +166,8 @@ module.exports = {
|
|||||||
}],
|
}],
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
|||||||
27
.github/release-drafter.yml
vendored
Normal file
27
.github/release-drafter.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
categories:
|
||||||
|
- title: '🚀 Features'
|
||||||
|
labels:
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
labels:
|
||||||
|
- 'chore'
|
||||||
|
- 'area/ci'
|
||||||
|
- 'area/tests'
|
||||||
|
|
||||||
|
template: |
|
||||||
|
## Changes since $PREVIOUS_TAG
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
- [Lens v$RESOLVED_VERSION - Linux](https://snapcraft.io/kontena-lens)
|
||||||
|
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.AppImage)
|
||||||
|
- [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 - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
||||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -83,12 +83,12 @@ jobs:
|
|||||||
mike deploy --push master
|
mike deploy --push master
|
||||||
|
|
||||||
- name: Get the release version
|
- name: Get the release version
|
||||||
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
|
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||||
|
|
||||||
- name: mkdocs deploy new release
|
- name: mkdocs deploy new release
|
||||||
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
|
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||||
run: |
|
run: |
|
||||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|||||||
16
.github/workflows/release-drafter.yml
vendored
Normal file
16
.github/workflows/release-drafter.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update_release_draft:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ types/extension-renderer-api.d.ts
|
|||||||
extensions/*/dist
|
extensions/*/dist
|
||||||
docs/extensions/api
|
docs/extensions/api
|
||||||
site/
|
site/
|
||||||
|
.vscode/
|
||||||
|
|||||||
20
Makefile
20
Makefile
@ -12,7 +12,7 @@ endif
|
|||||||
binaries/client:
|
binaries/client:
|
||||||
yarn download-bins
|
yarn download-bins
|
||||||
|
|
||||||
node_modules:
|
node_modules: yarn.lock
|
||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
yarn check --verify-tree --integrity
|
yarn check --verify-tree --integrity
|
||||||
|
|
||||||
@ -105,34 +105,18 @@ docs:
|
|||||||
|
|
||||||
.PHONY: clean-extensions
|
.PHONY: clean-extensions
|
||||||
clean-extensions:
|
clean-extensions:
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), if exist $(dir)\dist del /s /q $(dir)\dist)
|
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), if exist $(dir)\node_modules del /s /q $(dir)\node_modules)
|
|
||||||
else
|
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
|
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
|
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: clean-npm
|
.PHONY: clean-npm
|
||||||
clean-npm:
|
clean-npm:
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
|
||||||
if exist src\extensions\npm\extensions\dist del /s /q src\extensions\npm\extensions\dist
|
|
||||||
if exist src\extensions\npm\extensions\__mocks__ del /s /q src\extensions\npm\extensions\__mocks__
|
|
||||||
if exist src\extensions\npm\extensions\node_modules del /s /q src\extensions\npm\extensions\node_modules
|
|
||||||
else
|
|
||||||
rm -rf src/extensions/npm/extensions/dist
|
rm -rf src/extensions/npm/extensions/dist
|
||||||
rm -rf src/extensions/npm/extensions/__mocks__
|
rm -rf src/extensions/npm/extensions/__mocks__
|
||||||
rm -rf src/extensions/npm/extensions/node_modules
|
rm -rf src/extensions/npm/extensions/node_modules
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: clean-npm clean-extensions
|
clean: clean-npm clean-extensions
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
|
||||||
if exist binaries\client del /s /q binaries\client
|
|
||||||
if exist dist del /s /q dist\*.*
|
|
||||||
if exist static\build del /s /q static\build\*.*
|
|
||||||
else
|
|
||||||
rm -rf binaries/client
|
rm -rf binaries/client
|
||||||
rm -rf dist/*
|
rm -rf dist/*
|
||||||
rm -rf static/build/*
|
rm -rf static/build/*
|
||||||
endif
|
rm -rf node_modules/
|
||||||
|
|||||||
@ -19,6 +19,9 @@ When contributing to this repository, please consider first discussing the chang
|
|||||||
* `make dev` - builds and starts the app
|
* `make dev` - builds and starts the app
|
||||||
* `make clean` - cleanup local environment build artifacts
|
* `make clean` - cleanup local environment build artifacts
|
||||||
|
|
||||||
|
### Developing on Windows
|
||||||
|
|
||||||
|
On Windows we only support [Git Bash](https://gitforwindows.org/) (or similar shell) for running commands.
|
||||||
|
|
||||||
## Github Workflow
|
## Github Workflow
|
||||||
|
|
||||||
|
|||||||
BIN
docs/extensions/guides/images/clusterpagemenus.png
Normal file
BIN
docs/extensions/guides/images/clusterpagemenus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
BIN
docs/extensions/guides/images/globalpagemenus.png
Normal file
BIN
docs/extensions/guides/images/globalpagemenus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@ -1,13 +1,26 @@
|
|||||||
# Renderer Extension
|
# Renderer Extension
|
||||||
|
|
||||||
The renderer extension api is the interface to Lens's renderer process (Lens runs in main and renderer processes).
|
The Renderer Extension API is the interface to Lens's renderer process. Lens runs in both the main and renderer processes. The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, and run custom code in Lens's renderer process.
|
||||||
It allows you to access, configure, and customize Lens data, add custom Lens UI elements, and generally run custom code in Lens's renderer process.
|
|
||||||
The custom Lens UI elements that can be added include global pages, cluster pages, cluster page menus, cluster features, app preferences, status bar items, KubeObject menu items, and KubeObject details items.
|
The custom Lens UI elements that you can add include:
|
||||||
These UI elements are based on React components.
|
|
||||||
|
* [Cluster pages](#clusterpages)
|
||||||
|
* [Cluster page menus](#clusterpagemenus)
|
||||||
|
* [Global pages](#globalpages)
|
||||||
|
* [Global page menus](#globalpagemenus)
|
||||||
|
* [Cluster features](#clusterfeatures)
|
||||||
|
* [App preferences](#apppreferences)
|
||||||
|
* [Status bar items](#statusbaritems)
|
||||||
|
* [KubeObject menu items](#kubeobjectmenuitems)
|
||||||
|
* [KubeObject detail items](#kubeobjectdetailitems)
|
||||||
|
|
||||||
|
All UI elements are based on React components.
|
||||||
|
|
||||||
## `LensRendererExtension` Class
|
## `LensRendererExtension` Class
|
||||||
|
|
||||||
To create a renderer extension simply extend the `LensRendererExtension` class:
|
### `onActivate()` and `onDeactivate()` Methods
|
||||||
|
|
||||||
|
To create a renderer extension, extend the `LensRendererExtension` class:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
@ -23,21 +36,21 @@ export default class ExampleExtensionMain extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
There are two methods that you can implement to facilitate running your custom code.
|
Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. You can initiate custom code by implementing `onActivate()`. Implementing `onDeactivate()` gives you the opportunity to clean up after your extension.
|
||||||
`onActivate()` is called when your extension has been successfully enabled.
|
|
||||||
By implementing `onActivate()` you can initiate your custom code.
|
!!! info
|
||||||
`onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary.
|
Disable extensions from the Lens Extensions page:
|
||||||
The example above simply logs messages when the extension is enabled and disabled.
|
|
||||||
|
1. Navigate to **File** > **Extensions** in the top menu bar. (On Mac, it is **Lens** > **Extensions**.)
|
||||||
|
2. Click **Disable** on the extension you want to disable.
|
||||||
|
|
||||||
|
The example above logs messages when the extension is enabled and disabled.
|
||||||
|
|
||||||
### `clusterPages`
|
### `clusterPages`
|
||||||
|
|
||||||
Cluster pages appear as part of the cluster dashboard.
|
Cluster pages appear in the cluster dashboard. Use cluster pages to display information about or add functionality to the active cluster. It is also possible to include custom details from other clusters. Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore).
|
||||||
They are accessible from the side bar, and are shown in the menu list after *Custom Resources*.
|
|
||||||
It is conventional to use a cluster page to show information or provide functionality pertaining to the active cluster, along with custom data and functionality your extension may have.
|
|
||||||
However, it is not limited to the active cluster.
|
|
||||||
Also, your extension can gain access to the Kubernetes resources in the active cluster in a straightforward manner using the [`clusterStore`](../stores#clusterstore).
|
|
||||||
|
|
||||||
The following example adds a cluster page definition to a `LensRendererExtension` subclass:
|
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";
|
||||||
@ -56,12 +69,13 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Cluster pages are objects matching the `PageRegistration` interface.
|
`clusterPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `clusterPages` array objects are defined as follows:
|
||||||
The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above.
|
|
||||||
The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()).
|
* `id` is a string that identifies the page.
|
||||||
The `components` field matches the `PageComponents` interface for wich there is one field, `Page`.
|
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||||
`Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page.
|
* `Page` is of type ` React.ComponentType<any>`. It offers flexibility in defining the appearance and behavior of your page.
|
||||||
For the example above `ExamplePage` 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";
|
||||||
@ -78,14 +92,15 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the `ExamplePage` class defines a property named `extension`.
|
Note that the `ExamplePage` class defines the `extension` property. This allows the `ExampleExtension` object to be passed in the cluster page definition in the React style. This way, `ExamplePage` can access all `ExampleExtension` subclass data.
|
||||||
This allows the `ExampleExtension` object to be passed in React-style in the cluster page definition, so that `ExamplePage` can access any `ExampleExtension` subclass data.
|
|
||||||
|
The above example shows how to create a cluster page, but not how to make that page available to the Lens user. Use `clusterPageMenus`, covered in the next section, to add cluster pages to the Lens UI.
|
||||||
|
|
||||||
### `clusterPageMenus`
|
### `clusterPageMenus`
|
||||||
|
|
||||||
The above example code shows how to create a cluster page but not how to make it available to the Lens user.
|
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav.
|
||||||
Cluster pages are typically made available through a menu item in the cluster dashboard sidebar.
|
|
||||||
Expanding on the above example a cluster page menu is added 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";
|
||||||
@ -114,14 +129,16 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Cluster page menus are objects matching the `ClusterPageMenuRegistration` interface.
|
`clusterPageMenus` is an array of objects that satisfy the `ClusterPageMenuRegistration` interface. This element defines how the cluster page menu item will appear and what it will do when you click it. The properties of the `clusterPageMenus` array objects are defined as follows:
|
||||||
They define the appearance of the cluster page menu item in the cluster dashboard sidebar and the behaviour when the cluster page menu item is activated (typically by a mouse click).
|
|
||||||
The example above uses the `target` field to set the behaviour as a link to the cluster page with `id` of `"hello"`.
|
* `target` links to the relevant cluster page using `pageId`.
|
||||||
This is done by setting `target`'s `pageId` field to `"hello"`.
|
* `pageId` takes the value of the relevant cluster page's `id` property.
|
||||||
The cluster page menu item's appearance is defined by setting the `title` field to the text that is to be displayed in the cluster dashboard sidebar.
|
* `title` sets the name of the cluster page menu item that will appear in the left side menu.
|
||||||
The `components` field is used to set an icon that appears to the left of the `title` text in the sidebar.
|
* `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
|
||||||
Thus when the `"Hello World"` menu item is activated the cluster dashboard will show the contents of `ExamplePage`.
|
|
||||||
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`:
|
The above example creates a menu item that reads **Hello World**. When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`.
|
||||||
|
|
||||||
|
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||||
@ -142,14 +159,13 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExampleIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`.
|
Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The properties that `Component.Icon` uses are defined as follows:
|
||||||
Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/).
|
|
||||||
One can be selected by name via the `material` field.
|
* `material` takes the name of the icon you want to use.
|
||||||
`ExampleIcon` also sets a tooltip, shown when the Lens user hovers over the icon with a mouse, by setting the `tooltip` field.
|
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||||
|
|
||||||
|
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. The following example groups two sub menu items under one parent menu item:
|
||||||
|
|
||||||
A cluster page menu can also be used to define a foldout submenu in the cluster dashboard sidebar.
|
|
||||||
This enables the grouping of cluster pages.
|
|
||||||
The following example shows how to specify a submenu having two menu items:
|
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
@ -200,24 +216,17 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above defines two cluster pages and three cluster page menu objects.
|
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 cluster page definitons are straightforward.
|
|
||||||
The first cluster page menu object defines the parent of a foldout submenu.
|
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
||||||
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
|
|
||||||
|
|
||||||
### `globalPages`
|
### `globalPages`
|
||||||
|
|
||||||
Global pages appear independently of the cluster dashboard and they fill the Lens UI space.
|
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.
|
||||||
A global page is typically triggered from the cluster menu using a [global page menu](#globalpagemenus).
|
|
||||||
They can also be triggered by a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems).
|
Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page. You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). Unlike cluster pages, users can trigger global pages even when there is no active cluster.
|
||||||
Global pages can appear even when there is no active cluster, unlike cluster pages.
|
|
||||||
It is conventional to use a global page to show information and provide functionality relevant across clusters, along with custom data and functionality that your extension may have.
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@ -238,12 +247,13 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Global pages are objects matching the `PageRegistration` interface.
|
`globalPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `globalPages` array objects are defined as follows:
|
||||||
The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above.
|
|
||||||
The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()).
|
* `id` is a string that identifies the page.
|
||||||
The `components` field matches the `PageComponents` interface for which there is one field, `Page`.
|
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||||
`Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page.
|
* `Page` is of type `React.ComponentType<any>`. It offers flexibility in defining the appearance and behavior of your page.
|
||||||
For the example above `HelpPage` 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";
|
||||||
@ -260,20 +270,19 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the `HelpPage` class defines a property named `extension`.
|
Note that the `HelpPage` class defines the `extension` property. This allows the `HelpExtension` object to be passed in the global page definition in the React-style. This way, `HelpPage` can access all `HelpExtension` subclass data.
|
||||||
This allows the `HelpExtension` object to be passed in React-style in the global page definition, so that `HelpPage` can access any `HelpExtension` subclass data.
|
|
||||||
|
|
||||||
This example code shows how to create a global page but not how to make it available to the Lens user.
|
This example code shows how to create a global page, but not how to make that page available to the Lens user. Global pages can be made available in the following ways:
|
||||||
Global pages are typically made available through a number of ways.
|
|
||||||
Menu items can be added to the Lens app menu system and set to open a global page when activated (See [`appMenus` in the Main Extension guide](../main-extension#appmenus)).
|
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
||||||
Interactive elements can be placed on the status bar (the blue strip along the bottom of the Lens UI) and can be configured to link to a global page when activated (See [`statusBarItems`](#statusbaritems)).
|
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
||||||
As well, global pages can be made accessible from the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon.
|
* To add global pages to the left side menu, see [`globalPageMenus`](#globalpagemenus).
|
||||||
Global page menu icons that are defined using [`globalPageMenus`](#globalpagemenus) appear below the Add Cluster icon.
|
|
||||||
|
|
||||||
### `globalPageMenus`
|
### `globalPageMenus`
|
||||||
|
|
||||||
Global page menus connect a global page to the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon.
|
`globalPageMenus` allows you to add global page menu items to the left nav.
|
||||||
Expanding on the example from [`globalPages`](#globalPages) a global page menu is added 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";
|
||||||
@ -302,14 +311,16 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Global page menus are objects matching the `PageMenuRegistration` interface.
|
`globalPageMenus` is an array of objects that satisfy the `PageMenuRegistration` interface. This element defines how the global page menu item will appear and what it will do when you click it. The properties of the `globalPageMenus` array objects are defined as follows:
|
||||||
They define the appearance of the global page menu item in the cluster menu and the behaviour when the global page menu item is activated (typically by a mouse click).
|
|
||||||
The example above uses the `target` field to set the behaviour as a link to the global page with `id` of `"help"`.
|
* `target` links to the relevant global page using `pageId`.
|
||||||
This is done by setting `target`'s `pageId` field to `"help"`.
|
* `pageId` takes the value of the relevant global page's `id` property.
|
||||||
The global page menu item's appearance is defined by setting the `title` field to the text that is to be displayed as a tooltip in the cluster menu.
|
* `title` sets the name of the global page menu item that will display as a tooltip in the left nav.
|
||||||
The `components` field is used to set an icon that appears in the cluster menu.
|
* `components` is used to set an icon that appears in the left nav.
|
||||||
Thus when the `"Help"` icon is activated the contents of `ExamplePage` will be shown.
|
|
||||||
This example requires the definition of another React-based component, `HelpIcon`, which has been added to `page.tsx`:
|
The above example creates a "Help" icon menu item. When users click the icon, the Lens UI will display the contents of `ExamplePage`.
|
||||||
|
|
||||||
|
This example requires the definition of another React-based component, `HelpIcon`. Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||||
@ -330,14 +341,21 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`HelpIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`.
|
Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The property that `Component.Icon` uses is defined as follows:
|
||||||
Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/).
|
|
||||||
One can be selected by name via the `material` field.
|
* `material` takes the name of the icon you want to use.
|
||||||
|
|
||||||
|
This is what the example will look like, including how the menu item will appear in the left nav:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### `clusterFeatures`
|
### `clusterFeatures`
|
||||||
|
|
||||||
Cluster features are Kubernetes resources that can be applied to and managed within the active cluster.
|
Cluster features are Kubernetes resources that can be applied to and managed within the active cluster.
|
||||||
They can be installed/uninstalled by the Lens user from the [cluster settings page]().
|
They can be installed and uninstalled by the Lens user from the cluster **Settings** page.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**.
|
||||||
|
|
||||||
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
||||||
|
|
||||||
@ -364,8 +382,11 @@ export default class ExampleFeatureExtension extends LensRendererExtension {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
The `title` and `components.Description` fields provide content that appears on the cluster settings page, in the **Features** section.
|
|
||||||
The `feature` field must specify an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implement the following methods:
|
The properties of the `clusterFeatures` array objects are defined as follows:
|
||||||
|
|
||||||
|
* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section.
|
||||||
|
* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
abstract install(cluster: Cluster): Promise<void>;
|
abstract install(cluster: Cluster): Promise<void>;
|
||||||
@ -374,20 +395,21 @@ The `feature` field must specify an instance which extends the abstract class `C
|
|||||||
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||||
```
|
```
|
||||||
|
|
||||||
The `install()` method is typically called by Lens when a user has indicated that this feature is to be installed (i.e. clicked **Install** for the feature on the cluster settings page).
|
The four methods listed above are defined as follows:
|
||||||
The implementation of this method should install kubernetes resources using the `applyResources()` method, or by directly accessing the kubernetes api ([`K8sApi`](tbd)).
|
|
||||||
|
|
||||||
The `upgrade()` method is typically called by Lens when a user has indicated that this feature is to be upgraded (i.e. clicked **Upgrade** for the feature on 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 implementation of this method should upgrade the kubernetes resources already installed, if relevant to the feature.
|
|
||||||
|
|
||||||
The `uninstall()` method is typically called by Lens when a user has indicated that this feature is to be uninstalled (i.e. clicked **Uninstall** for the feature on 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 implementation of this method should uninstall kubernetes resources using the kubernetes api (`K8sApi`)
|
|
||||||
|
|
||||||
The `updateStatus()` method is called periodically by Lens to determine details about the feature's current status.
|
* 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 implementation of this method should provide the current status information in the `status` field of the `ClusterFeature.Feature` parent class.
|
|
||||||
The `status.currentVersion` and `status.latestVersion` fields may be displayed by Lens in describing the feature.
|
* 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 `status.installed` field should be set to true if the feature is currently installed, otherwise false.
|
|
||||||
Also, Lens relies on the `status.canUpgrade` field to determine if the feature can be upgraded (i.e a new version could be available) so the implementation should set the `status.canUpgrade` field according to specific rules for the feature, if relevant.
|
* `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description.
|
||||||
|
|
||||||
|
* `status.installed` should be set to `true` if the feature is installed, and `false` otherwise.
|
||||||
|
|
||||||
|
* `status.canUpgrade` is set according to a rule meant to determine whether the feature can be upgraded. This rule can involve `status.currentVersion` and `status.latestVersion`, if desired.
|
||||||
|
|
||||||
The following shows a very simple implementation of a `ClusterFeature`:
|
The following shows a very simple implementation of a `ClusterFeature`:
|
||||||
|
|
||||||
@ -435,9 +457,9 @@ export class ExampleFeature extends ClusterFeature.Feature {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This example implements the `install()` method by simply invoking the helper `applyResources()` method.
|
This example implements the `install()` method by invoking the helper `applyResources()` method.
|
||||||
`applyResources()` tries to apply all resources read from all files found in the folder path provided.
|
`applyResources()` tries to apply all resources read from all files found in the folder path provided.
|
||||||
In this case this folder path is the `../resources` subfolder relative to current source code's folder.
|
In this case the folder path is the `../resources` subfolder relative to the current source code's folder.
|
||||||
The file `../resources/example-pod.yml` could contain:
|
The file `../resources/example-pod.yml` could contain:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
@ -451,18 +473,18 @@ spec:
|
|||||||
image: nginx
|
image: nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
The `upgrade()` method in the example above is implemented by simply invoking the `install()` method.
|
The example above implements the four methods as follows:
|
||||||
Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps.
|
|
||||||
|
|
||||||
The `uninstall()` method is implemented in the example above by utilizing the [`K8sApi`](tbd) provided by Lens to simply delete the `example-pod` pod applied by the `install()` method.
|
* It implements `upgrade()` by invoking the `install()` method. Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps.
|
||||||
|
|
||||||
The `updateStatus()` method is implemented above by using the [`K8sApi`](tbd) as well, this time to get information from the `example-pod` pod, in particular to determine if it is installed, what version is associated with it, and if it can be upgraded.
|
* It implements `uninstall()` by utilizing the [Kubernetes API](../api/README.md) which Lens provides to delete the `example-pod` applied by the `install()` method.
|
||||||
How the status is updated for a specific cluster feature is up to the implementation.
|
|
||||||
|
* It implements `updateStatus()` by using the [Kubernetes API](../api/README.md) which Lens provides to determine whether the `example-pod` is installed, what version is associated with it, and whether it can be upgraded. The implementation determines what the status is for a specific cluster feature.
|
||||||
|
|
||||||
### `appPreferences`
|
### `appPreferences`
|
||||||
|
|
||||||
The Preferences page is a built-in global page.
|
The Lens **Preferences** page is a built-in global page. You can use Lens extensions to add custom preferences to the Preferences page, providing a single location for users to configure global options.
|
||||||
Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global options, for Lens and extensions alike.
|
|
||||||
The following example demonstrates adding a custom preference:
|
The following example demonstrates adding a custom preference:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
@ -487,13 +509,20 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
App preferences are objects matching the `AppPreferenceRegistration` interface.
|
`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface. The properties of the `appPreferences` array objects are defined as follows:
|
||||||
The `title` field specifies the text to show as the heading on the Preferences page.
|
|
||||||
The `components` field specifies two `React.Component` objects defining the interface for the preference.
|
* `title` sets the heading text displayed on the Preferences page.
|
||||||
`Input` should specify an interactive input element for your preference and `Hint` should provide descriptive information for the preference, which is shown below the `Input` element.
|
* `components` specifies two `React.Component` objects that define the interface for the preference.
|
||||||
`ExamplePreferenceInput` expects its React props set to an `ExamplePreferenceProps` instance, which is how `ExampleRendererExtension` handles the state of the preference input.
|
* `Input` specifies an interactive input element for the preference.
|
||||||
`ExampleRendererExtension` has the field `preference`, which is provided to `ExamplePreferenceInput` when it is created.
|
* `Hint` provides descriptive information for the preference, shown below the `Input` element.
|
||||||
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx`:
|
|
||||||
|
!!! note
|
||||||
|
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
|
||||||
|
|
||||||
|
`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance. This is how `ExampleRendererExtension` handles the state of the preference input.
|
||||||
|
`ExampleRendererExtension` has a `preference` field, which you will add to `ExamplePreferenceInput`.
|
||||||
|
|
||||||
|
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Component } from "@k8slens/extensions";
|
import { Component } from "@k8slens/extensions";
|
||||||
@ -530,28 +559,26 @@ export class ExamplePreferenceHint extends React.Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExamplePreferenceInput` implements a simple checkbox (using Lens's `Component.Checkbox`).
|
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties:
|
||||||
It provides `label` as the text to display next to the checkbox and an `onChange` function, which reacts to the checkbox state change.
|
|
||||||
The checkbox's `value` is initially set to `preference.enabled`.
|
* `label` sets the text that displays next to the checkbox.
|
||||||
`ExamplePreferenceInput` is defined with React props of `ExamplePreferenceProps` type, which is an object with a single field, `enabled`.
|
* `value` is initially set to `preference.enabled`.
|
||||||
This is used to indicate the state of the preference, and is bound to the checkbox state in `onChange`.
|
* `onChange` is a function that responds when the state of the checkbox changes.
|
||||||
|
|
||||||
|
`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props. This is an object with the single `enabled` property. It is used to indicate the state of the preference, and it is bound to the checkbox state in `onChange`.
|
||||||
|
|
||||||
`ExamplePreferenceHint` is a simple text span.
|
`ExamplePreferenceHint` is a simple text span.
|
||||||
Note that the input and the hint could comprise of more sophisticated elements, according to the needs of the extension.
|
|
||||||
|
|
||||||
Note that the above example introduces decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. `mobx` simplifies state management. Without it, this example would not visually update the checkbox properly when the user activates it. [Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements. We recommend that extensions rely on it, as well.
|
||||||
`mobx` simplifies state management and without it this example would not visually update the checkbox properly when the user activates it.
|
Alternatively, you can use React's state management, though `mobx` is typically simpler to use.
|
||||||
[Lens uses `mobx` extensively for state management](../working-with-mobx) of its own UI elements and it is recommended that extensions rely on it too.
|
|
||||||
Alternatively, React's state management can be used, though `mobx` is typically simpler to use.
|
|
||||||
|
|
||||||
Also note that an extension's state data can be managed using an `ExtensionStore` object, which conveniently handles persistence and synchronization.
|
Note that you can manage an extension's state data using an `ExtensionStore` object, which conveniently handles persistence and synchronization. To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state. However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore).
|
||||||
The example above defined a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state primarily to simplify the code for this guide, but it is recommended to manage your extension's state data using [`ExtensionStore`](../stores#extensionstore)
|
|
||||||
|
|
||||||
### `statusBarItems`
|
### `statusBarItems`
|
||||||
|
|
||||||
The Status bar is the blue strip along the bottom of the Lens UI.
|
The status bar is the blue strip along the bottom of the Lens UI. `statusBarItems` are `React.ReactNode` types. They can be used to display status information, or act as links to global pages as well as external pages.
|
||||||
Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page, or even an external page.
|
|
||||||
|
|
||||||
The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and 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';
|
||||||
@ -584,23 +611,23 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `item` field of a status bar item specifies the `React.Component` to be shown on the status bar.
|
The properties of the `statusBarItems` array objects are defined as follows:
|
||||||
By default items are added starting from the right side of the status bar.
|
|
||||||
Typically, `item` would specify an icon and/or a short string of text, considering the limited space on the status bar.
|
* `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).
|
||||||
In the example above the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus) is reused.
|
* `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.
|
||||||
Also, the `item` provides a link to the global page by setting the `onClick` property to a function that calls the `LensRendererExtension` `navigate()` method.
|
|
||||||
`navigate()` takes as a parameter the id of the global page, which is shown when `navigate()` is called.
|
|
||||||
|
|
||||||
### `kubeObjectMenuItems`
|
### `kubeObjectMenuItems`
|
||||||
|
|
||||||
An extension can add custom menu items (including actions) for specific Kubernetes resource kinds/apiVersions.
|
An extension can add custom menu items (`kubeObjectMenuItems`) for specific Kubernetes resource kinds and apiVersions.
|
||||||
These menu items appear under the `...` for each listed resource in the cluster dashboard, and on the title bar of the details page for a specific resource:
|
`kubeObjectMenuItems` appear under the vertical ellipsis for each listed resource in the cluster dashboard:
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
The following example shows how to add a menu for Namespace resources, and associate an action with it:
|
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"
|
||||||
@ -621,12 +648,13 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Kube object menu items are objects matching the `KubeObjectMenuRegistration` interface.
|
`kubeObjectMenuItems` is an array of objects matching the `KubeObjectMenuRegistration` interface. The example above adds a menu item for namespaces in the cluster dashboard. The properties of the `kubeObjectMenuItems` array objects are defined as follows:
|
||||||
The `kind` field specifies the kubernetes resource type to apply this menu item to, and the `apiVersion` field specifies the kubernetes api to use in relation to this resource type.
|
|
||||||
This example adds a menu item for namespaces in the cluster dashboard.
|
* `kind` specifies the Kubernetes resource type the menu item will apply to.
|
||||||
The `components` field defines the menu item's appearance and behaviour.
|
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||||
The `MenuItem` field provides a function that returns a `React.Component` given a set of menu item properties.
|
* `components` defines the menu item's appearance and behavior.
|
||||||
In this example a `NamespaceMenuItem` object is returned.
|
* `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties. In this example a `NamespaceMenuItem` object is returned.
|
||||||
|
|
||||||
`NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`:
|
`NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -661,22 +689,18 @@ export function NamespaceMenuItem(props: Component.KubeObjectMenuProps<K8sApi.Na
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`NamespaceMenuItem` returns a `Component.MenuItem` defining the menu item's appearance (icon and text) and behaviour when activated via the `onClick` property.
|
`NamespaceMenuItem` returns a `Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property. In the example, `getPods()` opens a terminal tab and runs `kubectl` to get a list of pods running in the current namespace.
|
||||||
`getPods()` shows how to open a terminal tab and run a command, specifically it runs `kubectl` to get a list of pods running in the current namespace.
|
|
||||||
See [`Component.terminalStore.sendCommand`](api-docs?) for more details on running terminal commands.
|
The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`. `namespace` is the `props.object`, which is of type `K8sApi.Namespace`. `K8sApi.Namespace` is the API for accessing namespaces. The current namespace in this example is simply given by `namespace.getName()`. Thus, `kubeObjectMenuItems` afford convenient access to the specific resource selected by the user.
|
||||||
The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`.
|
|
||||||
`namespace` is the `props.object`, which is of type `K8sApi.Namespace`.
|
|
||||||
This is the api for accessing namespaces, and the current namespace in this example is simply given by `namespace.getName()`.
|
|
||||||
Thus kube object menu items are afforded convenient access to the specific resource selected by the user.
|
|
||||||
|
|
||||||
### `kubeObjectDetailItems`
|
### `kubeObjectDetailItems`
|
||||||
|
|
||||||
An extension can add custom details (content) for specified Kubernetes resource kinds/apiVersions.
|
An extension can add custom details (`kubeObjectDetailItems`) for specified Kubernetes resource kinds and apiVersions.
|
||||||
These custom details appear on the details page for a specific resource, such as a Namespace:
|
These custom details appear on the details page for a specific resource, such as a Namespace as shown here:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The following example shows how to 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"
|
||||||
@ -697,12 +721,13 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Kube object detail items are objects matching the `KubeObjectDetailRegistration` interface.
|
`kubeObjectDetailItems` is an array of objects matching the `KubeObjectDetailRegistration` interface. This example above adds a detail item for namespaces in the cluster dashboard. The properties of the `kubeObjectDetailItems` array objects are defined as follows:
|
||||||
The `kind` field specifies the kubernetes resource type to apply this detail item to, and the `apiVersion` field specifies the kubernetes api to use in relation to this resource type.
|
|
||||||
This example adds a detail item for namespaces in the cluster dashboard.
|
* `kind` specifies the Kubernetes resource type the detail item will apply to.
|
||||||
The `components` field defines the detail item's appearance and behaviour.
|
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||||
The `Details` field provides a function that returns a `React.Component` given a set of detail item properties.
|
* `components` defines the detail item's appearance and behavior.
|
||||||
In this example a `NamespaceDetailsItem` object is returned.
|
* `Details` provides a function that returns a `React.Component` given a set of detail item properties. In this example a `NamespaceDetailsItem` object is returned.
|
||||||
|
|
||||||
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
@ -732,21 +757,11 @@ export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDe
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>` it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
Since `NamespaceDetailsItem` extends `React.Component<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.
|
||||||
This object can be queried for many details about the current namespace.
|
|
||||||
In this example the namespace's name is obtained in `componentDidMount()` using the `K8sApi.Namespace` `getName()` method.
|
|
||||||
The namespace's name is needed to limit the list of pods to only those in this namespace.
|
|
||||||
To get the list of pods this example uses the kubernetes pods api, specifically the `K8sApi.podsApi.list()` method.
|
|
||||||
The `K8sApi.podsApi` is automatically configured for the currently active cluster.
|
|
||||||
|
|
||||||
Note that `K8sApi.podsApi.list()` is an asynchronous method, and ideally getting the pods list should be done before rendering the `NamespaceDetailsItem`.
|
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`.
|
||||||
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 is updated.
|
|
||||||
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
|
||||||
|
|
||||||
Finally, the `NamespaceDetailsItem` is rendered 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.
|
||||||
Multiple details in a drawer can be placed in `<Component.DrawerItem>` elements for further separation, if desired.
|
Multiple details in a drawer can be placed in `<Component.DrawerItem>` elements for further separation, if desired.
|
||||||
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
||||||
@ -800,6 +815,9 @@ export class PodsDetailsList extends React.Component<Props> {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods. Construct the table using the `Component.Table` and related elements.
|
||||||
|
|
||||||
|
|
||||||
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
||||||
The table is constructed using the `Component.Table` and related elements.
|
The table is constructed using the `Component.Table` and related elements.
|
||||||
See [`Component` documentation](url?) for further details.
|
See [`Component` documentation](https://docs.k8slens.dev/master/extensions/api/modules/_renderer_api_components_/) for further details.
|
||||||
@ -1,18 +1,16 @@
|
|||||||
# Stores
|
# Stores
|
||||||
|
|
||||||
Stores are components that persist and synchronize state data. Lens utilizes a number of stores for maintaining a variety of state information.
|
Stores are components that persist and synchronize state data. Lens uses a number of stores to maintain various kinds of state information, including:
|
||||||
A few of these are exposed by the extensions api for use by the extension developer.
|
|
||||||
|
|
||||||
- The `ClusterStore` manages cluster state data such as cluster details, and which cluster is active.
|
* The `ClusterStore` manages cluster state data (such as cluster details), and it tracks which cluster is active.
|
||||||
- The `WorkspaceStore` similarly manages workspace state data, such as workspace name, and which clusters belong to a given workspace.
|
* The `WorkspaceStore` manages workspace state data (such as the workspace name), and and it tracks which clusters belong to a given workspace.
|
||||||
- The `ExtensionStore` is a store for managing custom extension state data.
|
* The `ExtensionStore` manages custom extension state data.
|
||||||
|
|
||||||
|
This guide focuses on the `ExtensionStore`.
|
||||||
|
|
||||||
## ExtensionStore
|
## ExtensionStore
|
||||||
|
|
||||||
Extension developers can create their own store for managing state data by extending the `ExtensionStore` class.
|
Extension developers can create their own store for managing state data by extending the `ExtensionStore` class. This guide shows how to create a store for the [`appPreferences`](../renderer-extension#apppreferences) guide example, which demonstrates how to add a custom preference to the **Preferences** page. The preference is a simple boolean that indicates whether or not something is enabled. However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted.
|
||||||
This guide shows how to create a store for the [`appPreferences` guide example](../renderer-extension#apppreferences), which demonstrates how to add a custom preference to the Preferences page.
|
|
||||||
The preference is a simple boolean that indicates whether something is enabled or not.
|
|
||||||
The problem with that example is that the enabled state is not stored anywhere, and reverts to the default the next time Lens is started.
|
|
||||||
|
|
||||||
The following example code creates a store for the `appPreferences` guide example:
|
The following example code creates a store for the `appPreferences` guide example:
|
||||||
|
|
||||||
@ -53,25 +51,14 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
|
|||||||
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
|
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
|
||||||
```
|
```
|
||||||
|
|
||||||
First the extension's data model is defined using a simple type, `ExamplePreferencesModel`, which has a single field, `enabled`, representing the preference's state.
|
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. This has a single field, `enabled`, which represents the preference's state. `ExamplePreferencesStore` extends `Store.ExtensionStore`, which is based on the `ExamplePreferencesModel`. The `enabled` field is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference. Note the use of the `observable` decorator on the `enabled` field. The [`appPreferences`](../renderer-extension#apppreferences) guide example uses [MobX](https://mobx.js.org/README.html) for the UI state management, ensuring the checkbox updates when it's activated by the user.
|
||||||
`ExamplePreferencesStore` extends `Store.ExtensionStore`, based on the `ExamplePreferencesModel`.
|
|
||||||
The field `enabled` is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference.
|
|
||||||
Note the use of the `observer` decorator on the `enabled` field.
|
|
||||||
As for the [`appPreferences` guide example](../renderer-extension#apppreferences), [`mobx`](https://mobx.js.org/README.html) is used for the UI state management, ensuring the checkbox updates when activated by the user.
|
|
||||||
|
|
||||||
Then the constructor and two abstract methods are implemented.
|
Next, our example implements the constructor and two abstract methods. The constructor specifies the name of the store (`"example-preferences-store"`) and the default (initial) value for the preference state (`enabled: false`). Lens internals call the `fromStore()` method when the store loads. It gives the extension the opportunity to retrieve the stored state data values based on the defined data model. The `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked. The `toJSON()` method is complementary to `fromStore()`. It is called when the store is being saved.
|
||||||
In the constructor, the name of the store (`"example-preferences-store"`), and the default (initial) value for the preference state (`enabled: false`) are specified.
|
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
|
||||||
The `fromStore()` method is called by Lens internals when the store is loaded, and gives the extension the opportunity to retrieve the stored state data values based on the defined data model.
|
|
||||||
Here, the `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked.
|
|
||||||
The `toJSON()` method is complementary to `fromStore()`, and is called when the store is being saved.
|
|
||||||
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
|
|
||||||
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
|
|
||||||
|
|
||||||
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension.
|
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension. Note that `examplePreferencesStore` is a singleton. Calling this function again will not create a new store.
|
||||||
Note that `examplePreferencesStore` is a singleton, calling this function again will not create a new store.
|
|
||||||
|
|
||||||
The following example code, modified from the [`appPreferences` guide example](../renderer-extension#apppreferences) demonstrates how to use the extension store.
|
The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store. `examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`:
|
||||||
`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`:
|
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { LensMainExtension } from "@k8slens/extensions";
|
||||||
@ -84,8 +71,8 @@ export default class ExampleMainExtension extends LensMainExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, `examplePreferencesStore` is loaded with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
||||||
Similarly, `examplePreferencesStore` must be loaded in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`:
|
Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
@ -111,8 +98,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
|
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
|
||||||
Also, there is no longer the need for the `preference` field in the `ExampleRendererExtension` class, as the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
|
|
||||||
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
@ -151,5 +137,5 @@ export class ExamplePreferenceHint extends React.Component {
|
|||||||
```
|
```
|
||||||
|
|
||||||
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
|
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
|
||||||
Everything else works as before except now the `enabled` state persists across Lens restarts because it is managed by the
|
Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
|
||||||
`examplePreferencesStore`.
|
`examplePreferencesStore`.
|
||||||
@ -1,23 +1,26 @@
|
|||||||
# Working with mobx
|
# Working with MobX
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Lens uses `mobx` as its state manager on top of React's state management system.
|
Lens uses MobX on top of React's state management system.
|
||||||
This helps with having a more declarative style of managing state, as opposed to `React`'s native `setState` mechanism.
|
The result is a more declarative state management style, rather than React's native `setState` mechanism.
|
||||||
You should already have a basic understanding of how `React` handles state ([read here](https://reactjs.org/docs/faq-state.html) for more information).
|
|
||||||
However, if you do not, here is a quick overview.
|
|
||||||
|
|
||||||
- A `React.Component` is generic over both `Props` and `State` (with default empty object types).
|
You can review how React handles state management [here](https://reactjs.org/docs/faq-state.html).
|
||||||
- `Props` should be considered read-only from the point of view of the component and is the mechanism for passing in "arguments" to a component.
|
|
||||||
- `State` is a component's internal state and can be read by accessing the parent field `state`.
|
|
||||||
- `State` **must** be updated using the `setState` parent method which merges the new data with the old state.
|
|
||||||
- `React` does do some optimizations around re-rendering components after quick successions of `setState` calls.
|
|
||||||
|
|
||||||
## How mobx works:
|
The following is a quick overview:
|
||||||
|
|
||||||
`mobx` is a package that provides an abstraction over `React`'s state management. The three main concepts are:
|
* `React.Component` is generic with respect to both `props` and `state` (which default to the empty object type).
|
||||||
- `observable`: data stored in the component's `state`
|
* `props` should be considered read-only from the point of view of the component, and it is the mechanism for passing in arguments to a component.
|
||||||
- `action`: a function that modifies any `observable` data
|
* `state` is a component's internal state, and can be read by accessing the super-class field `state`.
|
||||||
- `computed`: data that is derived from `observable` data but is not actually stored. Think of this as computing `isEmpty` vs an `observable` field called `count`.
|
* `state` **must** be updated using the `setState` parent method which merges the new data with the old state.
|
||||||
|
* React does some optimizations around re-rendering components after quick successions of `setState` calls.
|
||||||
|
|
||||||
Further reading is available from `mobx`'s [website](https://mobx.js.org/the-gist-of-mobx.html).
|
## How MobX Works:
|
||||||
|
|
||||||
|
MobX is a package that provides an abstraction over React's state management system. The three main concepts are:
|
||||||
|
|
||||||
|
* `observable` is a marker for data stored in the component's `state`.
|
||||||
|
* `action` is a function that modifies any `observable` data.
|
||||||
|
* `computed` is a marker for data that is derived from `observable` data, but that is not actually stored. Think of this as computing `isEmpty` rather than an observable field called `count`.
|
||||||
|
|
||||||
|
Further reading is available on the [MobX website](https://mobx.js.org/the-gist-of-mobx.html).
|
||||||
|
|||||||
17
extensions/example-extension/package-lock.json
generated
17
extensions/example-extension/package-lock.json
generated
@ -2796,7 +2796,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3226,6 +3227,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4367,6 +4369,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4381,6 +4384,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -4390,6 +4394,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
}
|
}
|
||||||
@ -4399,6 +4404,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4407,7 +4413,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5398,7 +5405,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6275,7 +6283,8 @@
|
|||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"v8-to-istanbul": {
|
"v8-to-istanbul": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
|||||||
17
extensions/license-menu-item/package-lock.json
generated
17
extensions/license-menu-item/package-lock.json
generated
@ -2868,7 +2868,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3298,6 +3299,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4460,6 +4462,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4474,6 +4477,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -4483,6 +4487,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
}
|
}
|
||||||
@ -4492,6 +4497,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4500,7 +4506,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5516,7 +5523,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6406,7 +6414,8 @@
|
|||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"v8-to-istanbul": {
|
"v8-to-istanbul": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
|||||||
12
extensions/metrics-cluster-feature/package-lock.json
generated
12
extensions/metrics-cluster-feature/package-lock.json
generated
@ -2816,7 +2816,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3246,6 +3247,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4394,6 +4396,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4408,6 +4411,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -5434,7 +5438,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6311,7 +6316,8 @@
|
|||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"v8-to-istanbul": {
|
"v8-to-istanbul": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
|||||||
17
extensions/node-menu/package-lock.json
generated
17
extensions/node-menu/package-lock.json
generated
@ -2796,7 +2796,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3226,6 +3227,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4382,6 +4384,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4396,6 +4399,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -4405,6 +4409,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
}
|
}
|
||||||
@ -4414,6 +4419,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4422,7 +4428,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5438,7 +5445,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6315,7 +6323,8 @@
|
|||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"v8-to-istanbul": {
|
"v8-to-istanbul": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
|||||||
654
extensions/pod-menu/package-lock.json
generated
654
extensions/pod-menu/package-lock.json
generated
@ -626,8 +626,645 @@
|
|||||||
},
|
},
|
||||||
"@k8slens/extensions": {
|
"@k8slens/extensions": {
|
||||||
"version": "file:../../src/extensions/npm/extensions",
|
"version": "file:../../src/extensions/npm/extensions",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@material-ui/core": "*",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/react-select": "*",
|
||||||
|
"conf": "^7.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||||
|
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@emotion/hash": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@material-ui/core": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.4",
|
||||||
|
"@material-ui/styles": "^4.11.2",
|
||||||
|
"@material-ui/system": "^4.11.2",
|
||||||
|
"@material-ui/types": "^5.1.0",
|
||||||
|
"@material-ui/utils": "^4.11.2",
|
||||||
|
"@types/react-transition-group": "^4.2.0",
|
||||||
|
"clsx": "^1.0.4",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"popper.js": "1.16.1-lts",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0",
|
||||||
|
"react-transition-group": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@material-ui/styles": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.4",
|
||||||
|
"@emotion/hash": "^0.8.0",
|
||||||
|
"@material-ui/types": "^5.1.0",
|
||||||
|
"@material-ui/utils": "^4.11.2",
|
||||||
|
"clsx": "^1.0.4",
|
||||||
|
"csstype": "^2.5.2",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"jss": "^10.0.3",
|
||||||
|
"jss-plugin-camel-case": "^10.0.3",
|
||||||
|
"jss-plugin-default-unit": "^10.0.3",
|
||||||
|
"jss-plugin-global": "^10.0.3",
|
||||||
|
"jss-plugin-nested": "^10.0.3",
|
||||||
|
"jss-plugin-props-sort": "^10.0.3",
|
||||||
|
"jss-plugin-rule-value-function": "^10.0.3",
|
||||||
|
"jss-plugin-vendor-prefixer": "^10.0.3",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@material-ui/system": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.4",
|
||||||
|
"@material-ui/utils": "^4.11.2",
|
||||||
|
"csstype": "^2.5.2",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@material-ui/types": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@material-ui/utils": {
|
||||||
|
"version": "4.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
|
||||||
|
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.4",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "14.14.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
|
||||||
|
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/prop-types": {
|
||||||
|
"version": "15.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||||
|
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/react": {
|
||||||
|
"version": "17.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz",
|
||||||
|
"integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"version": "17.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz",
|
||||||
|
"integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/react-select": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"@types/react-transition-group": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/react-transition-group": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"atomically": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"clsx": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"conf": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^6.12.2",
|
||||||
|
"atomically": "^1.3.1",
|
||||||
|
"debounce-fn": "^4.0.0",
|
||||||
|
"dot-prop": "^5.2.0",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
|
"json-schema-typed": "^7.0.3",
|
||||||
|
"make-dir": "^3.1.0",
|
||||||
|
"onetime": "^5.1.0",
|
||||||
|
"pkg-up": "^3.1.0",
|
||||||
|
"semver": "^7.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"css-vendor": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.8.3",
|
||||||
|
"is-in-browser": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"csstype": {
|
||||||
|
"version": "2.6.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz",
|
||||||
|
"integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"debounce-fn": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"mimic-fn": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dom-helpers": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dot-prop": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-obj": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env-paths": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fast-json-stable-stringify": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"find-up": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"locate-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hyphenate-style-name": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"indefinite-observable": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"symbol-observable": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-in-browser": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"is-obj": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"json-schema-typed": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"jss": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"csstype": "^3.0.2",
|
||||||
|
"indefinite-observable": "^2.0.1",
|
||||||
|
"is-in-browser": "^1.1.3",
|
||||||
|
"tiny-warning": "^1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-camel-case": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"hyphenate-style-name": "^1.0.3",
|
||||||
|
"jss": "10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-default-unit": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"jss": "10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-global": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"jss": "10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-nested": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"jss": "10.5.0",
|
||||||
|
"tiny-warning": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-props-sort": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"jss": "10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-rule-value-function": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"jss": "10.5.0",
|
||||||
|
"tiny-warning": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jss-plugin-vendor-prefixer": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"css-vendor": "^2.0.8",
|
||||||
|
"jss": "10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locate-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"p-locate": "^3.0.0",
|
||||||
|
"path-exists": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"make-dir": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"semver": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mimic-fn": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"onetime": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"mimic-fn": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-locate": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"p-limit": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"path-exists": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"pkg-up": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"find-up": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"popper.js": {
|
||||||
|
"version": "1.16.1-lts",
|
||||||
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
|
||||||
|
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"prop-types": {
|
||||||
|
"version": "15.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"punycode": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"react-is": {
|
||||||
|
"version": "17.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
|
||||||
|
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"react-transition-group": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.13.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||||
|
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "7.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lru-cache": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symbol-observable": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"uri-js": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
|
||||||
@ -2796,7 +3433,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3226,6 +3864,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4382,6 +5021,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4396,6 +5036,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -4405,6 +5046,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
}
|
}
|
||||||
@ -4414,6 +5056,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4422,7 +5065,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5438,7 +6082,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -6315,7 +6960,8 @@
|
|||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"v8-to-istanbul": {
|
"v8-to-istanbul": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
|||||||
@ -9,13 +9,9 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
Navigation.hideDetails();
|
Navigation.hideDetails();
|
||||||
const pod = this.props.object;
|
const pod = this.props.object;
|
||||||
|
|
||||||
Component.createPodLogsTab({
|
Component.logTabStore.createPodTab({
|
||||||
pod,
|
selectedPod: pod,
|
||||||
containers: pod.getContainers(),
|
|
||||||
initContainers: pod.getInitContainers(),
|
|
||||||
selectedContainer: container,
|
selectedContainer: container,
|
||||||
showTimestamps: false,
|
|
||||||
previous: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
extensions/telemetry/package-lock.json
generated
17
extensions/telemetry/package-lock.json
generated
@ -2901,7 +2901,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -3337,6 +3338,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4533,6 +4535,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
@ -4547,6 +4550,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -4556,6 +4560,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
}
|
}
|
||||||
@ -4564,13 +4569,15 @@
|
|||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -4579,7 +4586,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5595,7 +5603,8 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
|||||||
@ -1,82 +1,26 @@
|
|||||||
/*
|
|
||||||
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
|
|
||||||
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
|
|
||||||
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
|
||||||
cluster and vice versa.
|
|
||||||
*/
|
|
||||||
import { Application } from "spectron";
|
import { Application } from "spectron";
|
||||||
import * as util from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
import { spawnSync } from "child_process";
|
import { listHelmRepositories } from "../helpers/utils";
|
||||||
|
import { fail } from "assert";
|
||||||
|
|
||||||
|
|
||||||
jest.setTimeout(60000);
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
||||||
describe("Lens integration tests", () => {
|
describe("Lens integration tests", () => {
|
||||||
const TEST_NAMESPACE = "integration-tests";
|
|
||||||
const BACKSPACE = "\uE003";
|
|
||||||
let app: Application;
|
let app: Application;
|
||||||
const appStart = async () => {
|
|
||||||
app = util.setup();
|
|
||||||
await app.start();
|
|
||||||
// Wait for splash screen to be closed
|
|
||||||
while (await app.client.getWindowCount() > 1);
|
|
||||||
await app.client.windowByIndex(0);
|
|
||||||
await app.client.waitUntilWindowLoaded();
|
|
||||||
};
|
|
||||||
const clickWhatsNew = async (app: Application) => {
|
|
||||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
|
||||||
await app.client.click("button.primary");
|
|
||||||
await app.client.waitUntilTextExists("h1", "Welcome");
|
|
||||||
};
|
|
||||||
const minikubeReady = (): boolean => {
|
|
||||||
// determine if minikube is running
|
|
||||||
{
|
|
||||||
const { status } = spawnSync("minikube status", { shell: true });
|
|
||||||
|
|
||||||
if (status !== 0) {
|
|
||||||
console.warn("minikube not running");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove TEST_NAMESPACE if it already exists
|
|
||||||
{
|
|
||||||
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
|
||||||
|
|
||||||
if (status === 0) {
|
|
||||||
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
|
||||||
|
|
||||||
const { status, stdout, stderr } = spawnSync(
|
|
||||||
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
|
||||||
{ shell: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status !== 0) {
|
|
||||||
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(stdout.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const ready = minikubeReady();
|
|
||||||
|
|
||||||
describe("app start", () => {
|
describe("app start", () => {
|
||||||
beforeAll(appStart, 20000);
|
beforeAll(async () => app = await utils.appStart(), 20000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (app?.isRunning()) {
|
if (app?.isRunning()) {
|
||||||
await util.tearDown(app);
|
await utils.tearDown(app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "whats new"', async () => {
|
it('shows "whats new"', async () => {
|
||||||
await clickWhatsNew(app);
|
await utils.clickWhatsNew(app);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "add cluster"', async () => {
|
it('shows "add cluster"', async () => {
|
||||||
@ -93,7 +37,13 @@ describe("Lens integration tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ensures helm repos", async () => {
|
it("ensures helm repos", async () => {
|
||||||
await app.client.waitUntilTextExists("div.repos #message-bitnami", "bitnami"); // wait for the helm-cli to fetch the bitnami repo
|
const repos = await listHelmRepositories();
|
||||||
|
|
||||||
|
if (!repos[0]) {
|
||||||
|
fail("Lens failed to add Bitnami repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.client.waitUntilTextExists("div.repos #message-bitnami", repos[0].name); // wait for the helm-cli to fetch the repo(s)
|
||||||
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
|
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
|
||||||
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
|
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
|
||||||
});
|
});
|
||||||
@ -104,470 +54,4 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.keys("Meta");
|
await app.client.keys("Meta");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
util.describeIf(ready)("workspaces", () => {
|
|
||||||
beforeAll(appStart, 20000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return util.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates new workspace", async () => {
|
|
||||||
await clickWhatsNew(app);
|
|
||||||
await app.client.click("#current-workspace .Icon");
|
|
||||||
await app.client.click('a[href="/workspaces"]');
|
|
||||||
await app.client.click(".Workspaces button.Button");
|
|
||||||
await app.client.keys("test-workspace");
|
|
||||||
await app.client.click(".Workspaces .Input.description input");
|
|
||||||
await app.client.keys("test description");
|
|
||||||
await app.client.click(".Workspaces .workspace.editing .Icon");
|
|
||||||
await app.client.waitUntilTextExists(".workspace .name a", "test-workspace");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds cluster in default workspace", async () => {
|
|
||||||
await addMinikubeCluster(app);
|
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
|
||||||
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds cluster in test-workspace", async () => {
|
|
||||||
await app.client.click("#current-workspace .Icon");
|
|
||||||
await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]');
|
|
||||||
await app.client.click('.WorkspaceMenu li[title="test description"]');
|
|
||||||
await addMinikubeCluster(app);
|
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("checks if default workspace has active cluster", async () => {
|
|
||||||
await app.client.click("#current-workspace .Icon");
|
|
||||||
await app.client.waitForVisible(".WorkspaceMenu > li:first-of-type");
|
|
||||||
await app.client.click(".WorkspaceMenu > li:first-of-type");
|
|
||||||
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const addMinikubeCluster = async (app: Application) => {
|
|
||||||
await app.client.click("div.add-cluster");
|
|
||||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
|
||||||
await app.client.click("div.Select__control"); // show the context drop-down list
|
|
||||||
await app.client.waitUntilTextExists("div", "minikube");
|
|
||||||
|
|
||||||
if (!await app.client.$("button.primary").isEnabled()) {
|
|
||||||
await app.client.click("div.minikube"); // select minikube context
|
|
||||||
} // else the only context, which must be 'minikube', is automatically selected
|
|
||||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
|
||||||
await app.client.click("button.primary"); // add minikube cluster
|
|
||||||
};
|
|
||||||
const waitForMinikubeDashboard = async (app: Application) => {
|
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
|
||||||
await app.client.frame("minikube");
|
|
||||||
await app.client.waitUntilTextExists("span.link-text", "Cluster");
|
|
||||||
};
|
|
||||||
|
|
||||||
util.describeIf(ready)("cluster tests", () => {
|
|
||||||
let clusterAdded = false;
|
|
||||||
const addCluster = async () => {
|
|
||||||
await clickWhatsNew(app);
|
|
||||||
await addMinikubeCluster(app);
|
|
||||||
await waitForMinikubeDashboard(app);
|
|
||||||
await app.client.click('a[href="/nodes"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("cluster add", () => {
|
|
||||||
beforeAll(appStart, 20000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return util.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows to add a cluster", async () => {
|
|
||||||
await addCluster();
|
|
||||||
clusterAdded = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const appStartAddCluster = async () => {
|
|
||||||
if (clusterAdded) {
|
|
||||||
await appStart();
|
|
||||||
await addCluster();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("cluster pages", () => {
|
|
||||||
|
|
||||||
beforeAll(appStartAddCluster, 40000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return util.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tests: {
|
|
||||||
drawer?: string
|
|
||||||
drawerId?: string
|
|
||||||
pages: {
|
|
||||||
name: string,
|
|
||||||
href: string,
|
|
||||||
expectedSelector: string,
|
|
||||||
expectedText: string
|
|
||||||
}[]
|
|
||||||
}[] = [{
|
|
||||||
drawer: "",
|
|
||||||
drawerId: "",
|
|
||||||
pages: [{
|
|
||||||
name: "Cluster",
|
|
||||||
href: "cluster",
|
|
||||||
expectedSelector: "div.ClusterOverview div.label",
|
|
||||||
expectedText: "Master"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "",
|
|
||||||
drawerId: "",
|
|
||||||
pages: [{
|
|
||||||
name: "Nodes",
|
|
||||||
href: "nodes",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Nodes"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Workloads",
|
|
||||||
drawerId: "workloads",
|
|
||||||
pages: [{
|
|
||||||
name: "Overview",
|
|
||||||
href: "workloads",
|
|
||||||
expectedSelector: "h5.box",
|
|
||||||
expectedText: "Overview"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pods",
|
|
||||||
href: "pods",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Pods"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Deployments",
|
|
||||||
href: "deployments",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Deployments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "DaemonSets",
|
|
||||||
href: "daemonsets",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Daemon Sets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "StatefulSets",
|
|
||||||
href: "statefulsets",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Stateful Sets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ReplicaSets",
|
|
||||||
href: "replicasets",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Replica Sets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jobs",
|
|
||||||
href: "jobs",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Jobs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CronJobs",
|
|
||||||
href: "cronjobs",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Cron Jobs"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Configuration",
|
|
||||||
drawerId: "config",
|
|
||||||
pages: [{
|
|
||||||
name: "ConfigMaps",
|
|
||||||
href: "configmaps",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Config Maps"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Secrets",
|
|
||||||
href: "secrets",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Secrets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Resource Quotas",
|
|
||||||
href: "resourcequotas",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Resource Quotas"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "HPA",
|
|
||||||
href: "hpa",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Horizontal Pod Autoscalers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pod Disruption Budgets",
|
|
||||||
href: "poddisruptionbudgets",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Pod Disruption Budgets"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Network",
|
|
||||||
drawerId: "networks",
|
|
||||||
pages: [{
|
|
||||||
name: "Services",
|
|
||||||
href: "services",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Services"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Endpoints",
|
|
||||||
href: "endpoints",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Endpoints"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ingresses",
|
|
||||||
href: "ingresses",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Ingresses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Network Policies",
|
|
||||||
href: "network-policies",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Network Policies"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Storage",
|
|
||||||
drawerId: "storage",
|
|
||||||
pages: [{
|
|
||||||
name: "Persistent Volume Claims",
|
|
||||||
href: "persistent-volume-claims",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Persistent Volume Claims"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Persistent Volumes",
|
|
||||||
href: "persistent-volumes",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Persistent Volumes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Storage Classes",
|
|
||||||
href: "storage-classes",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Storage Classes"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "",
|
|
||||||
drawerId: "",
|
|
||||||
pages: [{
|
|
||||||
name: "Namespaces",
|
|
||||||
href: "namespaces",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Namespaces"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "",
|
|
||||||
drawerId: "",
|
|
||||||
pages: [{
|
|
||||||
name: "Events",
|
|
||||||
href: "events",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Events"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Apps",
|
|
||||||
drawerId: "apps",
|
|
||||||
pages: [{
|
|
||||||
name: "Charts",
|
|
||||||
href: "apps/charts",
|
|
||||||
expectedSelector: "div.HelmCharts input",
|
|
||||||
expectedText: ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Releases",
|
|
||||||
href: "apps/releases",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Releases"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Access Control",
|
|
||||||
drawerId: "users",
|
|
||||||
pages: [{
|
|
||||||
name: "Service Accounts",
|
|
||||||
href: "service-accounts",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Service Accounts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Role Bindings",
|
|
||||||
href: "role-bindings",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Role Bindings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Roles",
|
|
||||||
href: "roles",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Roles"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pod Security Policies",
|
|
||||||
href: "pod-security-policies",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Pod Security Policies"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
drawer: "Custom Resources",
|
|
||||||
drawerId: "custom-resources",
|
|
||||||
pages: [{
|
|
||||||
name: "Definitions",
|
|
||||||
href: "crd/definitions",
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
expectedText: "Custom Resources"
|
|
||||||
}]
|
|
||||||
}];
|
|
||||||
|
|
||||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
|
||||||
if (drawer !== "") {
|
|
||||||
it(`shows ${drawer} drawer`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
|
||||||
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
|
|
||||||
it(`shows ${drawer}->${name} page`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click(`a[href^="/${href}"]`);
|
|
||||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (drawer !== "") {
|
|
||||||
// hide the drawer
|
|
||||||
it(`hides ${drawer} drawer`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
|
||||||
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("viewing pod logs", () => {
|
|
||||||
beforeEach(appStartAddCluster, 40000);
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return util.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`shows a logs for a pod`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
// Go to Pods page
|
|
||||||
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
|
||||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
|
||||||
await app.client.click('a[href^="/pods"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
|
||||||
// Open logs tab in dock
|
|
||||||
await app.client.click(".list .TableRow:first-child");
|
|
||||||
await app.client.waitForVisible(".Drawer");
|
|
||||||
await app.client.click(".drawer-title .Menu li:nth-child(2)");
|
|
||||||
// Check if controls are available
|
|
||||||
await app.client.waitForVisible(".PodLogs .VirtualList");
|
|
||||||
await app.client.waitForVisible(".PodLogControls");
|
|
||||||
await app.client.waitForVisible(".PodLogControls .SearchInput");
|
|
||||||
await app.client.waitForVisible(".PodLogControls .SearchInput input");
|
|
||||||
// Search for semicolon
|
|
||||||
await app.client.keys(":");
|
|
||||||
await app.client.waitForVisible(".PodLogs .list span.active");
|
|
||||||
// Click through controls
|
|
||||||
await app.client.click(".PodLogControls .timestamps-icon");
|
|
||||||
await app.client.click(".PodLogControls .undo-icon");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cluster operations", () => {
|
|
||||||
beforeEach(appStartAddCluster, 40000);
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return util.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows default namespace", async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click('a[href="/namespaces"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click('a[href="/namespaces"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
|
||||||
await app.client.click("button.add-button");
|
|
||||||
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
|
|
||||||
await app.client.keys(`${TEST_NAMESPACE}\n`);
|
|
||||||
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
|
|
||||||
expect(clusterAdded).toBe(true);
|
|
||||||
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
|
||||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
|
||||||
await app.client.click('a[href^="/pods"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
|
||||||
await app.client.click(".Icon.new-dock-tab");
|
|
||||||
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
|
|
||||||
await app.client.click("li.MenuItem.create-resource-tab");
|
|
||||||
await app.client.waitForVisible(".CreateResource div.ace_content");
|
|
||||||
// Write pod manifest to editor
|
|
||||||
await app.client.keys("apiVersion: v1\n");
|
|
||||||
await app.client.keys("kind: Pod\n");
|
|
||||||
await app.client.keys("metadata:\n");
|
|
||||||
await app.client.keys(" name: nginx-create-pod-test\n");
|
|
||||||
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
|
|
||||||
await app.client.keys(`${BACKSPACE}spec:\n`);
|
|
||||||
await app.client.keys(" containers:\n");
|
|
||||||
await app.client.keys("- name: nginx-create-pod-test\n");
|
|
||||||
await app.client.keys(" image: nginx:alpine\n");
|
|
||||||
// Create deployment
|
|
||||||
await app.client.waitForEnabled("button.Button=Create & Close");
|
|
||||||
await app.client.click("button.Button=Create & Close");
|
|
||||||
// Wait until first bits of pod appears on dashboard
|
|
||||||
await app.client.waitForExist(".name=nginx-create-pod-test");
|
|
||||||
// Open pod details
|
|
||||||
await app.client.click(".name=nginx-create-pod-test");
|
|
||||||
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
450
integration/__tests__/cluster-pages.tests.ts
Normal file
450
integration/__tests__/cluster-pages.tests.ts
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
/*
|
||||||
|
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
|
||||||
|
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
|
||||||
|
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
||||||
|
cluster and vice versa.
|
||||||
|
*/
|
||||||
|
import { Application } from "spectron";
|
||||||
|
import * as utils from "../helpers/utils";
|
||||||
|
import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import * as util from "util";
|
||||||
|
|
||||||
|
export const promiseExec = util.promisify(exec);
|
||||||
|
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
|
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
||||||
|
describe("Lens cluster pages", () => {
|
||||||
|
const TEST_NAMESPACE = "integration-tests";
|
||||||
|
const BACKSPACE = "\uE003";
|
||||||
|
let app: Application;
|
||||||
|
const ready = minikubeReady(TEST_NAMESPACE);
|
||||||
|
|
||||||
|
utils.describeIf(ready)("test common pages", () => {
|
||||||
|
let clusterAdded = false;
|
||||||
|
const addCluster = async () => {
|
||||||
|
await utils.clickWhatsNew(app);
|
||||||
|
await addMinikubeCluster(app);
|
||||||
|
await waitForMinikubeDashboard(app);
|
||||||
|
await app.client.click('a[href="/nodes"]');
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("cluster add", () => {
|
||||||
|
beforeAll(async () => app = await utils.appStart(), 20000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows to add a cluster", async () => {
|
||||||
|
await addCluster();
|
||||||
|
clusterAdded = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const appStartAddCluster = async () => {
|
||||||
|
if (clusterAdded) {
|
||||||
|
app = await utils.appStart();
|
||||||
|
await addCluster();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("cluster pages", () => {
|
||||||
|
|
||||||
|
beforeAll(appStartAddCluster, 40000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tests: {
|
||||||
|
drawer?: string
|
||||||
|
drawerId?: string
|
||||||
|
pages: {
|
||||||
|
name: string,
|
||||||
|
href: string,
|
||||||
|
expectedSelector: string,
|
||||||
|
expectedText: string
|
||||||
|
}[]
|
||||||
|
}[] = [{
|
||||||
|
drawer: "",
|
||||||
|
drawerId: "",
|
||||||
|
pages: [{
|
||||||
|
name: "Cluster",
|
||||||
|
href: "cluster",
|
||||||
|
expectedSelector: "div.ClusterOverview div.label",
|
||||||
|
expectedText: "Master"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "",
|
||||||
|
drawerId: "",
|
||||||
|
pages: [{
|
||||||
|
name: "Nodes",
|
||||||
|
href: "nodes",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Nodes"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Workloads",
|
||||||
|
drawerId: "workloads",
|
||||||
|
pages: [{
|
||||||
|
name: "Overview",
|
||||||
|
href: "workloads",
|
||||||
|
expectedSelector: "h5.box",
|
||||||
|
expectedText: "Overview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pods",
|
||||||
|
href: "pods",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Pods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Deployments",
|
||||||
|
href: "deployments",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Deployments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DaemonSets",
|
||||||
|
href: "daemonsets",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Daemon Sets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "StatefulSets",
|
||||||
|
href: "statefulsets",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Stateful Sets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ReplicaSets",
|
||||||
|
href: "replicasets",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Replica Sets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jobs",
|
||||||
|
href: "jobs",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Jobs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CronJobs",
|
||||||
|
href: "cronjobs",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Cron Jobs"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Configuration",
|
||||||
|
drawerId: "config",
|
||||||
|
pages: [{
|
||||||
|
name: "ConfigMaps",
|
||||||
|
href: "configmaps",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Config Maps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Secrets",
|
||||||
|
href: "secrets",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Secrets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Resource Quotas",
|
||||||
|
href: "resourcequotas",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Resource Quotas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Limit Ranges",
|
||||||
|
href: "limitranges",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Limit Ranges"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HPA",
|
||||||
|
href: "hpa",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Horizontal Pod Autoscalers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod Disruption Budgets",
|
||||||
|
href: "poddisruptionbudgets",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Pod Disruption Budgets"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Network",
|
||||||
|
drawerId: "networks",
|
||||||
|
pages: [{
|
||||||
|
name: "Services",
|
||||||
|
href: "services",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Services"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Endpoints",
|
||||||
|
href: "endpoints",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Endpoints"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ingresses",
|
||||||
|
href: "ingresses",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Ingresses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Network Policies",
|
||||||
|
href: "network-policies",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Network Policies"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Storage",
|
||||||
|
drawerId: "storage",
|
||||||
|
pages: [{
|
||||||
|
name: "Persistent Volume Claims",
|
||||||
|
href: "persistent-volume-claims",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Persistent Volume Claims"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Persistent Volumes",
|
||||||
|
href: "persistent-volumes",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Persistent Volumes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Storage Classes",
|
||||||
|
href: "storage-classes",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Storage Classes"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "",
|
||||||
|
drawerId: "",
|
||||||
|
pages: [{
|
||||||
|
name: "Namespaces",
|
||||||
|
href: "namespaces",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Namespaces"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "",
|
||||||
|
drawerId: "",
|
||||||
|
pages: [{
|
||||||
|
name: "Events",
|
||||||
|
href: "events",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Events"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Apps",
|
||||||
|
drawerId: "apps",
|
||||||
|
pages: [{
|
||||||
|
name: "Charts",
|
||||||
|
href: "apps/charts",
|
||||||
|
expectedSelector: "div.HelmCharts input",
|
||||||
|
expectedText: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Releases",
|
||||||
|
href: "apps/releases",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Releases"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Access Control",
|
||||||
|
drawerId: "users",
|
||||||
|
pages: [{
|
||||||
|
name: "Service Accounts",
|
||||||
|
href: "service-accounts",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Service Accounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Role Bindings",
|
||||||
|
href: "role-bindings",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Role Bindings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Roles",
|
||||||
|
href: "roles",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod Security Policies",
|
||||||
|
href: "pod-security-policies",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Pod Security Policies"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawer: "Custom Resources",
|
||||||
|
drawerId: "custom-resources",
|
||||||
|
pages: [{
|
||||||
|
name: "Definitions",
|
||||||
|
href: "crd/definitions",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Custom Resources"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||||
|
if (drawer !== "") {
|
||||||
|
it(`shows ${drawer} drawer`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
||||||
|
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
|
||||||
|
it(`shows ${drawer}->${name} page`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click(`a[href^="/${href}"]`);
|
||||||
|
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (drawer !== "") {
|
||||||
|
// hide the drawer
|
||||||
|
it(`hides ${drawer} drawer`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
||||||
|
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("viewing pod logs", () => {
|
||||||
|
beforeEach(appStartAddCluster, 40000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`shows a logs for a pod`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
// Go to Pods page
|
||||||
|
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
||||||
|
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||||
|
await app.client.click('a[href^="/pods"]');
|
||||||
|
await app.client.click(".NamespaceSelect");
|
||||||
|
await app.client.keys("kube-system");
|
||||||
|
await app.client.keys("Enter");// "\uE007"
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
||||||
|
let podMenuItemEnabled = false;
|
||||||
|
|
||||||
|
// Wait until extensions are enabled on renderer
|
||||||
|
while (!podMenuItemEnabled) {
|
||||||
|
const logs = await app.client.getRenderProcessLogs();
|
||||||
|
|
||||||
|
podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@"));
|
||||||
|
|
||||||
|
if (!podMenuItemEnabled) {
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions
|
||||||
|
// Open logs tab in dock
|
||||||
|
await app.client.click(".list .TableRow:first-child");
|
||||||
|
await app.client.waitForVisible(".Drawer");
|
||||||
|
await app.client.click(".drawer-title .Menu li:nth-child(2)");
|
||||||
|
// Check if controls are available
|
||||||
|
await app.client.waitForVisible(".LogList .VirtualList");
|
||||||
|
await app.client.waitForVisible(".LogResourceSelector");
|
||||||
|
//await app.client.waitForVisible(".LogSearch .SearchInput");
|
||||||
|
await app.client.waitForVisible(".LogSearch .SearchInput input");
|
||||||
|
// Search for semicolon
|
||||||
|
await app.client.keys(":");
|
||||||
|
await app.client.waitForVisible(".LogList .list span.active");
|
||||||
|
// Click through controls
|
||||||
|
await app.client.click(".LogControls .show-timestamps");
|
||||||
|
await app.client.click(".LogControls .show-previous");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cluster operations", () => {
|
||||||
|
beforeEach(appStartAddCluster, 40000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows default namespace", async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click('a[href="/namespaces"]');
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click('a[href="/namespaces"]');
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||||
|
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||||
|
await app.client.click("button.add-button");
|
||||||
|
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
|
||||||
|
await app.client.keys(`${TEST_NAMESPACE}\n`);
|
||||||
|
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
|
||||||
|
expect(clusterAdded).toBe(true);
|
||||||
|
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
||||||
|
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||||
|
await app.client.click('a[href^="/pods"]');
|
||||||
|
|
||||||
|
await app.client.click(".NamespaceSelect");
|
||||||
|
await app.client.keys(TEST_NAMESPACE);
|
||||||
|
await app.client.keys("Enter");// "\uE007"
|
||||||
|
await app.client.click(".Icon.new-dock-tab");
|
||||||
|
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
|
||||||
|
await app.client.click("li.MenuItem.create-resource-tab");
|
||||||
|
await app.client.waitForVisible(".CreateResource div.ace_content");
|
||||||
|
// Write pod manifest to editor
|
||||||
|
await app.client.keys("apiVersion: v1\n");
|
||||||
|
await app.client.keys("kind: Pod\n");
|
||||||
|
await app.client.keys("metadata:\n");
|
||||||
|
await app.client.keys(" name: nginx-create-pod-test\n");
|
||||||
|
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
|
||||||
|
await app.client.keys(`${BACKSPACE}spec:\n`);
|
||||||
|
await app.client.keys(" containers:\n");
|
||||||
|
await app.client.keys("- name: nginx-create-pod-test\n");
|
||||||
|
await app.client.keys(" image: nginx:alpine\n");
|
||||||
|
// Create deployment
|
||||||
|
await app.client.waitForEnabled("button.Button=Create & Close");
|
||||||
|
await app.client.click("button.Button=Create & Close");
|
||||||
|
// Wait until first bits of pod appears on dashboard
|
||||||
|
await app.client.waitForExist(".name=nginx-create-pod-test");
|
||||||
|
// Open pod details
|
||||||
|
await app.client.click(".name=nginx-create-pod-test");
|
||||||
|
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
25
integration/__tests__/command-palette.tests.ts
Normal file
25
integration/__tests__/command-palette.tests.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Application } from "spectron";
|
||||||
|
import * as utils from "../helpers/utils";
|
||||||
|
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
|
describe("Lens command palette", () => {
|
||||||
|
let app: Application;
|
||||||
|
|
||||||
|
describe("menu", () => {
|
||||||
|
beforeAll(async () => app = await utils.appStart(), 20000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (app?.isRunning()) {
|
||||||
|
await utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens command dialog from menu", async () => {
|
||||||
|
await utils.clickWhatsNew(app);
|
||||||
|
await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette...");
|
||||||
|
await app.client.waitUntilTextExists(".Select__option", "Preferences: Open");
|
||||||
|
await app.client.keys("Escape");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
75
integration/__tests__/workspace.tests.ts
Normal file
75
integration/__tests__/workspace.tests.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Application } from "spectron";
|
||||||
|
import * as utils from "../helpers/utils";
|
||||||
|
import { addMinikubeCluster, minikubeReady } from "../helpers/minikube";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import * as util from "util";
|
||||||
|
|
||||||
|
export const promiseExec = util.promisify(exec);
|
||||||
|
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
|
describe("Lens integration tests", () => {
|
||||||
|
let app: Application;
|
||||||
|
const ready = minikubeReady("workspace-int-tests");
|
||||||
|
|
||||||
|
utils.describeIf(ready)("workspaces", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await utils.appStart();
|
||||||
|
await utils.clickWhatsNew(app);
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
return utils.tearDown(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchToWorkspace = async (name: string) => {
|
||||||
|
await app.client.click("[data-test-id=current-workspace]");
|
||||||
|
await app.client.keys(name);
|
||||||
|
await app.client.keys("Enter");
|
||||||
|
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWorkspace = async (name: string) => {
|
||||||
|
await app.client.click("[data-test-id=current-workspace]");
|
||||||
|
await app.client.keys("add workspace");
|
||||||
|
await app.client.keys("Enter");
|
||||||
|
await app.client.keys(name);
|
||||||
|
await app.client.keys("Enter");
|
||||||
|
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("creates new workspace", async () => {
|
||||||
|
const name = "test-workspace";
|
||||||
|
|
||||||
|
await createWorkspace(name);
|
||||||
|
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edits current workspaces", async () => {
|
||||||
|
await createWorkspace("to-be-edited");
|
||||||
|
await app.client.click("[data-test-id=current-workspace]");
|
||||||
|
await app.client.keys("edit current workspace");
|
||||||
|
await app.client.keys("Enter");
|
||||||
|
await app.client.keys("edited-workspace");
|
||||||
|
await app.client.keys("Enter");
|
||||||
|
await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", "edited-workspace");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds cluster in default workspace", async () => {
|
||||||
|
await switchToWorkspace("default");
|
||||||
|
await addMinikubeCluster(app);
|
||||||
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
|
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds cluster in test-workspace", async () => {
|
||||||
|
await switchToWorkspace("test-workspace");
|
||||||
|
await addMinikubeCluster(app);
|
||||||
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
59
integration/helpers/minikube.ts
Normal file
59
integration/helpers/minikube.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { Application } from "spectron";
|
||||||
|
|
||||||
|
export function minikubeReady(testNamespace: string): boolean {
|
||||||
|
// determine if minikube is running
|
||||||
|
{
|
||||||
|
const { status } = spawnSync("minikube status", { shell: true });
|
||||||
|
|
||||||
|
if (status !== 0) {
|
||||||
|
console.warn("minikube not running");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove TEST_NAMESPACE if it already exists
|
||||||
|
{
|
||||||
|
const { status } = spawnSync(`minikube kubectl -- get namespace ${testNamespace}`, { shell: true });
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
console.warn(`Removing existing ${testNamespace} namespace`);
|
||||||
|
|
||||||
|
const { status, stdout, stderr } = spawnSync(
|
||||||
|
`minikube kubectl -- delete namespace ${testNamespace}`,
|
||||||
|
{ shell: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status !== 0) {
|
||||||
|
console.warn(`Error removing ${testNamespace} namespace: ${stderr.toString()}`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(stdout.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addMinikubeCluster(app: Application) {
|
||||||
|
await app.client.click("div.add-cluster");
|
||||||
|
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
||||||
|
await app.client.click("div.Select__control"); // show the context drop-down list
|
||||||
|
await app.client.waitUntilTextExists("div", "minikube");
|
||||||
|
|
||||||
|
if (!await app.client.$("button.primary").isEnabled()) {
|
||||||
|
await app.client.click("div.minikube"); // select minikube context
|
||||||
|
} // else the only context, which must be 'minikube', is automatically selected
|
||||||
|
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||||
|
await app.client.click("button.primary"); // add minikube cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForMinikubeDashboard(app: Application) {
|
||||||
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
|
await app.client.frame("minikube");
|
||||||
|
await app.client.waitUntilTextExists("span.link-text", "Cluster");
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { Application } from "spectron";
|
import { Application } from "spectron";
|
||||||
|
import * as util from "util";
|
||||||
|
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/kontena-lens",
|
"linux": "./dist/linux-unpacked/lens",
|
||||||
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,6 +28,28 @@ export function setup(): Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const keys = {
|
||||||
|
backspace: "\uE003"
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function appStart() {
|
||||||
|
const app = setup();
|
||||||
|
|
||||||
|
await app.start();
|
||||||
|
// Wait for splash screen to be closed
|
||||||
|
while (await app.client.getWindowCount() > 1);
|
||||||
|
await app.client.windowByIndex(0);
|
||||||
|
await app.client.waitUntilWindowLoaded();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickWhatsNew(app: Application) {
|
||||||
|
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||||
|
await app.client.click("button.primary");
|
||||||
|
await app.client.waitUntilTextExists("h1", "Welcome");
|
||||||
|
}
|
||||||
|
|
||||||
type AsyncPidGetter = () => Promise<number>;
|
type AsyncPidGetter = () => Promise<number>;
|
||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app: Application) {
|
||||||
@ -39,3 +63,26 @@ export async function tearDown(app: Application) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const promiseExec = util.promisify(exec);
|
||||||
|
|
||||||
|
type HelmRepository = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listHelmRepositories(retries = 0): Promise<HelmRepository[]>{
|
||||||
|
if (retries < 5) {
|
||||||
|
try {
|
||||||
|
const { stdout: reposJson } = await promiseExec("helm repo list -o json");
|
||||||
|
|
||||||
|
return JSON.parse(reposJson);
|
||||||
|
} catch {
|
||||||
|
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
|
||||||
|
|
||||||
|
return await listHelmRepositories((retries + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ nav:
|
|||||||
- Main Extension: extensions/guides/main-extension.md
|
- Main Extension: extensions/guides/main-extension.md
|
||||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||||
- Stores: extensions/guides/stores.md
|
- Stores: extensions/guides/stores.md
|
||||||
- Working with mobx: extensions/guides/working-with-mobx.md
|
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||||
- Testing and Publishing:
|
- Testing and Publishing:
|
||||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||||
@ -81,6 +81,8 @@ markdown_extensions:
|
|||||||
- toc:
|
- toc:
|
||||||
permalink: "#"
|
permalink: "#"
|
||||||
toc_depth: 3
|
toc_depth: 3
|
||||||
|
- admonition: {}
|
||||||
|
- pymdownx.details: {}
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
generator: false
|
generator: false
|
||||||
|
|||||||
31
package.json
31
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.0",
|
"version": "4.1.0-alpha.1",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
||||||
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
||||||
"test": "jest --env=jsdom src $@",
|
"test": "jest --env=jsdom src $@",
|
||||||
"integration": "jest --coverage integration $@",
|
"integration": "jest --runInBand integration",
|
||||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||||
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
|
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.17.15",
|
"bundledKubectlVersion": "1.18.15",
|
||||||
"bundledHelmVersion": "3.4.2"
|
"bundledHelmVersion": "3.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -103,7 +103,11 @@
|
|||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
|
"executableName": "lens",
|
||||||
|
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
||||||
"target": [
|
"target": [
|
||||||
|
"deb",
|
||||||
|
"rpm",
|
||||||
"snap",
|
"snap",
|
||||||
"AppImage"
|
"AppImage"
|
||||||
],
|
],
|
||||||
@ -154,7 +158,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"include": "build/installer.nsh"
|
"include": "build/installer.nsh",
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
},
|
},
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
@ -200,7 +206,7 @@
|
|||||||
"jsonpath": "^1.0.2",
|
"jsonpath": "^1.0.2",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mac-ca": "^1.0.4",
|
"mac-ca": "^1.0.4",
|
||||||
"marked": "^1.1.0",
|
"marked": "^1.2.7",
|
||||||
"md5-file": "^5.0.0",
|
"md5-file": "^5.0.0",
|
||||||
"mobx": "^5.15.7",
|
"mobx": "^5.15.7",
|
||||||
"mobx-observable-history": "^1.0.3",
|
"mobx-observable-history": "^1.0.3",
|
||||||
@ -219,12 +225,12 @@
|
|||||||
"request-promise-native": "^1.0.8",
|
"request-promise-native": "^1.0.8",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"serializr": "^2.0.3",
|
"serializr": "^2.0.3",
|
||||||
"shell-env": "^3.0.0",
|
"shell-env": "^3.0.1",
|
||||||
"spdy": "^4.0.2",
|
"spdy": "^4.0.2",
|
||||||
"tar": "^6.0.5",
|
"tar": "^6.0.5",
|
||||||
"tcp-port-used": "^1.0.1",
|
"tcp-port-used": "^1.0.1",
|
||||||
"tempy": "^0.5.0",
|
"tempy": "^0.5.0",
|
||||||
"uuid": "^8.1.0",
|
"uuid": "^8.3.2",
|
||||||
"win-ca": "^3.2.0",
|
"win-ca": "^3.2.0",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.2.1",
|
||||||
"winston-transport-browserconsole": "^1.0.5",
|
"winston-transport-browserconsole": "^1.0.5",
|
||||||
@ -244,7 +250,7 @@
|
|||||||
"@types/electron-devtools-installer": "^2.2.0",
|
"@types/electron-devtools-installer": "^2.2.0",
|
||||||
"@types/electron-window-state": "^2.0.34",
|
"@types/electron-window-state": "^2.0.34",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.1",
|
||||||
"@types/hapi": "^18.0.3",
|
"@types/hapi": "^18.0.5",
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
"@types/html-webpack-plugin": "^3.2.3",
|
||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
@ -279,18 +285,18 @@
|
|||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/terser-webpack-plugin": "^3.0.0",
|
"@types/terser-webpack-plugin": "^3.0.0",
|
||||||
"@types/universal-analytics": "^0.4.4",
|
"@types/universal-analytics": "^0.4.4",
|
||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/webdriverio": "^4.13.0",
|
"@types/webdriverio": "^4.13.0",
|
||||||
"@types/webpack": "^4.41.17",
|
"@types/webpack": "^4.41.17",
|
||||||
"@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.0.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",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^2.9.3",
|
||||||
"circular-dependency-plugin": "^5.2.0",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"css-element-queries": "^1.2.3",
|
"css-element-queries": "^1.2.3",
|
||||||
@ -313,7 +319,7 @@
|
|||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.1",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
@ -328,6 +334,7 @@
|
|||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "^3.1.0",
|
||||||
|
"react-select-event": "^5.1.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.26.1",
|
"sharp": "^0.26.1",
|
||||||
|
|||||||
@ -36,6 +36,13 @@ describe("workspace store tests", () => {
|
|||||||
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
|
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("default workspace should be enabled", () => {
|
||||||
|
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||||
|
|
||||||
|
expect(ws.workspaces.size).toBe(1);
|
||||||
|
expect(ws.getById(WorkspaceStore.defaultId).enabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("cannot remove the default workspace", () => {
|
it("cannot remove the default workspace", () => {
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||||
|
|
||||||
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
||||||
|
import { toJS } from "mobx";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames";
|
||||||
|
|
||||||
|
const subFramesChannel = "ipc:get-sub-frames";
|
||||||
|
|
||||||
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
||||||
ipcMain.handle(channel, listener);
|
ipcMain.handle(channel, listener);
|
||||||
}
|
}
|
||||||
@ -14,38 +17,39 @@ export async function requestMain(channel: string, ...args: any[]) {
|
|||||||
return ipcRenderer.invoke(channel, ...args);
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubFrames(): Promise<ClusterFrameInfo[]> {
|
function getSubFrames(): ClusterFrameInfo[] {
|
||||||
const subFrames: ClusterFrameInfo[] = [];
|
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
|
||||||
|
|
||||||
clusterFrameMap.forEach(frameInfo => {
|
|
||||||
subFrames.push(frameInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
return subFrames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
export async function broadcastMessage(channel: string, ...args: any[]) {
|
||||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||||
|
|
||||||
if (!views) return;
|
if (!views) return;
|
||||||
|
|
||||||
views.forEach(webContent => {
|
|
||||||
const type = webContent.getType();
|
|
||||||
|
|
||||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
|
||||||
webContent.send(channel, ...args);
|
|
||||||
getSubFrames().then((frames) => {
|
|
||||||
frames.map((frameInfo) => {
|
|
||||||
webContent.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
|
||||||
});
|
|
||||||
}).catch((e) => e);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
} else {
|
} else {
|
||||||
ipcMain.emit(channel, ...args);
|
ipcMain.emit(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
const type = view.getType();
|
||||||
|
|
||||||
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args });
|
||||||
|
view.send(channel, ...args);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subFrames: ClusterFrameInfo[] = ipcRenderer
|
||||||
|
? await requestMain(subFramesChannel)
|
||||||
|
: getSubFrames();
|
||||||
|
|
||||||
|
for (const frameInfo of subFrames) {
|
||||||
|
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[IPC]: failed to send IPC message", { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
|
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
|
||||||
@ -73,3 +77,9 @@ export function unsubscribeAllFromBroadcast(channel: string) {
|
|||||||
ipcMain.removeAllListeners(channel);
|
ipcMain.removeAllListeners(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bindBroadcastHandlers() {
|
||||||
|
handleRequest(subFramesChannel, () => {
|
||||||
|
return getSubFrames();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,42 +1,44 @@
|
|||||||
import { getHostedCluster } from "./cluster-store";
|
import { getHostedCluster } from "./cluster-store";
|
||||||
|
|
||||||
export type KubeResource =
|
export type KubeResource =
|
||||||
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" |
|
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
|
||||||
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
|
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
|
||||||
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
|
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
|
||||||
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
||||||
|
|
||||||
export interface KubeApiResource {
|
export interface KubeApiResource {
|
||||||
resource: KubeResource; // valid resource name
|
kind: string; // resource type (e.g. "Namespace")
|
||||||
|
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
|
||||||
group?: string; // api-group
|
group?: string; // api-group
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
||||||
export const apiResources: KubeApiResource[] = [
|
export const apiResources: KubeApiResource[] = [
|
||||||
{ resource: "configmaps" },
|
{ kind: "ConfigMap", apiName: "configmaps" },
|
||||||
{ resource: "cronjobs", group: "batch" },
|
{ kind: "CronJob", apiName: "cronjobs", group: "batch" },
|
||||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
{ kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||||
{ resource: "daemonsets", group: "apps" },
|
{ kind: "DaemonSet", apiName: "daemonsets", group: "apps" },
|
||||||
{ resource: "deployments", group: "apps" },
|
{ kind: "Deployment", apiName: "deployments", group: "apps" },
|
||||||
{ resource: "endpoints" },
|
{ kind: "Endpoint", apiName: "endpoints" },
|
||||||
{ resource: "events" },
|
{ kind: "Event", apiName: "events" },
|
||||||
{ resource: "horizontalpodautoscalers" },
|
{ kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" },
|
||||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
{ kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" },
|
||||||
{ resource: "jobs", group: "batch" },
|
{ kind: "Job", apiName: "jobs", group: "batch" },
|
||||||
{ resource: "namespaces" },
|
{ kind: "Namespace", apiName: "namespaces" },
|
||||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
{ kind: "LimitRange", apiName: "limitranges" },
|
||||||
{ resource: "nodes" },
|
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
|
||||||
{ resource: "persistentvolumes" },
|
{ kind: "Node", apiName: "nodes" },
|
||||||
{ resource: "persistentvolumeclaims" },
|
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
||||||
{ resource: "pods" },
|
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
||||||
{ resource: "poddisruptionbudgets" },
|
{ kind: "Pod", apiName: "pods" },
|
||||||
{ resource: "podsecuritypolicies" },
|
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" },
|
||||||
{ resource: "resourcequotas" },
|
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
||||||
{ resource: "replicasets", group: "apps" },
|
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
||||||
{ resource: "secrets" },
|
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
||||||
{ resource: "services" },
|
{ kind: "Secret", apiName: "secrets" },
|
||||||
{ resource: "statefulsets", group: "apps" },
|
{ kind: "Service", apiName: "services" },
|
||||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
|
||||||
|
{ kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { action, computed, observable } from "mobx";
|
import { action, computed, observable,reaction } from "mobx";
|
||||||
|
import { dockStore } from "../renderer/components/dock/dock.store";
|
||||||
import { autobind } from "../renderer/utils";
|
import { autobind } from "../renderer/utils";
|
||||||
|
|
||||||
export class SearchStore {
|
export class SearchStore {
|
||||||
@ -6,6 +7,12 @@ export class SearchStore {
|
|||||||
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
|
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
|
||||||
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
|
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
reaction(() => dockStore.selectedTabId, () => {
|
||||||
|
searchStore.reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets default activeOverlayIndex
|
* Sets default activeOverlayIndex
|
||||||
* @param text An array of any textual data (logs, for example)
|
* @param text An array of any textual data (logs, for example)
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export interface UserPreferences {
|
|||||||
downloadBinariesPath?: string;
|
downloadBinariesPath?: string;
|
||||||
kubectlBinariesPath?: string;
|
kubectlBinariesPath?: string;
|
||||||
openAtLogin?: boolean;
|
openAtLogin?: boolean;
|
||||||
|
hiddenTableColumns?: Record<string, string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserStore extends BaseStore<UserStoreModel> {
|
export class UserStore extends BaseStore<UserStoreModel> {
|
||||||
@ -54,6 +55,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
downloadMirror: "default",
|
downloadMirror: "default",
|
||||||
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
||||||
openAtLogin: false,
|
openAtLogin: false,
|
||||||
|
hiddenTableColumns: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async handleOnLoad() {
|
protected async handleOnLoad() {
|
||||||
@ -82,6 +84,15 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setHiddenTableColumns(tableId: string, names: Set<string> | string[]) {
|
||||||
|
this.preferences.hiddenTableColumns[tableId] = Array.from(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiddenTableColumns(tableId: string): Set<string> {
|
||||||
|
return new Set(this.preferences.hiddenTableColumns[tableId]);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetKubeConfigPath() {
|
resetKubeConfigPath() {
|
||||||
this.kubeConfigPath = kubeConfigDefaultPath;
|
this.kubeConfigPath = kubeConfigDefaultPath;
|
||||||
|
|||||||
6
src/common/utils/delay.ts
Normal file
6
src/common/utils/delay.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Create async delay for provided timeout in milliseconds
|
||||||
|
|
||||||
|
export async function delay(timeoutMs = 1000) {
|
||||||
|
if (!timeoutMs) return;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, timeoutMs));
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ export * from "./autobind";
|
|||||||
export * from "./base64";
|
export * from "./base64";
|
||||||
export * from "./camelCase";
|
export * from "./camelCase";
|
||||||
export * from "./cloneJson";
|
export * from "./cloneJson";
|
||||||
|
export * from "./delay";
|
||||||
export * from "./debouncePromise";
|
export * from "./debouncePromise";
|
||||||
export * from "./defineGlobal";
|
export * from "./defineGlobal";
|
||||||
export * from "./getRandId";
|
export * from "./getRandId";
|
||||||
|
|||||||
@ -58,14 +58,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable ownerRef?: string;
|
@observable ownerRef?: string;
|
||||||
/**
|
|
||||||
* Is workspace enabled
|
|
||||||
*
|
|
||||||
* Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable enabled: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Last active cluster id
|
* Last active cluster id
|
||||||
*
|
*
|
||||||
@ -73,6 +66,9 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
*/
|
*/
|
||||||
@observable lastActiveClusterId?: ClusterId;
|
@observable lastActiveClusterId?: ClusterId;
|
||||||
|
|
||||||
|
|
||||||
|
@observable private _enabled: boolean;
|
||||||
|
|
||||||
constructor(data: WorkspaceModel) {
|
constructor(data: WorkspaceModel) {
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
|
|
||||||
@ -83,6 +79,21 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is workspace enabled
|
||||||
|
*
|
||||||
|
* Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
get enabled(): boolean {
|
||||||
|
return !this.isManaged || this._enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
set enabled(enabled: boolean) {
|
||||||
|
this._enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is workspace managed by an extension
|
* Is workspace managed by an extension
|
||||||
*/
|
*/
|
||||||
@ -134,10 +145,18 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
static readonly defaultId: WorkspaceId = "default";
|
static readonly defaultId: WorkspaceId = "default";
|
||||||
private static stateRequestChannel = "workspace:states";
|
private static stateRequestChannel = "workspace:states";
|
||||||
|
|
||||||
|
@observable currentWorkspaceId = WorkspaceStore.defaultId;
|
||||||
|
@observable workspaces = observable.map<WorkspaceId, Workspace>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-workspace-store",
|
configName: "lens-workspace-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.workspaces.set(WorkspaceStore.defaultId, new Workspace({
|
||||||
|
id: WorkspaceStore.defaultId,
|
||||||
|
name: "default"
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -186,15 +205,6 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
ipcRenderer.removeAllListeners("workspace:state");
|
ipcRenderer.removeAllListeners("workspace:state");
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable currentWorkspaceId = WorkspaceStore.defaultId;
|
|
||||||
|
|
||||||
@observable workspaces = observable.map<WorkspaceId, Workspace>({
|
|
||||||
[WorkspaceStore.defaultId]: new Workspace({
|
|
||||||
id: WorkspaceStore.defaultId,
|
|
||||||
name: "default"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
@computed get currentWorkspace(): Workspace {
|
@computed get currentWorkspace(): Workspace {
|
||||||
return this.getById(this.currentWorkspaceId);
|
return this.getById(this.currentWorkspaceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPage
|
|||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import { CommandRegistration } from "./registries/command-registry";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: PageRegistration[] = [];
|
globalPages: PageRegistration[] = [];
|
||||||
@ -14,6 +15,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||||
|
commands: CommandRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
37
src/extensions/registries/command-registry.ts
Normal file
37
src/extensions/registries/command-registry.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Extensions API -> Commands
|
||||||
|
|
||||||
|
import type { Cluster } from "../../main/cluster";
|
||||||
|
import type { Workspace } from "../../common/workspace-store";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
import { action } from "mobx";
|
||||||
|
import { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
|
export type CommandContext = {
|
||||||
|
cluster?: Cluster;
|
||||||
|
workspace?: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CommandRegistration {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
scope: "cluster" | "global";
|
||||||
|
action: (context: CommandContext) => void;
|
||||||
|
isActive?: (context: CommandContext) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
||||||
|
@action
|
||||||
|
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
||||||
|
const itemArray = [items].flat();
|
||||||
|
|
||||||
|
const newIds = itemArray.map((item) => item.id);
|
||||||
|
const currentIds = this.getItems().map((item) => item.id);
|
||||||
|
|
||||||
|
const filteredIds = newIds.filter((id) => !currentIds.includes(id));
|
||||||
|
const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id));
|
||||||
|
|
||||||
|
return super.add(filteredItems, extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandRegistry = new CommandRegistry();
|
||||||
@ -13,6 +13,9 @@ export * from "../../renderer/components/select";
|
|||||||
export * from "../../renderer/components/slider";
|
export * from "../../renderer/components/slider";
|
||||||
export * from "../../renderer/components/input/input";
|
export * from "../../renderer/components/input/input";
|
||||||
|
|
||||||
|
// command-overlay
|
||||||
|
export { CommandOverlay } from "../../renderer/components/command-palette";
|
||||||
|
|
||||||
// other components
|
// other components
|
||||||
export * from "../../renderer/components/icon";
|
export * from "../../renderer/components/icon";
|
||||||
export * from "../../renderer/components/tooltip";
|
export * from "../../renderer/components/tooltip";
|
||||||
@ -38,4 +41,4 @@ export * from "../../renderer/components/+events/kube-event-details";
|
|||||||
// specific exports
|
// specific exports
|
||||||
export * from "../../renderer/components/status-brick";
|
export * from "../../renderer/components/status-brick";
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
||||||
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
|
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
|
|||||||
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
|
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
|
||||||
export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
|
export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
|
||||||
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
|
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
|
||||||
|
export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints";
|
||||||
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
|
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
|
||||||
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
|
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
|
||||||
export { Service, serviceApi } from "../../renderer/api/endpoints";
|
export { Service, serviceApi } from "../../renderer/api/endpoints";
|
||||||
@ -46,6 +47,7 @@ export type { ConfigMapsStore } from "../../renderer/components/+config-maps/con
|
|||||||
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
|
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
|
||||||
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
|
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
|
||||||
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
|
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
|
||||||
|
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store";
|
||||||
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
|
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
|
||||||
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
|
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
|
||||||
export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
|
export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun, reaction } from "mobx";
|
||||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -12,14 +12,14 @@ export class ClusterManager extends Singleton {
|
|||||||
constructor(public readonly port: number) {
|
constructor(public readonly port: number) {
|
||||||
super();
|
super();
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
autorun(() => {
|
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
||||||
clusterStore.enabledClustersList.forEach(cluster => {
|
clusters.forEach((cluster) => {
|
||||||
if (!cluster.initialized) {
|
if (!cluster.initialized && !cluster.initializing) {
|
||||||
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
||||||
cluster.init(port);
|
cluster.init(port);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
|
|||||||
@ -84,6 +84,14 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
whenInitialized = when(() => this.initialized);
|
whenInitialized = when(() => this.initialized);
|
||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is cluster object initializinng on-going
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable initializing = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initialized
|
* Is cluster object initialized
|
||||||
*
|
*
|
||||||
@ -182,7 +190,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable metadata: ClusterMetadata = {};
|
@observable metadata: ClusterMetadata = {};
|
||||||
/**
|
/**
|
||||||
* List of allowed namespaces
|
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -195,7 +203,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
/**
|
/**
|
||||||
* List of accessible namespaces
|
* List of accessible namespaces provided by user in the Cluster Settings
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -216,7 +224,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @computed
|
* @computed
|
||||||
*/
|
*/
|
||||||
@computed get name() {
|
@computed get name() {
|
||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -271,8 +279,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @param port port where internal auth proxy is listening
|
* @param port port where internal auth proxy is listening
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async init(port: number) {
|
@action
|
||||||
|
async init(port: number) {
|
||||||
try {
|
try {
|
||||||
|
this.initializing = true;
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
||||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||||
@ -287,6 +297,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
error: err,
|
error: err,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
this.initializing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +335,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @param force force activation
|
* @param force force activation
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async activate(force = false) {
|
@action
|
||||||
|
async activate(force = false) {
|
||||||
if (this.activated && !force) {
|
if (this.activated && !force) {
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
@ -362,7 +375,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async reconnect() {
|
@action
|
||||||
|
async reconnect() {
|
||||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
await this.contextHandler?.ensureServer();
|
await this.contextHandler?.ensureServer();
|
||||||
@ -389,7 +403,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
* @param opts refresh options
|
* @param opts refresh options
|
||||||
*/
|
*/
|
||||||
@action async refresh(opts: ClusterRefreshOptions = {}) {
|
@action
|
||||||
|
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
@ -409,7 +424,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshMetadata() {
|
@action
|
||||||
|
async refreshMetadata() {
|
||||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const metadata = await detectorRegistry.detectForCluster(this);
|
const metadata = await detectorRegistry.detectForCluster(this);
|
||||||
const existingMetadata = this.metadata;
|
const existingMetadata = this.metadata;
|
||||||
@ -420,7 +436,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshConnectionStatus() {
|
@action
|
||||||
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
@ -430,7 +447,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshAllowedResources() {
|
@action
|
||||||
|
async refreshAllowedResources() {
|
||||||
this.allowedNamespaces = await this.getAllowedNamespaces();
|
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||||
this.allowedResources = await this.getAllowedResources();
|
this.allowedResources = await this.getAllowedResources();
|
||||||
}
|
}
|
||||||
@ -657,7 +675,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
||||||
if (!this.resourceAccessStatuses.get(apiResource)) {
|
if (!this.resourceAccessStatuses.get(apiResource)) {
|
||||||
const result = await this.canI({
|
const result = await this.canI({
|
||||||
resource: apiResource.resource,
|
resource: apiResource.apiName,
|
||||||
group: apiResource.group,
|
group: apiResource.group,
|
||||||
verb: "list",
|
verb: "list",
|
||||||
namespace
|
namespace
|
||||||
@ -672,9 +690,19 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
||||||
.map(apiResource => apiResource.resource);
|
.map(apiResource => apiResource.apiName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAllowedResource(kind: string): boolean {
|
||||||
|
const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind);
|
||||||
|
|
||||||
|
if (apiResource) {
|
||||||
|
return this.allowedResources.includes(apiResource.apiName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // allowed by default for other resources
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,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 { bindBroadcastHandlers } from "../common/ipc";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -63,6 +64,8 @@ app.on("ready", async () => {
|
|||||||
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
|
bindBroadcastHandlers();
|
||||||
|
|
||||||
powerMonitor.on("shutdown", () => {
|
powerMonitor.on("shutdown", () => {
|
||||||
app.exit();
|
app.exit();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,10 +23,10 @@ const kubectlMap: Map<string, string> = new Map([
|
|||||||
["1.14", "1.14.10"],
|
["1.14", "1.14.10"],
|
||||||
["1.15", "1.15.11"],
|
["1.15", "1.15.11"],
|
||||||
["1.16", "1.16.15"],
|
["1.16", "1.16.15"],
|
||||||
["1.17", bundledVersion],
|
["1.17", "1.17.17"],
|
||||||
["1.18", "1.18.14"],
|
["1.18", bundledVersion],
|
||||||
["1.19", "1.19.5"],
|
["1.19", "1.19.7"],
|
||||||
["1.20", "1.20.0"]
|
["1.20", "1.20.2"]
|
||||||
]);
|
]);
|
||||||
const packageMirrors: Map<string, string> = new Map([
|
const packageMirrors: Map<string, string> = new Map([
|
||||||
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { extensionsURL } from "../renderer/components/+extensions/extensions.rou
|
|||||||
import { menuRegistry } from "../extensions/registries/menu-registry";
|
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
|
import { broadcastMessage } from "../common/ipc";
|
||||||
|
|
||||||
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
||||||
|
|
||||||
@ -173,6 +174,14 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemConstructorOptions = {
|
||||||
label: "View",
|
label: "View",
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Command Palette...",
|
||||||
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
|
click() {
|
||||||
|
broadcastMessage("command-palette:open");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Back",
|
label: "Back",
|
||||||
accelerator: "CmdOrCtrl+[",
|
accelerator: "CmdOrCtrl+[",
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export class Router {
|
|||||||
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
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
|
this.router.add({ method: "post", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
|
||||||
|
|
||||||
// Metrics API
|
// Metrics API
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
|
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
|
||||||
|
|||||||
@ -1,10 +1,29 @@
|
|||||||
|
import type { KubeJsonApiData, KubeJsonApiError } from "../../renderer/api/kube-json-api";
|
||||||
|
|
||||||
|
import plimit from "p-limit";
|
||||||
|
import { delay } from "../../common/utils";
|
||||||
import { LensApiRequest } from "../router";
|
import { LensApiRequest } from "../router";
|
||||||
import { LensApi } from "../lens-api";
|
import { LensApi } from "../lens-api";
|
||||||
import { Watch, KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig, Watch } from "@kubernetes/client-node";
|
||||||
import { ServerResponse } from "http";
|
import { ServerResponse } from "http";
|
||||||
import { Request } from "request";
|
import { Request } from "request";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
|
||||||
|
export interface IKubeWatchEvent<T = KubeJsonApiData | KubeJsonApiError> {
|
||||||
|
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR" | "STREAM_END";
|
||||||
|
object?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKubeWatchEventStreamEnd extends IKubeWatchEvent {
|
||||||
|
type: "STREAM_END";
|
||||||
|
url: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWatchRoutePayload {
|
||||||
|
apis: string[]; // kube-api url list for subscribing to watch events
|
||||||
|
}
|
||||||
|
|
||||||
class ApiWatcher {
|
class ApiWatcher {
|
||||||
private apiUrl: string;
|
private apiUrl: string;
|
||||||
private response: ServerResponse;
|
private response: ServerResponse;
|
||||||
@ -24,6 +43,7 @@ class ApiWatcher {
|
|||||||
clearInterval(this.processor);
|
clearInterval(this.processor);
|
||||||
}
|
}
|
||||||
this.processor = setInterval(() => {
|
this.processor = setInterval(() => {
|
||||||
|
if (this.response.finished) return;
|
||||||
const events = this.eventBuffer.splice(0);
|
const events = this.eventBuffer.splice(0);
|
||||||
|
|
||||||
events.map(event => this.sendEvent(event));
|
events.map(event => this.sendEvent(event));
|
||||||
@ -33,7 +53,9 @@ class ApiWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
if (!this.watchRequest) { return; }
|
if (!this.watchRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.processor) {
|
if (this.processor) {
|
||||||
clearInterval(this.processor);
|
clearInterval(this.processor);
|
||||||
@ -42,11 +64,14 @@ class ApiWatcher {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.watchRequest.abort();
|
this.watchRequest.abort();
|
||||||
this.sendEvent({
|
|
||||||
|
const event: IKubeWatchEventStreamEnd = {
|
||||||
type: "STREAM_END",
|
type: "STREAM_END",
|
||||||
url: this.apiUrl,
|
url: this.apiUrl,
|
||||||
status: 410,
|
status: 410,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.sendEvent(event);
|
||||||
logger.debug("watch aborted");
|
logger.debug("watch aborted");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Watch abort errored:${error}`);
|
logger.error(`Watch abort errored:${error}`);
|
||||||
@ -65,50 +90,72 @@ class ApiWatcher {
|
|||||||
this.watchRequest.abort();
|
this.watchRequest.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendEvent(evt: any) {
|
private sendEvent(evt: IKubeWatchEvent) {
|
||||||
// convert to "text/event-stream" format
|
this.response.write(`${JSON.stringify(evt)}\n`);
|
||||||
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WatchRoute extends LensApi {
|
class WatchRoute extends LensApi {
|
||||||
|
private response: ServerResponse;
|
||||||
|
|
||||||
public async routeWatch(request: LensApiRequest) {
|
private setResponse(response: ServerResponse) {
|
||||||
const { response, cluster} = request;
|
// clean up previous connection and stop all corresponding watch-api requests
|
||||||
const apis: string[] = request.query.getAll("api");
|
// otherwise it happens only by request timeout or something else..
|
||||||
const watchers: ApiWatcher[] = [];
|
this.response?.destroy();
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
if (!apis.length) {
|
public async routeWatch(request: LensApiRequest<IWatchRoutePayload>) {
|
||||||
|
const { response, cluster, payload: { apis } = {} } = request;
|
||||||
|
|
||||||
|
if (!apis?.length) {
|
||||||
this.respondJson(response, {
|
this.respondJson(response, {
|
||||||
message: "Empty request. Query params 'api' are not provided.",
|
message: "watch apis list is empty"
|
||||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.setHeader("Content-Type", "text/event-stream");
|
this.setResponse(response);
|
||||||
|
response.setHeader("Content-Type", "application/json");
|
||||||
response.setHeader("Cache-Control", "no-cache");
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
response.setHeader("Connection", "keep-alive");
|
response.setHeader("Connection", "keep-alive");
|
||||||
logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`);
|
logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`);
|
||||||
|
|
||||||
|
// limit concurrent k8s requests to avoid possible ECONNRESET-error
|
||||||
|
const requests = plimit(5);
|
||||||
|
const watchers = new Map<string, ApiWatcher>();
|
||||||
|
let isWatchRequestEnded = false;
|
||||||
|
|
||||||
apis.forEach(apiUrl => {
|
apis.forEach(apiUrl => {
|
||||||
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
|
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
|
||||||
|
|
||||||
watcher.start();
|
watchers.set(apiUrl, watcher);
|
||||||
watchers.push(watcher);
|
|
||||||
|
requests(async () => {
|
||||||
|
if (isWatchRequestEnded) return;
|
||||||
|
await watcher.start();
|
||||||
|
await delay(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function onRequestEnd() {
|
||||||
|
if (isWatchRequestEnded) return;
|
||||||
|
isWatchRequestEnded = true;
|
||||||
|
requests.clearQueue();
|
||||||
|
watchers.forEach(watcher => watcher.stop());
|
||||||
|
watchers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
request.raw.req.on("end", () => {
|
||||||
|
logger.info("Watch request end");
|
||||||
|
onRequestEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
request.raw.req.on("close", () => {
|
request.raw.req.on("close", () => {
|
||||||
logger.debug("Watch request closed");
|
logger.info("Watch request close");
|
||||||
watchers.map(watcher => watcher.stop());
|
onRequestEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
request.raw.req.on("end", () => {
|
|
||||||
logger.debug("Watch request ended");
|
|
||||||
watchers.map(watcher => watcher.stop());
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { workspaceStore } from "../common/workspace-store";
|
|||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { isDevelopment } from "../common/vars";
|
import { isDevelopment, isWindows } from "../common/vars";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
|
|
||||||
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
||||||
@ -29,7 +29,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
try {
|
try {
|
||||||
const menu = createTrayMenu(windowManager);
|
const menu = createTrayMenu(windowManager);
|
||||||
|
|
||||||
buildTray(getTrayIcon(), menu);
|
buildTray(getTrayIcon(), menu, windowManager);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[TRAY]: building failed: ${err}`);
|
logger.error(`[TRAY]: building failed: ${err}`);
|
||||||
}
|
}
|
||||||
@ -42,31 +42,26 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTray(icon: string | NativeImage, menu: Menu) {
|
function buildTray(icon: string | NativeImage, menu: Menu, windowManager: WindowManager) {
|
||||||
if (!tray) {
|
if (!tray) {
|
||||||
tray = new Tray(icon);
|
tray = new Tray(icon);
|
||||||
tray.setToolTip(packageInfo.description);
|
tray.setToolTip(packageInfo.description);
|
||||||
tray.setIgnoreDoubleClickEvents(true);
|
tray.setIgnoreDoubleClickEvents(true);
|
||||||
}
|
|
||||||
|
|
||||||
tray.setImage(icon);
|
tray.setImage(icon);
|
||||||
tray.setContextMenu(menu);
|
tray.setContextMenu(menu);
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
tray.on("click", () => {
|
||||||
|
windowManager.ensureMainWindow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tray;
|
return tray;
|
||||||
}
|
}
|
||||||
|
|
||||||
export 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() {
|
||||||
@ -119,6 +114,15 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { KubeObjectStore } from "../kube-object.store";
|
|||||||
|
|
||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { autobind } from "../utils";
|
import { autobind } from "../utils";
|
||||||
import { KubeApi } from "./kube-api";
|
import { KubeApi, parseKubeApi } from "./kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ApiManager {
|
export class ApiManager {
|
||||||
@ -11,7 +11,7 @@ export class ApiManager {
|
|||||||
|
|
||||||
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
||||||
if (typeof pathOrCallback === "string") {
|
if (typeof pathOrCallback === "string") {
|
||||||
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
|
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true));
|
return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true));
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export class HelmChart {
|
|||||||
tillerVersion?: string;
|
tillerVersion?: string;
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
return this.digest;
|
return `${this.apiVersion}/${this.name}@${this.getAppVersion()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export * from "./events.api";
|
|||||||
export * from "./hpa.api";
|
export * from "./hpa.api";
|
||||||
export * from "./ingress.api";
|
export * from "./ingress.api";
|
||||||
export * from "./job.api";
|
export * from "./job.api";
|
||||||
|
export * from "./limit-range.api";
|
||||||
export * from "./namespaces.api";
|
export * from "./namespaces.api";
|
||||||
export * from "./network-policy.api";
|
export * from "./network-policy.api";
|
||||||
export * from "./nodes.api";
|
export * from "./nodes.api";
|
||||||
|
|||||||
57
src/renderer/api/endpoints/limit-range.api.ts
Normal file
57
src/renderer/api/endpoints/limit-range.api.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { KubeObject } from "../kube-object";
|
||||||
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
|
||||||
|
export enum LimitType {
|
||||||
|
CONTAINER = "Container",
|
||||||
|
POD = "Pod",
|
||||||
|
PVC = "PersistentVolumeClaim",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Resource {
|
||||||
|
MEMORY = "memory",
|
||||||
|
CPU = "cpu",
|
||||||
|
STORAGE = "storage",
|
||||||
|
EPHEMERAL_STORAGE = "ephemeral-storage",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LimitPart {
|
||||||
|
MAX = "max",
|
||||||
|
MIN = "min",
|
||||||
|
DEFAULT = "default",
|
||||||
|
DEFAULT_REQUEST = "defaultRequest",
|
||||||
|
MAX_LIMIT_REQUEST_RATIO = "maxLimitRequestRatio",
|
||||||
|
}
|
||||||
|
|
||||||
|
type LimitRangeParts = Partial<Record<LimitPart, Record<string, string>>>;
|
||||||
|
|
||||||
|
export interface LimitRangeItem extends LimitRangeParts {
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
export class LimitRange extends KubeObject {
|
||||||
|
static kind = "LimitRange";
|
||||||
|
static namespaced = true;
|
||||||
|
static apiBase = "/api/v1/limitranges";
|
||||||
|
|
||||||
|
spec: {
|
||||||
|
limits: LimitRangeItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
getContainerLimits() {
|
||||||
|
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPodLimits() {
|
||||||
|
return this.spec.limits.filter(limit => limit.type === LimitType.POD);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPVCLimits() {
|
||||||
|
return this.spec.limits.filter(limit => limit.type === LimitType.PVC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const limitRangeApi = new KubeApi({
|
||||||
|
objectConstructor: LimitRange,
|
||||||
|
});
|
||||||
@ -78,11 +78,25 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
|||||||
result.forEach(res => {
|
result.forEach(res => {
|
||||||
if (!res.values || !res.values.length) return;
|
if (!res.values || !res.values.length) return;
|
||||||
|
|
||||||
|
let now = moment().startOf("minute").subtract(1, "minute").unix();
|
||||||
|
let timestamp = res.values[0][0];
|
||||||
|
|
||||||
|
while (timestamp <= now) {
|
||||||
|
timestamp = moment.unix(timestamp).add(1, "minute").unix();
|
||||||
|
|
||||||
|
if (!res.values.find((value) => value[0] === timestamp)) {
|
||||||
|
res.values.push([timestamp, "0"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (res.values.length < frames) {
|
while (res.values.length < frames) {
|
||||||
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
|
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
|
||||||
|
|
||||||
|
if (!res.values.find((value) => value[0] === timestamp)) {
|
||||||
res.values.unshift([timestamp, "0"]);
|
res.values.unshift([timestamp, "0"]);
|
||||||
}
|
}
|
||||||
|
now = timestamp;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,9 @@ export interface IPodContainer {
|
|||||||
configMapRef?: {
|
configMapRef?: {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
secretRef?: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
}[];
|
}[];
|
||||||
volumeMounts?: {
|
volumeMounts?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const resourceApplierApi = {
|
|||||||
.post<KubeJsonApiData[]>("/stack", { data: resource })
|
.post<KubeJsonApiData[]>("/stack", { data: resource })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const items = data.map(obj => {
|
const items = data.map(obj => {
|
||||||
const api = apiManager.getApi(obj.metadata.selfLink);
|
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
|
||||||
|
|
||||||
if (api) {
|
if (api) {
|
||||||
return new api.objectConstructor(obj);
|
return new api.objectConstructor(obj);
|
||||||
|
|||||||
@ -92,14 +92,6 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class KubeApi<T extends KubeObject = any> {
|
export class KubeApi<T extends KubeObject = any> {
|
||||||
static parseApi = parseKubeApi;
|
|
||||||
|
|
||||||
static watchAll(...apis: KubeApi[]) {
|
|
||||||
const disposers = apis.map(api => api.watch());
|
|
||||||
|
|
||||||
return () => disposers.forEach(unwatch => unwatch());
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly kind: string;
|
readonly kind: string;
|
||||||
readonly apiBase: string;
|
readonly apiBase: string;
|
||||||
readonly apiPrefix: string;
|
readonly apiPrefix: string;
|
||||||
@ -124,7 +116,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
if (!options.apiBase) {
|
if (!options.apiBase) {
|
||||||
options.apiBase = objectConstructor.apiBase;
|
options.apiBase = objectConstructor.apiBase;
|
||||||
}
|
}
|
||||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase);
|
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase);
|
||||||
|
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
this.isNamespaced = isNamespaced;
|
this.isNamespaced = isNamespaced;
|
||||||
@ -157,7 +149,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
|
|
||||||
for (const apiUrl of apiBases) {
|
for (const apiUrl of apiBases) {
|
||||||
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
||||||
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl);
|
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl);
|
||||||
|
|
||||||
// Request available resources
|
// Request available resources
|
||||||
try {
|
try {
|
||||||
@ -366,7 +358,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(): () => void {
|
watch(): () => void {
|
||||||
return kubeWatchApi.subscribe(this);
|
return kubeWatchApi.subscribeApi(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export interface KubeJsonApiData extends JsonApiData {
|
|||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
continue?: string;
|
continue?: string;
|
||||||
finalizers?: string[];
|
finalizers?: string[];
|
||||||
selfLink: string;
|
selfLink?: string;
|
||||||
labels?: {
|
labels?: {
|
||||||
[label: string]: string;
|
[label: string]: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,183 +1,348 @@
|
|||||||
// Kubernetes watch-api consumer
|
// Kubernetes watch-api client
|
||||||
|
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||||
|
|
||||||
import { computed, observable, reaction } from "mobx";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import { stringify } from "querystring";
|
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
||||||
import { autobind, EventEmitter } from "../utils";
|
import type { KubeObject } from "./kube-object";
|
||||||
import { KubeJsonApiData } from "./kube-json-api";
|
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
import { ensureObjectSelfLink, KubeApi } from "./kube-api";
|
import type { NamespaceStore } from "../components/+namespaces/namespace.store";
|
||||||
|
|
||||||
|
import plimit from "p-limit";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { comparer, computed, observable, reaction } from "mobx";
|
||||||
|
import { autobind, EventEmitter } from "../utils";
|
||||||
|
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
||||||
|
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
||||||
|
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
|
||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
|
||||||
import { getHostedCluster } from "../../common/cluster-store";
|
|
||||||
|
|
||||||
export interface IKubeWatchEvent<T = any> {
|
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
|
||||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
|
||||||
object?: T;
|
export interface IKubeWatchMessage<T extends KubeObject = any> {
|
||||||
|
data?: IKubeWatchEvent<KubeJsonApiData>
|
||||||
|
error?: IKubeWatchEvent<KubeJsonApiError>;
|
||||||
|
api?: KubeApi<T>;
|
||||||
|
store?: KubeObjectStore<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchRouteEvent {
|
export interface IKubeWatchSubscribeStoreOptions {
|
||||||
type: "STREAM_END";
|
preload?: boolean; // preload store items, default: true
|
||||||
url: string;
|
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
||||||
status: number;
|
cacheLoading?: boolean; // when enabled loading store will be skipped, default: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchRouteQuery {
|
export interface IKubeWatchReconnectOptions {
|
||||||
api: string | string[];
|
reconnectAttempts: number;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKubeWatchLog {
|
||||||
|
message: string | Error;
|
||||||
|
meta?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
protected evtSource: EventSource;
|
private cluster: Cluster;
|
||||||
protected onData = new EventEmitter<[IKubeWatchEvent]>();
|
private namespaceStore: NamespaceStore;
|
||||||
protected subscribers = observable.map<KubeApi, number>();
|
|
||||||
protected reconnectTimeoutMs = 5000;
|
|
||||||
protected maxReconnectsOnError = 10;
|
|
||||||
protected reconnectAttempts = this.maxReconnectsOnError;
|
|
||||||
|
|
||||||
constructor() {
|
private requestId = 0;
|
||||||
reaction(() => this.activeApis, () => this.connect(), {
|
private isConnected = false;
|
||||||
fireImmediately: true,
|
private reader: ReadableStreamReader<string>;
|
||||||
delay: 500,
|
private subscribers = observable.map<KubeApi, number>();
|
||||||
});
|
|
||||||
|
// events
|
||||||
|
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
||||||
|
|
||||||
|
@computed get isActive(): boolean {
|
||||||
|
return this.apis.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get activeApis() {
|
@computed get apis(): string[] {
|
||||||
return Array.from(this.subscribers.keys());
|
const { cluster, namespaceStore } = this;
|
||||||
|
const activeApis = Array.from(this.subscribers.keys());
|
||||||
|
|
||||||
|
return activeApis.map(api => {
|
||||||
|
if (!cluster.isAllowedResource(api.kind)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api.isNamespaced) {
|
||||||
|
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
||||||
|
} else {
|
||||||
|
return api.getWatchUrl();
|
||||||
|
}
|
||||||
|
}).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
const { getHostedCluster } = await import("../../common/cluster-store");
|
||||||
|
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
|
||||||
|
|
||||||
|
await namespaceStore.whenReady;
|
||||||
|
|
||||||
|
this.cluster = getHostedCluster();
|
||||||
|
this.namespaceStore = namespaceStore;
|
||||||
|
this.bindAutoConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindAutoConnect() {
|
||||||
|
const connect = debounce(() => this.connect(), 1000);
|
||||||
|
|
||||||
|
reaction(() => this.apis, connect, {
|
||||||
|
fireImmediately: true,
|
||||||
|
equals: comparer.structural,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("online", () => this.connect());
|
||||||
|
window.addEventListener("offline", () => this.disconnect());
|
||||||
|
setInterval(() => this.connectionCheck(), 60000 * 5); // every 5m
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribersCount(api: KubeApi) {
|
getSubscribersCount(api: KubeApi) {
|
||||||
return this.subscribers.get(api) || 0;
|
return this.subscribers.get(api) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(...apis: KubeApi[]) {
|
isAllowedApi(api: KubeApi): boolean {
|
||||||
|
return !!this?.cluster.isAllowedResource(api.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
||||||
|
const apis: KubeApi[] = [api].flat();
|
||||||
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
|
if (!this.isAllowedApi(api)) return; // skip
|
||||||
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
|
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => apis.forEach(api => {
|
return () => {
|
||||||
|
apis.forEach(api => {
|
||||||
const count = this.getSubscribersCount(api) - 1;
|
const count = this.getSubscribersCount(api) - 1;
|
||||||
|
|
||||||
if (count <= 0) this.subscribers.delete(api);
|
if (count <= 0) this.subscribers.delete(api);
|
||||||
else this.subscribers.set(api, count);
|
else this.subscribers.set(api, count);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
|
||||||
|
|
||||||
return {
|
|
||||||
api: this.activeApis.map(api => {
|
|
||||||
if (isAdmin) return api.getWatchUrl();
|
|
||||||
|
|
||||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
|
|
||||||
}).flat()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: maybe switch to websocket to avoid often reconnects
|
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
||||||
@autobind()
|
const { preload = true, waitUntilLoaded = true, cacheLoading = false } = options;
|
||||||
protected connect() {
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
if (this.evtSource) this.disconnect(); // close previous connection
|
const preloading: Promise<any>[] = [];
|
||||||
|
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
|
||||||
|
const unsubscribeList: (() => void)[] = [];
|
||||||
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
|
const subscribe = () => {
|
||||||
|
if (isUnsubscribed) return;
|
||||||
|
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preload) {
|
||||||
|
for (const store of stores) {
|
||||||
|
preloading.push(limitRequests(async () => {
|
||||||
|
if (cacheLoading && store.isLoaded) return; // skip
|
||||||
|
|
||||||
|
return store.loadAll();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitUntilLoaded) {
|
||||||
|
Promise.all(preloading).then(subscribe, error => {
|
||||||
|
this.log({
|
||||||
|
message: new Error("Loading stores has failed"),
|
||||||
|
meta: { stores, error, options },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
return () => {
|
||||||
|
if (isUnsubscribed) return;
|
||||||
|
isUnsubscribed = true;
|
||||||
|
limitRequests.clearQueue();
|
||||||
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async connectionCheck() {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.log({ message: "Offline: reconnecting.." });
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log({
|
||||||
|
message: `Connection check: ${this.isConnected ? "online" : "offline"}`,
|
||||||
|
meta: { connected: this.isConnected },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async connect(apis = this.apis) {
|
||||||
|
this.disconnect(); // close active connections first
|
||||||
|
|
||||||
|
if (!navigator.onLine || !apis.length) {
|
||||||
|
this.isConnected = false;
|
||||||
|
|
||||||
if (!this.activeApis.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = this.getQuery();
|
|
||||||
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
|
|
||||||
|
|
||||||
this.evtSource = new EventSource(apiUrl);
|
this.log({
|
||||||
this.evtSource.onmessage = this.onMessage;
|
message: "Connecting",
|
||||||
this.evtSource.onerror = this.onError;
|
meta: { apis }
|
||||||
this.writeLog("CONNECTING", query.api);
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestId = ++this.requestId;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
const request = await fetch(`${apiPrefix}/watch`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ apis } as IWatchRoutePayload),
|
||||||
|
signal: abortController.signal,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// request above is stale since new request-id has been issued
|
||||||
|
if (this.requestId !== requestId) {
|
||||||
|
abortController.abort();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect() {
|
let jsonBuffer = "";
|
||||||
if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
|
const stream = request.body.pipeThrough(new TextDecoderStream());
|
||||||
this.reconnectAttempts = this.maxReconnectsOnError;
|
const reader = stream.getReader();
|
||||||
this.connect();
|
|
||||||
|
this.isConnected = true;
|
||||||
|
this.reader = reader;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) break; // exit
|
||||||
|
|
||||||
|
const events = (jsonBuffer + value).split("\n");
|
||||||
|
|
||||||
|
jsonBuffer = this.processBuffer(events);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log({ message: error });
|
||||||
|
} finally {
|
||||||
|
this.isConnected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected disconnect() {
|
protected disconnect() {
|
||||||
if (!this.evtSource) return;
|
this.reader?.cancel();
|
||||||
this.evtSource.close();
|
this.reader = null;
|
||||||
this.evtSource.onmessage = null;
|
this.isConnected = false;
|
||||||
this.evtSource = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onMessage(evt: MessageEvent) {
|
// process received stream events, returns unprocessed buffer chunk if any
|
||||||
if (!evt.data) return;
|
protected processBuffer(events: string[]): string {
|
||||||
const data = JSON.parse(evt.data);
|
for (const json of events) {
|
||||||
|
try {
|
||||||
|
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
||||||
|
const message = this.getMessage(kubeEvent);
|
||||||
|
|
||||||
if ((data as IKubeWatchEvent).object) {
|
this.onMessage.emit(message);
|
||||||
this.onData.emit(data);
|
} catch (error) {
|
||||||
} else {
|
return json;
|
||||||
this.onRouteEvent(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onRouteEvent(event: IKubeWatchRouteEvent) {
|
return "";
|
||||||
if (event.type === "STREAM_END") {
|
}
|
||||||
this.disconnect();
|
|
||||||
const { apiBase, namespace } = KubeApi.parseApi(event.url);
|
protected getMessage(event: IKubeWatchEvent): IKubeWatchMessage {
|
||||||
const api = apiManager.getApi(apiBase);
|
const message: IKubeWatchMessage = {};
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "ADDED":
|
||||||
|
case "DELETED":
|
||||||
|
|
||||||
|
case "MODIFIED": {
|
||||||
|
const data = event as IKubeWatchEvent<KubeJsonApiData>;
|
||||||
|
const api = apiManager.getApiByKind(data.object.kind, data.object.apiVersion);
|
||||||
|
|
||||||
|
message.data = data;
|
||||||
|
|
||||||
if (api) {
|
if (api) {
|
||||||
try {
|
ensureObjectSelfLink(api, data.object);
|
||||||
await api.refreshResourceVersion({ namespace });
|
|
||||||
this.reconnect();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("failed to refresh resource version", error);
|
|
||||||
|
|
||||||
if (this.subscribers.size > 0) {
|
const { namespace, resourceVersion } = data.object.metadata;
|
||||||
setTimeout(() => {
|
|
||||||
this.onRouteEvent(event);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onError(evt: MessageEvent) {
|
|
||||||
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
|
|
||||||
|
|
||||||
if (evt.eventPhase === EventSource.CLOSED) {
|
|
||||||
if (attemptsRemain > 0) {
|
|
||||||
this.reconnectAttempts--;
|
|
||||||
setTimeout(() => this.connect(), reconnectTimeoutMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected writeLog(...data: any[]) {
|
|
||||||
if (isDevelopment) {
|
|
||||||
console.log("%cKUBE-WATCH-API:", `font-weight: bold`, ...data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) {
|
|
||||||
const listener = (evt: IKubeWatchEvent<KubeJsonApiData>) => {
|
|
||||||
const { namespace, resourceVersion } = evt.object.metadata;
|
|
||||||
const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion);
|
|
||||||
|
|
||||||
api.setResourceVersion(namespace, resourceVersion);
|
api.setResourceVersion(namespace, resourceVersion);
|
||||||
api.setResourceVersion("", resourceVersion);
|
api.setResourceVersion("", resourceVersion);
|
||||||
|
|
||||||
ensureObjectSelfLink(api, evt.object);
|
message.api = api;
|
||||||
|
message.store = apiManager.getStore(api);
|
||||||
if (store == apiManager.getStore(api)) {
|
|
||||||
callback(evt);
|
|
||||||
}
|
}
|
||||||
};
|
break;
|
||||||
|
|
||||||
this.onData.addListener(listener);
|
|
||||||
|
|
||||||
return () => this.onData.removeListener(listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
case "ERROR":
|
||||||
this.subscribers.clear();
|
message.error = event as IKubeWatchEvent<KubeJsonApiError>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "STREAM_END": {
|
||||||
|
this.onServerStreamEnd(event as IKubeWatchEventStreamEnd, {
|
||||||
|
reconnectAttempts: 5,
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onServerStreamEnd(event: IKubeWatchEventStreamEnd, opts?: IKubeWatchReconnectOptions) {
|
||||||
|
const { apiBase, namespace } = parseKubeApi(event.url);
|
||||||
|
const api = apiManager.getApi(apiBase);
|
||||||
|
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.refreshResourceVersion({ namespace });
|
||||||
|
this.connect();
|
||||||
|
} catch (error) {
|
||||||
|
this.log({
|
||||||
|
message: new Error(`Failed to connect on single stream end: ${error}`),
|
||||||
|
meta: { event, error },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.isActive && opts?.reconnectAttempts > 0) {
|
||||||
|
opts.reconnectAttempts--;
|
||||||
|
setTimeout(() => this.onServerStreamEnd(event, opts), opts.timeout); // repeat event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected log({ message, meta = {} }: IKubeWatchLog) {
|
||||||
|
if (isProduction && !isDebugging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMessage = `%c[KUBE-WATCH-API]: ${String(message).toUpperCase()}`;
|
||||||
|
const isError = message instanceof Error;
|
||||||
|
const textStyle = `font-weight: bold;`;
|
||||||
|
const time = new Date().toLocaleString();
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
console.error(logMessage, textStyle, { time, ...meta });
|
||||||
|
} else {
|
||||||
|
console.info(logMessage, textStyle, { time, ...meta });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { KubeObject } from "./kube-object";
|
import { KubeObject } from "./kube-object";
|
||||||
|
|
||||||
interface IToleration {
|
export interface IToleration {
|
||||||
key?: string;
|
key?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
effect?: string;
|
effect?: string;
|
||||||
|
|||||||
@ -11,8 +11,11 @@ import { navigation } from "../../navigation";
|
|||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
import { SearchInputUrl } from "../input";
|
import { SearchInputUrl } from "../input";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
|
description = "description",
|
||||||
|
version = "version",
|
||||||
|
appVersion = "app-version",
|
||||||
repo = "repo",
|
repo = "repo",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,13 +56,15 @@ export class HelmCharts extends Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="helm_charts"
|
||||||
className="HelmCharts"
|
className="HelmCharts"
|
||||||
store={helmChartStore}
|
store={helmChartStore}
|
||||||
isClusterScoped={true}
|
isClusterScoped={true}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (chart: HelmChart) => chart.getName(),
|
[columnId.name]: (chart: HelmChart) => chart.getName(),
|
||||||
[sortBy.repo]: (chart: HelmChart) => chart.getRepository(),
|
[columnId.repo]: (chart: HelmChart) => chart.getRepository(),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(chart: HelmChart) => chart.getName(),
|
(chart: HelmChart) => chart.getName(),
|
||||||
@ -74,13 +79,12 @@ export class HelmCharts extends Component<Props> {
|
|||||||
<SearchInputUrl placeholder={`Search Helm Charts`} />
|
<SearchInputUrl placeholder={`Search Helm Charts`} />
|
||||||
)}
|
)}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ className: "icon" },
|
{ className: "icon", showWithColumn: columnId.name },
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ title: "Description", className: "description" },
|
{ title: "Description", className: "description", id: columnId.description },
|
||||||
{ title: "Version", className: "version" },
|
{ title: "Version", className: "version", id: columnId.version },
|
||||||
{ title: "App Version", className: "app-version" },
|
{ title: "App Version", className: "app-version", id: columnId.appVersion },
|
||||||
{ title: "Repository", className: "repository", sortBy: sortBy.repo },
|
{ title: "Repository", className: "repository", sortBy: columnId.repo, id: columnId.repo },
|
||||||
|
|
||||||
]}
|
]}
|
||||||
renderTableContents={(chart: HelmChart) => [
|
renderTableContents={(chart: HelmChart) => [
|
||||||
<figure key="image">
|
<figure key="image">
|
||||||
@ -93,7 +97,8 @@ export class HelmCharts extends Component<Props> {
|
|||||||
chart.getDescription(),
|
chart.getDescription(),
|
||||||
chart.getVersion(),
|
chart.getVersion(),
|
||||||
chart.getAppVersion(),
|
chart.getAppVersion(),
|
||||||
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() }
|
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() },
|
||||||
|
{ className: "menu" }
|
||||||
]}
|
]}
|
||||||
detailsItem={this.selectedChart}
|
detailsItem={this.selectedChart}
|
||||||
onDetails={this.showDetails}
|
onDetails={this.showDetails}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
|
|||||||
import { ItemStore } from "../../item.store";
|
import { ItemStore } from "../../item.store";
|
||||||
import { Secret } from "../../api/endpoints";
|
import { Secret } from "../../api/endpoints";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||||
@ -60,31 +60,24 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
const items = await this.loadItems(namespaceStore.getContextNamespaces());
|
||||||
|
|
||||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
this.items.replace(this.sortItems(items));
|
||||||
} finally {
|
|
||||||
if (items) {
|
|
||||||
items = this.sortItems(items);
|
|
||||||
this.items.replace(items);
|
|
||||||
}
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Loading Helm Chart releases has failed: ${error}`);
|
||||||
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadItems(namespaces?: string[]) {
|
async loadItems(namespaces: string[]) {
|
||||||
if (!namespaces) {
|
|
||||||
return helmReleasesApi.list();
|
|
||||||
} else {
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
.then(items => items.flat());
|
.then(items => items.flat());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async create(payload: IReleaseCreatePayload) {
|
async create(payload: IReleaseCreatePayload) {
|
||||||
const response = await helmReleasesApi.create(payload);
|
const response = await helmReleasesApi.create(payload);
|
||||||
|
|||||||
@ -14,11 +14,13 @@ import { ItemListLayout } from "../item-object-list/item-list-layout";
|
|||||||
import { HelmReleaseMenu } from "./release-menu";
|
import { HelmReleaseMenu } from "./release-menu";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
revision = "revision",
|
revision = "revision",
|
||||||
chart = "chart",
|
chart = "chart",
|
||||||
|
version = "version",
|
||||||
|
appVersion = "app-version",
|
||||||
status = "status",
|
status = "status",
|
||||||
updated = "update"
|
updated = "update"
|
||||||
}
|
}
|
||||||
@ -81,16 +83,18 @@ export class HelmReleases extends Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="helm_releases"
|
||||||
className="HelmReleases"
|
className="HelmReleases"
|
||||||
store={releaseStore}
|
store={releaseStore}
|
||||||
dependentStores={[secretsStore]}
|
dependentStores={[secretsStore]}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (release: HelmRelease) => release.getName(),
|
[columnId.name]: (release: HelmRelease) => release.getName(),
|
||||||
[sortBy.namespace]: (release: HelmRelease) => release.getNs(),
|
[columnId.namespace]: (release: HelmRelease) => release.getNs(),
|
||||||
[sortBy.revision]: (release: HelmRelease) => release.getRevision(),
|
[columnId.revision]: (release: HelmRelease) => release.getRevision(),
|
||||||
[sortBy.chart]: (release: HelmRelease) => release.getChart(),
|
[columnId.chart]: (release: HelmRelease) => release.getChart(),
|
||||||
[sortBy.status]: (release: HelmRelease) => release.getStatus(),
|
[columnId.status]: (release: HelmRelease) => release.getStatus(),
|
||||||
[sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false),
|
[columnId.updated]: (release: HelmRelease) => release.getUpdated(false, false),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(release: HelmRelease) => release.getName(),
|
(release: HelmRelease) => release.getName(),
|
||||||
@ -101,14 +105,14 @@ export class HelmReleases extends Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Releases"
|
renderHeaderTitle="Releases"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Chart", className: "chart", sortBy: sortBy.chart },
|
{ title: "Chart", className: "chart", sortBy: columnId.chart, id: columnId.chart },
|
||||||
{ title: "Revision", className: "revision", sortBy: sortBy.revision },
|
{ title: "Revision", className: "revision", sortBy: columnId.revision, id: columnId.revision },
|
||||||
{ title: "Version", className: "version" },
|
{ title: "Version", className: "version", id: columnId.version },
|
||||||
{ title: "App Version", className: "app-version" },
|
{ title: "App Version", className: "app-version", id: columnId.appVersion },
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||||
{ title: "Updated", className: "updated", sortBy: sortBy.updated },
|
{ title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(release: HelmRelease) => {
|
renderTableContents={(release: HelmRelease) => {
|
||||||
const version = release.getVersion();
|
const version = release.getVersion();
|
||||||
|
|||||||
18
src/renderer/components/+apps/apps.command.ts
Normal file
18
src/renderer/components/+apps/apps.command.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
|
import { helmChartsURL } from "../+apps-helm-charts";
|
||||||
|
import { releaseURL } from "../+apps-releases";
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewHelmCharts",
|
||||||
|
title: "Cluster: View Helm Charts",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(helmChartsURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewHelmReleases",
|
||||||
|
title: "Cluster: View Helm Releases",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(releaseURL())
|
||||||
|
});
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./apps";
|
export * from "./apps";
|
||||||
export * from "./apps.route";
|
export * from "./apps.route";
|
||||||
|
export * from "./apps.command";
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
|
import { clusterSettingsURL } from "./cluster-settings.route";
|
||||||
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewCurrentClusterSettings",
|
||||||
|
title: "Cluster: View Settings",
|
||||||
|
scope: "global",
|
||||||
|
action: () => navigate(clusterSettingsURL({
|
||||||
|
params: {
|
||||||
|
clusterId: clusterStore.active.id
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
isActive: (context) => !!context.cluster
|
||||||
|
});
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { workspacesURL } from "../../+workspaces";
|
|
||||||
import { workspaceStore } from "../../../../common/workspace-store";
|
import { workspaceStore } from "../../../../common/workspace-store";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { Select } from "../../../components/select";
|
import { Select } from "../../../components/select";
|
||||||
@ -18,10 +16,7 @@ export class ClusterWorkspaceSetting extends React.Component<Props> {
|
|||||||
<>
|
<>
|
||||||
<SubTitle title="Cluster Workspace"/>
|
<SubTitle title="Cluster Workspace"/>
|
||||||
<p>
|
<p>
|
||||||
Define cluster{" "}
|
Define cluster workspace.
|
||||||
<Link to={workspacesURL()}>
|
|
||||||
workspace
|
|
||||||
</Link>.
|
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={this.props.cluster.workspace}
|
value={this.props.cluster.workspace}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./cluster-settings.route";
|
export * from "./cluster-settings.route";
|
||||||
export * from "./cluster-settings";
|
export * from "./cluster-settings";
|
||||||
|
export * from "./cluster-settings.command";
|
||||||
|
|||||||
@ -23,11 +23,13 @@ interface IWarning extends ItemObject {
|
|||||||
kind: string;
|
kind: string;
|
||||||
message: string;
|
message: string;
|
||||||
selfLink: string;
|
selfLink: string;
|
||||||
|
age: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
type = "type",
|
type = "type",
|
||||||
object = "object"
|
object = "object",
|
||||||
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -35,6 +37,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
private sortCallbacks = {
|
private sortCallbacks = {
|
||||||
[sortBy.type]: (warning: IWarning) => warning.kind,
|
[sortBy.type]: (warning: IWarning) => warning.kind,
|
||||||
[sortBy.object]: (warning: IWarning) => warning.getName(),
|
[sortBy.object]: (warning: IWarning) => warning.getName(),
|
||||||
|
[sortBy.age]: (warning: IWarning) => warning.age || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@computed get warnings() {
|
@computed get warnings() {
|
||||||
@ -42,15 +45,16 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
|
|
||||||
// Node bad conditions
|
// Node bad conditions
|
||||||
nodesStore.items.forEach(node => {
|
nodesStore.items.forEach(node => {
|
||||||
const { kind, selfLink, getId, getName } = node;
|
const { kind, selfLink, getId, getName, getAge } = node;
|
||||||
|
|
||||||
node.getWarningConditions().forEach(({ message }) => {
|
node.getWarningConditions().forEach(({ message }) => {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
kind,
|
age: getAge(),
|
||||||
getId,
|
getId,
|
||||||
getName,
|
getName,
|
||||||
selfLink,
|
kind,
|
||||||
message,
|
message,
|
||||||
|
selfLink,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -59,12 +63,13 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
const events = eventStore.getWarnings();
|
const events = eventStore.getWarnings();
|
||||||
|
|
||||||
events.forEach(error => {
|
events.forEach(error => {
|
||||||
const { message, involvedObject } = error;
|
const { message, involvedObject, getAge } = error;
|
||||||
const { uid, name, kind } = involvedObject;
|
const { uid, name, kind } = involvedObject;
|
||||||
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
getId: () => uid,
|
getId: () => uid,
|
||||||
getName: () => name,
|
getName: () => name,
|
||||||
|
age: getAge(),
|
||||||
message,
|
message,
|
||||||
kind,
|
kind,
|
||||||
selfLink: lookupApiLink(involvedObject, error),
|
selfLink: lookupApiLink(involvedObject, error),
|
||||||
@ -78,7 +83,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
getTableRow(uid: string) {
|
getTableRow(uid: string) {
|
||||||
const { warnings } = this;
|
const { warnings } = this;
|
||||||
const warning = warnings.find(warn => warn.getId() == uid);
|
const warning = warnings.find(warn => warn.getId() == uid);
|
||||||
const { getId, getName, message, kind, selfLink } = warning;
|
const { getId, getName, message, kind, selfLink, age } = warning;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
@ -96,6 +101,9 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
<TableCell className="kind">
|
<TableCell className="kind">
|
||||||
{kind}
|
{kind}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="age">
|
||||||
|
{age}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -139,6 +147,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
<TableCell className="message">Message</TableCell>
|
<TableCell className="message">Message</TableCell>
|
||||||
<TableCell className="object" sortBy={sortBy.object}>Object</TableCell>
|
<TableCell className="object" sortBy={sortBy.object}>Object</TableCell>
|
||||||
<TableCell className="kind" sortBy={sortBy.type}>Type</TableCell>
|
<TableCell className="kind" sortBy={sortBy.type}>Type</TableCell>
|
||||||
|
<TableCell className="timestamp" sortBy={sortBy.age}>Age</TableCell>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,13 +3,9 @@ import "./cluster-overview.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
import { eventStore } from "../+events/event.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 { getHostedCluster } from "../../../common/cluster-store";
|
import { getHostedCluster } from "../../../common/cluster-store";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
|
||||||
import { interval } from "../../utils";
|
import { interval } from "../../utils";
|
||||||
import { TabLayout } from "../layout/tab-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
@ -17,45 +13,33 @@ import { ClusterIssues } from "./cluster-issues";
|
|||||||
import { ClusterMetrics } from "./cluster-metrics";
|
import { ClusterMetrics } from "./cluster-metrics";
|
||||||
import { clusterOverviewStore } from "./cluster-overview.store";
|
import { clusterOverviewStore } from "./cluster-overview.store";
|
||||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||||
|
import { eventStore } from "../+events/event.store";
|
||||||
|
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterOverview extends React.Component {
|
export class ClusterOverview extends React.Component {
|
||||||
private stores: KubeObjectStore<any>[] = [];
|
private metricPoller = interval(60, () => this.loadMetrics());
|
||||||
private subscribers: Array<() => void> = [];
|
|
||||||
private metricPoller = interval(60, this.loadMetrics);
|
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
fetchMetrics = reaction(
|
|
||||||
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
|
||||||
() => this.metricPoller.restart(true)
|
|
||||||
);
|
|
||||||
|
|
||||||
loadMetrics() {
|
loadMetrics() {
|
||||||
getHostedCluster().available && clusterOverviewStore.loadMetrics();
|
getHostedCluster().available && clusterOverviewStore.loadMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
if (isAllowedResource("nodes")) {
|
this.metricPoller.start(true);
|
||||||
this.stores.push(nodesStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAllowedResource("pods")) {
|
disposeOnUnmount(this, [
|
||||||
this.stores.push(podsStore);
|
kubeWatchApi.subscribeStores([nodesStore, podsStore, eventStore], {
|
||||||
}
|
preload: true,
|
||||||
|
}),
|
||||||
|
|
||||||
if (isAllowedResource("events")) {
|
reaction(
|
||||||
this.stores.push(eventStore);
|
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||||
}
|
() => this.metricPoller.restart(true)
|
||||||
|
),
|
||||||
await Promise.all(this.stores.map(store => store.loadAll()));
|
]);
|
||||||
this.loadMetrics();
|
|
||||||
|
|
||||||
this.subscribers = this.stores.map(store => store.subscribe());
|
|
||||||
this.metricPoller.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subscribers.forEach(dispose => dispose()); // unsubscribe all
|
|
||||||
this.metricPoller.stop();
|
this.metricPoller.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,13 +11,15 @@ import { Badge } from "../badge";
|
|||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
|
metrics = "metrics",
|
||||||
minPods = "min-pods",
|
minPods = "min-pods",
|
||||||
maxPods = "max-pods",
|
maxPods = "max-pods",
|
||||||
replicas = "replicas",
|
replicas = "replicas",
|
||||||
age = "age",
|
age = "age",
|
||||||
|
status = "status"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IHpaRouteParams> {
|
interface Props extends RouteComponentProps<IHpaRouteParams> {
|
||||||
@ -37,28 +39,30 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_hpa"
|
||||||
className="HorizontalPodAutoscalers" store={hpaStore}
|
className="HorizontalPodAutoscalers" store={hpaStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: HorizontalPodAutoscaler) => item.getName(),
|
[columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(),
|
||||||
[sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
|
[columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
|
||||||
[sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
|
[columnId.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
|
||||||
[sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
|
[columnId.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
|
||||||
[sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
|
[columnId.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: HorizontalPodAutoscaler) => item.getSearchFields()
|
(item: HorizontalPodAutoscaler) => item.getSearchFields()
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle="Horizontal Pod Autoscalers"
|
renderHeaderTitle="Horizontal Pod Autoscalers"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Metrics", className: "metrics" },
|
{ title: "Metrics", className: "metrics", id: columnId.metrics },
|
||||||
{ title: "Min Pods", className: "min-pods", sortBy: sortBy.minPods },
|
{ title: "Min Pods", className: "min-pods", sortBy: columnId.minPods, id: columnId.minPods },
|
||||||
{ title: "Max Pods", className: "max-pods", sortBy: sortBy.maxPods },
|
{ title: "Max Pods", className: "max-pods", sortBy: columnId.maxPods, id: columnId.maxPods },
|
||||||
{ title: "Replicas", className: "replicas", sortBy: sortBy.replicas },
|
{ title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
{ title: "Status", className: "status" },
|
{ title: "Status", className: "status", id: columnId.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(hpa: HorizontalPodAutoscaler) => [
|
renderTableContents={(hpa: HorizontalPodAutoscaler) => [
|
||||||
hpa.getName(),
|
hpa.getName(),
|
||||||
|
|||||||
3
src/renderer/components/+config-limit-ranges/index.ts
Normal file
3
src/renderer/components/+config-limit-ranges/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./limit-ranges";
|
||||||
|
export * from "./limit-ranges.route";
|
||||||
|
export * from "./limit-range-details";
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
.LimitRangeDetails {
|
||||||
|
|
||||||
|
.DrawerItem {
|
||||||
|
> .name {
|
||||||
|
font-weight: $font-weight-normal;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
.DrawerItem {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import "./limit-range-details.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { KubeObjectDetailsProps } from "../kube-object";
|
||||||
|
import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../api/endpoints/limit-range.api";
|
||||||
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
|
import { DrawerItem } from "../drawer/drawer-item";
|
||||||
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
|
interface Props extends KubeObjectDetailsProps<LimitRange> {
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLimit(limit: LimitRangeItem, part: LimitPart, resource: Resource) {
|
||||||
|
|
||||||
|
const resourceLimit = limit[part]?.[resource];
|
||||||
|
|
||||||
|
if (!resourceLimit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge label={`${part}:${resourceLimit}`}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResourceLimits(limit: LimitRangeItem, resource: Resource) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={limit.type + resource}>
|
||||||
|
{renderLimit(limit, LimitPart.MIN, resource)}
|
||||||
|
{renderLimit(limit, LimitPart.MAX, resource)}
|
||||||
|
{renderLimit(limit, LimitPart.DEFAULT, resource)}
|
||||||
|
{renderLimit(limit, LimitPart.DEFAULT_REQUEST, resource)}
|
||||||
|
{renderLimit(limit, LimitPart.MAX_LIMIT_REQUEST_RATIO, resource)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) {
|
||||||
|
|
||||||
|
return resources.map(resource =>
|
||||||
|
<DrawerItem key={resource} name={resource}>
|
||||||
|
{
|
||||||
|
limits.map(limit =>
|
||||||
|
renderResourceLimits(limit, resource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</DrawerItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class LimitRangeDetails extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { object: limitRange } = this.props;
|
||||||
|
|
||||||
|
if (!limitRange) return null;
|
||||||
|
const containerLimits = limitRange.getContainerLimits();
|
||||||
|
const podLimits = limitRange.getPodLimits();
|
||||||
|
const pvcLimits = limitRange.getPVCLimits();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="LimitRangeDetails">
|
||||||
|
<KubeObjectMeta object={limitRange}/>
|
||||||
|
|
||||||
|
{containerLimits.length > 0 &&
|
||||||
|
<DrawerItem name={`Container Limits`} labelsOnly>
|
||||||
|
{
|
||||||
|
renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||||
|
}
|
||||||
|
</DrawerItem>
|
||||||
|
}
|
||||||
|
{podLimits.length > 0 &&
|
||||||
|
<DrawerItem name={`Pod Limits`} labelsOnly>
|
||||||
|
{
|
||||||
|
renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE])
|
||||||
|
}
|
||||||
|
</DrawerItem>
|
||||||
|
}
|
||||||
|
{pvcLimits.length > 0 &&
|
||||||
|
<DrawerItem name={`Persistent Volume Claim Limits`} labelsOnly>
|
||||||
|
{
|
||||||
|
renderLimitDetails(pvcLimits, [Resource.STORAGE])
|
||||||
|
}
|
||||||
|
</DrawerItem>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeObjectDetailRegistry.add({
|
||||||
|
kind: "LimitRange",
|
||||||
|
apiVersions: ["v1"],
|
||||||
|
components: {
|
||||||
|
Details: (props) => <LimitRangeDetails {...props} />
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
import { buildURL } from "../../../common/utils/buildUrl";
|
||||||
|
|
||||||
|
export const limitRangesRoute: RouteProps = {
|
||||||
|
path: "/limitranges"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LimitRangeRouteParams {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const limitRangeURL = buildURL<LimitRangeRouteParams>(limitRangesRoute.path);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
.LimitRanges {
|
||||||
|
.TableCell {
|
||||||
|
&.warning {
|
||||||
|
@include table-cell-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { autobind } from "../../../common/utils/autobind";
|
||||||
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
export class LimitRangesStore extends KubeObjectStore<LimitRange> {
|
||||||
|
api = limitRangeApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const limitRangeStore = new LimitRangesStore();
|
||||||
|
apiManager.registerStore(limitRangeStore);
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import "./limit-ranges.scss";
|
||||||
|
|
||||||
|
import { RouteComponentProps } from "react-router";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout";
|
||||||
|
import { limitRangeStore } from "./limit-ranges.store";
|
||||||
|
import { LimitRangeRouteParams } from "./limit-ranges.route";
|
||||||
|
import React from "react";
|
||||||
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
import { LimitRange } from "../../api/endpoints/limit-range.api";
|
||||||
|
|
||||||
|
enum columnId {
|
||||||
|
name = "name",
|
||||||
|
namespace = "namespace",
|
||||||
|
age = "age",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends RouteComponentProps<LimitRangeRouteParams> {
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class LimitRanges extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_limitranges"
|
||||||
|
className="LimitRanges"
|
||||||
|
store={limitRangeStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[columnId.name]: (item: LimitRange) => item.getName(),
|
||||||
|
[columnId.namespace]: (item: LimitRange) => item.getNs(),
|
||||||
|
[columnId.age]: (item: LimitRange) => item.metadata.creationTimestamp,
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(item: LimitRange) => item.getName(),
|
||||||
|
(item: LimitRange) => item.getNs(),
|
||||||
|
]}
|
||||||
|
renderHeaderTitle={"Limit Ranges"}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
|
]}
|
||||||
|
renderTableContents={(limitRange: LimitRange) => [
|
||||||
|
limitRange.getName(),
|
||||||
|
<KubeObjectStatusIcon key="icon" object={limitRange}/>,
|
||||||
|
limitRange.getNs(),
|
||||||
|
limitRange.getAge(),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object";
|
|||||||
import { IConfigMapsRouteParams } from "./config-maps.route";
|
import { IConfigMapsRouteParams } from "./config-maps.route";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
keys = "keys",
|
keys = "keys",
|
||||||
@ -24,12 +24,14 @@ export class ConfigMaps extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_configmaps"
|
||||||
className="ConfigMaps" store={configMapsStore}
|
className="ConfigMaps" store={configMapsStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: ConfigMap) => item.getName(),
|
[columnId.name]: (item: ConfigMap) => item.getName(),
|
||||||
[sortBy.namespace]: (item: ConfigMap) => item.getNs(),
|
[columnId.namespace]: (item: ConfigMap) => item.getNs(),
|
||||||
[sortBy.keys]: (item: ConfigMap) => item.getKeys(),
|
[columnId.keys]: (item: ConfigMap) => item.getKeys(),
|
||||||
[sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: ConfigMap) => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: ConfigMap) => item.getSearchFields(),
|
(item: ConfigMap) => item.getSearchFields(),
|
||||||
@ -37,11 +39,11 @@ export class ConfigMaps extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Config Maps"
|
renderHeaderTitle="Config Maps"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Keys", className: "keys", sortBy: sortBy.keys },
|
{ title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(configMap: ConfigMap) => [
|
renderTableContents={(configMap: ConfigMap) => [
|
||||||
configMap.getName(),
|
configMap.getName(),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api
|
|||||||
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
minAvailable = "min-available",
|
minAvailable = "min-available",
|
||||||
@ -25,30 +25,32 @@ export class PodDisruptionBudgets extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_distribution_budgets"
|
||||||
className="PodDisruptionBudgets"
|
className="PodDisruptionBudgets"
|
||||||
store={podDisruptionBudgetsStore}
|
store={podDisruptionBudgetsStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
|
[columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
|
||||||
[sortBy.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),
|
[columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),
|
||||||
[sortBy.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(),
|
[columnId.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(),
|
||||||
[sortBy.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(),
|
[columnId.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(),
|
||||||
[sortBy.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(),
|
[columnId.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(),
|
||||||
[sortBy.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(),
|
[columnId.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(),
|
||||||
[sortBy.age]: (pdb: PodDisruptionBudget) => pdb.getAge(),
|
[columnId.age]: (pdb: PodDisruptionBudget) => pdb.getAge(),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(pdb: PodDisruptionBudget) => pdb.getSearchFields(),
|
(pdb: PodDisruptionBudget) => pdb.getSearchFields(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle="Pod Disruption Budgets"
|
renderHeaderTitle="Pod Disruption Budgets"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Min Available", className: "min-available", sortBy: sortBy.minAvailable },
|
{ title: "Min Available", className: "min-available", sortBy: columnId.minAvailable, id: columnId.minAvailable },
|
||||||
{ title: "Max Unavailable", className: "max-unavailable", sortBy: sortBy.maxUnavailable },
|
{ title: "Max Unavailable", className: "max-unavailable", sortBy: columnId.maxUnavailable, id: columnId.maxUnavailable },
|
||||||
{ title: "Current Healthy", className: "current-healthy", sortBy: sortBy.currentHealthy },
|
{ title: "Current Healthy", className: "current-healthy", sortBy: columnId.currentHealthy, id: columnId.currentHealthy },
|
||||||
{ title: "Desired Healthy", className: "desired-healthy", sortBy: sortBy.desiredHealthy },
|
{ title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(pdb: PodDisruptionBudget) => {
|
renderTableContents={(pdb: PodDisruptionBudget) => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { resourceQuotaStore } from "./resource-quotas.store";
|
|||||||
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
|
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
age = "age"
|
age = "age"
|
||||||
@ -25,11 +25,13 @@ export class ResourceQuotas extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_quotas"
|
||||||
className="ResourceQuotas" store={resourceQuotaStore}
|
className="ResourceQuotas" store={resourceQuotaStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: ResourceQuota) => item.getName(),
|
[columnId.name]: (item: ResourceQuota) => item.getName(),
|
||||||
[sortBy.namespace]: (item: ResourceQuota) => item.getNs(),
|
[columnId.namespace]: (item: ResourceQuota) => item.getNs(),
|
||||||
[sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: ResourceQuota) => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: ResourceQuota) => item.getSearchFields(),
|
(item: ResourceQuota) => item.getSearchFields(),
|
||||||
@ -37,10 +39,10 @@ export class ResourceQuotas extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Resource Quotas"
|
renderHeaderTitle="Resource Quotas"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(resourceQuota: ResourceQuota) => [
|
renderTableContents={(resourceQuota: ResourceQuota) => [
|
||||||
resourceQuota.getName(),
|
resourceQuota.getName(),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Badge } from "../badge";
|
|||||||
import { secretsStore } from "./secrets.store";
|
import { secretsStore } from "./secrets.store";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
labels = "labels",
|
labels = "labels",
|
||||||
@ -29,14 +29,16 @@ export class Secrets extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_secrets"
|
||||||
className="Secrets" store={secretsStore}
|
className="Secrets" store={secretsStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: Secret) => item.getName(),
|
[columnId.name]: (item: Secret) => item.getName(),
|
||||||
[sortBy.namespace]: (item: Secret) => item.getNs(),
|
[columnId.namespace]: (item: Secret) => item.getNs(),
|
||||||
[sortBy.labels]: (item: Secret) => item.getLabels(),
|
[columnId.labels]: (item: Secret) => item.getLabels(),
|
||||||
[sortBy.keys]: (item: Secret) => item.getKeys(),
|
[columnId.keys]: (item: Secret) => item.getKeys(),
|
||||||
[sortBy.type]: (item: Secret) => item.type,
|
[columnId.type]: (item: Secret) => item.type,
|
||||||
[sortBy.age]: (item: Secret) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: Secret) => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: Secret) => item.getSearchFields(),
|
(item: Secret) => item.getSearchFields(),
|
||||||
@ -44,13 +46,13 @@ export class Secrets extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Secrets"
|
renderHeaderTitle="Secrets"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Labels", className: "labels", sortBy: sortBy.labels },
|
{ title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels },
|
||||||
{ title: "Keys", className: "keys", sortBy: sortBy.keys },
|
{ title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys },
|
||||||
{ title: "Type", className: "type", sortBy: sortBy.type },
|
{ title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(secret: Secret) => [
|
renderTableContents={(secret: Secret) => [
|
||||||
secret.getName(),
|
secret.getName(),
|
||||||
|
|||||||
50
src/renderer/components/+config/config.command.ts
Normal file
50
src/renderer/components/+config/config.command.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
|
import { configMapsURL } from "../+config-maps";
|
||||||
|
import { secretsURL } from "../+config-secrets";
|
||||||
|
import { resourceQuotaURL } from "../+config-resource-quotas";
|
||||||
|
import { limitRangeURL } from "../+config-limit-ranges";
|
||||||
|
import { hpaURL } from "../+config-autoscalers";
|
||||||
|
import { pdbURL } from "../+config-pod-disruption-budgets";
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewConfigMaps",
|
||||||
|
title: "Cluster: View ConfigMaps",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(configMapsURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewSecrets",
|
||||||
|
title: "Cluster: View Secrets",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(secretsURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewResourceQuotas",
|
||||||
|
title: "Cluster: View ResourceQuotas",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(resourceQuotaURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewLimitRanges",
|
||||||
|
title: "Cluster: View LimitRanges",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(limitRangeURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewHorizontalPodAutoscalers",
|
||||||
|
title: "Cluster: View HorizontalPodAutoscalers (HPA)",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(hpaURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewPodDisruptionBudget",
|
||||||
|
title: "Cluster: View PodDisruptionBudgets",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(pdbURL())
|
||||||
|
});
|
||||||
@ -8,6 +8,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
|
|||||||
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
||||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
import { LimitRanges, limitRangesRoute, limitRangeURL } from "../+config-limit-ranges";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Config extends React.Component {
|
export class Config extends React.Component {
|
||||||
@ -42,6 +43,15 @@ export class Config extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAllowedResource("limitranges")) {
|
||||||
|
routes.push({
|
||||||
|
title: "Limit Ranges",
|
||||||
|
component: LimitRanges,
|
||||||
|
url: limitRangeURL({ query }),
|
||||||
|
routePath: limitRangesRoute.path.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isAllowedResource("horizontalpodautoscalers")) {
|
if (isAllowedResource("horizontalpodautoscalers")) {
|
||||||
routes.push({
|
routes.push({
|
||||||
title: "HPA",
|
title: "HPA",
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./config.route";
|
export * from "./config.route";
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
|
export * from "./config.command";
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const crdGroupsUrlParam = createPageParam<string[]>({
|
|||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
kind = "kind",
|
kind = "kind",
|
||||||
group = "group",
|
group = "group",
|
||||||
version = "version",
|
version = "version",
|
||||||
@ -47,14 +47,16 @@ export class CrdList extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const selectedGroups = this.groups;
|
const selectedGroups = this.groups;
|
||||||
const sortingCallbacks = {
|
const sortingCallbacks = {
|
||||||
[sortBy.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(),
|
[columnId.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(),
|
||||||
[sortBy.group]: (crd: CustomResourceDefinition) => crd.getGroup(),
|
[columnId.group]: (crd: CustomResourceDefinition) => crd.getGroup(),
|
||||||
[sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
|
[columnId.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
|
||||||
[sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
|
[columnId.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="crd"
|
||||||
className="CrdList"
|
className="CrdList"
|
||||||
isClusterScoped={true}
|
isClusterScoped={true}
|
||||||
store={crdStore}
|
store={crdStore}
|
||||||
@ -97,11 +99,11 @@ export class CrdList extends React.Component {
|
|||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Resource", className: "kind", sortBy: sortBy.kind },
|
{ title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind },
|
||||||
{ title: "Group", className: "group", sortBy: sortBy.group },
|
{ title: "Group", className: "group", sortBy: columnId.group, id: columnId.group },
|
||||||
{ title: "Version", className: "version", sortBy: sortBy.group },
|
{ title: "Version", className: "version", sortBy: columnId.version, id: columnId.version },
|
||||||
{ title: "Scope", className: "scope", sortBy: sortBy.scope },
|
{ title: "Scope", className: "scope", sortBy: columnId.scope, id: columnId.scope },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(crd: CustomResourceDefinition) => [
|
renderTableContents={(crd: CustomResourceDefinition) => [
|
||||||
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { parseJsonPath } from "../../utils/jsonPath";
|
|||||||
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
age = "age",
|
age = "age",
|
||||||
@ -55,9 +55,9 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
const isNamespaced = crd.isNamespaced();
|
const isNamespaced = crd.isNamespaced();
|
||||||
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
|
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
|
||||||
const sortingCallbacks: { [sortBy: string]: TableSortCallback } = {
|
const sortingCallbacks: { [sortBy: string]: TableSortCallback } = {
|
||||||
[sortBy.name]: (item: KubeObject) => item.getName(),
|
[columnId.name]: (item: KubeObject) => item.getName(),
|
||||||
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
|
[columnId.namespace]: (item: KubeObject) => item.getNs(),
|
||||||
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: KubeObject) => item.metadata.creationTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
extraColumns.forEach(column => {
|
extraColumns.forEach(column => {
|
||||||
@ -66,6 +66,8 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="crd_resources"
|
||||||
className="CrdResources"
|
className="CrdResources"
|
||||||
isClusterScoped={!isNamespaced}
|
isClusterScoped={!isNamespaced}
|
||||||
store={store}
|
store={store}
|
||||||
@ -75,18 +77,19 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle={crd.getResourceTitle()}
|
renderHeaderTitle={crd.getResourceTitle()}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
isNamespaced && { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
...extraColumns.map(column => {
|
...extraColumns.map(column => {
|
||||||
const { name } = column;
|
const { name } = column;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: name,
|
title: name,
|
||||||
className: name.toLowerCase(),
|
className: name.toLowerCase(),
|
||||||
sortBy: name
|
sortBy: name,
|
||||||
|
id: name
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(crdInstance: KubeObject) => [
|
renderTableContents={(crdInstance: KubeObject) => [
|
||||||
crdInstance.getName(),
|
crdInstance.getName(),
|
||||||
|
|||||||
@ -52,6 +52,10 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
|||||||
|
|
||||||
return compact(eventsWithError);
|
return compact(eventsWithError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWarningsCount() {
|
||||||
|
return this.getWarnings().length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventStore = new EventStore();
|
export const eventStore = new EventStore();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, { Fragment } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { TabLayout } from "../layout/tab-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import { KubeObjectListLayout, KubeObjectListLayoutProps, getDetailsUrl } from "../kube-object";
|
import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@ -12,11 +12,13 @@ import { cssNames, IClassName, stopPropagation } from "../../utils";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
|
message = "message",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
object = "object",
|
object = "object",
|
||||||
type = "type",
|
type = "type",
|
||||||
count = "count",
|
count = "count",
|
||||||
|
source = "source",
|
||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,15 +41,17 @@ export class Events extends React.Component<Props> {
|
|||||||
const events = (
|
const events = (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
{...layoutProps}
|
{...layoutProps}
|
||||||
|
isConfigurable
|
||||||
|
tableId="events"
|
||||||
className={cssNames("Events", className, { compact })}
|
className={cssNames("Events", className, { compact })}
|
||||||
store={eventStore}
|
store={eventStore}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.namespace]: (event: KubeEvent) => event.getNs(),
|
[columnId.namespace]: (event: KubeEvent) => event.getNs(),
|
||||||
[sortBy.type]: (event: KubeEvent) => event.involvedObject.kind,
|
[columnId.type]: (event: KubeEvent) => event.involvedObject.kind,
|
||||||
[sortBy.object]: (event: KubeEvent) => event.involvedObject.name,
|
[columnId.object]: (event: KubeEvent) => event.involvedObject.name,
|
||||||
[sortBy.count]: (event: KubeEvent) => event.count,
|
[columnId.count]: (event: KubeEvent) => event.count,
|
||||||
[sortBy.age]: (event: KubeEvent) => event.metadata.creationTimestamp,
|
[columnId.age]: (event: KubeEvent) => event.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(event: KubeEvent) => event.getSearchFields(),
|
(event: KubeEvent) => event.getSearchFields(),
|
||||||
@ -65,20 +69,20 @@ export class Events extends React.Component<Props> {
|
|||||||
small
|
small
|
||||||
material="help_outline"
|
material="help_outline"
|
||||||
className="help-icon"
|
className="help-icon"
|
||||||
tooltip="Limited to {eventStore.limit}"
|
tooltip={`Limited to ${eventStore.limit}`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Message", className: "message" },
|
{ title: "Message", className: "message", id: columnId.message },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Type", className: "type", sortBy: sortBy.type },
|
{ title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
|
||||||
{ title: "Involved Object", className: "object", sortBy: sortBy.object },
|
{ title: "Involved Object", className: "object", sortBy: columnId.object, id: columnId.object },
|
||||||
{ title: "Source", className: "source" },
|
{ title: "Source", className: "source", id: columnId.source },
|
||||||
{ title: "Count", className: "count", sortBy: sortBy.count },
|
{ title: "Count", className: "count", sortBy: columnId.count, id: columnId.count },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(event: KubeEvent) => {
|
renderTableContents={(event: KubeEvent) => {
|
||||||
const { involvedObject, type, message } = event;
|
const { involvedObject, type, message } = event;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { Spinner } from "../spinner";
|
|||||||
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
|
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
import { limitRangeStore } from "../+config-limit-ranges/limit-ranges.store";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Namespace> {
|
interface Props extends KubeObjectDetailsProps<Namespace> {
|
||||||
}
|
}
|
||||||
@ -24,8 +25,15 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
return resourceQuotaStore.getAllByNs(namespace);
|
return resourceQuotaStore.getAllByNs(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get limitranges() {
|
||||||
|
const namespace = this.props.object.getName();
|
||||||
|
|
||||||
|
return limitRangeStore.getAllByNs(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
resourceQuotaStore.loadAll();
|
resourceQuotaStore.loadAll();
|
||||||
|
limitRangeStore.loadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -52,6 +60,16 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
<DrawerItem name={`Limit Ranges`}>
|
||||||
|
{!this.limitranges && limitRangeStore.isLoading && <Spinner/>}
|
||||||
|
{this.limitranges.map(limitrange => {
|
||||||
|
return (
|
||||||
|
<Link key={limitrange.getId()} to={getDetailsUrl(limitrange.selfLink)}>
|
||||||
|
{limitrange.getName()}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DrawerItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import "./namespace-select.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Select, SelectOption, SelectProps } from "../select";
|
import { Select, SelectOption, SelectProps } from "../select";
|
||||||
import { cssNames, noop } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { namespaceStore } from "./namespace.store";
|
import { namespaceStore } from "./namespace.store";
|
||||||
import { FilterIcon } from "../item-object-list/filter-icon";
|
import { FilterIcon } from "../item-object-list/filter-icon";
|
||||||
import { FilterType } from "../item-object-list/page-filters.store";
|
import { FilterType } from "../item-object-list/page-filters.store";
|
||||||
|
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
showIcons?: boolean;
|
showIcons?: boolean;
|
||||||
@ -28,17 +29,13 @@ const defaultProps: Partial<Props> = {
|
|||||||
@observer
|
@observer
|
||||||
export class NamespaceSelect extends React.Component<Props> {
|
export class NamespaceSelect extends React.Component<Props> {
|
||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
private unsubscribe = noop;
|
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
if (!namespaceStore.isLoaded) {
|
disposeOnUnmount(this, [
|
||||||
await namespaceStore.loadAll();
|
kubeWatchApi.subscribeStores([namespaceStore], {
|
||||||
}
|
preload: true,
|
||||||
this.unsubscribe = namespaceStore.subscribe();
|
})
|
||||||
}
|
]);
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get options(): SelectOption[] {
|
@computed get options(): SelectOption[] {
|
||||||
|
|||||||
@ -1,83 +1,25 @@
|
|||||||
import { action, comparer, observable, reaction } from "mobx";
|
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||||
import { createPageParam } from "../../navigation";
|
import { createPageParam } from "../../navigation";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||||
import { 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",
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
multiValues: true,
|
multiValues: true,
|
||||||
get defaultValue() {
|
get defaultValue() {
|
||||||
return storage.get(); // initial namespaces coming from URL or local-storage (default)
|
return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@autobind()
|
export function getDummyNamespace(name: string) {
|
||||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|
||||||
api = namespacesApi;
|
|
||||||
contextNs = observable.array<string>();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
this.setContext(this.initNamespaces);
|
|
||||||
|
|
||||||
return reaction(() => this.contextNs.toJS(), namespaces => {
|
|
||||||
storage.set(namespaces); // save to local-storage
|
|
||||||
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
equals: comparer.identity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get initNamespaces() {
|
|
||||||
return namespaceUrlParam.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
getContextParams() {
|
|
||||||
return {
|
|
||||||
namespaces: this.contextNs.toJS(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(apis = [this.api]) {
|
|
||||||
const { accessibleNamespaces } = getHostedCluster();
|
|
||||||
|
|
||||||
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
|
||||||
if (accessibleNamespaces.length > 0) {
|
|
||||||
return Function; // no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.subscribe(apis);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async loadItems(namespaces?: string[]) {
|
|
||||||
if (!isAllowedResource("namespaces")) {
|
|
||||||
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (namespaces) {
|
|
||||||
return Promise.all(namespaces.map(name => this.api.get({ name })));
|
|
||||||
} else {
|
|
||||||
return super.loadItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getDummyNamespace(name: string) {
|
|
||||||
return new Namespace({
|
return new Namespace({
|
||||||
kind: "Namespace",
|
kind: Namespace.kind,
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
metadata: {
|
metadata: {
|
||||||
name,
|
name,
|
||||||
@ -88,6 +30,118 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||||
|
api = namespacesApi;
|
||||||
|
|
||||||
|
@observable contextNs = observable.array<string>();
|
||||||
|
@observable isReady = false;
|
||||||
|
|
||||||
|
whenReady = when(() => this.isReady);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await clusterStore.whenLoaded;
|
||||||
|
if (!getHostedCluster()) return;
|
||||||
|
await getHostedCluster().whenReady; // wait for cluster-state from main
|
||||||
|
|
||||||
|
this.setContext(this.initialNamespaces);
|
||||||
|
this.autoLoadAllowedNamespaces();
|
||||||
|
this.autoUpdateUrlAndLocalStorage();
|
||||||
|
|
||||||
|
this.isReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
||||||
|
return reaction(() => this.contextNs.toJS(), callback, {
|
||||||
|
equals: comparer.shallow,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoUpdateUrlAndLocalStorage(): IReactionDisposer {
|
||||||
|
return this.onContextChange(namespaces => {
|
||||||
|
storage.set(namespaces); // save to local-storage
|
||||||
|
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
|
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
||||||
|
fireImmediately: true,
|
||||||
|
equals: comparer.shallow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get allowedNamespaces(): string[] {
|
||||||
|
return toJS(getHostedCluster().allowedNamespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get initialNamespaces(): string[] {
|
||||||
|
const allowed = new Set(this.allowedNamespaces);
|
||||||
|
const prevSelected = storage.get();
|
||||||
|
|
||||||
|
if (Array.isArray(prevSelected)) {
|
||||||
|
return prevSelected.filter(namespace => allowed.has(namespace));
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise select "default" or first allowed namespace
|
||||||
|
if (allowed.has("default")) {
|
||||||
|
return ["default"];
|
||||||
|
} else if (allowed.size) {
|
||||||
|
return [Array.from(allowed)[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getContextNamespaces(): string[] {
|
||||||
|
const namespaces = this.contextNs.toJS();
|
||||||
|
|
||||||
|
// show all namespaces when nothing selected
|
||||||
|
if (!namespaces.length) {
|
||||||
|
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.allowedNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscribeApis() {
|
||||||
|
const { accessibleNamespaces } = getHostedCluster();
|
||||||
|
|
||||||
|
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
||||||
|
if (accessibleNamespaces.length > 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getSubscribeApis();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadItems(params: KubeObjectStoreLoadingParams) {
|
||||||
|
const { allowedNamespaces } = this;
|
||||||
|
|
||||||
|
let namespaces = await super.loadItems(params);
|
||||||
|
|
||||||
|
namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
|
||||||
|
|
||||||
|
if (!namespaces.length && allowedNamespaces.length > 0) {
|
||||||
|
return allowedNamespaces.map(getDummyNamespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setContext(namespaces: string[]) {
|
setContext(namespaces: string[]) {
|
||||||
this.contextNs.replace(namespaces);
|
this.contextNs.replace(namespaces);
|
||||||
@ -105,12 +159,6 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
else this.contextNs.push(namespace);
|
else this.contextNs.push(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
reset() {
|
|
||||||
super.reset();
|
|
||||||
this.contextNs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async remove(item: Namespace) {
|
async remove(item: Namespace) {
|
||||||
await super.remove(item);
|
await super.remove(item);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { INamespacesRouteParams } from "./namespaces.route";
|
|||||||
import { namespaceStore } from "./namespace.store";
|
import { namespaceStore } from "./namespace.store";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
labels = "labels",
|
labels = "labels",
|
||||||
age = "age",
|
age = "age",
|
||||||
@ -27,12 +27,14 @@ export class Namespaces extends React.Component<Props> {
|
|||||||
<TabLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
isClusterScoped
|
isClusterScoped
|
||||||
|
isConfigurable
|
||||||
|
tableId="namespaces"
|
||||||
className="Namespaces" store={namespaceStore}
|
className="Namespaces" store={namespaceStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (ns: Namespace) => ns.getName(),
|
[columnId.name]: (ns: Namespace) => ns.getName(),
|
||||||
[sortBy.labels]: (ns: Namespace) => ns.getLabels(),
|
[columnId.labels]: (ns: Namespace) => ns.getLabels(),
|
||||||
[sortBy.age]: (ns: Namespace) => ns.metadata.creationTimestamp,
|
[columnId.age]: (ns: Namespace) => ns.metadata.creationTimestamp,
|
||||||
[sortBy.status]: (ns: Namespace) => ns.getStatus(),
|
[columnId.status]: (ns: Namespace) => ns.getStatus(),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: Namespace) => item.getSearchFields(),
|
(item: Namespace) => item.getSearchFields(),
|
||||||
@ -40,11 +42,11 @@ export class Namespaces extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Namespaces"
|
renderHeaderTitle="Namespaces"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Labels", className: "labels", sortBy: sortBy.labels },
|
{ title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(item: Namespace) => [
|
renderTableContents={(item: Namespace) => [
|
||||||
item.getName(),
|
item.getName(),
|
||||||
|
|||||||
@ -9,9 +9,10 @@ import { endpointStore } from "./endpoints.store";
|
|||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
|
endpoints = "endpoints",
|
||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,22 +24,24 @@ export class Endpoints extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="network_endpoints"
|
||||||
className="Endpoints" store={endpointStore}
|
className="Endpoints" store={endpointStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (endpoint: Endpoint) => endpoint.getName(),
|
[columnId.name]: (endpoint: Endpoint) => endpoint.getName(),
|
||||||
[sortBy.namespace]: (endpoint: Endpoint) => endpoint.getNs(),
|
[columnId.namespace]: (endpoint: Endpoint) => endpoint.getNs(),
|
||||||
[sortBy.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp,
|
[columnId.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(endpoint: Endpoint) => endpoint.getSearchFields()
|
(endpoint: Endpoint) => endpoint.getSearchFields()
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle="Endpoints"
|
renderHeaderTitle="Endpoints"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Endpoints", className: "endpoints" },
|
{ title: "Endpoints", className: "endpoints", id: columnId.endpoints },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(endpoint: Endpoint) => [
|
renderTableContents={(endpoint: Endpoint) => [
|
||||||
endpoint.getName(),
|
endpoint.getName(),
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import { ingressStore } from "./ingress.store";
|
|||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { KubeObjectListLayout } from "../kube-object";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
|
loadBalancers ="load-balancers",
|
||||||
|
rules = "rules",
|
||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,11 +25,13 @@ export class Ingresses extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="network_ingresses"
|
||||||
className="Ingresses" store={ingressStore}
|
className="Ingresses" store={ingressStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (ingress: Ingress) => ingress.getName(),
|
[columnId.name]: (ingress: Ingress) => ingress.getName(),
|
||||||
[sortBy.namespace]: (ingress: Ingress) => ingress.getNs(),
|
[columnId.namespace]: (ingress: Ingress) => ingress.getNs(),
|
||||||
[sortBy.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp,
|
[columnId.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(ingress: Ingress) => ingress.getSearchFields(),
|
(ingress: Ingress) => ingress.getSearchFields(),
|
||||||
@ -35,12 +39,12 @@ export class Ingresses extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Ingresses"
|
renderHeaderTitle="Ingresses"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "LoadBalancers", className: "loadbalancers" },
|
{ title: "LoadBalancers", className: "loadbalancers", id: columnId.loadBalancers },
|
||||||
{ title: "Rules", className: "rules" },
|
{ title: "Rules", className: "rules", id: columnId.rules },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(ingress: Ingress) => [
|
renderTableContents={(ingress: Ingress) => [
|
||||||
ingress.getName(),
|
ingress.getName(),
|
||||||
|
|||||||
@ -9,9 +9,10 @@ import { INetworkPoliciesRouteParams } from "./network-policies.route";
|
|||||||
import { networkPolicyStore } from "./network-policy.store";
|
import { networkPolicyStore } from "./network-policy.store";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
|
types = "types",
|
||||||
age = "age",
|
age = "age",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,22 +24,24 @@ export class NetworkPolicies extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="network_policies"
|
||||||
className="NetworkPolicies" store={networkPolicyStore}
|
className="NetworkPolicies" store={networkPolicyStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: NetworkPolicy) => item.getName(),
|
[columnId.name]: (item: NetworkPolicy) => item.getName(),
|
||||||
[sortBy.namespace]: (item: NetworkPolicy) => item.getNs(),
|
[columnId.namespace]: (item: NetworkPolicy) => item.getNs(),
|
||||||
[sortBy.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: NetworkPolicy) => item.getSearchFields(),
|
(item: NetworkPolicy) => item.getSearchFields(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle="Network Policies"
|
renderHeaderTitle="Network Policies"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Policy Types", className: "type" },
|
{ title: "Policy Types", className: "type", id: columnId.types },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(item: NetworkPolicy) => [
|
renderTableContents={(item: NetworkPolicy) => [
|
||||||
item.getName(),
|
item.getName(),
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import "./service-details.scss";
|
import "./service-details.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { Service, endpointApi } from "../../api/endpoints";
|
import { Service } from "../../api/endpoints";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { ServicePortComponent } from "./service-port-component";
|
import { ServicePortComponent } from "./service-port-component";
|
||||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||||
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Service> {
|
interface Props extends KubeObjectDetailsProps<Service> {
|
||||||
}
|
}
|
||||||
@ -19,10 +20,11 @@ interface Props extends KubeObjectDetailsProps<Service> {
|
|||||||
@observer
|
@observer
|
||||||
export class ServiceDetails extends React.Component<Props> {
|
export class ServiceDetails extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!endpointStore.isLoaded) {
|
disposeOnUnmount(this, [
|
||||||
endpointStore.loadAll();
|
kubeWatchApi.subscribeStores([endpointStore], {
|
||||||
}
|
preload: true,
|
||||||
endpointApi.watch();
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import { Badge } from "../badge";
|
|||||||
import { serviceStore } from "./services.store";
|
import { serviceStore } from "./services.store";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
selector = "selector",
|
selector = "selector",
|
||||||
ports = "port",
|
ports = "port",
|
||||||
clusterIp = "cluster-ip",
|
clusterIp = "cluster-ip",
|
||||||
|
externalIp = "external-ip",
|
||||||
age = "age",
|
age = "age",
|
||||||
type = "type",
|
type = "type",
|
||||||
status = "status",
|
status = "status",
|
||||||
@ -29,16 +30,18 @@ export class Services extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="network_services"
|
||||||
className="Services" store={serviceStore}
|
className="Services" store={serviceStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (service: Service) => service.getName(),
|
[columnId.name]: (service: Service) => service.getName(),
|
||||||
[sortBy.namespace]: (service: Service) => service.getNs(),
|
[columnId.namespace]: (service: Service) => service.getNs(),
|
||||||
[sortBy.selector]: (service: Service) => service.getSelector(),
|
[columnId.selector]: (service: Service) => service.getSelector(),
|
||||||
[sortBy.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0],
|
[columnId.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0],
|
||||||
[sortBy.clusterIp]: (service: Service) => service.getClusterIp(),
|
[columnId.clusterIp]: (service: Service) => service.getClusterIp(),
|
||||||
[sortBy.type]: (service: Service) => service.getType(),
|
[columnId.type]: (service: Service) => service.getType(),
|
||||||
[sortBy.age]: (service: Service) => service.metadata.creationTimestamp,
|
[columnId.age]: (service: Service) => service.metadata.creationTimestamp,
|
||||||
[sortBy.status]: (service: Service) => service.getStatus(),
|
[columnId.status]: (service: Service) => service.getStatus(),
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(service: Service) => service.getSearchFields(),
|
(service: Service) => service.getSearchFields(),
|
||||||
@ -47,16 +50,16 @@ export class Services extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle="Services"
|
renderHeaderTitle="Services"
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ className: "warning" },
|
{ className: "warning", showWithColumn: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
{ title: "Type", className: "type", sortBy: sortBy.type },
|
{ title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
|
||||||
{ title: "Cluster IP", className: "clusterIp", sortBy: sortBy.clusterIp, },
|
{ title: "Cluster IP", className: "clusterIp", sortBy: columnId.clusterIp, id: columnId.clusterIp },
|
||||||
{ title: "Ports", className: "ports", sortBy: sortBy.ports },
|
{ title: "Ports", className: "ports", sortBy: columnId.ports, id: columnId.ports },
|
||||||
{ title: "External IP", className: "externalIp" },
|
{ title: "External IP", className: "externalIp", id: columnId.externalIp },
|
||||||
{ title: "Selector", className: "selector", sortBy: sortBy.selector },
|
{ title: "Selector", className: "selector", sortBy: columnId.selector, id: columnId.selector },
|
||||||
{ title: "Age", className: "age", sortBy: sortBy.age },
|
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(service: Service) => [
|
renderTableContents={(service: Service) => [
|
||||||
service.getName(),
|
service.getName(),
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./network.route";
|
export * from "./network.route";
|
||||||
export * from "./network";
|
export * from "./network";
|
||||||
|
export * from "./network.command";
|
||||||
|
|||||||
34
src/renderer/components/+network/network.command.ts
Normal file
34
src/renderer/components/+network/network.command.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
|
import { servicesURL } from "../+network-services";
|
||||||
|
import { endpointURL } from "../+network-endpoints";
|
||||||
|
import { ingressURL } from "../+network-ingresses";
|
||||||
|
import { networkPoliciesURL } from "../+network-policies";
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewServices",
|
||||||
|
title: "Cluster: View Services",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(servicesURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewEndpoints",
|
||||||
|
title: "Cluster: View Endpoints",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(endpointURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewIngresses",
|
||||||
|
title: "Cluster: View Ingresses",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(ingressURL())
|
||||||
|
});
|
||||||
|
|
||||||
|
commandRegistry.add({
|
||||||
|
id: "cluster.viewNetworkPolicies",
|
||||||
|
title: "Cluster: View NetworkPolicies",
|
||||||
|
scope: "cluster",
|
||||||
|
action: () => navigate(networkPoliciesURL())
|
||||||
|
});
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./nodes";
|
export * from "./nodes";
|
||||||
export * from "./nodes.route";
|
export * from "./nodes.route";
|
||||||
export * from "./node-details";
|
export * from "./node-details";
|
||||||
|
export * from "./node.command";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user