mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into feature/lens-proxy-tls
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
17477c4fd7
@ -58,6 +58,7 @@ jobs:
|
|||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
- bash: |
|
- bash: |
|
||||||
|
set -e
|
||||||
rm -rf extensions/telemetry
|
rm -rf extensions/telemetry
|
||||||
make integration-win
|
make integration-win
|
||||||
git checkout extensions/telemetry
|
git checkout extensions/telemetry
|
||||||
@ -102,6 +103,7 @@ jobs:
|
|||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
- bash: |
|
- bash: |
|
||||||
|
set -e
|
||||||
rm -rf extensions/telemetry
|
rm -rf extensions/telemetry
|
||||||
make integration-mac
|
make integration-mac
|
||||||
git checkout extensions/telemetry
|
git checkout extensions/telemetry
|
||||||
@ -159,6 +161,7 @@ jobs:
|
|||||||
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
|
||||||
- bash: |
|
- bash: |
|
||||||
|
set -e
|
||||||
rm -rf extensions/telemetry
|
rm -rf extensions/telemetry
|
||||||
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||||
git checkout extensions/telemetry
|
git checkout extensions/telemetry
|
||||||
|
|||||||
17
.dependabot/config.yml
Normal file
17
.dependabot/config.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# for config options
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 4
|
||||||
|
reviewers:
|
||||||
|
- "lensapp/lens-maintainers"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
versioning-strategy:
|
||||||
|
lockfile-only: false
|
||||||
|
increase: true
|
||||||
@ -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",
|
||||||
|
|||||||
30
.github/release-drafter.yml
vendored
Normal file
30
.github/release-drafter.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
categories:
|
||||||
|
- title: '🚀 Features'
|
||||||
|
labels:
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
labels:
|
||||||
|
- 'chore'
|
||||||
|
- 'area/ci'
|
||||||
|
- 'area/tests'
|
||||||
|
- 'dependencies'
|
||||||
|
|
||||||
|
template: |
|
||||||
|
## Changes since $PREVIOUS_TAG
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
- Lens v$RESOLVED_VERSION - Linux
|
||||||
|
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
|
||||||
|
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
|
||||||
|
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
|
||||||
|
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
||||||
|
- [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)
|
||||||
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 }}
|
||||||
2
Makefile
2
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
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 23 KiB |
BIN
build/icons/512x512@2x.png
Normal file
BIN
build/icons/512x512@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@ -43,6 +43,7 @@ You can use theme-based CSS Variables to style an extension according to the act
|
|||||||
## Button Colors
|
## Button Colors
|
||||||
- `--buttonPrimaryBackground`: button background color for primary actions.
|
- `--buttonPrimaryBackground`: button background color for primary actions.
|
||||||
- `--buttonDefaultBackground`: default button background color.
|
- `--buttonDefaultBackground`: default button background color.
|
||||||
|
- `--buttonLightBackground`: light button background color.
|
||||||
- `--buttonAccentBackground`: accent button background color.
|
- `--buttonAccentBackground`: accent button background color.
|
||||||
- `--buttonDisabledBackground`: disabled button background color.
|
- `--buttonDisabledBackground`: disabled button background color.
|
||||||
|
|
||||||
|
|||||||
@ -216,12 +216,14 @@ import { Component, LensRendererExtension, Navigation } from "@k8slens/extension
|
|||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends LensRendererExtension {
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
item: (
|
components: {
|
||||||
|
Item: (
|
||||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||||
<Component.Icon material="favorite" />
|
<Component.Icon material="favorite" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ Next, you'll try changing the way the new menu item appears in the UI. You'll ch
|
|||||||
|
|
||||||
Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`:
|
Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`:
|
||||||
|
|
||||||
```tsx
|
```typescript
|
||||||
clusterPageMenus = [
|
clusterPageMenus = [
|
||||||
{
|
{
|
||||||
target: { pageId: "hello" },
|
target: { pageId: "hello" },
|
||||||
|
|||||||
@ -216,12 +216,20 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above defines two cluster pages and three cluster page menu objects. The three cluster page menu objects include one parent menu item and two sub menu items. Parent items require an `id` value, whereas sub items require a `parentId` value. The value of the sub item `parentId` will match the value of the corresponding parent item `id`. Parent items don't require a `target` value. Assign values to the remaining properties as explained above.
|
The above defines two cluster pages and three cluster page menu objects.
|
||||||
|
The cluster page definitions are straightforward.
|
||||||
|
The three cluster page menu objects include one parent menu item and two sub menu items.
|
||||||
|
The first cluster page menu object defines the parent of a foldout submenu.
|
||||||
|
Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu.
|
||||||
|
Also note that the `target` field is not specified (it is ignored if the `id` field is specified).
|
||||||
|
This cluster page menu object specifies the `title` and `components` fields, which are used in displaying the menu item in the cluster dashboard sidebar.
|
||||||
|
Initially the submenu is hidden.
|
||||||
|
Activating this menu item toggles on and off the appearance of the submenu below it.
|
||||||
|
The remaining two cluster page menu objects define the contents of the submenu.
|
||||||
|
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case.
|
||||||
|
|
||||||
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### `globalPages`
|
### `globalPages`
|
||||||
|
|
||||||
Global pages are independent of the cluster dashboard and can fill the entire Lens UI. Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
|
Global pages are independent of the cluster dashboard and can fill the entire Lens UI. Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
|
||||||
@ -397,13 +405,19 @@ The properties of the `clusterFeatures` array objects are defined as follows:
|
|||||||
|
|
||||||
The four methods listed above are defined as follows:
|
The four methods listed above are defined as follows:
|
||||||
|
|
||||||
* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../api/README.md). This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page).
|
* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../api/README.md).
|
||||||
|
This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature. This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page).
|
* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature.
|
||||||
|
This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../api/README.md). This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page).
|
* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../api/README.md).
|
||||||
|
This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page).
|
||||||
|
|
||||||
* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class. Lens periodically calls this method to determine details about the feature's current status. Consider using the following properties with `updateStatus()`:
|
* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class.
|
||||||
|
Lens periodically calls this method to determine details about the feature's current status.
|
||||||
|
The implementation of this method should uninstall Kubernetes resources using the Kubernetes api (`K8sApi`)
|
||||||
|
Consider using the following properties with `updateStatus()`:
|
||||||
|
|
||||||
* `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description.
|
* `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description.
|
||||||
|
|
||||||
@ -597,7 +611,8 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
item: (
|
components: {
|
||||||
|
Item: (
|
||||||
<div
|
<div
|
||||||
className="flex align-center gaps"
|
className="flex align-center gaps"
|
||||||
onClick={() => this.navigate("help")}
|
onClick={() => this.navigate("help")}
|
||||||
@ -605,7 +620,8 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
<HelpIcon />
|
<HelpIcon />
|
||||||
My Status Bar Item
|
My Status Bar Item
|
||||||
</div>
|
</div>
|
||||||
),
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -613,7 +629,7 @@ export default class HelpExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
The properties of the `statusBarItems` array objects are defined as follows:
|
The properties of the `statusBarItems` array objects are defined as follows:
|
||||||
|
|
||||||
* `item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
* `Item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
||||||
* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages.
|
* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages.
|
||||||
|
|
||||||
### `kubeObjectMenuItems`
|
### `kubeObjectMenuItems`
|
||||||
@ -757,9 +773,20 @@ export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDe
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`. You can query this object for many details about the current namespace. In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method. Use the namespace's name to limit the list of pods only to those in the relevant namespace. To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method. The `K8sApi.podsApi` is automatically configured for the active cluster.
|
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
||||||
|
You can query this object for many details about the current namespace.
|
||||||
|
In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method.
|
||||||
|
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
||||||
|
To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method.
|
||||||
|
The `K8sApi.podsApi` is automatically configured for the active cluster.
|
||||||
|
|
||||||
Note that `K8sApi.podsApi.list()` is an asynchronous method. Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`. It is a common technique in React development to await async calls in `componentDidMount()`. However, `componentDidMount()` is called right after the first call to `render()`. In order to effect a subsequent `render()` call, React must be made aware of a state change. Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates. This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
Note that `K8sApi.podsApi.list()` is an asynchronous method.
|
||||||
|
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
||||||
|
It is a common technique in React development to await async calls in `componentDidMount()`.
|
||||||
|
However, `componentDidMount()` is called right after the first call to `render()`.
|
||||||
|
In order to effect a subsequent `render()` call, React must be made aware of a state change.
|
||||||
|
Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates.
|
||||||
|
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
||||||
|
|
||||||
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
||||||
Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one.
|
Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one.
|
||||||
|
|||||||
@ -10,7 +10,7 @@ For example, I have a component `GlobalPageMenuIcon` and want to test if `props.
|
|||||||
|
|
||||||
My component `GlobalPageMenuIcon`
|
My component `GlobalPageMenuIcon`
|
||||||
|
|
||||||
```tsx
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component: { Icon } } from "@k8slens/extensions";
|
import { Component: { Icon } } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
|||||||
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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
extensions/survey/main.ts
Normal file
9
extensions/survey/main.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { LensMainExtension } from "@k8slens/extensions";
|
||||||
|
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||||
|
|
||||||
|
export default class SurveyMainExtension extends LensMainExtension {
|
||||||
|
|
||||||
|
async onActivate() {
|
||||||
|
await surveyPreferencesStore.loadExtension(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
7928
extensions/survey/package-lock.json
generated
Normal file
7928
extensions/survey/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
extensions/survey/package.json
Normal file
28
extensions/survey/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-survey",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Lens survey",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"renderer": "dist/renderer.js",
|
||||||
|
"lens": {
|
||||||
|
"metadata": {},
|
||||||
|
"styles": []
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p",
|
||||||
|
"dev": "webpack --watch",
|
||||||
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
|
"got": "^11.8.1",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"node-machine-id": "^1.1.12",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"refiner-js": "^1.0.1",
|
||||||
|
"ts-loader": "^8.0.4",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"webpack": "^4.44.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
extensions/survey/renderer.tsx
Normal file
21
extensions/survey/renderer.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import { survey } from "./src/survey";
|
||||||
|
import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference";
|
||||||
|
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class SurveyRendererExtension extends LensRendererExtension {
|
||||||
|
appPreferences = [
|
||||||
|
{
|
||||||
|
title: "In-App Surveys",
|
||||||
|
components: {
|
||||||
|
Hint: () => <SurveyPreferenceHint/>,
|
||||||
|
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
async onActivate() {
|
||||||
|
await surveyPreferencesStore.loadExtension(this);
|
||||||
|
survey.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module "refiner-js" {
|
||||||
|
export default function Refiner(key: string, value: string|object|number|Boolean|Array): void;
|
||||||
|
}
|
||||||
27
extensions/survey/src/survey-preference.tsx
Normal file
27
extensions/survey/src/survey-preference.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from "@k8slens/extensions";
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { SurveyPreferencesStore } from "./survey-preferences-store";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> {
|
||||||
|
render() {
|
||||||
|
const { survey } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component.Checkbox
|
||||||
|
label="Allow in-app surveys"
|
||||||
|
value={survey.enabled}
|
||||||
|
onChange={v => survey.enabled = v }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SurveyPreferenceHint extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span>This will allow you to participate in surveys to improve the Lens experience.</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
extensions/survey/src/survey-preferences-store.ts
Normal file
36
extensions/survey/src/survey-preferences-store.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Store } from "@k8slens/extensions";
|
||||||
|
import { observable, toJS, when } from "mobx";
|
||||||
|
|
||||||
|
export type SurveyPreferencesModel = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SurveyPreferencesStore extends Store.ExtensionStore<SurveyPreferencesModel> {
|
||||||
|
|
||||||
|
@observable enabled = true;
|
||||||
|
|
||||||
|
whenEnabled = when(() => this.enabled);
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super({
|
||||||
|
configName: "preferences-store",
|
||||||
|
defaults: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fromStore({ enabled }: SurveyPreferencesModel): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): SurveyPreferencesModel {
|
||||||
|
return toJS({
|
||||||
|
enabled: this.enabled
|
||||||
|
}, {
|
||||||
|
recurseEverything: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const surveyPreferencesStore = SurveyPreferencesStore.getInstance<SurveyPreferencesStore>();
|
||||||
46
extensions/survey/src/survey.ts
Normal file
46
extensions/survey/src/survey.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Util } from "@k8slens/extensions";
|
||||||
|
import { machineId } from "node-machine-id";
|
||||||
|
import Refiner from "refiner-js";
|
||||||
|
import got from "got";
|
||||||
|
import { surveyPreferencesStore } from "./survey-preferences-store";
|
||||||
|
|
||||||
|
type SurveyIdResponse = {
|
||||||
|
surveyId: string;
|
||||||
|
};
|
||||||
|
export class Survey extends Util.Singleton {
|
||||||
|
static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43";
|
||||||
|
protected anonymousId: string;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await surveyPreferencesStore.whenEnabled;
|
||||||
|
|
||||||
|
const surveyId = await this.fetchSurveyId();
|
||||||
|
|
||||||
|
if (surveyId) {
|
||||||
|
Refiner("setProject", Survey.PROJECT_ID);
|
||||||
|
Refiner("identifyUser", {
|
||||||
|
id: surveyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSurveyId() {
|
||||||
|
try {
|
||||||
|
const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev";
|
||||||
|
const anonymousId = await machineId();
|
||||||
|
const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"});
|
||||||
|
|
||||||
|
return (body as SurveyIdResponse).surveyId;
|
||||||
|
} catch(error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const survey = Survey.getInstance<Survey>();
|
||||||
29
extensions/survey/tsconfig.json
Normal file
29
extensions/survey/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"sourceMap": false,
|
||||||
|
"declaration": false,
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"node_modules/*",
|
||||||
|
"../../types/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"renderer.ts",
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
67
extensions/survey/webpack.config.js
Normal file
67
extensions/survey/webpack.config.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
entry: "./main.ts",
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-main",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
"@k8slens/extensions": "var global.LensExtensions",
|
||||||
|
"react": "var global.React",
|
||||||
|
"mobx": "var global.Mobx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [ ".tsx", ".ts", ".js" ],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: "main.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: "./renderer.tsx",
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-renderer",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
"@k8slens/extensions": "var global.LensExtensions",
|
||||||
|
"react": "var global.React",
|
||||||
|
"mobx": "var global.Mobx",
|
||||||
|
"mobx-react": "var global.MobxReact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [ ".tsx", ".ts", ".js" ],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: "renderer.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
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,76 +1,17 @@
|
|||||||
/*
|
|
||||||
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 utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
import { spawnSync, exec } from "child_process";
|
import { listHelmRepositories } from "../helpers/utils";
|
||||||
import * as util from "util";
|
import { fail } from "assert";
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
|
||||||
|
|
||||||
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 = utils.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()) {
|
||||||
@ -79,7 +20,7 @@ describe("Lens integration tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
@ -96,8 +37,11 @@ describe("Lens integration tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ensures helm repos", async () => {
|
it("ensures helm repos", async () => {
|
||||||
const { stdout: reposJson } = await promiseExec("helm repo list -o json");
|
const repos = await listHelmRepositories();
|
||||||
const repos = JSON.parse(reposJson);
|
|
||||||
|
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.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
|
||||||
@ -110,476 +54,4 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.keys("Meta");
|
await app.client.keys("Meta");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.describeIf(ready)("workspaces", () => {
|
|
||||||
beforeAll(appStart, 20000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
return utils.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");
|
|
||||||
};
|
|
||||||
|
|
||||||
utils.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 utils.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 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.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(".Logs .VirtualList");
|
|
||||||
await app.client.waitForVisible(".LogResourceSelector");
|
|
||||||
await app.client.waitForVisible(".LogResourceSelector .SearchInput");
|
|
||||||
await app.client.waitForVisible(".LogResourceSelector .SearchInput input");
|
|
||||||
// Search for semicolon
|
|
||||||
await app.client.keys(":");
|
|
||||||
await app.client.waitForVisible(".Logs .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.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,4 +1,6 @@
|
|||||||
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",
|
||||||
@ -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 [];
|
||||||
|
}
|
||||||
|
|||||||
38
package.json
38
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-beta.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
||||||
"dev:main": "yarn run compile:main --watch",
|
"dev:main": "yarn run compile:main --watch",
|
||||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
"dev:extension-types": "yarn run compile:extension-types --watch --progress",
|
||||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||||
@ -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,10 @@
|
|||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
|
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
||||||
"target": [
|
"target": [
|
||||||
|
"deb",
|
||||||
|
"rpm",
|
||||||
"snap",
|
"snap",
|
||||||
"AppImage"
|
"AppImage"
|
||||||
],
|
],
|
||||||
@ -154,7 +157,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"include": "build/installer.nsh"
|
"include": "build/installer.nsh",
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
},
|
||||||
|
"snap": {
|
||||||
|
"confinement": "classic"
|
||||||
},
|
},
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
@ -162,10 +170,7 @@
|
|||||||
"repo": "lens",
|
"repo": "lens",
|
||||||
"owner": "lensapp"
|
"owner": "lensapp"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"snap": {
|
|
||||||
"confinement": "classic"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"lens": {
|
"lens": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@ -174,7 +179,8 @@
|
|||||||
"node-menu",
|
"node-menu",
|
||||||
"metrics-cluster-feature",
|
"metrics-cluster-feature",
|
||||||
"license-menu-item",
|
"license-menu-item",
|
||||||
"kube-object-event-status"
|
"kube-object-event-status",
|
||||||
|
"survey"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -183,6 +189,7 @@
|
|||||||
"@kubernetes/client-node": "^0.12.0",
|
"@kubernetes/client-node": "^0.12.0",
|
||||||
"array-move": "^3.0.0",
|
"array-move": "^3.0.0",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
|
"byline": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
"command-exists": "1.2.9",
|
"command-exists": "1.2.9",
|
||||||
@ -200,7 +207,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",
|
||||||
@ -215,12 +222,13 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
|
"readable-web-to-node-stream": "^3.0.1",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.8",
|
"request-promise-native": "^1.0.8",
|
||||||
"selfsigned": "^1.10.8",
|
"selfsigned": "^1.10.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",
|
||||||
@ -237,6 +245,7 @@
|
|||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@testing-library/jest-dom": "^5.11.5",
|
"@testing-library/jest-dom": "^5.11.5",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
|
"@types/byline": "^4.2.32",
|
||||||
"@types/chart.js": "^2.9.21",
|
"@types/chart.js": "^2.9.21",
|
||||||
"@types/circular-dependency-plugin": "^5.0.1",
|
"@types/circular-dependency-plugin": "^5.0.1",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
@ -286,7 +295,7 @@
|
|||||||
"@types/webpack-dev-server": "^3.11.1",
|
"@types/webpack-dev-server": "^3.11.1",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/webpack-node-externals": "^1.7.1",
|
"@types/webpack-node-externals": "^1.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.12.0",
|
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||||
"@typescript-eslint/parser": "^4.0.0",
|
"@typescript-eslint/parser": "^4.0.0",
|
||||||
"ace-builds": "^1.4.11",
|
"ace-builds": "^1.4.11",
|
||||||
"ansi_up": "^4.0.4",
|
"ansi_up": "^4.0.4",
|
||||||
@ -314,7 +323,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",
|
||||||
@ -329,6 +338,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",
|
||||||
|
|||||||
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { onCorrect, onceCorrect } from "../type-enforced-ipc";
|
||||||
|
|
||||||
|
describe("type enforced ipc tests", () => {
|
||||||
|
describe("onCorrect tests", () => {
|
||||||
|
it("should call the handler if the args are valid", () => {
|
||||||
|
let called = false;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called = true;
|
||||||
|
const verifier = (args: unknown[]): args is [] => true;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call the handler if the args are not valid", () => {
|
||||||
|
let called = false;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called = true;
|
||||||
|
const verifier = (args: unknown[]): args is [] => false;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the handler twice if the args are valid on two emits", () => {
|
||||||
|
let called = 0;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called += 1;
|
||||||
|
const verifier = (args: unknown[]): args is [] => true;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the handler twice if the args are [valid, invalid, valid]", () => {
|
||||||
|
let called = 0;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called += 1;
|
||||||
|
const results = [true, false, true];
|
||||||
|
const verifier = (args: unknown[]): args is [] => results.pop();
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
source.emit(channel);
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onceCorrect tests", () => {
|
||||||
|
it("should call the handler if the args are valid", () => {
|
||||||
|
let called = false;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called = true;
|
||||||
|
const verifier = (args: unknown[]): args is [] => true;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onceCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call the handler if the args are not valid", () => {
|
||||||
|
let called = false;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called = true;
|
||||||
|
const verifier = (args: unknown[]): args is [] => false;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onceCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the handler only once even if args are valid multiple times", () => {
|
||||||
|
let called = 0;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = () => called += 1;
|
||||||
|
const verifier = (args: unknown[]): args is [] => true;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onceCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel);
|
||||||
|
source.emit(channel);
|
||||||
|
expect(called).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the handler on only the first valid set of args", () => {
|
||||||
|
let called = "";
|
||||||
|
let verifierCalled = 0;
|
||||||
|
const source = new EventEmitter();
|
||||||
|
const listener = (info: any, arg: string) => called = arg;
|
||||||
|
const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0;
|
||||||
|
const channel = "foobar";
|
||||||
|
|
||||||
|
onceCorrect({ source, listener, verifier, channel });
|
||||||
|
|
||||||
|
source.emit(channel, {}, "a");
|
||||||
|
source.emit(channel, {}, "b");
|
||||||
|
source.emit(channel, {}, "c");
|
||||||
|
source.emit(channel, {}, "d");
|
||||||
|
source.emit(channel, {}, "e");
|
||||||
|
source.emit(channel, {}, "f");
|
||||||
|
source.emit(channel, {}, "g");
|
||||||
|
source.emit(channel, {}, "h");
|
||||||
|
source.emit(channel, {}, "i");
|
||||||
|
expect(called).toBe("c");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
3
src/common/ipc/index.ts
Normal file
3
src/common/ipc/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./ipc";
|
||||||
|
export * from "./update-available";
|
||||||
|
export * from "./type-enforced-ipc";
|
||||||
@ -3,10 +3,13 @@
|
|||||||
// 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 logger from "../main/logger";
|
import { toJS } from "mobx";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames";
|
import logger from "../../main/logger";
|
||||||
|
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||||
|
|
||||||
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
const subFramesChannel = "ipc:get-sub-frames";
|
||||||
|
|
||||||
|
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...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();
|
||||||
|
});
|
||||||
|
}
|
||||||
71
src/common/ipc/type-enforced-ipc.ts
Normal file
71
src/common/ipc/type-enforced-ipc.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
|
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||||
|
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
||||||
|
export type Rest<T> = T extends [any, ...infer R] ? R : [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to `source` that waits for the first IPC message with the correct
|
||||||
|
* argument data is sent.
|
||||||
|
* @param channel The channel to be listened on
|
||||||
|
* @param listener The function for the channel to be called if the args of the correct type
|
||||||
|
* @param verifier The function to be called to verify that the args are the correct type
|
||||||
|
*/
|
||||||
|
export function onceCorrect<
|
||||||
|
EM extends EventEmitter,
|
||||||
|
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
||||||
|
>({
|
||||||
|
source,
|
||||||
|
channel,
|
||||||
|
listener,
|
||||||
|
verifier,
|
||||||
|
}: {
|
||||||
|
source: EM,
|
||||||
|
channel: string | symbol,
|
||||||
|
listener: L,
|
||||||
|
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||||
|
}): void {
|
||||||
|
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
|
||||||
|
if (verifier(args)) {
|
||||||
|
source.removeListener(channel, handler); // remove immediately
|
||||||
|
|
||||||
|
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
|
||||||
|
.catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error }));
|
||||||
|
} else {
|
||||||
|
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.on(channel, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to `source` that checks to verify the arguments before calling the handler.
|
||||||
|
* @param channel The channel to be listened on
|
||||||
|
* @param listener The function for the channel to be called if the args of the correct type
|
||||||
|
* @param verifier The function to be called to verify that the args are the correct type
|
||||||
|
*/
|
||||||
|
export function onCorrect<
|
||||||
|
EM extends EventEmitter,
|
||||||
|
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
||||||
|
>({
|
||||||
|
source,
|
||||||
|
channel,
|
||||||
|
listener,
|
||||||
|
verifier,
|
||||||
|
}: {
|
||||||
|
source: EM,
|
||||||
|
channel: string | symbol,
|
||||||
|
listener: L,
|
||||||
|
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||||
|
}): void {
|
||||||
|
source.on(channel, (event, ...args: unknown[]) => {
|
||||||
|
if (verifier(args)) {
|
||||||
|
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
|
||||||
|
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
|
||||||
|
} else {
|
||||||
|
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
48
src/common/ipc/update-available/index.ts
Normal file
48
src/common/ipc/update-available/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { UpdateInfo } from "electron-updater";
|
||||||
|
|
||||||
|
export const UpdateAvailableChannel = "update-available";
|
||||||
|
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [<back-channel>, <update-info>]
|
||||||
|
*/
|
||||||
|
export type UpdateAvailableFromMain = [string, UpdateInfo];
|
||||||
|
|
||||||
|
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
|
||||||
|
if (args.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[0] !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[1] !== "object" || args[1] === null) {
|
||||||
|
// TODO: improve this checking
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackchannelArg = {
|
||||||
|
doUpdate: false;
|
||||||
|
} | {
|
||||||
|
doUpdate: true;
|
||||||
|
now: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateAvailableToBackchannel = [BackchannelArg];
|
||||||
|
|
||||||
|
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[0] !== "object" || args[0] === null) {
|
||||||
|
// TODO: improve this checking
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -7,37 +7,38 @@ export type KubeResource =
|
|||||||
"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: "limitranges" },
|
{ kind: "Namespace", apiName: "namespaces" },
|
||||||
{ resource: "namespaces" },
|
{ kind: "LimitRange", apiName: "limitranges" },
|
||||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
|
||||||
{ resource: "nodes" },
|
{ kind: "Node", apiName: "nodes" },
|
||||||
{ resource: "persistentvolumes" },
|
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
||||||
{ resource: "persistentvolumeclaims" },
|
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
||||||
{ resource: "pods" },
|
{ kind: "Pod", apiName: "pods" },
|
||||||
{ resource: "poddisruptionbudgets" },
|
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" },
|
||||||
{ resource: "podsecuritypolicies" },
|
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
||||||
{ resource: "resourcequotas" },
|
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
||||||
{ resource: "replicasets", group: "apps" },
|
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
||||||
{ resource: "secrets" },
|
{ kind: "Secret", apiName: "secrets" },
|
||||||
{ resource: "services" },
|
{ kind: "Service", apiName: "services" },
|
||||||
{ resource: "statefulsets", group: "apps" },
|
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
|
||||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
{ 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)
|
||||||
|
|||||||
@ -84,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;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import requestPromise from "request-promise-native";
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
|
|
||||||
export function getAppVersion(): string {
|
export function getAppVersion(): string {
|
||||||
@ -11,3 +12,13 @@ export function getBundledKubectlVersion(): string {
|
|||||||
export function getBundledExtensions(): string[] {
|
export function getBundledExtensions(): string[] {
|
||||||
return packageInfo.lens?.extensions || [];
|
return packageInfo.lens?.extensions || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
|
||||||
|
const response = await requestPromise({
|
||||||
|
method: "GET",
|
||||||
|
uri: `http://localhost:${proxyPort}/version`,
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(response.body).version;
|
||||||
|
}
|
||||||
|
|||||||
8
src/common/utils/delay.ts
Normal file
8
src/common/utils/delay.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Return a promise that will be resolved after at least `timeout` ms have
|
||||||
|
* passed
|
||||||
|
* @param timeout The number of milliseconds before resolving
|
||||||
|
*/
|
||||||
|
export function delay(timeout = 1000): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
|
}
|
||||||
@ -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";
|
||||||
@ -17,3 +18,4 @@ export * from "./openExternal";
|
|||||||
export * from "./downloadFile";
|
export * from "./downloadFile";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
|
export * from "./delay";
|
||||||
|
|||||||
@ -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();
|
||||||
@ -3,7 +3,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface StatusBarRegistration {
|
interface StatusBarComponents {
|
||||||
|
Item?: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusBarRegistrationV2 {
|
||||||
|
components: StatusBarComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusBarRegistration extends StatusBarRegistrationV2 {
|
||||||
|
/**
|
||||||
|
* @deprecated use components.Item instead
|
||||||
|
*/
|
||||||
item?: React.ReactNode;
|
item?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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/log.store";
|
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||||
|
|||||||
@ -126,6 +126,7 @@ describe("create clusters", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
||||||
|
jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true));
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
|
|||||||
@ -1,20 +1,78 @@
|
|||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
import { isDevelopment, isTestEnv } from "../common/vars";
|
||||||
|
import { delay } from "../common/utils";
|
||||||
|
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
export class AppUpdater {
|
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
|
||||||
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
|
if (arg.doUpdate) {
|
||||||
|
if (arg.now) {
|
||||||
static checkForUpdates() {
|
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
|
||||||
return autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.downloadUpdate()
|
||||||
|
.then(() => autoUpdater.quitAndInstall())
|
||||||
|
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error }));
|
||||||
|
} else {
|
||||||
|
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.downloadUpdate()
|
||||||
|
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`${AutoUpdateLogPrefix}: User chose not to update`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts the automatic update checking
|
||||||
|
* @param interval milliseconds between interval to check on, defaults to 24h
|
||||||
|
*/
|
||||||
|
export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
||||||
|
if (isDevelopment || isTestEnv) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
|
|
||||||
autoUpdater.logger = logger;
|
autoUpdater.logger = logger;
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
autoUpdater
|
||||||
|
.on("update-available", (args: UpdateInfo) => {
|
||||||
|
try {
|
||||||
|
const backchannel = `auto-update:${args.version}`;
|
||||||
|
|
||||||
|
ipcMain.removeAllListeners(backchannel); // only one handler should be present
|
||||||
|
|
||||||
|
// make sure that the handler is in place before broadcasting (prevent race-condition)
|
||||||
|
onceCorrect({
|
||||||
|
source: ipcMain,
|
||||||
|
channel: backchannel,
|
||||||
|
listener: handleAutoUpdateBackChannel,
|
||||||
|
verifier: areArgsUpdateAvailableToBackchannel,
|
||||||
|
});
|
||||||
|
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version });
|
||||||
|
broadcastMessage(UpdateAvailableChannel, backchannel, args);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function helper() {
|
||||||
|
while (true) {
|
||||||
|
await checkForUpdates();
|
||||||
|
await delay(interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {
|
helper();
|
||||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
}
|
||||||
|
|
||||||
return AppUpdater.checkForUpdates();
|
export async function checkForUpdates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`📡 Checking for app updates`);
|
||||||
|
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 && !cluster.initializing) {
|
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(() => {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export interface ClusterState {
|
|||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
|
isGlobalWatchEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +93,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable initializing = false;
|
@observable initializing = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initialized
|
* Is cluster object initialized
|
||||||
*
|
*
|
||||||
@ -178,6 +178,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
|
/**
|
||||||
|
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable isGlobalWatchEnabled = false;
|
||||||
/**
|
/**
|
||||||
* Preferences
|
* Preferences
|
||||||
*
|
*
|
||||||
@ -191,7 +197,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
|
||||||
*/
|
*/
|
||||||
@ -204,7 +210,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
|
||||||
*/
|
*/
|
||||||
@ -225,7 +231,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -280,7 +286,8 @@ 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.initializing = true;
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
@ -335,7 +342,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();
|
||||||
}
|
}
|
||||||
@ -352,9 +360,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAccessibility();
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
|
||||||
this.ready = true;
|
|
||||||
this.ensureKubectl();
|
this.ensureKubectl();
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
@ -374,7 +380,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();
|
||||||
@ -401,19 +408,18 @@ 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();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
await this.refreshAccessibility();
|
||||||
await this.refreshAllowedResources();
|
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
if (opts.refreshMetadata) {
|
||||||
this.refreshMetadata();
|
this.refreshMetadata();
|
||||||
}
|
}
|
||||||
this.ready = true;
|
|
||||||
}
|
}
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
@ -421,7 +427,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;
|
||||||
@ -432,7 +439,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action async refreshConnectionStatus() {
|
private async refreshAccessibility(): Promise<void> {
|
||||||
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
|
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
|
||||||
|
|
||||||
|
await this.refreshAllowedResources();
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
@ -442,7 +462,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();
|
||||||
}
|
}
|
||||||
@ -568,6 +589,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
|
||||||
|
return this.canI({
|
||||||
|
verb: "watch",
|
||||||
|
resource: "*",
|
||||||
|
...customizeResource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(): ClusterModel {
|
toJSON(): ClusterModel {
|
||||||
const model: ClusterModel = {
|
const model: ClusterModel = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -601,6 +633,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
|
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
@ -672,7 +705,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
|
||||||
@ -687,9 +720,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs Electron developer tools in the development build.
|
* Installs Electron developer tools in the development build.
|
||||||
* The dependency is not bundled to the production build.
|
* The dependency is not bundled to the production build.
|
||||||
*/
|
*/
|
||||||
export const installDeveloperTools = async () => {
|
export const installDeveloperTools = async () => {
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
logger.info("🤓 Installing developer tools");
|
||||||
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
||||||
|
|
||||||
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import path from "path";
|
|||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { ClusterManager } from "./cluster-manager";
|
import { ClusterManager } from "./cluster-manager";
|
||||||
import { AppUpdater } from "./app-updater";
|
|
||||||
import { shellSync } from "./shell-sync";
|
import { shellSync } from "./shell-sync";
|
||||||
import { getFreePort } from "./port";
|
import { getFreePort } from "./port";
|
||||||
import { mangleProxyEnv } from "./proxy-env";
|
import { mangleProxyEnv } from "./proxy-env";
|
||||||
@ -27,6 +26,9 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension-
|
|||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { filesystemProvisionerStore } from "./extension-filesystem";
|
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||||
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
|
import { startUpdateChecking } from "./app-updater";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -62,20 +64,20 @@ if (process.env.LENS_DISABLE_GPU) {
|
|||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
||||||
|
logger.info("🐚 Syncing shell environment");
|
||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
|
bindBroadcastHandlers();
|
||||||
|
|
||||||
powerMonitor.on("shutdown", () => {
|
powerMonitor.on("shutdown", () => {
|
||||||
app.exit();
|
app.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const updater = new AppUpdater();
|
|
||||||
|
|
||||||
updater.start();
|
|
||||||
|
|
||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
|
|
||||||
await installDeveloperTools();
|
await installDeveloperTools();
|
||||||
|
|
||||||
|
logger.info("💾 Loading stores");
|
||||||
// preload
|
// preload
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
@ -87,6 +89,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
// find free port
|
// find free port
|
||||||
try {
|
try {
|
||||||
|
logger.info("🔑 Getting free port for LensProxy server");
|
||||||
proxyPort = await getFreePort();
|
proxyPort = await getFreePort();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
@ -99,6 +102,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
// run proxy
|
// run proxy
|
||||||
try {
|
try {
|
||||||
|
logger.info("🔌 Starting LensProxy");
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
proxyServer = LensProxy.create(proxyPort, clusterManager);
|
proxyServer = LensProxy.create(proxyPort, clusterManager);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,9 +121,27 @@ app.on("ready", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// test proxy connection
|
||||||
|
try {
|
||||||
|
logger.info("🔎 Testing LensProxy connection ...");
|
||||||
|
const versionFromProxy = await getAppVersionFromProxyServer(proxyPort);
|
||||||
|
|
||||||
|
if (getAppVersion() !== versionFromProxy) {
|
||||||
|
logger.error(`Proxy server responded with invalid response`);
|
||||||
|
}
|
||||||
|
logger.info("⚡ LensProxy connection OK");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Checking proxy server connection failed", error);
|
||||||
|
}
|
||||||
|
|
||||||
extensionLoader.init();
|
extensionLoader.init();
|
||||||
extensionDiscovery.init();
|
extensionDiscovery.init();
|
||||||
|
|
||||||
|
logger.info("🖥️ Starting WindowManager");
|
||||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||||
|
windowManager.whenLoaded.then(() => startUpdateChecking());
|
||||||
|
|
||||||
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
// call after windowManager to see splash earlier
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class LensProxy {
|
|||||||
|
|
||||||
listen(port = this.port): this {
|
listen(port = this.port): this {
|
||||||
this.proxyServer = this.buildCustomProxy().listen(port);
|
this.proxyServer = this.buildCustomProxy().listen(port);
|
||||||
logger.info(`LensProxy server has started at ${this.origin}`);
|
logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -43,13 +43,13 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected buildCustomProxy(): http.Server {
|
protected buildCustomProxy(): http.Server {
|
||||||
const proxy = this.createProxy();
|
const proxy = this.createProxy();
|
||||||
const proxyCert = getProxyCertificate()
|
const proxyCert = getProxyCertificate();
|
||||||
const spdyProxy = spdy.createServer({
|
const spdyProxy = spdy.createServer({
|
||||||
key: proxyCert.private,
|
key: proxyCert.private,
|
||||||
cert: proxyCert.cert,
|
cert: proxyCert.cert,
|
||||||
spdy: {
|
spdy: {
|
||||||
plain: false,
|
plain: false,
|
||||||
protocols: ["http/1.1", "spdy/3.1"]
|
protocols: ["h2", "http/1.1", "spdy/3.1"]
|
||||||
}
|
}
|
||||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
this.handleRequest(proxy, req, res);
|
this.handleRequest(proxy, req, res);
|
||||||
@ -198,7 +198,8 @@ export class LensProxy {
|
|||||||
|
|
||||||
if (proxyTarget) {
|
if (proxyTarget) {
|
||||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||||
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
// this should be safe because we have already validated cluster uuid
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
return proxy.web(req, res, proxyTarget);
|
return proxy.web(req, res, proxyTarget);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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+[",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import path from "path";
|
|||||||
import { readFile } from "fs-extra";
|
import { readFile } from "fs-extra";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
|
||||||
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
|
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, versionRoute } from "./routes";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
@ -143,11 +143,9 @@ export class Router {
|
|||||||
this.handleStaticFile(params.path, response, req);
|
this.handleStaticFile(params.path, response, req);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
|
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
|
||||||
|
|
||||||
// Watch API
|
|
||||||
this.router.add({ method: "get", 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,6 +1,6 @@
|
|||||||
export * from "./kubeconfig-route";
|
export * from "./kubeconfig-route";
|
||||||
export * from "./metrics-route";
|
export * from "./metrics-route";
|
||||||
export * from "./port-forward-route";
|
export * from "./port-forward-route";
|
||||||
export * from "./watch-route";
|
|
||||||
export * from "./helm-route";
|
export * from "./helm-route";
|
||||||
export * from "./resource-applier-route";
|
export * from "./resource-applier-route";
|
||||||
|
export * from "./version-route";
|
||||||
|
|||||||
13
src/main/routes/version-route.ts
Normal file
13
src/main/routes/version-route.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { LensApiRequest } from "../router";
|
||||||
|
import { LensApi } from "../lens-api";
|
||||||
|
import { getAppVersion } from "../../common/utils";
|
||||||
|
|
||||||
|
class VersionRoute extends LensApi {
|
||||||
|
public async getVersion(request: LensApiRequest) {
|
||||||
|
const { response } = request;
|
||||||
|
|
||||||
|
this.respondJson(response, { version: getAppVersion()}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versionRoute = new VersionRoute();
|
||||||
@ -1,115 +0,0 @@
|
|||||||
import { LensApiRequest } from "../router";
|
|
||||||
import { LensApi } from "../lens-api";
|
|
||||||
import { Watch, KubeConfig } from "@kubernetes/client-node";
|
|
||||||
import { ServerResponse } from "http";
|
|
||||||
import { Request } from "request";
|
|
||||||
import logger from "../logger";
|
|
||||||
|
|
||||||
class ApiWatcher {
|
|
||||||
private apiUrl: string;
|
|
||||||
private response: ServerResponse;
|
|
||||||
private watchRequest: Request;
|
|
||||||
private watch: Watch;
|
|
||||||
private processor: NodeJS.Timeout;
|
|
||||||
private eventBuffer: any[] = [];
|
|
||||||
|
|
||||||
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
|
|
||||||
this.apiUrl = apiUrl;
|
|
||||||
this.watch = new Watch(kubeConfig);
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
if (this.processor) {
|
|
||||||
clearInterval(this.processor);
|
|
||||||
}
|
|
||||||
this.processor = setInterval(() => {
|
|
||||||
const events = this.eventBuffer.splice(0);
|
|
||||||
|
|
||||||
events.map(event => this.sendEvent(event));
|
|
||||||
this.response.flushHeaders();
|
|
||||||
}, 50);
|
|
||||||
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop() {
|
|
||||||
if (!this.watchRequest) { return; }
|
|
||||||
|
|
||||||
if (this.processor) {
|
|
||||||
clearInterval(this.processor);
|
|
||||||
}
|
|
||||||
logger.debug(`Stopping watcher for api: ${this.apiUrl}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.watchRequest.abort();
|
|
||||||
this.sendEvent({
|
|
||||||
type: "STREAM_END",
|
|
||||||
url: this.apiUrl,
|
|
||||||
status: 410,
|
|
||||||
});
|
|
||||||
logger.debug("watch aborted");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Watch abort errored:${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private watchHandler(phase: string, obj: any) {
|
|
||||||
this.eventBuffer.push({
|
|
||||||
type: phase,
|
|
||||||
object: obj
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private doneHandler(error: Error) {
|
|
||||||
if (error) logger.warn(`watch ended: ${error.toString()}`);
|
|
||||||
this.watchRequest.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendEvent(evt: any) {
|
|
||||||
// convert to "text/event-stream" format
|
|
||||||
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WatchRoute extends LensApi {
|
|
||||||
|
|
||||||
public async routeWatch(request: LensApiRequest) {
|
|
||||||
const { response, cluster} = request;
|
|
||||||
const apis: string[] = request.query.getAll("api");
|
|
||||||
const watchers: ApiWatcher[] = [];
|
|
||||||
|
|
||||||
if (!apis.length) {
|
|
||||||
this.respondJson(response, {
|
|
||||||
message: "Empty request. Query params 'api' are not provided.",
|
|
||||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
|
||||||
}, 400);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader("Content-Type", "text/event-stream");
|
|
||||||
response.setHeader("Cache-Control", "no-cache");
|
|
||||||
response.setHeader("Connection", "keep-alive");
|
|
||||||
logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`);
|
|
||||||
|
|
||||||
apis.forEach(apiUrl => {
|
|
||||||
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
|
|
||||||
|
|
||||||
watcher.start();
|
|
||||||
watchers.push(watcher);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.raw.req.on("close", () => {
|
|
||||||
logger.debug("Watch request closed");
|
|
||||||
watchers.map(watcher => watcher.stop());
|
|
||||||
});
|
|
||||||
|
|
||||||
request.raw.req.on("end", () => {
|
|
||||||
logger.debug("Watch request ended");
|
|
||||||
watchers.map(watcher => watcher.stop());
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const watchRoute = new WatchRoute();
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../package.json";
|
||||||
import { dialog, Menu, NativeImage, Tray } from "electron";
|
import { Menu, NativeImage, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu";
|
||||||
import { AppUpdater } from "./app-updater";
|
import { checkForUpdates } from "./app-updater";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
@ -62,16 +62,6 @@ function buildTray(icon: string | NativeImage, menu: Menu, windowManager: Window
|
|||||||
|
|
||||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
function createTrayMenu(windowManager: WindowManager): Menu {
|
||||||
return Menu.buildFromTemplate([
|
return Menu.buildFromTemplate([
|
||||||
{
|
|
||||||
label: "About Lens",
|
|
||||||
async click() {
|
|
||||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
|
||||||
const browserWindow = await windowManager.ensureMainWindow();
|
|
||||||
|
|
||||||
showAbout(browserWindow);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
{
|
||||||
label: "Open Lens",
|
label: "Open Lens",
|
||||||
async click() {
|
async click() {
|
||||||
@ -112,16 +102,17 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
{
|
{
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
async click() {
|
async click() {
|
||||||
const result = await AppUpdater.checkForUpdates();
|
await checkForUpdates();
|
||||||
|
await windowManager.ensureMainWindow();
|
||||||
if (!result) {
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "About Lens",
|
||||||
|
async click() {
|
||||||
|
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||||
const browserWindow = await windowManager.ensureMainWindow();
|
const browserWindow = await windowManager.ensureMainWindow();
|
||||||
|
|
||||||
dialog.showMessageBoxSync(browserWindow, {
|
showAbout(browserWindow);
|
||||||
message: "No updates available",
|
|
||||||
type: "info",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { observable } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
|
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
|
||||||
import windowStateKeeper from "electron-window-state";
|
import windowStateKeeper from "electron-window-state";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
@ -8,6 +8,7 @@ import { initMenu } from "./menu";
|
|||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export class WindowManager extends Singleton {
|
export class WindowManager extends Singleton {
|
||||||
protected mainWindow: BrowserWindow;
|
protected mainWindow: BrowserWindow;
|
||||||
@ -15,6 +16,9 @@ export class WindowManager extends Singleton {
|
|||||||
protected windowState: windowStateKeeper.State;
|
protected windowState: windowStateKeeper.State;
|
||||||
protected disposers: Record<string, Function> = {};
|
protected disposers: Record<string, Function> = {};
|
||||||
|
|
||||||
|
@observable mainViewInitiallyLoaded = false;
|
||||||
|
whenLoaded = when(() => this.mainViewInitiallyLoaded);
|
||||||
|
|
||||||
@observable activeClusterId: ClusterId;
|
@observable activeClusterId: ClusterId;
|
||||||
|
|
||||||
constructor(protected proxyPort: number) {
|
constructor(protected proxyPort: number) {
|
||||||
@ -81,16 +85,26 @@ export class WindowManager extends Singleton {
|
|||||||
this.splashWindow = null;
|
this.splashWindow = null;
|
||||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => {
|
||||||
|
logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mainWindow.webContents.on("did-finish-load", () => {
|
||||||
|
logger.info("[WINDOW-MANAGER]: Main window loaded");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (showSplash) await this.showSplash();
|
if (showSplash) await this.showSplash();
|
||||||
|
logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainUrl} ...`);
|
||||||
await this.mainWindow.loadURL(this.mainUrl);
|
await this.mainWindow.loadURL(this.mainUrl);
|
||||||
this.mainWindow.show();
|
this.mainWindow.show();
|
||||||
this.splashWindow?.close();
|
this.splashWindow?.close();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({ name: "app", action: "start" });
|
appEventBus.emit({ name: "app", action: "start" });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
this.mainViewInitiallyLoaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dialog.showErrorBox("ERROR!", err.toString());
|
dialog.showErrorBox("ERROR!", err.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,16 @@ 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 {
|
||||||
private apis = observable.map<string, KubeApi>();
|
private apis = observable.map<string, KubeApi>();
|
||||||
private stores = observable.map<KubeApi, KubeObjectStore>();
|
private stores = observable.map<string, KubeObjectStore>();
|
||||||
|
|
||||||
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));
|
||||||
@ -46,12 +46,12 @@ export class ApiManager {
|
|||||||
@action
|
@action
|
||||||
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
|
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
this.stores.set(api, store);
|
this.stores.set(api.apiBase, store);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
||||||
return this.stores.get(this.resolveApi(api)) as S;
|
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export interface IReleaseRevision {
|
|||||||
updated: string;
|
updated: string;
|
||||||
status: string;
|
status: string;
|
||||||
chart: string;
|
chart: string;
|
||||||
|
app_version: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,9 @@ export interface IPodContainer {
|
|||||||
configMapRef?: {
|
configMapRef?: {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
secretRef?: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
}[];
|
}[];
|
||||||
volumeMounts?: {
|
volumeMounts?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { EventEmitter } from "../../common/event-emitter";
|
import { EventEmitter } from "../../common/event-emitter";
|
||||||
import { cancelableFetch } from "../utils/cancelableFetch";
|
import { cancelableFetch } from "../utils/cancelableFetch";
|
||||||
|
|
||||||
export interface JsonApiData {
|
export interface JsonApiData {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +54,34 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
|
||||||
|
const reqPath = `${this.config.apiBase}${path}`;
|
||||||
|
//const subdomain = randomBytes(2).toString("hex");
|
||||||
|
let reqUrl = reqPath; //`http://${subdomain}.${window.location.host}${reqPath}`; // hack around browser connection limits (chromium allows 6 per domain)
|
||||||
|
const reqInit: RequestInit = { ...init };
|
||||||
|
const { query } = params || {} as P;
|
||||||
|
|
||||||
|
if (!reqInit.method) {
|
||||||
|
reqInit.method = "get";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const queryString = stringify(query);
|
||||||
|
|
||||||
|
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoLog: JsonApiLog = {
|
||||||
|
method: reqInit.method.toUpperCase(),
|
||||||
|
reqUrl: reqPath,
|
||||||
|
reqInit,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.writeLog({ ...infoLog });
|
||||||
|
|
||||||
|
return fetch(reqUrl, reqInit);
|
||||||
|
}
|
||||||
|
|
||||||
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||||
return this.request<T>(path, params, { ...reqInit, method: "post" });
|
return this.request<T>(path, params, { ...reqInit, method: "post" });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import { apiKube } from "./index";
|
|||||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||||
import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api";
|
||||||
import { IKubeObjectConstructor, KubeObject } from "./kube-object";
|
import { IKubeObjectConstructor, KubeObject } from "./kube-object";
|
||||||
import { kubeWatchApi } from "./kube-watch-api";
|
import byline from "byline";
|
||||||
|
import { ReadableWebToNodeStream } from "readable-web-to-node-stream";
|
||||||
|
import { IKubeWatchEvent } from "./kube-watch-api";
|
||||||
|
|
||||||
export interface IKubeApiOptions<T extends KubeObject> {
|
export interface IKubeApiOptions<T extends KubeObject> {
|
||||||
/**
|
/**
|
||||||
@ -91,15 +93,13 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KubeApiWatchOptions = {
|
||||||
|
namespace: string;
|
||||||
|
callback?: (data: IKubeWatchEvent) => void;
|
||||||
|
abortController?: AbortController
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
@ -112,6 +112,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
public objectConstructor: IKubeObjectConstructor<T>;
|
public objectConstructor: IKubeObjectConstructor<T>;
|
||||||
protected request: KubeJsonApi;
|
protected request: KubeJsonApi;
|
||||||
protected resourceVersions = new Map<string, string>();
|
protected resourceVersions = new Map<string, string>();
|
||||||
|
protected watchDisposer: () => void;
|
||||||
|
|
||||||
constructor(protected options: IKubeApiOptions<T>) {
|
constructor(protected options: IKubeApiOptions<T>) {
|
||||||
const {
|
const {
|
||||||
@ -124,7 +125,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 +158,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 {
|
||||||
@ -365,8 +366,88 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(): () => void {
|
watch(opts: KubeApiWatchOptions = { namespace: "" }): () => void {
|
||||||
return kubeWatchApi.subscribe(this);
|
if (!opts.abortController) {
|
||||||
|
opts.abortController = new AbortController();
|
||||||
|
}
|
||||||
|
const { abortController, namespace, callback } = opts;
|
||||||
|
|
||||||
|
const watchUrl = this.getWatchUrl(namespace);
|
||||||
|
const responsePromise = this.request.getResponse(watchUrl, null, {
|
||||||
|
signal: abortController.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
responsePromise.then((response) => {
|
||||||
|
if (!response.ok && !abortController.signal.aborted) {
|
||||||
|
if (response.status === 410) { // resourceVersion has gone
|
||||||
|
setTimeout(() => {
|
||||||
|
this.refreshResourceVersion().then(() => {
|
||||||
|
this.watch({...opts, abortController});
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} else if (response.status >= 500) { // k8s is having hard time
|
||||||
|
setTimeout(() => {
|
||||||
|
this.watch({...opts, abortController});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeStream = new ReadableWebToNodeStream(response.body);
|
||||||
|
const stream = byline(nodeStream);
|
||||||
|
|
||||||
|
stream.on("data", (line) => {
|
||||||
|
try {
|
||||||
|
const event: IKubeWatchEvent = JSON.parse(line);
|
||||||
|
|
||||||
|
this.modifyWatchEvent(event);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
} catch (ignore) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("close", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!abortController.signal.aborted) this.watch({...opts, namespace, callback});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}, (error) => {
|
||||||
|
if (error instanceof DOMException) return; // AbortController rejects, we can ignore it
|
||||||
|
|
||||||
|
console.error("watch rejected", error);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("watch error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const disposer = () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
|
||||||
|
return disposer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected modifyWatchEvent(event: IKubeWatchEvent) {
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "ADDED":
|
||||||
|
case "DELETED":
|
||||||
|
|
||||||
|
case "MODIFIED": {
|
||||||
|
ensureObjectSelfLink(this, event.object);
|
||||||
|
|
||||||
|
const { namespace, resourceVersion } = event.object.metadata;
|
||||||
|
|
||||||
|
this.setResourceVersion(namespace, resourceVersion);
|
||||||
|
this.setResourceVersion("", resourceVersion);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,141 @@
|
|||||||
// 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 { stringify } from "querystring";
|
|
||||||
import { autobind, EventEmitter } from "../utils";
|
|
||||||
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 { ClusterContext } from "../components/context";
|
||||||
import { apiManager } from "./api-manager";
|
|
||||||
import { apiPrefix, isDevelopment } from "../../common/vars";
|
|
||||||
import { getHostedCluster } from "../../common/cluster-store";
|
|
||||||
|
|
||||||
export interface IKubeWatchEvent<T = any> {
|
import plimit from "p-limit";
|
||||||
|
import { comparer, IReactionDisposer, observable, reaction, when } from "mobx";
|
||||||
|
import { autobind, noop } from "../utils";
|
||||||
|
import { KubeApi } from "./kube-api";
|
||||||
|
import { KubeJsonApiData } from "./kube-json-api";
|
||||||
|
import { isDebugging, isProduction } from "../../common/vars";
|
||||||
|
|
||||||
|
export interface IKubeWatchEvent<T = KubeJsonApiData> {
|
||||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
type: "ADDED" | "MODIFIED" | "DELETED";
|
||||||
object?: T;
|
object?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchRouteEvent {
|
export interface IKubeWatchSubscribeStoreOptions {
|
||||||
type: "STREAM_END";
|
namespaces?: string[]; // default: all accessible namespaces
|
||||||
url: string;
|
preload?: boolean; // preload store items, default: true
|
||||||
status: number;
|
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
||||||
|
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchRouteQuery {
|
export interface IKubeWatchLog {
|
||||||
api: string | string[];
|
message: string | string[] | Error;
|
||||||
|
meta?: object;
|
||||||
|
cssStyle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
protected evtSource: EventSource;
|
@observable context: ClusterContext = null;
|
||||||
protected onData = new EventEmitter<[IKubeWatchEvent]>();
|
@observable subscribers = observable.map<KubeApi, number>();
|
||||||
protected subscribers = observable.map<KubeApi, number>();
|
@observable isConnected = false;
|
||||||
protected reconnectTimeoutMs = 5000;
|
|
||||||
protected maxReconnectsOnError = 10;
|
contextReady = when(() => Boolean(this.context));
|
||||||
protected reconnectAttempts = this.maxReconnectsOnError;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
reaction(() => this.activeApis, () => this.connect(), {
|
this.init();
|
||||||
fireImmediately: true,
|
|
||||||
delay: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get activeApis() {
|
private async init() {
|
||||||
return Array.from(this.subscribers.keys());
|
await this.contextReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribersCount(api: KubeApi) {
|
isAllowedApi(api: KubeApi): boolean {
|
||||||
return this.subscribers.get(api) || 0;
|
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(...apis: KubeApi[]) {
|
preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
|
||||||
apis.forEach(api => {
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
|
const preloading: Promise<any>[] = [];
|
||||||
});
|
|
||||||
|
|
||||||
return () => apis.forEach(api => {
|
for (const store of stores) {
|
||||||
const count = this.getSubscribersCount(api) - 1;
|
preloading.push(limitRequests(async () => {
|
||||||
|
if (store.isLoaded && opts.loadOnce) return; // skip
|
||||||
|
|
||||||
if (count <= 0) this.subscribers.delete(api);
|
return store.loadAll({ namespaces: opts.namespaces });
|
||||||
else this.subscribers.set(api, count);
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getQuery(): Partial<IKubeWatchRouteQuery> {
|
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api: this.activeApis.map(api => {
|
loading: Promise.allSettled(preloading),
|
||||||
if (isAdmin) return api.getWatchUrl();
|
cancelLoading: () => limitRequests.clearQueue(),
|
||||||
|
|
||||||
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
|
|
||||||
}).flat()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: maybe switch to websocket to avoid often reconnects
|
subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
||||||
@autobind()
|
const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts;
|
||||||
protected connect() {
|
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
||||||
if (this.evtSource) this.disconnect(); // close previous connection
|
const unsubscribeList: Function[] = [];
|
||||||
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
if (!this.activeApis.length) {
|
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
|
||||||
|
let preloading = preload && load();
|
||||||
|
let cancelReloading: IReactionDisposer = noop;
|
||||||
|
|
||||||
|
const subscribe = () => {
|
||||||
|
if (isUnsubscribed) return;
|
||||||
|
|
||||||
|
stores.forEach((store) => {
|
||||||
|
unsubscribeList.push(store.subscribe());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preloading) {
|
||||||
|
if (waitUntilLoaded) {
|
||||||
|
preloading.loading.then(subscribe, error => {
|
||||||
|
this.log({
|
||||||
|
message: new Error("Loading stores has failed"),
|
||||||
|
meta: { stores, error, options: opts },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload stores only for context namespaces change
|
||||||
|
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
||||||
|
preloading?.cancelLoading();
|
||||||
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
|
unsubscribeList.length = 0;
|
||||||
|
preloading = load(namespaces);
|
||||||
|
preloading.loading.then(subscribe);
|
||||||
|
}, {
|
||||||
|
equals: comparer.shallow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
return () => {
|
||||||
|
if (isUnsubscribed) return;
|
||||||
|
isUnsubscribed = true;
|
||||||
|
cancelReloading();
|
||||||
|
preloading?.cancelLoading();
|
||||||
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
|
unsubscribeList.length = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected log({ message, cssStyle = "", meta = {} }: IKubeWatchLog) {
|
||||||
|
if (isProduction && !isDebugging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = this.getQuery();
|
|
||||||
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
|
|
||||||
|
|
||||||
this.evtSource = new EventSource(apiUrl);
|
const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String);
|
||||||
this.evtSource.onmessage = this.onMessage;
|
const logMeta = {
|
||||||
this.evtSource.onerror = this.onError;
|
time: new Date().toLocaleString(),
|
||||||
this.writeLog("CONNECTING", query.api);
|
...meta,
|
||||||
}
|
|
||||||
|
|
||||||
reconnect() {
|
|
||||||
if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) {
|
|
||||||
this.reconnectAttempts = this.maxReconnectsOnError;
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected disconnect() {
|
|
||||||
if (!this.evtSource) return;
|
|
||||||
this.evtSource.close();
|
|
||||||
this.evtSource.onmessage = null;
|
|
||||||
this.evtSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onMessage(evt: MessageEvent) {
|
|
||||||
if (!evt.data) return;
|
|
||||||
const data = JSON.parse(evt.data);
|
|
||||||
|
|
||||||
if ((data as IKubeWatchEvent).object) {
|
|
||||||
this.onData.emit(data);
|
|
||||||
} else {
|
|
||||||
this.onRouteEvent(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onRouteEvent(event: IKubeWatchRouteEvent) {
|
|
||||||
if (event.type === "STREAM_END") {
|
|
||||||
this.disconnect();
|
|
||||||
const { apiBase, namespace } = KubeApi.parseApi(event.url);
|
|
||||||
const api = apiManager.getApi(apiBase);
|
|
||||||
|
|
||||||
if (api) {
|
|
||||||
try {
|
|
||||||
await api.refreshResourceVersion({ namespace });
|
|
||||||
this.reconnect();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("failed to refresh resource version", error);
|
|
||||||
|
|
||||||
if (this.subscribers.size > 0) {
|
|
||||||
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("", resourceVersion);
|
|
||||||
|
|
||||||
ensureObjectSelfLink(api, evt.object);
|
|
||||||
|
|
||||||
if (store == apiManager.getStore(api)) {
|
|
||||||
callback(evt);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onData.addListener(listener);
|
if (message instanceof Error) {
|
||||||
|
console.error(...logInfo, logMeta);
|
||||||
return () => this.onData.removeListener(listener);
|
} else {
|
||||||
|
console.info(...logInfo, logMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.subscribers.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -77,7 +77,8 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
themeName="light"
|
themeName="light"
|
||||||
value={revision}
|
value={revision}
|
||||||
options={revisions}
|
options={revisions}
|
||||||
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`}
|
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}
|
||||||
|
- ${value.app_version}, updated: ${new Date(value.updated).toLocaleString()}`}
|
||||||
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
|
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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> {
|
||||||
@ -58,38 +58,35 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll(namespaces = namespaceStore.allowedNamespaces) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
let items;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
const items = await this.loadItems(namespaces);
|
||||||
|
|
||||||
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 loadFromContextNamespaces(): Promise<void> {
|
||||||
if (!namespaces) {
|
return this.loadAll(namespaceStore.contextNamespaces);
|
||||||
return helmReleasesApi.list();
|
}
|
||||||
} else {
|
|
||||||
|
async loadItems(namespaces: string[]) {
|
||||||
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);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -97,7 +94,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -105,7 +102,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async rollback(name: string, namespace: string, revision: number) {
|
async rollback(name: string, namespace: string, revision: number) {
|
||||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
@ -20,42 +16,24 @@ import { ClusterPieCharts } from "./cluster-pie-charts";
|
|||||||
|
|
||||||
@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);
|
reaction(
|
||||||
}
|
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||||
|
() => this.metricPoller.restart(true)
|
||||||
if (isAllowedResource("events")) {
|
),
|
||||||
this.stores.push(eventStore);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import React from "react";
|
|||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
import { LimitRange } from "../../api/endpoints/limit-range.api";
|
import { LimitRange } from "../../api/endpoints/limit-range.api";
|
||||||
|
|
||||||
enum sortBy {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
age = "age",
|
age = "age",
|
||||||
@ -23,12 +23,14 @@ export class LimitRanges extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="configuration_limitranges"
|
||||||
className="LimitRanges"
|
className="LimitRanges"
|
||||||
store={limitRangeStore}
|
store={limitRangeStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (item: LimitRange) => item.getName(),
|
[columnId.name]: (item: LimitRange) => item.getName(),
|
||||||
[sortBy.namespace]: (item: LimitRange) => item.getNs(),
|
[columnId.namespace]: (item: LimitRange) => item.getNs(),
|
||||||
[sortBy.age]: (item: LimitRange) => item.metadata.creationTimestamp,
|
[columnId.age]: (item: LimitRange) => item.metadata.creationTimestamp,
|
||||||
}}
|
}}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
(item: LimitRange) => item.getName(),
|
(item: LimitRange) => item.getName(),
|
||||||
@ -36,10 +38,10 @@ export class LimitRanges extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderHeaderTitle={"Limit Ranges"}
|
renderHeaderTitle={"Limit Ranges"}
|
||||||
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={(limitRange: LimitRange) => [
|
renderTableContents={(limitRange: LimitRange) => [
|
||||||
limitRange.getName(),
|
limitRange.getName(),
|
||||||
|
|||||||
@ -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())
|
||||||
|
});
|
||||||
@ -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",
|
||||||
@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
const { store } = this;
|
const { store } = this;
|
||||||
|
|
||||||
if (store && !store.isLoading && !store.isLoaded) {
|
if (store && !store.isLoading && !store.isLoaded) {
|
||||||
store.loadAll();
|
store.reloadAll();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
@ -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(),
|
||||||
@ -94,7 +97,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
...extraColumns.map((column) => {
|
...extraColumns.map((column) => {
|
||||||
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
|
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
|
||||||
|
|
||||||
if (Array.isArray(value) || typeof value === "object") {
|
if (Array.isArray(value) || typeof value === "object") {
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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(),
|
||||||
@ -72,13 +76,13 @@ export class Events extends React.Component<Props> {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
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;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
eventStore.loadAll();
|
eventStore.reloadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user