mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
f1abfb88bf
@ -19,27 +19,26 @@ jobs:
|
|||||||
node_12.x:
|
node_12.x:
|
||||||
node_version: 12.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- displayName: Set the tag name as an environment variable
|
- powershell: |
|
||||||
powershell: |
|
|
||||||
$CI_BUILD_TAG = git describe --tags
|
$CI_BUILD_TAG = git describe --tags
|
||||||
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
|
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- displayName: Install Node.js
|
- task: NodeTool@0
|
||||||
task: NodeTool@0
|
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
|
displayName: Install Node.js
|
||||||
|
|
||||||
- displayName: Cache Yarn packages
|
- task: Cache@2
|
||||||
task: Cache@2
|
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
|
||||||
restoreKeys: |
|
restoreKeys: |
|
||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- displayName: Customize config
|
- bash: |
|
||||||
bash: |
|
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
rm -rf .lens-ide-overlay/.git
|
rm -rf .lens-ide-overlay/.git
|
||||||
@ -47,15 +46,15 @@ jobs:
|
|||||||
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
|
displayName: Customize config
|
||||||
|
|
||||||
- displayName: Install dependencies
|
- script: make node_modules
|
||||||
script: make node_modules
|
displayName: Install dependencies
|
||||||
|
|
||||||
- displayName: Generate npm package
|
- script: make build-npm
|
||||||
script: make build-npm
|
displayName: Generate npm package
|
||||||
|
|
||||||
- displayName: Build
|
- script: make build
|
||||||
script: make build
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
||||||
@ -63,6 +62,7 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||||
BUILD_NUMBER: $(Build.BuildNumber)
|
BUILD_NUMBER: $(Build.BuildNumber)
|
||||||
|
displayName: Build
|
||||||
|
|
||||||
- job: macOS
|
- job: macOS
|
||||||
pool:
|
pool:
|
||||||
@ -72,25 +72,24 @@ jobs:
|
|||||||
node_12.x:
|
node_12.x:
|
||||||
node_version: 12.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- displayName: Set the tag name as an environment variable
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- displayName: Install Node.js
|
- task: NodeTool@0
|
||||||
task: NodeTool@0
|
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
|
displayName: Install Node.js
|
||||||
|
|
||||||
- displayName: Cache Yarn packages
|
- task: Cache@2
|
||||||
task: Cache@2
|
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||||
restoreKeys: |
|
restoreKeys: |
|
||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- displayName: Customize config
|
- bash: |
|
||||||
bash: |
|
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
rm -rf .lens-ide-overlay/.git
|
rm -rf .lens-ide-overlay/.git
|
||||||
@ -98,15 +97,15 @@ jobs:
|
|||||||
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
|
displayName: Customize config
|
||||||
|
|
||||||
- displayName: Install dependencies
|
- script: make node_modules
|
||||||
script: make node_modules
|
displayName: Install dependencies
|
||||||
|
|
||||||
- displayName: Generate npm package
|
- script: make build-npm
|
||||||
script: make build-npm
|
displayName: Generate npm package
|
||||||
|
|
||||||
- displayName: Build
|
- script: make build
|
||||||
script: make build
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
APPLEID: $(APPLEID)
|
APPLEID: $(APPLEID)
|
||||||
@ -116,6 +115,7 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||||
BUILD_NUMBER: $(Build.BuildNumber)
|
BUILD_NUMBER: $(Build.BuildNumber)
|
||||||
|
displayName: Build
|
||||||
|
|
||||||
- job: Linux
|
- job: Linux
|
||||||
pool:
|
pool:
|
||||||
@ -125,25 +125,24 @@ jobs:
|
|||||||
node_12.x:
|
node_12.x:
|
||||||
node_version: 12.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- displayName: Set the tag name as an environment variable
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- displayName: Install Node.js
|
- task: NodeTool@0
|
||||||
task: NodeTool@0
|
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
|
displayName: Install Node.js
|
||||||
|
|
||||||
- displayName: Cache Yarn packages
|
- task: Cache@2
|
||||||
task: Cache@2
|
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||||
restoreKeys: |
|
restoreKeys: |
|
||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- displayName: Customize config
|
- bash: |
|
||||||
bash: |
|
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
rm -rf .lens-ide-overlay/.git
|
rm -rf .lens-ide-overlay/.git
|
||||||
@ -151,15 +150,15 @@ jobs:
|
|||||||
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
|
displayName: Customize config
|
||||||
|
|
||||||
- displayName: Install dependencies
|
- script: make node_modules
|
||||||
script: make node_modules
|
displayName: Install dependencies
|
||||||
|
|
||||||
- displayName: Generate npm package
|
- script: make build-npm
|
||||||
script: make build-npm
|
displayName: Generate npm package
|
||||||
|
|
||||||
- displayName: Setup snapcraft
|
- bash: |
|
||||||
bash: |
|
|
||||||
sudo chown root:root /
|
sudo chown root:root /
|
||||||
sudo apt-get update && sudo apt-get install -y snapd
|
sudo apt-get update && sudo apt-get install -y snapd
|
||||||
sudo snap install snapcraft --classic
|
sudo snap install snapcraft --classic
|
||||||
@ -168,11 +167,12 @@ jobs:
|
|||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
SNAP_LOGIN: $(SNAP_LOGIN)
|
SNAP_LOGIN: $(SNAP_LOGIN)
|
||||||
|
displayName: Setup snapcraft
|
||||||
|
|
||||||
- displayName: Build
|
- script: make build
|
||||||
script: make build
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||||
BUILD_NUMBER: $(Build.BuildNumber)
|
BUILD_NUMBER: $(Build.BuildNumber)
|
||||||
|
displayName: Build
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
||||||
ignore:
|
|
||||||
- dependency-name: "*"
|
|
||||||
update-types:
|
|
||||||
- version-update:semver-major
|
|
||||||
@ -129,6 +129,7 @@ module.exports = {
|
|||||||
"avoidEscape": true,
|
"avoidEscape": true,
|
||||||
"allowTemplateLiterals": true,
|
"allowTemplateLiterals": true,
|
||||||
}],
|
}],
|
||||||
|
"react/prop-types": "off",
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
@ -196,6 +197,7 @@ module.exports = {
|
|||||||
"avoidEscape": true,
|
"avoidEscape": true,
|
||||||
"allowTemplateLiterals": true,
|
"allowTemplateLiterals": true,
|
||||||
}],
|
}],
|
||||||
|
"react/prop-types": "off",
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
|
|||||||
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
@ -1,9 +1,19 @@
|
|||||||
# Please see the documentation for all configuration options:
|
# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
# for config options
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "npm" # See documentation for possible values
|
- package-ecosystem: npm
|
||||||
directory: "/" # Location of package manifests
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: daily
|
||||||
|
open-pull-requests-limit: 4
|
||||||
|
reviewers:
|
||||||
|
- lensapp/lens-ide
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
versioning-strategy: increase
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types:
|
||||||
|
- version-update:semver-major
|
||||||
|
|||||||
1
.github/release-drafter.yml
vendored
1
.github/release-drafter.yml
vendored
@ -10,6 +10,7 @@ categories:
|
|||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
labels:
|
labels:
|
||||||
- 'chore'
|
- 'chore'
|
||||||
|
- 'area/documentation'
|
||||||
- 'area/ci'
|
- 'area/ci'
|
||||||
- 'area/tests'
|
- 'area/tests'
|
||||||
- 'dependencies'
|
- 'dependencies'
|
||||||
|
|||||||
4
Makefile
4
Makefile
@ -44,7 +44,7 @@ release-version:
|
|||||||
|
|
||||||
.PHONY: tag-release
|
.PHONY: tag-release
|
||||||
tag-release:
|
tag-release:
|
||||||
scripts/tag-release.sh
|
scripts/tag-release.sh $(CMD_ARGS)
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: binaries/client
|
test: binaries/client
|
||||||
@ -70,7 +70,7 @@ integration-win: binaries/client build-extension-types build-extensions
|
|||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: node_modules binaries/client
|
build: node_modules binaries/client
|
||||||
yarn run npm:fix-build-version
|
yarn run npm:fix-build-version
|
||||||
$(MAKE) build-extensions
|
$(MAKE) build-extensions -B
|
||||||
yarn run compile
|
yarn run compile
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
yarn run electron-builder --publish onTag --x64 --ia32
|
yarn run electron-builder --publish onTag --x64 --ia32
|
||||||
|
|||||||
@ -33,6 +33,7 @@ function getBuildChannel(): string {
|
|||||||
* Note: it is by design that we don't use `rc` as a build channel for these versions
|
* Note: it is by design that we don't use `rc` as a build channel for these versions
|
||||||
*/
|
*/
|
||||||
switch (versionInfo.prerelease?.[0]) {
|
switch (versionInfo.prerelease?.[0]) {
|
||||||
|
case "rc":
|
||||||
case "beta":
|
case "beta":
|
||||||
return "beta";
|
return "beta";
|
||||||
case undefined:
|
case undefined:
|
||||||
|
|||||||
@ -35,6 +35,14 @@ Just like Lens itself, the extension API updates on a monthly cadence, rolling o
|
|||||||
|
|
||||||
Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases).
|
Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases).
|
||||||
|
|
||||||
|
## Important changes since Lens v4
|
||||||
|
|
||||||
|
Lens has undergone major design improvements in v5, which have resulted in several large changes to the extension API.
|
||||||
|
Workspaces are gone, and the catalog is introduced for containing clusters, as well as other items, including custom entities.
|
||||||
|
Lens has migrated from using mobx 5 to mobx 6 for internal state management, and this may have ramifications for extension implementations.
|
||||||
|
Although the API retains many components from v4, given these changes, extensions written for Lens v4 are not compatible with the Lens v5 extension API.
|
||||||
|
See the [Lens v4 to v5 extension migration notes](extensions/extension-migration.md) on getting old extensions working in Lens v5.
|
||||||
|
|
||||||
## Looking for Help
|
## Looking for Help
|
||||||
|
|
||||||
If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time.
|
If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time.
|
||||||
@ -43,4 +51,4 @@ To provide feedback on the documentation or issues with the Lens Extension API,
|
|||||||
|
|
||||||
## Downloading Lens
|
## Downloading Lens
|
||||||
|
|
||||||
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.
|
[Download Lens](https://k8slens.dev/) for macOS, Windows, or Linux.
|
||||||
|
|||||||
@ -189,36 +189,6 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cluster Features
|
|
||||||
|
|
||||||
This extension can register installable features for a cluster.
|
|
||||||
These features are visible in the "Cluster Settings" page.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import React from "react"
|
|
||||||
import { Renderer } from "@k8slens/extensions"
|
|
||||||
import { MyCustomFeature } from "./src/my-custom-feature"
|
|
||||||
|
|
||||||
export default class ExampleExtension extends Renderer.LensExtension {
|
|
||||||
clusterFeatures = [
|
|
||||||
{
|
|
||||||
title: "My Custom Feature",
|
|
||||||
components: {
|
|
||||||
Description: () => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
Just an example.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
feature: new MyCustomFeature()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Top Bar Items
|
### Top Bar Items
|
||||||
|
|
||||||
This extension can register custom components to a top bar area.
|
This extension can register custom components to a top bar area.
|
||||||
|
|||||||
24
docs/extensions/extension-migration.md
Normal file
24
docs/extensions/extension-migration.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Lens v4 to v5 Extension Migration Notes
|
||||||
|
|
||||||
|
* Lens v5 inspects the version of the extension to ensure it is compatible.
|
||||||
|
The `package.json` for your extension must have an `"engines"` field specifying the lens version that your extension is targeted for, e.g:
|
||||||
|
```
|
||||||
|
"engines": {
|
||||||
|
"lens": "^5.0.0-beta.7"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
Note that Lens v5 supports all the range semantics that [semver](https://www.npmjs.com/package/semver) provides.
|
||||||
|
* Types and components have been reorganized, many have been grouped by process (`Main` and `Renderer`) plus those not specific to a process (`Common`).
|
||||||
|
For example the `LensMainExtension` class is now referred to by `Main.LensExtension`.
|
||||||
|
See the [API Reference](api/README.md) for the new organization.
|
||||||
|
* The `globalPageMenus` field of the Renderer extension class (now `Renderer.LensExtension`) is removed.
|
||||||
|
Global pages can still be made accessible via the application menus and the status bar, as well as from the newly added Welcome menu.
|
||||||
|
* The `clusterFeatures` field of the Renderer extension class (now `Renderer.LensExtension`) is removed.
|
||||||
|
Cluster features can still be implemented but Lens no longer dictates how a feature's lifecycle (install/upgrade/uninstall) is managed.
|
||||||
|
`Renderer.K8sApi.ResourceStack` provides the functionality to input and apply kubernetes resources to a cluster.
|
||||||
|
It is up to the extension developer to manage the lifecycle.
|
||||||
|
It could be applied automatically to a cluster by the extension or the end-user could be expected to install it, etc. from the cluster **Settings** page.
|
||||||
|
* Lens v5 now relies on mobx 6 for state management. Extensions that use mobx will need to be modified to work with mobx 6.
|
||||||
|
See [Migrating from Mobx 4/5](https://mobx.js.org/migrating-from-4-or-5.html) for specific details.
|
||||||
|
|
||||||
|
For an example of an existing extension that is compatible with Lens v5 see the [Lens Resource Map Extension](https://github.com/nevalla/lens-resource-map-extension)
|
||||||
@ -78,7 +78,7 @@ npm run dev
|
|||||||
You must restart Lens for the extension to load.
|
You must restart Lens for the extension to load.
|
||||||
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
|
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
|
||||||
|
|
||||||
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/latest/clusters/adding-clusters) for details on how to add a cluster in Lens IDE.
|
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE.
|
||||||
You will see the "Hello World" page in the left-side cluster menu.
|
You will see the "Hello World" page in the left-side cluster menu.
|
||||||
|
|
||||||
## Develop the Extension
|
## Develop the Extension
|
||||||
|
|||||||
@ -17,8 +17,9 @@ Each guide or code sample includes the following:
|
|||||||
| Guide | APIs |
|
| Guide | APIs |
|
||||||
| ----- | ----- |
|
| ----- | ----- |
|
||||||
| [Generate new extension project](generator.md) ||
|
| [Generate new extension project](generator.md) ||
|
||||||
| [Main process extension](main-extension.md) | LensMainExtension |
|
| [Main process extension](main-extension.md) | Main.LensExtension |
|
||||||
| [Renderer process extension](renderer-extension.md) | LensRendererExtension |
|
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
|
||||||
|
| [Resource stack (cluster feature)](resource-stack.md) | |
|
||||||
| [Stores](stores.md) | |
|
| [Stores](stores.md) | |
|
||||||
| [Components](components.md) | |
|
| [Components](components.md) | |
|
||||||
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
||||||
|
|||||||
5
docs/extensions/guides/catalog.md
Normal file
5
docs/extensions/guides/catalog.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Catalog (WIP)
|
||||||
|
|
||||||
|
## CatalogCategoryRegistry
|
||||||
|
|
||||||
|
## CatalogEntityRegistry
|
||||||
BIN
docs/extensions/guides/images/clusterfeature.png
Normal file
BIN
docs/extensions/guides/images/clusterfeature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 712 KiB |
@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
The Main Extension API is the interface to Lens's main process.
|
The Main Extension API is the interface to Lens's main process.
|
||||||
Lens runs in both main and renderer processes.
|
Lens runs in both main and renderer processes.
|
||||||
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items, and run custom code in Lens's main process.
|
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
|
||||||
|
It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
|
||||||
|
|
||||||
## `LensMainExtension` Class
|
## `Main.LensExtension` Class
|
||||||
|
|
||||||
### `onActivate()` and `onDeactivate()` Methods
|
### `onActivate()` and `onDeactivate()` Methods
|
||||||
|
|
||||||
To create a main extension simply extend the `LensMainExtension` class:
|
To create a main extension simply extend the `Main.LensExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Main } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
@ -75,3 +76,27 @@ Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
|
|||||||
In this example, we simply log a message.
|
In this example, we simply log a message.
|
||||||
However, you would typically have this navigate to a specific page or perform another operation.
|
However, you would typically have this navigate to a specific page or perform another operation.
|
||||||
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
||||||
|
|
||||||
|
The following example demonstrates how an application menu can be used to navigate to such a page:
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
export default class SamplePageMainExtension extends Main.LensExtension {
|
||||||
|
appMenus = [
|
||||||
|
{
|
||||||
|
parentId: "help",
|
||||||
|
label: "Sample",
|
||||||
|
click: () => this.navigate("myGlobalPage")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||||
|
This page would be defined in your extension's `Renderer.LensExtension` implmentation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||||
|
|
||||||
|
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||||
|
|
||||||
|
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
||||||
|
See the [`Catalog`](catalog.md) documentation for full details about the catalog.
|
||||||
@ -1,27 +1,40 @@
|
|||||||
# Renderer Extension
|
# Renderer Extension (WIP)
|
||||||
|
|
||||||
The Renderer Extension API is the interface to Lens's renderer process.
|
The Renderer Extension API is the interface to Lens's renderer process.
|
||||||
Lens runs in both the main and renderer processes.
|
Lens runs in both the main and renderer processes.
|
||||||
The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, and run custom code in Lens's renderer process.
|
The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, protocol handlers, and command palette commands, as well as run custom code in Lens's renderer process.
|
||||||
|
|
||||||
The custom Lens UI elements that you can add include:
|
The custom Lens UI elements that you can add include:
|
||||||
|
|
||||||
* [Cluster pages](#clusterpages)
|
* [Cluster pages](#clusterpages)
|
||||||
* [Cluster page menus](#clusterpagemenus)
|
* [Cluster page menus](#clusterpagemenus)
|
||||||
* [Global pages](#globalpages)
|
* [Global pages](#globalpages)
|
||||||
* [Global page menus](#globalpagemenus)
|
* [Welcome menus](#welcomemenus)
|
||||||
* [App preferences](#apppreferences)
|
* [App preferences](#apppreferences)
|
||||||
|
* [Top bar items](#topbaritems)
|
||||||
* [Status bar items](#statusbaritems)
|
* [Status bar items](#statusbaritems)
|
||||||
* [KubeObject menu items](#kubeobjectmenuitems)
|
* [KubeObject menu items](#kubeobjectmenuitems)
|
||||||
* [KubeObject detail items](#kubeobjectdetailitems)
|
* [KubeObject detail items](#kubeobjectdetailitems)
|
||||||
|
* [KubeObject status texts](#kubeobjectstatustexts)
|
||||||
|
* [Kube workloads overview items](#kubeworkloadsoverviewitems)
|
||||||
|
|
||||||
|
as well as catalog-related UI elements:
|
||||||
|
|
||||||
|
* [Entity settings](#entitysettings)
|
||||||
|
* [Catalog entity detail items](#catalogentitydetailitems)
|
||||||
|
|
||||||
All UI elements are based on React components.
|
All UI elements are based on React components.
|
||||||
|
|
||||||
## `LensRendererExtension` Class
|
Finally, you can also add commands and protocol handlers:
|
||||||
|
|
||||||
|
* [Command palette commands](#commandpalettecommands)
|
||||||
|
* [protocol handlers](protocol-handlers.md)
|
||||||
|
|
||||||
|
## `Renderer.LensExtension` Class
|
||||||
|
|
||||||
### `onActivate()` and `onDeactivate()` Methods
|
### `onActivate()` and `onDeactivate()` Methods
|
||||||
|
|
||||||
To create a renderer extension, extend the `LensRendererExtension` class:
|
To create a renderer extension, extend the `Renderer.LensExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
@ -47,7 +60,7 @@ Implementing `onDeactivate()` gives you the opportunity to clean up after your e
|
|||||||
|
|
||||||
1. Navigate to **File** > **Extensions** in the top menu bar.
|
1. Navigate to **File** > **Extensions** in the top menu bar.
|
||||||
(On Mac, it is **Lens** > **Extensions**.)
|
(On Mac, it is **Lens** > **Extensions**.)
|
||||||
2. Click **Disable** on the extension you want to disable.
|
2. For the extension you want to disable, open the context menu (click on the three vertical dots) and choose **Disable**.
|
||||||
|
|
||||||
The example above logs messages when the extension is enabled and disabled.
|
The example above logs messages when the extension is enabled and disabled.
|
||||||
|
|
||||||
@ -58,7 +71,7 @@ Use cluster pages to display information about or add functionality to the activ
|
|||||||
It is also possible to include custom details from other clusters.
|
It is also possible to include custom details from other clusters.
|
||||||
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
|
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
|
||||||
|
|
||||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
Add a cluster page definition to a `Renderer.LensExtension` subclass with the following example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
@ -111,7 +124,7 @@ Use `clusterPageMenus`, covered in the next section, to add cluster pages to the
|
|||||||
|
|
||||||
### `clusterPageMenus`
|
### `clusterPageMenus`
|
||||||
|
|
||||||
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav.
|
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav, below Lens's standard cluster menus like **Workloads**, **Custom Resources**, etc.
|
||||||
|
|
||||||
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
||||||
|
|
||||||
@ -253,18 +266,18 @@ Activating this menu item toggles on and off the appearance of the submenu below
|
|||||||
The remaining two cluster page menu objects define the contents of the submenu.
|
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.
|
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 could 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.
|
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.
|
Their primary use is to display information and provide functionality across clusters (or catalog entities), including customized data and functionality unique to your extension.
|
||||||
|
|
||||||
Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page.
|
Unlike cluster pages, users can trigger global pages even when there is no active cluster (or catalog entity).
|
||||||
You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems).
|
|
||||||
Unlike cluster pages, users can trigger global pages even when there is no active cluster.
|
|
||||||
|
|
||||||
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
The following example defines a `Renderer.LensExtension` subclass with a single global page definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
@ -313,260 +326,13 @@ This allows the `HelpExtension` object to be passed in the global page definitio
|
|||||||
This way, `HelpPage` can access all `HelpExtension` subclass data.
|
This way, `HelpPage` can access all `HelpExtension` subclass data.
|
||||||
|
|
||||||
This example code shows how to create a global page, but not how to make that page available to the Lens user.
|
This example code shows how to create a global page, but not how to make that page available to the Lens user.
|
||||||
Global pages can be made available in the following ways:
|
Global pages are typically made available in the following ways:
|
||||||
|
|
||||||
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
||||||
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
||||||
* To add global pages to the left side menu, see [`globalPageMenus`](#globalpagemenus).
|
* To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
|
||||||
|
|
||||||
### `globalPageMenus`
|
|
||||||
|
|
||||||
`globalPageMenus` allows you to add global page menu items to the left nav.
|
|
||||||
|
|
||||||
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Renderer } from "@k8slens/extensions";
|
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
export default class HelpExtension extends Renderer.LensExtension {
|
|
||||||
globalPages = [
|
|
||||||
{
|
|
||||||
id: "help",
|
|
||||||
components: {
|
|
||||||
Page: () => <HelpPage extension={this}/>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
globalPageMenus = [
|
|
||||||
{
|
|
||||||
target: { pageId: "help" },
|
|
||||||
title: "Help",
|
|
||||||
components: {
|
|
||||||
Icon: HelpIcon,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`globalPageMenus` is an array of objects that satisfy the `PageMenuRegistration` interface.
|
|
||||||
This element defines how the global page menu item will appear and what it will do when you click it.
|
|
||||||
The properties of the `globalPageMenus` array objects are defined as follows:
|
|
||||||
|
|
||||||
* `target` links to the relevant global page using `pageId`.
|
|
||||||
* `pageId` takes the value of the relevant global page's `id` property.
|
|
||||||
* `title` sets the name of the global page menu item that will display as a tooltip in the left nav.
|
|
||||||
* `components` is used to set an icon that appears in the left nav.
|
|
||||||
|
|
||||||
The above example creates a "Help" icon menu item.
|
|
||||||
When users click the icon, the Lens UI will display the contents of `ExamplePage`.
|
|
||||||
|
|
||||||
This example requires the definition of another React-based component, `HelpIcon`.
|
|
||||||
Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Renderer } from "@k8slens/extensions";
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
type IconProps = Renderer.Component.IconProps;
|
|
||||||
|
|
||||||
const {
|
|
||||||
Component: { Icon },
|
|
||||||
} = Renderer;
|
|
||||||
|
|
||||||
export function HelpIcon(props: IconProps) {
|
|
||||||
return <Icon {...props} material="help"/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HelpPage extends React.Component<{ extension: Renderer.LensExtension }> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Help</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Lens includes various built-in components available for extension developers to use.
|
|
||||||
One of these is the `Renderer.Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
|
||||||
The property that `Renderer.Component.Icon` uses is defined as follows:
|
|
||||||
|
|
||||||
* `material` takes the name of the icon you want to use.
|
|
||||||
|
|
||||||
This is what the example will look like, including how the menu item will appear in the left nav:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### `clusterFeatures`
|
|
||||||
|
|
||||||
Cluster features are Kubernetes resources that can be applied to and managed within the active cluster.
|
|
||||||
They can be installed and uninstalled by the Lens user from the cluster **Settings** page.
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**.
|
|
||||||
|
|
||||||
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Renderer } from "@k8slens/extensions"
|
|
||||||
import { ExampleFeature } from "./src/example-feature"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
export default class ExampleFeatureExtension extends Renderer.LensExtension {
|
|
||||||
clusterFeatures = [
|
|
||||||
{
|
|
||||||
title: "Example Feature",
|
|
||||||
components: {
|
|
||||||
Description: () => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
Enable an example feature.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
feature: new ExampleFeature()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The properties of the `clusterFeatures` array objects are defined as follows:
|
|
||||||
|
|
||||||
* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section.
|
|
||||||
* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
abstract install(cluster: Cluster): Promise<void>;
|
|
||||||
abstract upgrade(cluster: Cluster): Promise<void>;
|
|
||||||
abstract uninstall(cluster: Cluster): Promise<void>;
|
|
||||||
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
|
||||||
```
|
|
||||||
|
|
||||||
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 `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 `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.installed` should be set to `true` if the feature is installed, and `false` otherwise.
|
|
||||||
|
|
||||||
* `status.canUpgrade` is set according to a rule meant to determine whether the feature can be upgraded.
|
|
||||||
This rule can involve `status.currentVersion` and `status.latestVersion`, if desired.
|
|
||||||
|
|
||||||
The following shows a very simple implementation of a `ClusterFeature`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Renderer, Common } from "@k8slens/extensions";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
const {
|
|
||||||
K8sApi: {
|
|
||||||
ResourceStack,
|
|
||||||
forCluster,
|
|
||||||
StorageClass,
|
|
||||||
Namespace,
|
|
||||||
}
|
|
||||||
} = Renderer;
|
|
||||||
|
|
||||||
type ResourceStack = Renderer.K8sApi.ResourceStack;
|
|
||||||
type Pod = Renderer.K8sApi.Pod;
|
|
||||||
type KubernetesCluster = Common.Catalog.KubernetesCluster;
|
|
||||||
|
|
||||||
export interface MetricsStatus {
|
|
||||||
installed: boolean;
|
|
||||||
canUpgrade: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExampleFeature {
|
|
||||||
protected stack: ResourceStack;
|
|
||||||
|
|
||||||
constructor(protected cluster: KubernetesCluster) {
|
|
||||||
this.stack = new ResourceStack(cluster, this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
install(): Promise<string> {
|
|
||||||
return this.stack.kubectlApplyFolder(path.join(__dirname, "../resources/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
upgrade(): Promise<string> {
|
|
||||||
return this.install(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatus(): Promise<MetricsStatus> {
|
|
||||||
const status: MetricsStatus = { installed: false, canUpgrade: false};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pod = forCluster(cluster, Pod);
|
|
||||||
const examplePod = await pod.get({name: "example-pod", namespace: "default"});
|
|
||||||
|
|
||||||
if (examplePod?.kind) {
|
|
||||||
status.installed = true;
|
|
||||||
status.currentVersion = examplePod.spec.containers[0].image.split(":")[1];
|
|
||||||
status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature
|
|
||||||
} else {
|
|
||||||
status.installed = false;
|
|
||||||
status.canUpgrade = false;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
if (e?.error?.code === 404) {
|
|
||||||
status.installed = false;
|
|
||||||
status.canUpgrade = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
async uninstall(): Promise<string> {
|
|
||||||
return this.stack.kubectlDeleteFolder(this.resourceFolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This example implements the `install()` method by invoking the helper `applyResources()` method.
|
|
||||||
`applyResources()` tries to apply all resources read from all files found in the folder path provided.
|
|
||||||
In this case the folder path is the `../resources` subfolder relative to the current source code's folder.
|
|
||||||
The file `../resources/example-pod.yml` could contain:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: example-pod
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: example-pod
|
|
||||||
image: nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
The example above implements the four methods as follows:
|
|
||||||
|
|
||||||
* It implements `upgrade()` by invoking the `install()` method.
|
|
||||||
Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps.
|
|
||||||
|
|
||||||
* It implements `uninstall()` by utilizing the [Kubernetes API](../api/README.md) which Lens provides to delete the `example-pod` applied by the `install()` method.
|
|
||||||
|
|
||||||
* It implements `updateStatus()` by using the [Kubernetes API](../api/README.md) which Lens provides to determine whether the `example-pod` is installed, what version is associated with it, and whether it can be upgraded.
|
|
||||||
The implementation determines what the status is for a specific cluster feature.
|
|
||||||
|
|
||||||
|
### `welcomeMenus`
|
||||||
### `appPreferences`
|
### `appPreferences`
|
||||||
|
|
||||||
The Lens **Preferences** page is a built-in global page.
|
The Lens **Preferences** page is a built-in global page.
|
||||||
@ -615,6 +381,7 @@ In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExampleP
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
import { makeObservable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@ -633,6 +400,11 @@ export class ExamplePreferenceProps {
|
|||||||
@observer
|
@observer
|
||||||
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super({preference: { enabled: false}});
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { preference } = this.props;
|
const { preference } = this.props;
|
||||||
return (
|
return (
|
||||||
@ -666,7 +438,7 @@ It is used to indicate the state of the preference, and it is bound to the check
|
|||||||
|
|
||||||
`ExamplePreferenceHint` is a simple text span.
|
`ExamplePreferenceHint` is a simple text span.
|
||||||
|
|
||||||
The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
The above example introduces the decorators `makeObservable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
||||||
`mobx` simplifies state management.
|
`mobx` simplifies state management.
|
||||||
Without it, this example would not visually update the checkbox properly when the user activates it.
|
Without it, this example would not visually update the checkbox properly when the user activates it.
|
||||||
[Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements.
|
[Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements.
|
||||||
@ -677,6 +449,8 @@ Note that you can manage an extension's state data using an `ExtensionStore` obj
|
|||||||
To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state.
|
To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state.
|
||||||
However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore).
|
However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore).
|
||||||
|
|
||||||
|
### `topBarItems`
|
||||||
|
|
||||||
### `statusBarItems`
|
### `statusBarItems`
|
||||||
|
|
||||||
The status bar is the blue strip along the bottom of the Lens UI.
|
The status bar is the blue strip along the bottom of the Lens UI.
|
||||||
@ -998,3 +772,17 @@ Construct the table using the `Renderer.Component.Table` and related elements.
|
|||||||
For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods.
|
For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods.
|
||||||
The table is constructed using the `Renderer.Component.Table` and related elements.
|
The table is constructed using the `Renderer.Component.Table` and related elements.
|
||||||
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
||||||
|
|
||||||
|
### `kubeObjectStatusTexts`
|
||||||
|
|
||||||
|
### `kubeWorkloadsOverviewItems`
|
||||||
|
|
||||||
|
### `entitySettings`
|
||||||
|
|
||||||
|
### `catalogEntityDetailItems`
|
||||||
|
|
||||||
|
### `commandPaletteCommands`
|
||||||
|
|
||||||
|
### `protocolHandlers`
|
||||||
|
|
||||||
|
See the [Protocol Handlers Guide](protocol-handlers.md)
|
||||||
|
|||||||
238
docs/extensions/guides/resource-stack.md
Normal file
238
docs/extensions/guides/resource-stack.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# Resource Stack (Cluster Feature)
|
||||||
|
|
||||||
|
A cluster feature is a set of Kubernetes resources that can be applied to and managed within the active cluster.
|
||||||
|
The `Renderer.K8sApi.ResourceStack` class provides the functionality to input and apply kubernetes resources to a cluster.
|
||||||
|
It is up to the extension developer to manage the life cycle of the resource stack.
|
||||||
|
It could be applied automatically to a cluster by the extension, or the end-user could be required to install it.
|
||||||
|
|
||||||
|
The code examples in this section show how to create a resource stack, and define a cluster feature that is configurable from the cluster **Settings** page.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**.
|
||||||
|
|
||||||
|
The resource stack in this example consists of a single kubernetes resource:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: example-pod
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: example-pod
|
||||||
|
image: nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
It is simply a pod named `example-pod`, running nginx. Assume this content is in the file `../resources/example-pod.yml`.
|
||||||
|
|
||||||
|
The following code sample shows how to use the `Renderer.K8sApi.ResourceStack` to manage installing and uninstalling this resource stack:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Renderer, Common } from "@k8slens/extensions";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const {
|
||||||
|
K8sApi: {
|
||||||
|
ResourceStack,
|
||||||
|
forCluster,
|
||||||
|
Pod,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
type ResourceStack = Renderer.K8sApi.ResourceStack;
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
type KubernetesCluster = Common.Catalog.KubernetesCluster;
|
||||||
|
|
||||||
|
export class ExampleClusterFeature {
|
||||||
|
protected stack: ResourceStack;
|
||||||
|
|
||||||
|
constructor(protected cluster: KubernetesCluster) {
|
||||||
|
this.stack = new ResourceStack(cluster, "example-resource-stack");
|
||||||
|
}
|
||||||
|
|
||||||
|
get resourceFolder() {
|
||||||
|
return path.join(__dirname, "../resources/");
|
||||||
|
}
|
||||||
|
|
||||||
|
install(): Promise<string> {
|
||||||
|
console.log("installing example-pod");
|
||||||
|
return this.stack.kubectlApplyFolder(this.resourceFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isInstalled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const podApi = forCluster(this.cluster, Pod);
|
||||||
|
const examplePod = await podApi.get({name: "example-pod", namespace: "default"});
|
||||||
|
|
||||||
|
if (examplePod?.kind) {
|
||||||
|
console.log("found example-pod");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Error getting example-pod:", e);
|
||||||
|
}
|
||||||
|
console.log("didn't find example-pod");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uninstall(): Promise<string> {
|
||||||
|
console.log("uninstalling example-pod");
|
||||||
|
return this.stack.kubectlDeleteFolder(this.resourceFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ExampleClusterFeature` class constructor takes a `Common.Catalog.KubernetesCluster` argument.
|
||||||
|
This is the cluster that the resource stack will be applied to, and the constructor instantiates a `Renderer.K8sApi.ResourceStack` as such.
|
||||||
|
`ExampleClusterFeature` implements an `install()` method which simply invokes the `kubectlApplyFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||||||
|
`kubectlApplyFolder()` applies to the cluster all kubernetes resources found in the folder passed to it, in this case `../resources`.
|
||||||
|
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||||||
|
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
||||||
|
|
||||||
|
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utiliize the kubernetes api to inspect the resource stack status.
|
||||||
|
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
||||||
|
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
||||||
|
|
||||||
|
To allow the end-user to control the life cycle of this cluster feature the following code sample shows how to implement a user interface based on React and custom Lens UI components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import React from "react";
|
||||||
|
import { Common, Renderer } from "@k8slens/extensions";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { computed, observable, makeObservable } from "mobx";
|
||||||
|
import { ExampleClusterFeature } from "./example-cluster-feature";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
SubTitle, Button,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cluster: Common.Catalog.KubernetesCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ExampleClusterFeatureSettings extends React.Component<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable installed = false;
|
||||||
|
@observable inProgress = false;
|
||||||
|
|
||||||
|
feature: ExampleClusterFeature;
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.feature = new ExampleClusterFeature(this.props.cluster);
|
||||||
|
|
||||||
|
await this.updateFeatureState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFeatureState() {
|
||||||
|
this.installed = await this.feature.isInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.installed) {
|
||||||
|
await this.feature.uninstall();
|
||||||
|
} else {
|
||||||
|
await this.feature.install();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.inProgress = false;
|
||||||
|
|
||||||
|
await this.updateFeatureState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get buttonLabel() {
|
||||||
|
if (this.inProgress && this.installed) return "Uninstalling ...";
|
||||||
|
if (this.inProgress) return "Applying ...";
|
||||||
|
|
||||||
|
if (this.installed) {
|
||||||
|
return "Uninstall";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Apply";
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
<SubTitle title="Example Cluster Feature using a Resource Stack" />
|
||||||
|
<Button
|
||||||
|
label={this.buttonLabel}
|
||||||
|
waiting={this.inProgress}
|
||||||
|
onClick={() => this.save()}
|
||||||
|
primary />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ExampleClusterFeatureSettings` class extends `React.Component` and simply renders a subtitle and a button.
|
||||||
|
`ExampleClusterFeatureSettings` takes the cluster as a prop and when the React component has mounted the `ExampleClusterFeature` is instantiated using this cluster (in `componentDidMount()`).
|
||||||
|
The rest of the logic concerns the button appearance and action, based on the `ExampleClusterFeatureSettings` fields `installed` and `inProgress`.
|
||||||
|
The `installed` value is of course determined using the aforementioned `ExampleClusterFeature` method `isInstalled()`.
|
||||||
|
The `inProgress` value is true while waiting for the feature to be installed (or uninstalled).
|
||||||
|
|
||||||
|
Note that the button is a `Renderer.Component.Button` element and this example takes advantage of its `waiting` prop to show a "waiting" animation while the install (or uninstall) is in progress.
|
||||||
|
Using elements from `Renderer.Component` is encouraged, to take advantage of their built-in properties, and to ensure a common look and feel.
|
||||||
|
|
||||||
|
Also note that [MobX 6](https://mobx.js.org/README.html) is used for state management, ensuring that the UI is rerendered when state has changed.
|
||||||
|
The `ExampleClusterFeatureSettings` class is marked as an `@observer`, and its constructor must call `makeObservable()`.
|
||||||
|
As well, the `installed` and `inProgress` fields are marked as `@observable`, ensuring that the button gets rerendered any time these fields change.
|
||||||
|
|
||||||
|
Finally, `ExampleClusterFeatureSettings` needs to be connected to the extension, and would typically appear on the cluster **Settings** page via a `Renderer.LensExtension.entitySettings` entry.
|
||||||
|
The `ExampleExtension` would look like this:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Common, Renderer } from "@k8slens/extensions";
|
||||||
|
import { ExampleClusterFeatureSettings } from "./src/example-cluster-feature-settings"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
|
entitySettings = [
|
||||||
|
{
|
||||||
|
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
title: "Example Cluster Feature",
|
||||||
|
priority: 5,
|
||||||
|
components: {
|
||||||
|
View: ({ entity = null }: { entity: Common.Catalog.KubernetesCluster}) => {
|
||||||
|
return (
|
||||||
|
<ExampleClusterFeatureSettings cluster={entity} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An entity setting is added to the `entitySettings` array field of the `Renderer.LensExtension` class.
|
||||||
|
Because Lens's catalog can contain different kinds of entities, the kind must be identified.
|
||||||
|
For more details about the catalog see the [Catalog Guide](catalog.md).
|
||||||
|
Clusters are a built-in kind, so the `apiVersions` and `kind` fields should be set as above.
|
||||||
|
The `title` is shown as a navigation item on the cluster **Settings** page and the `components.View` is displayed when the navigation item is clicked on.
|
||||||
|
The `components.View` definition above shows how the `ExampleClusterFeatureSettings` element is included, and how its `cluster` prop is set.
|
||||||
|
`priority` determines the order of the entity settings, the higher the number the higher in the navigation panel the setting is placed. The default value is 50.
|
||||||
|
|
||||||
|
The final result looks like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
`ExampleClusterFeature` and `ExampleClusterFeatureSettings` demonstrate a cluster feature for a simple resource stack.
|
||||||
|
In practice a resource stack can include many resources, and require more sophisticated life cycle management (upgrades, partial installations, etc.)
|
||||||
|
Using `Renderer.K8sApi.ResourceStack` and `entitySettings` it is possible to implement solutions for more complex cluster features.
|
||||||
|
The **Lens Metrics** setting (on the cluster **Settings** page) is a good example of an advanced solution.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kube-object-event-status",
|
"name": "kube-object-event-status",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-metrics-cluster-feature",
|
"name": "lens-metrics-cluster-feature",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
|
|
||||||
@computed get isTogglable() {
|
@computed get isTogglable() {
|
||||||
if (this.inProgress) return false;
|
if (this.inProgress) return false;
|
||||||
if (!this.props.cluster.status.active) return false;
|
if (this.props.cluster.status.phase !== "connected") return false;
|
||||||
if (this.canUpgrade) return false;
|
if (this.canUpgrade) return false;
|
||||||
if (!this.isActiveMetricsProvider) return false;
|
if (!this.isActiveMetricsProvider) return false;
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ !this.props.cluster.status.active && (
|
{ this.props.cluster.status.phase !== "connected" && (
|
||||||
<section>
|
<section>
|
||||||
<p style={ {color: "var(--colorError)"} }>
|
<p style={ {color: "var(--colorError)"} }>
|
||||||
Lens Metrics settings requires established connection to the cluster.
|
Lens Metrics settings requires established connection to the cluster.
|
||||||
@ -225,7 +225,7 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
control={
|
control={
|
||||||
<Switcher
|
<Switcher
|
||||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||||
checked={!!this.featureStates.prometheus && this.props.cluster.status.active}
|
checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"}
|
||||||
onChange={v => this.togglePrometheus(v.target.checked)}
|
onChange={v => this.togglePrometheus(v.target.checked)}
|
||||||
name="prometheus"
|
name="prometheus"
|
||||||
/>
|
/>
|
||||||
@ -243,7 +243,7 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
control={
|
control={
|
||||||
<Switcher
|
<Switcher
|
||||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||||
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active}
|
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"}
|
||||||
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
||||||
name="node-exporter"
|
name="node-exporter"
|
||||||
/>
|
/>
|
||||||
@ -262,7 +262,7 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
control={
|
control={
|
||||||
<Switcher
|
<Switcher
|
||||||
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
|
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
|
||||||
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active}
|
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"}
|
||||||
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
||||||
name="node-exporter"
|
name="node-exporter"
|
||||||
/>
|
/>
|
||||||
@ -281,7 +281,9 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
waiting={this.inProgress}
|
waiting={this.inProgress}
|
||||||
onClick={() => this.save()}
|
onClick={() => this.save()}
|
||||||
primary
|
primary
|
||||||
disabled={!this.changed} />
|
disabled={!this.changed}
|
||||||
|
className="w-60 h-14"
|
||||||
|
/>
|
||||||
|
|
||||||
{this.canUpgrade && (<small className="hint">
|
{this.canUpgrade && (<small className="hint">
|
||||||
An update is available for enabled metrics components.
|
An update is available for enabled metrics components.
|
||||||
|
|||||||
2
extensions/node-menu/package-lock.json
generated
2
extensions/node-menu/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-node-menu",
|
"name": "lens-node-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -86,23 +86,26 @@ export function NodeMenu(props: NodeMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={shell}>
|
<MenuItem onClick={shell}>
|
||||||
<Icon svg="ssh" interactive={toolbar} title="Node shell"/>
|
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/>
|
||||||
<span className="title">Shell</span>
|
<span className="title">Shell</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!node.isUnschedulable() && (
|
{
|
||||||
<MenuItem onClick={cordon}>
|
node.isUnschedulable()
|
||||||
<Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/>
|
? (
|
||||||
<span className="title">Cordon</span>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{node.isUnschedulable() && (
|
|
||||||
<MenuItem onClick={unCordon}>
|
<MenuItem onClick={unCordon}>
|
||||||
<Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/>
|
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} />
|
||||||
<span className="title">Uncordon</span>
|
<span className="title">Uncordon</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)
|
||||||
|
: (
|
||||||
|
<MenuItem onClick={cordon}>
|
||||||
|
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} />
|
||||||
|
<span className="title">Cordon</span>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
<MenuItem onClick={drain}>
|
<MenuItem onClick={drain}>
|
||||||
<Icon material="delete_sweep" title="Drain" interactive={toolbar}/>
|
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/>
|
||||||
<span className="title">Drain</span>
|
<span className="title">Drain</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
2
extensions/pod-menu/package-lock.json
generated
2
extensions/pod-menu/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-pod-menu",
|
"name": "lens-pod-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
||||||
<Icon material="pageview" interactive={toolbar} title="Attach to Pod"/>
|
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/>
|
||||||
<span className="title">Attach Pod</span>
|
<span className="title">Attach Pod</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
<Icon material="subject" title="Logs" interactive={toolbar}/>
|
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/>
|
||||||
<span className="title">Logs</span>
|
<span className="title">Logs</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
<Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} />
|
||||||
<span className="title">Shell</span>
|
<span className="title">Shell</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -41,40 +41,38 @@ describe("Lens cluster pages", () => {
|
|||||||
const BACKSPACE = "\uE003";
|
const BACKSPACE = "\uE003";
|
||||||
let app: Application;
|
let app: Application;
|
||||||
const ready = minikubeReady(TEST_NAMESPACE);
|
const ready = minikubeReady(TEST_NAMESPACE);
|
||||||
|
let clusterAdded = false;
|
||||||
|
|
||||||
utils.describeIf(ready)("test common pages", () => {
|
utils.describeIf(ready)("test common pages", () => {
|
||||||
let clusterAdded = false;
|
|
||||||
const addCluster = async () => {
|
const addCluster = async () => {
|
||||||
await app.client.waitUntilTextExists("div", "Catalog");
|
|
||||||
await waitForMinikubeDashboard(app);
|
await waitForMinikubeDashboard(app);
|
||||||
await app.client.click('a[href="/nodes"]');
|
await app.client.click('a[href="/nodes"]');
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appStartAddCluster = async () => {
|
||||||
|
app = await utils.appStart();
|
||||||
|
await addCluster();
|
||||||
|
clusterAdded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tearDown = async () => {
|
||||||
|
await utils.tearDown(app);
|
||||||
|
clusterAdded = false;
|
||||||
|
};
|
||||||
|
|
||||||
describe("cluster add", () => {
|
describe("cluster add", () => {
|
||||||
utils.beforeAllWrapped(async () => {
|
utils.beforeAllWrapped(async () => {
|
||||||
app = await utils.appStart();
|
app = await utils.appStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.afterAllWrapped(async () => {
|
utils.afterAllWrapped(tearDown);
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows to add a cluster", async () => {
|
it("allows to add a cluster", async () => {
|
||||||
await addCluster();
|
await addCluster();
|
||||||
clusterAdded = true;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStartAddCluster = async () => {
|
|
||||||
if (clusterAdded) {
|
|
||||||
app = await utils.appStart();
|
|
||||||
await addCluster();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSidebarSelectors(itemId: string) {
|
function getSidebarSelectors(itemId: string) {
|
||||||
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
||||||
|
|
||||||
@ -86,12 +84,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("cluster pages", () => {
|
describe("cluster pages", () => {
|
||||||
utils.beforeAllWrapped(appStartAddCluster);
|
utils.beforeAllWrapped(appStartAddCluster);
|
||||||
|
utils.afterAllWrapped(tearDown);
|
||||||
utils.afterAllWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tests: {
|
const tests: {
|
||||||
drawer?: string
|
drawer?: string
|
||||||
@ -376,12 +369,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("viewing pod logs", () => {
|
describe("viewing pod logs", () => {
|
||||||
utils.beforeEachWrapped(appStartAddCluster);
|
utils.beforeEachWrapped(appStartAddCluster);
|
||||||
|
utils.afterEachWrapped(tearDown);
|
||||||
utils.afterEachWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`shows a log for a pod`, async () => {
|
it(`shows a log for a pod`, async () => {
|
||||||
expect(clusterAdded).toBe(true);
|
expect(clusterAdded).toBe(true);
|
||||||
@ -409,8 +397,12 @@ describe("Lens cluster pages", () => {
|
|||||||
// Open logs tab in dock
|
// Open logs tab in dock
|
||||||
await app.client.click(".list .TableRow:first-child");
|
await app.client.click(".list .TableRow:first-child");
|
||||||
await app.client.waitForVisible(".Drawer");
|
await app.client.waitForVisible(".Drawer");
|
||||||
await app.client.waitForVisible(`ul.KubeObjectMenu li.MenuItem i[title="Logs"]`);
|
|
||||||
await app.client.click("ul.KubeObjectMenu li.MenuItem i[title='Logs']");
|
const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']";
|
||||||
|
|
||||||
|
await app.client.waitForVisible(logsButton);
|
||||||
|
await app.client.click(logsButton);
|
||||||
|
|
||||||
// Check if controls are available
|
// Check if controls are available
|
||||||
await app.client.waitForVisible(".LogList .VirtualList");
|
await app.client.waitForVisible(".LogList .VirtualList");
|
||||||
await app.client.waitForVisible(".LogResourceSelector");
|
await app.client.waitForVisible(".LogResourceSelector");
|
||||||
@ -427,12 +419,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("cluster operations", () => {
|
describe("cluster operations", () => {
|
||||||
utils.beforeEachWrapped(appStartAddCluster);
|
utils.beforeEachWrapped(appStartAddCluster);
|
||||||
|
utils.afterEachWrapped(tearDown);
|
||||||
utils.afterEachWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows default namespace", async () => {
|
it("shows default namespace", async () => {
|
||||||
expect(clusterAdded).toBe(true);
|
expect(clusterAdded).toBe(true);
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export async function waitForMinikubeDashboard(app: Application) {
|
|||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
await app.client.click("div.TableRow");
|
await app.client.click("div.TableRow");
|
||||||
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
||||||
|
await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
|
|||||||
@ -37,27 +37,27 @@ interface DoneCallback {
|
|||||||
* This is necessary because Jest doesn't do this correctly.
|
* This is necessary because Jest doesn't do this correctly.
|
||||||
* @param fn The function to call
|
* @param fn The function to call
|
||||||
*/
|
*/
|
||||||
export function wrapJestLifecycle(fn: () => Promise<void>): (done: DoneCallback) => void {
|
export function wrapJestLifecycle(fn: () => Promise<void> | void): (done: DoneCallback) => void {
|
||||||
return function (done: DoneCallback) {
|
return function (done: DoneCallback) {
|
||||||
fn()
|
(async () => fn())()
|
||||||
.then(() => done())
|
.then(() => done())
|
||||||
.catch(error => done.fail(error));
|
.catch(error => done.fail(error));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeAllWrapped(fn: () => Promise<void>): void {
|
export function beforeAllWrapped(fn: () => Promise<void> | void): void {
|
||||||
beforeAll(wrapJestLifecycle(fn));
|
beforeAll(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeEachWrapped(fn: () => Promise<void>): void {
|
export function beforeEachWrapped(fn: () => Promise<void> | void): void {
|
||||||
beforeEach(wrapJestLifecycle(fn));
|
beforeEach(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function afterAllWrapped(fn: () => Promise<void>): void {
|
export function afterAllWrapped(fn: () => Promise<void> | void): void {
|
||||||
afterAll(wrapJestLifecycle(fn));
|
afterAll(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function afterEachWrapped(fn: () => Promise<void>): void {
|
export function afterEachWrapped(fn: () => Promise<void> | void): void {
|
||||||
afterEach(wrapJestLifecycle(fn));
|
afterEach(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,13 +99,17 @@ export async function appStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showCatalog(app: Application) {
|
export async function showCatalog(app: Application) {
|
||||||
await app.client.waitUntilTextExists("[data-test-id=catalog-link]", "Catalog");
|
await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon");
|
||||||
await app.client.click("[data-test-id=catalog-link]");
|
await app.client.click("#hotbarIcon-catalog-entity .Icon");
|
||||||
}
|
}
|
||||||
|
|
||||||
type AsyncPidGetter = () => Promise<number>;
|
type AsyncPidGetter = () => Promise<number>;
|
||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app?: Application) {
|
||||||
|
if (!app?.isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
||||||
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
|
|||||||
@ -22,6 +22,8 @@ nav:
|
|||||||
- Generator: extensions/guides/generator.md
|
- Generator: extensions/guides/generator.md
|
||||||
- Main Extension: extensions/guides/main-extension.md
|
- Main Extension: extensions/guides/main-extension.md
|
||||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||||
|
- Catalog: extensions/guides/catalog.md
|
||||||
|
- Resource Stack: extensions/guides/resource-stack.md
|
||||||
- Stores: extensions/guides/stores.md
|
- Stores: extensions/guides/stores.md
|
||||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||||
- Protocol Handlers: extensions/guides/protocol-handlers.md
|
- Protocol Handlers: extensions/guides/protocol-handlers.md
|
||||||
@ -83,7 +85,7 @@ extra:
|
|||||||
link: https://k8slens.dev/
|
link: https://k8slens.dev/
|
||||||
name: Lens Website
|
name: Lens Website
|
||||||
version:
|
version:
|
||||||
method: mike
|
provider: mike
|
||||||
analytics:
|
analytics:
|
||||||
provider: google
|
provider: google
|
||||||
property: UA-159377374-2
|
property: UA-159377374-2
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.0-beta.8",
|
"version": "5.0.0-beta.13",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -222,7 +222,7 @@
|
|||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"path-to-regexp": "^6.1.0",
|
"path-to-regexp": "^6.1.0",
|
||||||
"proper-lockfile": "^4.1.1",
|
"proper-lockfile": "^4.1.2",
|
||||||
"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",
|
||||||
@ -321,7 +321,7 @@
|
|||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-plugin-unused-imports": "^1.0.1",
|
"eslint-plugin-unused-imports": "^1.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
@ -367,7 +367,7 @@
|
|||||||
"typed-emitter": "^1.3.1",
|
"typed-emitter": "^1.3.1",
|
||||||
"typedoc": "0.21.0-beta.2",
|
"typedoc": "0.21.0-beta.2",
|
||||||
"typedoc-plugin-markdown": "^3.9.0",
|
"typedoc-plugin-markdown": "^3.9.0",
|
||||||
"typeface-roboto": "^0.0.75",
|
"typeface-roboto": "^1.1.13",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.3.2",
|
||||||
"typescript-plugin-css-modules": "^3.2.0",
|
"typescript-plugin-css-modules": "^3.2.0",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.0",
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
key="$1"
|
||||||
|
|
||||||
|
case $key in
|
||||||
|
-f|--force)
|
||||||
|
FORCE="--force"
|
||||||
|
shift # past argument
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
if [[ `git branch --show-current` =~ ^release/v ]]
|
if [[ `git branch --show-current` =~ ^release/v ]]
|
||||||
then
|
then
|
||||||
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
|
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
|
||||||
git tag ${VERSION_STRING}
|
git tag ${VERSION_STRING} ${FORCE}
|
||||||
git push ${GIT_REMOTE:-origin} ${VERSION_STRING}
|
git push ${GIT_REMOTE:-origin} ${VERSION_STRING} ${FORCE}
|
||||||
else
|
else
|
||||||
echo "You must be in a release branch"
|
echo "You must be in a release branch"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -95,7 +95,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
await ClusterStore.createInstance().load();
|
ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -125,33 +125,28 @@ describe("empty config", () => {
|
|||||||
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
||||||
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes cluster from store", async () => {
|
|
||||||
await ClusterStore.getInstance().removeById("foo");
|
|
||||||
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with prod and dev clusters added", () => {
|
describe("with prod and dev clusters added", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ClusterStore.getInstance().addClusters(
|
const store = ClusterStore.getInstance();
|
||||||
new Cluster({
|
|
||||||
|
store.addCluster({
|
||||||
id: "prod",
|
id: "prod",
|
||||||
contextName: "foo",
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "prod"
|
clusterName: "prod"
|
||||||
},
|
},
|
||||||
kubeConfigPath: embed("prod", kubeconfig)
|
kubeConfigPath: embed("prod", kubeconfig)
|
||||||
}),
|
});
|
||||||
new Cluster({
|
store.addCluster({
|
||||||
id: "dev",
|
id: "dev",
|
||||||
contextName: "foo2",
|
contextName: "foo2",
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "dev"
|
clusterName: "dev"
|
||||||
},
|
},
|
||||||
kubeConfigPath: embed("dev", kubeconfig)
|
kubeConfigPath: embed("dev", kubeconfig)
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("check if store can contain multiple clusters", () => {
|
it("check if store can contain multiple clusters", () => {
|
||||||
@ -208,7 +203,7 @@ describe("config with existing clusters", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -222,16 +217,6 @@ describe("config with existing clusters", () => {
|
|||||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows to delete a cluster", () => {
|
|
||||||
ClusterStore.getInstance().removeById("cluster2");
|
|
||||||
const storedCluster = ClusterStore.getInstance().getById("cluster1");
|
|
||||||
|
|
||||||
expect(storedCluster).toBeTruthy();
|
|
||||||
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
|
|
||||||
|
|
||||||
expect(storedCluster2).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows getting all of the clusters", async () => {
|
it("allows getting all of the clusters", async () => {
|
||||||
const storedClusters = ClusterStore.getInstance().clustersList;
|
const storedClusters = ClusterStore.getInstance().clustersList;
|
||||||
|
|
||||||
@ -300,7 +285,7 @@ users:
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -359,7 +344,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -429,7 +414,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -471,7 +456,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -510,7 +495,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -546,7 +531,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import mockFs from "mock-fs";
|
|||||||
import { ClusterStore } from "../cluster-store";
|
import { ClusterStore } from "../cluster-store";
|
||||||
import { HotbarStore } from "../hotbar-store";
|
import { HotbarStore } from "../hotbar-store";
|
||||||
|
|
||||||
jest.mock("../../renderer/api/catalog-entity-registry", () => ({
|
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
||||||
catalogEntityRegistry: {
|
catalogEntityRegistry: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -39,7 +39,14 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({
|
|||||||
name: "my_shiny_cluster",
|
name: "my_shiny_cluster",
|
||||||
source: "remote"
|
source: "remote"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
uid: "catalog-entity",
|
||||||
|
name: "Catalog",
|
||||||
|
source: "app"
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -120,29 +127,31 @@ jest.mock("electron", () => {
|
|||||||
|
|
||||||
describe("HotbarStore", () => {
|
describe("HotbarStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ClusterStore.resetInstance();
|
mockFs({
|
||||||
|
"tmp": {
|
||||||
|
"lens-hotbar-store.json": JSON.stringify({})
|
||||||
|
}
|
||||||
|
});
|
||||||
ClusterStore.createInstance();
|
ClusterStore.createInstance();
|
||||||
|
HotbarStore.createInstance();
|
||||||
HotbarStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
ClusterStore.resetInstance();
|
||||||
|
HotbarStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("load", () => {
|
describe("load", () => {
|
||||||
it("loads one hotbar by default", () => {
|
it("loads one hotbar by default", () => {
|
||||||
HotbarStore.createInstance().load();
|
|
||||||
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("add", () => {
|
describe("add", () => {
|
||||||
it("adds a hotbar", () => {
|
it("adds a hotbar", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.add({ name: "hottest" });
|
hotbarStore.add({ name: "hottest" });
|
||||||
expect(hotbarStore.hotbars.length).toEqual(2);
|
expect(hotbarStore.hotbars.length).toEqual(2);
|
||||||
});
|
});
|
||||||
@ -150,106 +159,104 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
describe("hotbar items", () => {
|
describe("hotbar items", () => {
|
||||||
it("initially creates 12 empty cells", () => {
|
it("initially creates 12 empty cells", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
expect(hotbarStore.getActive().items.length).toEqual(12);
|
expect(hotbarStore.getActive().items.length).toEqual(12);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds items", () => {
|
it("initially adds catalog entity as first item", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
|
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds items", () => {
|
||||||
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes items", () => {
|
it("removes items", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.removeFromHotbar("test");
|
hotbarStore.removeFromHotbar("test");
|
||||||
|
hotbarStore.removeFromHotbar("catalog-entity");
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(0);
|
expect(items.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if removing with invalid uid", () => {
|
it("does nothing if removing with invalid uid", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.removeFromHotbar("invalid uid");
|
hotbarStore.removeFromHotbar("invalid uid");
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves item to empty cell", () => {
|
it("moves item to empty cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[5]).toBeNull();
|
expect(hotbarStore.getActive().items[6]).toBeNull();
|
||||||
|
|
||||||
hotbarStore.restackItems(1, 5);
|
hotbarStore.restackItems(1, 5);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
||||||
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("minikube");
|
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves items down", () => {
|
it("moves items down", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
// aws -> test
|
// aws -> catalog
|
||||||
hotbarStore.restackItems(2, 0);
|
hotbarStore.restackItems(3, 0);
|
||||||
|
|
||||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||||
|
|
||||||
expect(items.slice(0, 4)).toEqual(["aws", "test", "minikube", null]);
|
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves items up", () => {
|
it("moves items up", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
// test -> aws
|
// test -> aws
|
||||||
hotbarStore.restackItems(0, 2);
|
hotbarStore.restackItems(1, 3);
|
||||||
|
|
||||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||||
|
|
||||||
expect(items.slice(0, 4)).toEqual(["minikube", "aws", "test", null]);
|
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing when item moved to same cell", () => {
|
it("does nothing when item moved to same cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.restackItems(0, 0);
|
hotbarStore.restackItems(1, 1);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
|
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("new items takes first empty cell", () => {
|
it("new items takes first empty cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
hotbarStore.restackItems(0, 3);
|
hotbarStore.restackItems(0, 3);
|
||||||
@ -260,13 +267,13 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
it("throws if invalid arguments provided", () => {
|
it("throws if invalid arguments provided", () => {
|
||||||
// Prevent writing to stderr during this render.
|
// Prevent writing to stderr during this render.
|
||||||
const err = console.error;
|
const { error, warn } = console;
|
||||||
|
|
||||||
console.error = jest.fn();
|
console.error = jest.fn();
|
||||||
|
console.warn = jest.fn();
|
||||||
|
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
|
|
||||||
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
||||||
@ -275,7 +282,8 @@ describe("HotbarStore", () => {
|
|||||||
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
|
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
|
||||||
|
|
||||||
// Restore writing to stderr.
|
// Restore writing to stderr.
|
||||||
console.error = err;
|
console.error = error;
|
||||||
|
console.warn = warn;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -346,7 +354,7 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return HotbarStore.createInstance().load();
|
HotbarStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
return UserStore.getInstance().load();
|
UserStore.getInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -81,10 +81,8 @@ describe("user store tests", () => {
|
|||||||
it("correctly resets theme to default value", async () => {
|
it("correctly resets theme to default value", async () => {
|
||||||
const us = UserStore.getInstance();
|
const us = UserStore.getInstance();
|
||||||
|
|
||||||
us.isLoaded = true;
|
|
||||||
|
|
||||||
us.colorTheme = "some other theme";
|
us.colorTheme = "some other theme";
|
||||||
await us.resetTheme();
|
us.resetTheme();
|
||||||
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,7 +109,7 @@ describe("user store tests", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return UserStore.createInstance().load();
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -23,41 +23,42 @@ import path from "path";
|
|||||||
import Config from "conf";
|
import Config from "conf";
|
||||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||||
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
||||||
import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
|
||||||
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
||||||
autoLoad?: boolean;
|
|
||||||
syncEnabled?: boolean;
|
|
||||||
syncOptions?: IReactionOptions;
|
syncOptions?: IReactionOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: T should only contain base JSON serializable types.
|
* Note: T should only contain base JSON serializable types.
|
||||||
*/
|
*/
|
||||||
export abstract class BaseStore<T = any> extends Singleton {
|
export abstract class BaseStore<T> extends Singleton {
|
||||||
protected storeConfig?: Config<T>;
|
protected storeConfig?: Config<T>;
|
||||||
protected syncDisposers: Disposer[] = [];
|
protected syncDisposers: Disposer[] = [];
|
||||||
|
|
||||||
@observable isLoaded = false;
|
protected constructor(protected params: BaseStoreParams<T>) {
|
||||||
|
|
||||||
get whenLoaded() {
|
|
||||||
return when(() => this.isLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(protected params: BaseStoreParams) {
|
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.params = {
|
/**
|
||||||
autoLoad: false,
|
* This must be called after the last child's constructor is finished (or just before it finishes)
|
||||||
syncEnabled: true,
|
*/
|
||||||
...params,
|
load() {
|
||||||
};
|
this.storeConfig = new Config({
|
||||||
this.init();
|
...this.params,
|
||||||
|
projectName: "lens",
|
||||||
|
projectVersion: getAppVersion(),
|
||||||
|
cwd: this.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[STORE]: LOADED from ${this.path}`);
|
||||||
|
this.fromStore(this.storeConfig.store);
|
||||||
|
this.enableSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -76,31 +77,6 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
return this.storeConfig?.path || "";
|
return this.storeConfig?.path || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
|
||||||
if (this.params.autoLoad) {
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
|
||||||
await this.whenLoaded;
|
|
||||||
this.enableSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
|
||||||
|
|
||||||
this.storeConfig = new Config({
|
|
||||||
...confOptions,
|
|
||||||
projectName: "lens",
|
|
||||||
projectVersion: getAppVersion(),
|
|
||||||
cwd: this.cwd(),
|
|
||||||
});
|
|
||||||
logger.info(`[STORE]: LOADED from ${this.path}`);
|
|
||||||
this.fromStore(this.storeConfig.store);
|
|
||||||
this.isLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected cwd() {
|
protected cwd() {
|
||||||
return (app || remote.app).getPath("userData");
|
return (app || remote.app).getPath("userData");
|
||||||
}
|
}
|
||||||
@ -159,11 +135,8 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
protected applyWithoutSync(callback: () => void) {
|
protected applyWithoutSync(callback: () => void) {
|
||||||
this.disableSync();
|
this.disableSync();
|
||||||
runInAction(callback);
|
runInAction(callback);
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected onSync(model: T) {
|
protected onSync(model: T) {
|
||||||
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
|
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
|
||||||
|
|||||||
76
src/common/catalog-entities/general.ts
Normal file
76
src/common/catalog-entities/general.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { navigate } from "../../renderer/navigation";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
|
||||||
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
|
|
||||||
|
interface GeneralEntitySpec extends CatalogEntitySpec {
|
||||||
|
path: string;
|
||||||
|
icon?: {
|
||||||
|
material?: string;
|
||||||
|
background?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogEntityStatus, GeneralEntitySpec> {
|
||||||
|
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "General";
|
||||||
|
|
||||||
|
async onRun() {
|
||||||
|
navigate(this.spec.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSettingsOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDetailsOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onContextMenuOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeneralCategory extends CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "General",
|
||||||
|
icon: "settings"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: GeneralEntity
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "General"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry.add(new GeneralCategory());
|
||||||
@ -19,5 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./general";
|
||||||
export * from "./kubernetes-cluster";
|
export * from "./kubernetes-cluster";
|
||||||
export * from "./web-link";
|
export * from "./web-link";
|
||||||
|
|||||||
@ -21,15 +21,16 @@
|
|||||||
|
|
||||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||||
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||||
import { ClusterStore } from "../cluster-store";
|
import { ClusterStore } from "../cluster-store";
|
||||||
import { requestMain } from "../ipc";
|
import { requestMain } from "../ipc";
|
||||||
import { productName } from "../vars";
|
|
||||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||||
import { addClusterURL } from "../routes";
|
import { addClusterURL } from "../routes";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||||
|
import { HotbarStore } from "../hotbar-store";
|
||||||
|
|
||||||
export type KubernetesClusterPrometheusMetrics = {
|
export interface KubernetesClusterPrometheusMetrics {
|
||||||
address?: {
|
address?: {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
service: string;
|
service: string;
|
||||||
@ -37,20 +38,25 @@ export type KubernetesClusterPrometheusMetrics = {
|
|||||||
prefix: string;
|
prefix: string;
|
||||||
};
|
};
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type KubernetesClusterSpec = {
|
export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||||
kubeconfigPath: string;
|
kubeconfigPath: string;
|
||||||
kubeconfigContext: string;
|
kubeconfigContext: string;
|
||||||
iconData?: string;
|
|
||||||
metrics?: {
|
metrics?: {
|
||||||
source: string;
|
source: string;
|
||||||
prometheus?: KubernetesClusterPrometheusMetrics;
|
prometheus?: KubernetesClusterPrometheusMetrics;
|
||||||
}
|
};
|
||||||
};
|
icon?: {
|
||||||
|
// TODO: move to CatalogEntitySpec once any-entity icons are supported
|
||||||
|
src?: string;
|
||||||
|
material?: string;
|
||||||
|
background?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
phase: "connected" | "disconnected";
|
phase: "connected" | "disconnected" | "deleting";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
@ -103,39 +109,38 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
|
|
||||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||||
if (!this.metadata.source || this.metadata.source === "local") {
|
if (!this.metadata.source || this.metadata.source === "local") {
|
||||||
context.menuItems.push({
|
context.menuItems.push(
|
||||||
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
icon: "edit",
|
icon: "edit",
|
||||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
onClick: () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||||
});
|
},
|
||||||
}
|
{
|
||||||
|
|
||||||
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
|
||||||
context.menuItems.push({
|
|
||||||
title: "Delete",
|
title: "Delete",
|
||||||
icon: "delete",
|
icon: "delete",
|
||||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
onClick: () => {
|
||||||
|
HotbarStore.getInstance().removeAllHotbarItems(this.getId());
|
||||||
|
requestMain(clusterDeleteHandler, this.metadata.uid);
|
||||||
|
},
|
||||||
confirm: {
|
confirm: {
|
||||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
|
// TODO: change this to be a <p> tag with better formatting once this code can accept it.
|
||||||
|
message: `Delete the "${this.metadata.name}" context from "${this.metadata.labels.file}"?`
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.status.phase == "connected") {
|
if (this.status.phase == "connected") {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Disconnect",
|
title: "Disconnect",
|
||||||
icon: "link_off",
|
icon: "link_off",
|
||||||
onClick: async () => {
|
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
||||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Connect",
|
title: "Connect",
|
||||||
icon: "link",
|
icon: "link",
|
||||||
onClick: async () => {
|
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`)
|
||||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +154,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
public readonly kind = "CatalogCategory";
|
public readonly kind = "CatalogCategory";
|
||||||
public metadata = {
|
public metadata = {
|
||||||
name: "Kubernetes Clusters",
|
name: "Clusters",
|
||||||
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
|
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
|
||||||
};
|
};
|
||||||
public spec: CatalogCategorySpec = {
|
public spec: CatalogCategorySpec = {
|
||||||
|
|||||||
@ -19,11 +19,13 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
import { CatalogCategory, CatalogEntity, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
|
import { productName } from "../vars";
|
||||||
|
import { WeblinkStore } from "../weblink-store";
|
||||||
|
|
||||||
export interface WebLinkStatus extends CatalogEntityStatus {
|
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||||
phase: "valid" | "invalid";
|
phase: "available" | "unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebLinkSpec = {
|
export type WebLinkSpec = {
|
||||||
@ -42,12 +44,17 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDetailsOpen(): void {
|
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||||
return;
|
if (this.metadata.source === "local") {
|
||||||
|
context.menuItems.push({
|
||||||
|
title: "Delete",
|
||||||
|
icon: "delete",
|
||||||
|
onClick: async () => WeblinkStore.getInstance().removeById(this.metadata.uid),
|
||||||
|
confirm: {
|
||||||
|
message: `Remove Web Link "${this.metadata.name}" from ${productName}?`
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onContextMenuOpen(): void {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +63,7 @@ export class WebLinkCategory extends CatalogCategory {
|
|||||||
public readonly kind = "CatalogCategory";
|
public readonly kind = "CatalogCategory";
|
||||||
public metadata = {
|
public metadata = {
|
||||||
name: "Web Links",
|
name: "Web Links",
|
||||||
icon: "link"
|
icon: "public"
|
||||||
};
|
};
|
||||||
public spec = {
|
public spec = {
|
||||||
group: "entity.k8slens.dev",
|
group: "entity.k8slens.dev",
|
||||||
@ -70,6 +77,21 @@ export class WebLinkCategory extends CatalogCategory {
|
|||||||
kind: "WebLink"
|
kind: "WebLink"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
public static onAdd?: () => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
||||||
|
ctx.menuItems.push({
|
||||||
|
icon: "public",
|
||||||
|
title: "Add web link",
|
||||||
|
onClick: () => {
|
||||||
|
WebLinkCategory.onAdd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||||
|
|||||||
@ -86,6 +86,11 @@ export interface CatalogEntityMetadata {
|
|||||||
export interface CatalogEntityStatus {
|
export interface CatalogEntityStatus {
|
||||||
phase: string;
|
phase: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enabled?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
@ -144,6 +149,7 @@ export interface CatalogEntityAddMenuContext {
|
|||||||
|
|
||||||
export type CatalogEntitySpec = Record<string, any>;
|
export type CatalogEntitySpec = Record<string, any>;
|
||||||
|
|
||||||
|
|
||||||
export interface CatalogEntityData<
|
export interface CatalogEntityData<
|
||||||
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
|
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
|
||||||
Status extends CatalogEntityStatus = CatalogEntityStatus,
|
Status extends CatalogEntityStatus = CatalogEntityStatus,
|
||||||
|
|||||||
@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
|
|
||||||
export type ClusterFrameInfo = {
|
export interface ClusterFrameInfo {
|
||||||
frameId: number;
|
frameId: number;
|
||||||
processId: number
|
processId: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();
|
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();
|
||||||
|
|||||||
@ -24,5 +24,6 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
|||||||
export const clusterVisibilityHandler = "cluster:visibility";
|
export const clusterVisibilityHandler = "cluster:visibility";
|
||||||
export const clusterRefreshHandler = "cluster:refresh";
|
export const clusterRefreshHandler = "cluster:refresh";
|
||||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||||
|
export const clusterDeleteHandler = "cluster:delete";
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||||
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
|
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
|
||||||
import { unlink } from "fs-extra";
|
|
||||||
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
|
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import { Cluster, ClusterState } from "../main/cluster";
|
import { Cluster, ClusterState } from "../main/cluster";
|
||||||
@ -30,7 +29,7 @@ import * as uuid from "uuid";
|
|||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "./event-bus";
|
||||||
import { ipcMainHandle, ipcMainOn, ipcRendererOn, requestMain } from "./ipc";
|
import { ipcMainHandle, ipcMainOn, ipcRendererOn, requestMain } from "./ipc";
|
||||||
import { disposer, noop, toJS } from "./utils";
|
import { disposer, toJS } from "./utils";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
@ -73,7 +72,7 @@ export interface ClusterModel {
|
|||||||
workspace?: string;
|
workspace?: string;
|
||||||
|
|
||||||
/** User context in kubeconfig */
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName: string;
|
||||||
|
|
||||||
/** Preferences */
|
/** Preferences */
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
@ -81,11 +80,13 @@ export interface ClusterModel {
|
|||||||
/** Metadata */
|
/** Metadata */
|
||||||
metadata?: ClusterMetadata;
|
metadata?: ClusterMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Labels for the catalog entity
|
||||||
|
*/
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
|
||||||
/** List of accessible namespaces */
|
/** List of accessible namespaces */
|
||||||
accessibleNamespaces?: string[];
|
accessibleNamespaces?: string[];
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
kubeConfig?: string; // yaml
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
||||||
@ -109,21 +110,22 @@ export interface ClusterPrometheusPreferences {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialStates = "cluster:states";
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
private static StateChannel = "cluster:state";
|
private static StateChannel = "cluster:state";
|
||||||
|
|
||||||
static get storedKubeConfigFolder(): string {
|
static get storedKubeConfigFolder(): string {
|
||||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
|
return path.resolve((app ?? remote.app).getPath("userData"), "kubeconfigs");
|
||||||
}
|
}
|
||||||
|
|
||||||
static getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
|
static getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
|
||||||
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
clusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
private static stateRequestChannel = "cluster:states";
|
|
||||||
protected disposer = disposer();
|
protected disposer = disposer();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -137,50 +139,31 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
this.pushStateToViewsAutomatically();
|
this.pushStateToViewsAutomatically();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async loadInitialOnRenderer() {
|
||||||
await super.load();
|
|
||||||
type clusterStateSync = {
|
|
||||||
id: string;
|
|
||||||
state: ClusterState;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
|
||||||
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||||
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
|
|
||||||
|
|
||||||
clusterStates.forEach((clusterState) => {
|
for (const { id, state } of await requestMain(initialStates)) {
|
||||||
const cluster = this.getById(clusterState.id);
|
this.getById(id)?.setState(state);
|
||||||
|
}
|
||||||
if (cluster) {
|
|
||||||
cluster.setState(clusterState.state);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else if (ipcMain) {
|
|
||||||
ipcMainHandle(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
|
|
||||||
const clusterStates: clusterStateSync[] = [];
|
|
||||||
|
|
||||||
this.clustersList.forEach((cluster) => {
|
provideInitialFromMain() {
|
||||||
clusterStates.push({
|
ipcMainHandle(initialStates, () => {
|
||||||
|
return this.clustersList.map(cluster => ({
|
||||||
|
id: cluster.id,
|
||||||
state: cluster.getState(),
|
state: cluster.getState(),
|
||||||
id: cluster.id
|
}));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return clusterStates;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected pushStateToViewsAutomatically() {
|
protected pushStateToViewsAutomatically() {
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
this.disposer.push(
|
this.disposer.push(
|
||||||
reaction(() => this.connectedClustersList, () => {
|
reaction(() => this.connectedClustersList, () => this.pushState()),
|
||||||
this.pushState();
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,18 +212,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.clusters.get(id) ?? null;
|
return this.clusters.get(id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
|
||||||
const clusters: Cluster[] = [];
|
|
||||||
|
|
||||||
models.forEach(model => {
|
|
||||||
clusters.push(this.addCluster(model));
|
|
||||||
});
|
|
||||||
|
|
||||||
return clusters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({ name: "cluster", action: "add" });
|
appEventBus.emit({ name: "cluster", action: "add" });
|
||||||
|
|
||||||
@ -253,25 +224,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeCluster(model: ClusterModel) {
|
|
||||||
await this.removeById(model.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async removeById(clusterId: ClusterId) {
|
|
||||||
appEventBus.emit({ name: "cluster", action: "remove" });
|
|
||||||
const cluster = this.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
this.clusters.delete(clusterId);
|
|
||||||
|
|
||||||
// remove only custom kubeconfigs (pasted as text)
|
|
||||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
|
||||||
await unlink(cluster.kubeConfigPath).catch(noop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
|
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
|
||||||
const currentClusters = new Map(this.clusters);
|
const currentClusters = new Map(this.clusters);
|
||||||
|
|||||||
@ -38,16 +38,12 @@ export interface HotbarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hotbar {
|
export type Hotbar = Required<HotbarCreateOptions>;
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
items: HotbarItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HotbarCreateOptions {
|
export interface HotbarCreateOptions {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
items?: HotbarItem[];
|
items?: (HotbarItem | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotbarStoreModel {
|
export interface HotbarStoreModel {
|
||||||
@ -71,6 +67,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarId() {
|
get activeHotbarId() {
|
||||||
@ -91,20 +88,13 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbarIndex(this.activeHotbarId);
|
return this.hotbarIndex(this.activeHotbarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get initialItems() {
|
static getInitialItems() {
|
||||||
return [...Array.from(Array(defaultHotbarCells).fill(null))];
|
return [...Array.from(Array(defaultHotbarCells).fill(null))];
|
||||||
}
|
}
|
||||||
|
|
||||||
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
@action
|
||||||
if (data.hotbars?.length === 0) {
|
protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||||
this.hotbars = [{
|
|
||||||
id: uuid.v4(),
|
|
||||||
name: "Default",
|
|
||||||
items: this.initialItems,
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
this.hotbars = data.hotbars;
|
this.hotbars = data.hotbars;
|
||||||
}
|
|
||||||
|
|
||||||
if (data.activeHotbarId) {
|
if (data.activeHotbarId) {
|
||||||
if (this.getById(data.activeHotbarId)) {
|
if (this.getById(data.activeHotbarId)) {
|
||||||
@ -129,18 +119,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(data: HotbarCreateOptions) {
|
@action
|
||||||
|
add(data: HotbarCreateOptions, { setActive = false } = {}) {
|
||||||
const {
|
const {
|
||||||
id = uuid.v4(),
|
id = uuid.v4(),
|
||||||
items = this.initialItems,
|
items = HotbarStore.getInitialItems(),
|
||||||
name,
|
name,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const hotbar = { id, name, items };
|
this.hotbars.push({ id, name, items });
|
||||||
|
|
||||||
this.hotbars.push(hotbar as Hotbar);
|
if (setActive) {
|
||||||
|
this._activeHotbarId = id;
|
||||||
return hotbar as Hotbar;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -181,7 +172,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeFromHotbar(uid: string) {
|
removeFromHotbar(uid: string): void {
|
||||||
const hotbar = this.getActive();
|
const hotbar = this.getActive();
|
||||||
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
||||||
|
|
||||||
@ -192,6 +183,25 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
hotbar.items[index] = null;
|
hotbar.items[index] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remvove all hotbar items that reference the `uid`.
|
||||||
|
* @param uid The `EntityId` that each hotbar item refers to
|
||||||
|
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
removeAllHotbarItems(uid: string) {
|
||||||
|
const undoItems: [Hotbar, number, HotbarItem][] = [];
|
||||||
|
|
||||||
|
for (const hotbar of this.hotbars) {
|
||||||
|
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
undoItems.push([hotbar, index, hotbar.items[index]]);
|
||||||
|
hotbar.items[index] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findClosestEmptyIndex(from: number, direction = 1) {
|
findClosestEmptyIndex(from: number, direction = 1) {
|
||||||
let index = from;
|
let index = from;
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import { ExtensionsStore } from "../../extensions/extensions-store";
|
|||||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||||
import type { LensExtension } from "../../extensions/lens-extension";
|
import type { LensExtension } from "../../extensions/lens-extension";
|
||||||
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
||||||
|
import { when } from "mobx";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
@ -178,16 +180,28 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
|
|
||||||
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||||
const name = [publisher, partialName].filter(Boolean).join("/");
|
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||||
|
const extensionLoader = ExtensionLoader.getInstance();
|
||||||
|
|
||||||
const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
|
try {
|
||||||
|
/**
|
||||||
if (!extension) {
|
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
|
*/
|
||||||
|
await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
|
||||||
|
} catch(error) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
|
const extension = extensionLoader.getInstanceByName(name);
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExtensionsStore.getInstance().isEnabled(extension)) {
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -68,7 +68,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
configName: "lens-user-store",
|
configName: "lens-user-store",
|
||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
fileNameMigration();
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable lastSeenAppVersion = "0.0.0";
|
@observable lastSeenAppVersion = "0.0.0";
|
||||||
@ -101,15 +104,15 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
[path.join(os.homedir(), ".kube"), {}]
|
[path.join(os.homedir(), ".kube"), {}]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
async load(): Promise<void> {
|
@computed get isNewVersion() {
|
||||||
/**
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
* This has to be here before the call to `new Config` in `super.load()`
|
}
|
||||||
* as we have to make sure that file is in the expected place for that call
|
|
||||||
*/
|
|
||||||
await fileNameMigration();
|
|
||||||
await super.load();
|
|
||||||
|
|
||||||
if (app) {
|
@computed get resolvedShell(): string | undefined {
|
||||||
|
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
||||||
|
}
|
||||||
|
|
||||||
|
startMainReactions() {
|
||||||
// track telemetry availability
|
// track telemetry availability
|
||||||
reaction(() => this.allowTelemetry, allowed => {
|
reaction(() => this.allowTelemetry, allowed => {
|
||||||
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
|
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
|
||||||
@ -126,15 +129,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@computed get isNewVersion() {
|
|
||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get resolvedShell(): string | undefined {
|
|
||||||
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
|
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
|
||||||
@ -165,8 +159,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async resetTheme() {
|
resetTheme() {
|
||||||
await this.whenLoaded;
|
|
||||||
this.colorTheme = UserStore.defaultTheme;
|
this.colorTheme = UserStore.defaultTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,9 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Common utils (main OR renderer)
|
/**
|
||||||
|
* A function that does nothing
|
||||||
|
*/
|
||||||
export function noop<T extends any[]>(...args: T): void {
|
export function noop<T extends any[]>(...args: T): void {
|
||||||
return void args;
|
return void args;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,3 +140,19 @@ export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U | nu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate through `src` until `match` returns a truthy value
|
||||||
|
* @param src A type that can be iterated over
|
||||||
|
* @param match A function that should return a truthy value for the item that you want to find
|
||||||
|
* @returns The first entry that `match` returns a truthy value for, or `undefined`
|
||||||
|
*/
|
||||||
|
export function find<T>(src: Iterable<T>, match: (i: T) => any): T | undefined {
|
||||||
|
for (const from of src) {
|
||||||
|
if (match(from)) {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|||||||
91
src/common/weblink-store.ts
Normal file
91
src/common/weblink-store.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { action, comparer, observable, makeObservable } from "mobx";
|
||||||
|
import { BaseStore } from "./base-store";
|
||||||
|
import migrations from "../migrations/hotbar-store";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import { toJS } from "./utils";
|
||||||
|
|
||||||
|
export interface WeblinkData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeblinkCreateOptions {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface WeblinkStoreModel {
|
||||||
|
weblinks: WeblinkData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
|
||||||
|
@observable weblinks: WeblinkData[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
configName: "lens-weblink-store",
|
||||||
|
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||||
|
syncOptions: {
|
||||||
|
equals: comparer.structural,
|
||||||
|
},
|
||||||
|
migrations,
|
||||||
|
});
|
||||||
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action protected async fromStore(data: Partial<WeblinkStoreModel> = {}) {
|
||||||
|
this.weblinks = data.weblinks || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
add(data: WeblinkCreateOptions) {
|
||||||
|
const {
|
||||||
|
id = uuid.v4(),
|
||||||
|
name,
|
||||||
|
url
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const weblink = { id, name, url };
|
||||||
|
|
||||||
|
this.weblinks.push(weblink as WeblinkData);
|
||||||
|
|
||||||
|
return weblink as WeblinkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeById(id: string) {
|
||||||
|
this.weblinks = this.weblinks.filter((w) => w.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): WeblinkStoreModel {
|
||||||
|
const model: WeblinkStoreModel = {
|
||||||
|
weblinks: this.weblinks
|
||||||
|
};
|
||||||
|
|
||||||
|
return toJS(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,6 +39,12 @@ jest.mock("../extension-installer", () => ({
|
|||||||
installPackage: jest.fn()
|
installPackage: jest.fn()
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||||
|
|||||||
@ -24,6 +24,6 @@ export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from ".
|
|||||||
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
||||||
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
||||||
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
|
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
|
||||||
export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
|
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
|
||||||
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
||||||
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
|
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
|
||||||
|
|||||||
@ -357,8 +357,8 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||||
try {
|
try {
|
||||||
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
||||||
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
|
const id = this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled });
|
||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
@ -369,9 +369,9 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: installedManifestPath,
|
id,
|
||||||
absolutePath,
|
absolutePath,
|
||||||
manifestPath: installedManifestPath,
|
manifestPath: id,
|
||||||
manifest,
|
manifest,
|
||||||
isBundled,
|
isBundled,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import { app, ipcRenderer, remote } from "electron";
|
import { app, ipcRenderer, remote } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getHostedCluster } from "../common/cluster-store";
|
import { getHostedCluster } from "../common/cluster-store";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
|
||||||
@ -48,6 +48,18 @@ export class ExtensionLoader extends Singleton {
|
|||||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the set of extensions that don't come with either
|
||||||
|
* - Main.LensExtension when running in the main process
|
||||||
|
* - Renderer.LensExtension when running in the renderer process
|
||||||
|
*/
|
||||||
|
protected nonInstancesByName = observable.set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
||||||
|
*/
|
||||||
|
protected instancesByName = observable.map<string, LensExtension>();
|
||||||
|
|
||||||
// IPC channel to broadcast changes to extensions from main
|
// IPC channel to broadcast changes to extensions from main
|
||||||
protected static readonly extensionsMainChannel = "extensions:main";
|
protected static readonly extensionsMainChannel = "extensions:main";
|
||||||
|
|
||||||
@ -65,8 +77,23 @@ export class ExtensionLoader extends Singleton {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
observe(this.instances, change => {
|
||||||
|
switch (change.type) {
|
||||||
|
case "add":
|
||||||
|
if (this.instancesByName.has(change.newValue.name)) {
|
||||||
|
throw new TypeError("Extension names must be unique");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instancesByName.set(change.newValue.name, change.newValue);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
this.instancesByName.delete(change.oldValue.name);
|
||||||
|
break;
|
||||||
|
case "update":
|
||||||
|
throw new Error("Extension instances shouldn't be updated");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
||||||
@ -81,30 +108,23 @@ export class ExtensionLoader extends Singleton {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get userExtensionsByName(): Map<string, LensExtension> {
|
/**
|
||||||
const extensions = new Map();
|
* Get the extension instance by its manifest name
|
||||||
|
* @param name The name of the extension
|
||||||
for (const [, val] of this.instances.toJSON()) {
|
* @returns one of the following:
|
||||||
if (val.isBundled) {
|
* - the instance of `Main.LensExtension` on the main process if created
|
||||||
continue;
|
* - the instance of `Renderer.LensExtension` on the renderer process if created
|
||||||
}
|
* - `null` if no class definition is provided for the current process
|
||||||
|
* - `undefined` if the name is not known about
|
||||||
extensions.set(val.manifest.name, val);
|
*/
|
||||||
}
|
getInstanceByName(name: string): LensExtension | null | undefined {
|
||||||
|
if (this.nonInstancesByName.has(name)) {
|
||||||
return extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtensionByName(name: string): LensExtension | null {
|
|
||||||
for (const [, val] of this.instances) {
|
|
||||||
if (val.name === name) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.instancesByName.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||||
@computed get storeState() {
|
@computed get storeState() {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
@ -124,7 +144,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
await this.initMain();
|
await this.initMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
|
await Promise.all([this.whenLoaded]);
|
||||||
|
|
||||||
// broadcasting extensions between main/renderer processes
|
// broadcasting extensions between main/renderer processes
|
||||||
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
||||||
@ -145,6 +165,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
this.extensions.set(extension.id, extension);
|
this.extensions.set(extension.id, extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
removeInstance(lensExtensionId: LensExtensionId) {
|
removeInstance(lensExtensionId: LensExtensionId) {
|
||||||
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||||
const instance = this.instances.get(lensExtensionId);
|
const instance = this.instances.get(lensExtensionId);
|
||||||
@ -157,6 +178,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
instance.disable();
|
instance.disable();
|
||||||
this.events.emit("remove", instance);
|
this.events.emit("remove", instance);
|
||||||
this.instances.delete(lensExtensionId);
|
this.instances.delete(lensExtensionId);
|
||||||
|
this.nonInstancesByName.delete(instance.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||||
}
|
}
|
||||||
@ -170,6 +192,10 @@ export class ExtensionLoader extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
||||||
|
this.extensions.get(lensExtensionId).isEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
protected async initMain() {
|
protected async initMain() {
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
this.loadOnMain();
|
this.loadOnMain();
|
||||||
@ -302,13 +328,14 @@ export class ExtensionLoader extends Singleton {
|
|||||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||||
return reaction(() => this.toJSON(), installedExtensions => {
|
return reaction(() => this.toJSON(), installedExtensions => {
|
||||||
for (const [extId, extension] of installedExtensions) {
|
for (const [extId, extension] of installedExtensions) {
|
||||||
const alreadyInit = this.instances.has(extId);
|
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||||
|
|
||||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = this.requireExtension(extension);
|
const LensExtensionClass = this.requireExtension(extension);
|
||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
|
this.nonInstancesByName.add(extension.manifest.name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +382,10 @@ export class ExtensionLoader extends Singleton {
|
|||||||
return this.extensions.get(extId);
|
return this.extensions.get(extId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInstanceById<E extends LensExtension>(extId: LensExtensionId): E {
|
||||||
|
return this.instances.get(extId) as E;
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(): Map<LensExtensionId, InstalledExtension> {
|
toJSON(): Map<LensExtensionId, InstalledExtension> {
|
||||||
return toJS(this.extensions);
|
return toJS(this.extensions);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,13 +26,13 @@ import type { LensExtension } from "./lens-extension";
|
|||||||
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
||||||
protected extension: LensExtension;
|
protected extension: LensExtension;
|
||||||
|
|
||||||
async loadExtension(extension: LensExtension) {
|
loadExtension(extension: LensExtension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
load() {
|
||||||
if (!this.extension) { return; }
|
if (!this.extension) { return; }
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
configName: "lens-extensions",
|
configName: "lens-extensions",
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
@ -50,12 +51,10 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||||
|
|
||||||
isEnabled(extId: LensExtensionId): boolean {
|
isEnabled({ id, isBundled }: { id: string, isBundled: boolean }): boolean {
|
||||||
const state = this.state.get(extId);
|
|
||||||
|
|
||||||
// By default false, so that copied extensions are disabled by default.
|
// By default false, so that copied extensions are disabled by default.
|
||||||
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
||||||
return Boolean(state?.enabled);
|
return isBundled || Boolean(this.state.get(id)?.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { WindowManager } from "../main/window-manager";
|
import { WindowManager } from "../main/window-manager";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
|
||||||
import { catalogEntityRegistry } from "../main/catalog";
|
import { catalogEntityRegistry } from "../main/catalog";
|
||||||
import type { CatalogEntity } from "../common/catalog";
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
import type { IObservableArray } from "mobx";
|
import type { IObservableArray } from "mobx";
|
||||||
@ -30,15 +29,8 @@ import type { MenuRegistration } from "./registries";
|
|||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||||
const windowManager = WindowManager.getInstance();
|
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
||||||
const pageUrl = getExtensionPageUrl({
|
|
||||||
extensionId: this.name,
|
|
||||||
pageId,
|
|
||||||
params: params ?? {}, // compile to url with params
|
|
||||||
});
|
|
||||||
|
|
||||||
await windowManager.navigate(pageUrl, frameId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||||
|
|||||||
@ -19,33 +19,26 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type * as registries from "./registries";
|
||||||
AppPreferenceRegistration, CatalogEntityDetailRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
|
||||||
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
|
|
||||||
} from "./registries";
|
|
||||||
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 type { CommandRegistration } from "./registries/command-registry";
|
|
||||||
import type { EntitySettingRegistration } from "./registries/entity-setting-registry";
|
|
||||||
import type { TopBarRegistration } from "./registries/topbar-registry";
|
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
clusterPages: PageRegistration[] = [];
|
clusterPages: registries.PageRegistration[] = [];
|
||||||
globalPageMenus: PageMenuRegistration[] = [];
|
clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
|
||||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
|
||||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
appPreferences: registries.AppPreferenceRegistration[] = [];
|
||||||
appPreferences: AppPreferenceRegistration[] = [];
|
entitySettings: registries.EntitySettingRegistration[] = [];
|
||||||
entitySettings: EntitySettingRegistration[] = [];
|
statusBarItems: registries.StatusBarRegistration[] = [];
|
||||||
statusBarItems: StatusBarRegistration[] = [];
|
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
commands: CommandRegistration[] = [];
|
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
|
||||||
catalogEntityDetailItems: CatalogEntityDetailRegistration[] = [];
|
topBarItems: registries.TopBarRegistration[] = [];
|
||||||
topBarItems: TopBarRegistration[] = [];
|
|
||||||
|
|
||||||
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");
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "*",
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "*",
|
||||||
"conf": "^7.0.1"
|
"conf": "^7.0.1",
|
||||||
|
"typed-emitter": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,23 +26,20 @@ import type { PageTarget, RegisteredPage } from "./page-registry";
|
|||||||
import type { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface PageMenuRegistration {
|
export interface ClusterPageMenuRegistration {
|
||||||
target?: PageTarget;
|
|
||||||
title: React.ReactNode;
|
|
||||||
components: PageMenuComponents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClusterPageMenuRegistration extends PageMenuRegistration {
|
|
||||||
id?: string;
|
id?: string;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
target?: PageTarget;
|
||||||
|
title: React.ReactNode;
|
||||||
|
components: ClusterPageMenuComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageMenuComponents {
|
export interface ClusterPageMenuComponents {
|
||||||
Icon: React.ComponentType<IconProps>;
|
Icon: React.ComponentType<IconProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistration> {
|
||||||
add(items: T[], ext: LensExtension) {
|
add(items: ClusterPageMenuRegistration[], ext: LensExtension) {
|
||||||
const normalizedItems = items.map(menuItem => {
|
const normalizedItems = items.map(menuItem => {
|
||||||
menuItem.target = {
|
menuItem.target = {
|
||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
@ -54,9 +51,7 @@ export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegist
|
|||||||
|
|
||||||
return super.add(normalizedItems);
|
return super.add(normalizedItems);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
|
|
||||||
getRootItems() {
|
getRootItems() {
|
||||||
return this.getItems().filter((item) => !item.parentId);
|
return this.getItems().filter((item) => !item.parentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// layouts
|
// layouts
|
||||||
|
export * from "../../renderer/components/layout/main-layout";
|
||||||
|
export * from "../../renderer/components/layout/setting-layout";
|
||||||
export * from "../../renderer/components/layout/page-layout";
|
export * from "../../renderer/components/layout/page-layout";
|
||||||
export * from "../../renderer/components/layout/wizard-layout";
|
export * from "../../renderer/components/layout/wizard-layout";
|
||||||
export * from "../../renderer/components/layout/tab-layout";
|
export * from "../../renderer/components/layout/tab-layout";
|
||||||
|
|||||||
@ -120,8 +120,8 @@ describe("create clusters", () => {
|
|||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
protected async ensureKubectl() {
|
async ensureKubectl() {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}({
|
}({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
|
|||||||
@ -22,6 +22,14 @@
|
|||||||
import { UserStore } from "../../common/user-store";
|
import { UserStore } from "../../common/user-store";
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
|
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
enum ServiceResult {
|
enum ServiceResult {
|
||||||
Success,
|
Success,
|
||||||
@ -70,6 +78,10 @@ function getHandler() {
|
|||||||
|
|
||||||
describe("ContextHandler", () => {
|
describe("ContextHandler", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockFs({
|
||||||
|
"tmp": {}
|
||||||
|
});
|
||||||
|
|
||||||
PrometheusProviderRegistry.createInstance();
|
PrometheusProviderRegistry.createInstance();
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
@ -77,6 +89,7 @@ describe("ContextHandler", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
PrometheusProviderRegistry.resetInstance();
|
PrometheusProviderRegistry.resetInstance();
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPrometheusService", () => {
|
describe("getPrometheusService", () => {
|
||||||
|
|||||||
@ -46,7 +46,8 @@ jest.mock("winston", () => ({
|
|||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
getPath: () => "/foo",
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -77,8 +78,6 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
|
|||||||
describe("kube auth proxy tests", () => {
|
describe("kube auth proxy tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
UserStore.resetInstance();
|
|
||||||
UserStore.createInstance();
|
|
||||||
|
|
||||||
const mockMinikubeConfig = {
|
const mockMinikubeConfig = {
|
||||||
"minikube-config.yml": JSON.stringify({
|
"minikube-config.yml": JSON.stringify({
|
||||||
@ -102,13 +101,16 @@ describe("kube auth proxy tests", () => {
|
|||||||
}],
|
}],
|
||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
}),
|
||||||
|
"tmp": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockMinikubeConfig);
|
mockFs(mockMinikubeConfig);
|
||||||
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import type { CatalogEntity } from "../common/catalog";
|
|||||||
|
|
||||||
const broadcaster = debounce((items: CatalogEntity[]) => {
|
const broadcaster = debounce((items: CatalogEntity[]) => {
|
||||||
broadcastMessage("catalog:items", items);
|
broadcastMessage("catalog:items", items);
|
||||||
}, 1_000, { trailing: true });
|
}, 1_000, { leading: true, trailing: true });
|
||||||
|
|
||||||
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
||||||
return reaction(() => toJS(catalog.items), (items) => {
|
return reaction(() => toJS(catalog.items), (items) => {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { computeDiff, configToModels } from "../kubeconfig-sync";
|
|||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
|
import { ClusterManager } from "../../cluster-manager";
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
@ -38,10 +39,13 @@ describe("kubeconfig-sync.source tests", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockFs();
|
mockFs();
|
||||||
ClusterStore.createInstance();
|
ClusterStore.createInstance();
|
||||||
|
ClusterManager.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
|
ClusterStore.resetInstance();
|
||||||
|
ClusterManager.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("configsToModels", () => {
|
describe("configsToModels", () => {
|
||||||
|
|||||||
72
src/main/catalog-sources/general.ts
Normal file
72
src/main/catalog-sources/general.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { GeneralEntity } from "../../common/catalog-entities/general";
|
||||||
|
import { catalogURL, preferencesURL } from "../../common/routes";
|
||||||
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
|
|
||||||
|
export const catalogEntity = new GeneralEntity({
|
||||||
|
metadata: {
|
||||||
|
uid: "catalog-entity",
|
||||||
|
name: "Catalog",
|
||||||
|
source: "app",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
path: catalogURL(),
|
||||||
|
icon: {
|
||||||
|
material: "view_list",
|
||||||
|
background: "#3d90ce"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "active",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const preferencesEntity = new GeneralEntity({
|
||||||
|
metadata: {
|
||||||
|
uid: "preferences-entity",
|
||||||
|
name: "Preferences",
|
||||||
|
source: "app",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
path: preferencesURL(),
|
||||||
|
icon: {
|
||||||
|
material: "settings",
|
||||||
|
background: "#3d90ce"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "active",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const generalEntities = observable([
|
||||||
|
catalogEntity,
|
||||||
|
preferencesEntity
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function syncGeneralEntities() {
|
||||||
|
catalogEntityRegistry.addObservableSource("lens:general", generalEntities);
|
||||||
|
}
|
||||||
@ -19,4 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { syncWeblinks } from "./weblinks";
|
||||||
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
||||||
|
export { syncGeneralEntities } from "./general";
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import logger from "../logger";
|
|||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
|
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { catalogEntityFromCluster } from "../cluster-manager";
|
import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager";
|
||||||
import { UserStore } from "../../common/user-store";
|
import { UserStore } from "../../common/user-store";
|
||||||
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
@ -170,6 +170,9 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
|||||||
|
|
||||||
// remove and disconnect clusters that were removed from the config
|
// remove and disconnect clusters that were removed from the config
|
||||||
if (!model) {
|
if (!model) {
|
||||||
|
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
|
||||||
|
ClusterManager.getInstance().deleting.delete(value[0].id);
|
||||||
|
|
||||||
value[0].disconnect();
|
value[0].disconnect();
|
||||||
source.delete(contextName);
|
source.delete(contextName);
|
||||||
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
|
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
|
||||||
|
|||||||
102
src/main/catalog-sources/weblinks.ts
Normal file
102
src/main/catalog-sources/weblinks.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observable, reaction } from "mobx";
|
||||||
|
import { WeblinkStore } from "../../common/weblink-store";
|
||||||
|
import { WebLink } from "../../common/catalog-entities";
|
||||||
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
|
import got from "got";
|
||||||
|
import logger from "../logger";
|
||||||
|
import { docsUrl, slackUrl } from "../../common/vars";
|
||||||
|
|
||||||
|
const defaultLinks = [
|
||||||
|
{ title: "Lens Website", url: "https://k8slens.dev" },
|
||||||
|
{ title: "Lens Documentation", url: docsUrl },
|
||||||
|
{ title: "Lens Community Slack", url: slackUrl },
|
||||||
|
{ title: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" },
|
||||||
|
{ title: "Lens on Twitter", url: "https://twitter.com/k8slens" },
|
||||||
|
{ title: "Lens Official Blog", url: "https://medium.com/k8slens" }
|
||||||
|
].map((link) => (
|
||||||
|
new WebLink({
|
||||||
|
metadata: {
|
||||||
|
uid: link.url,
|
||||||
|
name: link.title,
|
||||||
|
source: "app",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
url: link.url
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "available",
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
async function validateLink(link: WebLink) {
|
||||||
|
try {
|
||||||
|
const response = await got.get(link.spec.url, {
|
||||||
|
throwHttpErrors: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 500) {
|
||||||
|
link.status.phase = "available";
|
||||||
|
} else {
|
||||||
|
link.status.phase = "unavailable";
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
link.status.phase = "unavailable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function syncWeblinks() {
|
||||||
|
const weblinkStore = WeblinkStore.getInstance();
|
||||||
|
const weblinks = observable.array(defaultLinks);
|
||||||
|
|
||||||
|
reaction(() => weblinkStore.weblinks, (links) => {
|
||||||
|
weblinks.replace(links.map((data) => new WebLink({
|
||||||
|
metadata: {
|
||||||
|
uid: data.id,
|
||||||
|
name: data.name,
|
||||||
|
source: "local",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
url: data.url
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "available",
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
weblinks.push(...defaultLinks);
|
||||||
|
|
||||||
|
for (const link of weblinks) {
|
||||||
|
validateLink(link).catch((error) => {
|
||||||
|
logger.error(`failed to validate link ${link.spec.url}: %s`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {fireImmediately: true});
|
||||||
|
|
||||||
|
catalogEntityRegistry.addObservableSource("weblinks", weblinks);
|
||||||
|
}
|
||||||
@ -58,7 +58,7 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
url: "https://k8slens.dev"
|
url: "https://k8slens.dev"
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: "valid"
|
phase: "available"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const invalidEntity = new InvalidEntity({
|
const invalidEntity = new InvalidEntity({
|
||||||
@ -72,7 +72,7 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
url: "https://k8slens.dev"
|
url: "https://k8slens.dev"
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: "valid"
|
phase: "available"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { action, autorun, makeObservable, reaction, toJS } from "mobx";
|
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
@ -30,17 +30,18 @@ import { Singleton } from "../common/utils";
|
|||||||
import { catalogEntityRegistry } from "./catalog";
|
import { catalogEntityRegistry } from "./catalog";
|
||||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
|
import { once } from "lodash";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
|
deleting = observable.set<ClusterId>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.bindEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindEvents() {
|
init = once(() => {
|
||||||
// reacting to every cluster's state change and total amount of items
|
// reacting to every cluster's state change and total amount of items
|
||||||
reaction(
|
reaction(
|
||||||
() => this.store.clustersList.map(c => c.getState()),
|
() => this.store.clustersList.map(c => c.getState()),
|
||||||
@ -59,6 +60,12 @@ export class ClusterManager extends Singleton {
|
|||||||
this.syncClustersFromCatalog(entities);
|
this.syncClustersFromCatalog(entities);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
observe(this.deleting, change => {
|
||||||
|
if (change.type === "add") {
|
||||||
|
this.updateEntityStatus(catalogEntityRegistry.getById(change.newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(this.store.removedClusters.values());
|
const removedClusters = Array.from(this.store.removedClusters.values());
|
||||||
@ -76,7 +83,7 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
ipcMainOn("network:offline", this.onNetworkOffline);
|
ipcMainOn("network:offline", this.onNetworkOffline);
|
||||||
ipcMainOn("network:online", this.onNetworkOnline);
|
ipcMainOn("network:online", this.onNetworkOnline);
|
||||||
}
|
});
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected updateCatalog(clusters: Cluster[]) {
|
protected updateCatalog(clusters: Cluster[]) {
|
||||||
@ -96,6 +103,9 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
this.updateEntityStatus(entity, cluster);
|
||||||
|
|
||||||
|
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
|
||||||
|
entity.metadata.labels.distro = cluster.distribution;
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
if (cluster.preferences?.clusterName) {
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
entity.metadata.name = cluster.preferences.clusterName;
|
||||||
}
|
}
|
||||||
@ -110,13 +120,25 @@ export class ClusterManager extends Singleton {
|
|||||||
entity.spec.metrics.prometheus = prometheus;
|
entity.spec.metrics.prometheus = prometheus;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.spec.iconData = cluster.preferences.icon;
|
if (cluster.preferences.icon) {
|
||||||
|
entity.spec.icon ??= {};
|
||||||
|
entity.spec.icon.src = cluster.preferences.icon;
|
||||||
|
} else {
|
||||||
|
entity.spec.icon = null;
|
||||||
|
}
|
||||||
|
|
||||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
@action
|
||||||
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
||||||
|
if (this.deleting.has(entity.getId())) {
|
||||||
|
entity.status.phase = "deleting";
|
||||||
|
entity.status.enabled = false;
|
||||||
|
} else {
|
||||||
|
entity.status.phase = cluster?.accessible ? "connected" : "disconnected";
|
||||||
|
entity.status.enabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
@ -204,7 +226,8 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
|||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
kubeconfigPath: cluster.kubeConfigPath,
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
kubeconfigContext: cluster.contextName
|
kubeconfigContext: cluster.contextName,
|
||||||
|
icon: {}
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
|||||||
@ -88,12 +88,7 @@ export interface ClusterState {
|
|||||||
export class Cluster implements ClusterModel, ClusterState {
|
export class Cluster implements ClusterModel, ClusterState {
|
||||||
/** Unique id for a cluster */
|
/** Unique id for a cluster */
|
||||||
public readonly id: ClusterId;
|
public readonly id: ClusterId;
|
||||||
/**
|
private kubeCtl: Kubectl;
|
||||||
* Kubectl
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public kubeCtl: Kubectl;
|
|
||||||
/**
|
/**
|
||||||
* Context handler
|
* Context handler
|
||||||
*
|
*
|
||||||
@ -212,6 +207,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable accessibleNamespaces: string[] = [];
|
@observable accessibleNamespaces: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Labels for the catalog entity
|
||||||
|
*/
|
||||||
|
@observable labels: Record<string, string> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster available
|
* Is cluster available
|
||||||
*
|
*
|
||||||
@ -309,6 +309,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
if (model.accessibleNamespaces) {
|
if (model.accessibleNamespaces) {
|
||||||
this.accessibleNamespaces = model.accessibleNamespaces;
|
this.accessibleNamespaces = model.accessibleNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.labels) {
|
||||||
|
this.labels = model.labels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -363,7 +367,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAccessibility();
|
await this.refreshAccessibility();
|
||||||
this.ensureKubectl();
|
this.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
|
|
||||||
@ -373,10 +377,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected async ensureKubectl() {
|
async ensureKubectl() {
|
||||||
this.kubeCtl = new Kubectl(this.version);
|
this.kubeCtl ??= new Kubectl(this.version);
|
||||||
|
|
||||||
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
await this.kubeCtl.ensureKubectl();
|
||||||
|
|
||||||
|
return this.kubeCtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -586,6 +592,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
metadata: this.metadata,
|
metadata: this.metadata,
|
||||||
accessibleNamespaces: this.accessibleNamespaces,
|
accessibleNamespaces: this.accessibleNamespaces,
|
||||||
|
labels: this.labels,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(model);
|
return toJS(model);
|
||||||
@ -650,7 +657,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { body: { items }} = await api.listNamespace();
|
const { body: { items } } = await api.listNamespace();
|
||||||
const namespaces = items.map(ns => ns.metadata.name);
|
const namespaces = items.map(ns => ns.metadata.name);
|
||||||
|
|
||||||
this.getAllowedNamespacesErrorCount = 0; // reset on success
|
this.getAllowedNamespacesErrorCount = 0; // reset on success
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -169,9 +169,10 @@ export async function rollback(name: string, namespace: string, revision: number
|
|||||||
async function getResources(name: string, namespace: string, cluster: Cluster) {
|
async function getResources(name: string, namespace: string, cluster: Cluster) {
|
||||||
try {
|
try {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const kubectl = await cluster.kubeCtl.getPath();
|
const kubectl = await cluster.ensureKubectl();
|
||||||
|
const kubectlPath = await kubectl.getPath();
|
||||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||||
|
|
||||||
return JSON.parse(stdout).items;
|
return JSON.parse(stdout).items;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -35,14 +35,10 @@ import { shellSync } from "./shell-sync";
|
|||||||
import { mangleProxyEnv } from "./proxy-env";
|
import { mangleProxyEnv } from "./proxy-env";
|
||||||
import { registerFileProtocol } from "../common/register-protocol";
|
import { registerFileProtocol } from "../common/register-protocol";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { ClusterStore } from "../common/cluster-store";
|
|
||||||
import { UserStore } from "../common/user-store";
|
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
import { ExtensionLoader } from "../extensions/extension-loader";
|
||||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
|
||||||
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { LensProtocolRouterMain } from "./protocol-handler";
|
import { LensProtocolRouterMain } from "./protocol-handler";
|
||||||
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||||
@ -51,13 +47,18 @@ import { startUpdateChecking } from "./app-updater";
|
|||||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||||
import { pushCatalogToRenderer } from "./catalog-pusher";
|
import { pushCatalogToRenderer } from "./catalog-pusher";
|
||||||
import { catalogEntityRegistry } from "./catalog";
|
import { catalogEntityRegistry } from "./catalog";
|
||||||
import { HotbarStore } from "../common/hotbar-store";
|
|
||||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||||
import { KubeconfigSyncManager } from "./catalog-sources";
|
import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources";
|
||||||
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
||||||
import configurePackages from "../common/configure-packages";
|
import configurePackages from "../common/configure-packages";
|
||||||
import { PrometheusProviderRegistry } from "./prometheus";
|
import { PrometheusProviderRegistry } from "./prometheus";
|
||||||
import * as initializers from "./initializers";
|
import * as initializers from "./initializers";
|
||||||
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
|
import { HotbarStore } from "../common/hotbar-store";
|
||||||
|
import { UserStore } from "../common/user-store";
|
||||||
|
import { WeblinkStore } from "../common/weblink-store";
|
||||||
|
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||||
|
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
const cleanup = disposer();
|
const cleanup = disposer();
|
||||||
@ -128,27 +129,32 @@ app.on("ready", async () => {
|
|||||||
PrometheusProviderRegistry.createInstance();
|
PrometheusProviderRegistry.createInstance();
|
||||||
initializers.initPrometheusProviderRegistry();
|
initializers.initPrometheusProviderRegistry();
|
||||||
|
|
||||||
const userStore = UserStore.createInstance();
|
/**
|
||||||
const clusterStore = ClusterStore.createInstance();
|
* The following sync MUST be done before HotbarStore creation, because that
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
* store has migrations that will remove items that previous migrations add
|
||||||
const extensionsStore = ExtensionsStore.createInstance();
|
* if this is not presant
|
||||||
const filesystemStore = FilesystemProvisionerStore.createInstance();
|
*/
|
||||||
|
syncGeneralEntities();
|
||||||
|
|
||||||
|
logger.info("💾 Loading stores");
|
||||||
|
|
||||||
|
UserStore.createInstance().startMainReactions();
|
||||||
|
ClusterStore.createInstance().provideInitialFromMain();
|
||||||
|
HotbarStore.createInstance();
|
||||||
|
ExtensionsStore.createInstance();
|
||||||
|
FilesystemProvisionerStore.createInstance();
|
||||||
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
|
syncWeblinks();
|
||||||
|
|
||||||
HelmRepoManager.createInstance(); // create the instance
|
HelmRepoManager.createInstance(); // create the instance
|
||||||
|
|
||||||
logger.info("💾 Loading stores");
|
const lensProxy = LensProxy.createInstance(
|
||||||
// preload
|
handleWsUpgrade,
|
||||||
await Promise.all([
|
req => ClusterManager.getInstance().getClusterForRequest(req),
|
||||||
userStore.load(),
|
);
|
||||||
clusterStore.load(),
|
|
||||||
hotbarStore.load(),
|
|
||||||
extensionsStore.load(),
|
|
||||||
filesystemStore.load(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lensProxy = LensProxy.createInstance(handleWsUpgrade);
|
ClusterManager.createInstance().init();
|
||||||
|
|
||||||
ClusterManager.createInstance();
|
|
||||||
KubeconfigSyncManager.createInstance();
|
KubeconfigSyncManager.createInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -201,10 +207,6 @@ app.on("ready", async () => {
|
|||||||
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ExtensionLoader.getInstance().whenLoaded.then(() => {
|
|
||||||
LensProtocolRouterMain.getInstance().extensionsLoaded = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("🧩 Initializing extensions");
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
// call after windowManager to see splash earlier
|
||||||
|
|||||||
@ -22,11 +22,15 @@
|
|||||||
import type { IpcMainInvokeEvent } from "electron";
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../common/cluster-ipc";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { ipcMainHandle } from "../../common/ipc";
|
import { ipcMainHandle } from "../../common/ipc";
|
||||||
import { catalogEntityRegistry } from "../catalog";
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
|
import { ClusterManager } from "../cluster-manager";
|
||||||
|
import { bundledKubectlPath } from "../kubectl";
|
||||||
|
import logger from "../logger";
|
||||||
|
import { promiseExecFile } from "../promise-exec";
|
||||||
import { ResourceApplier } from "../resource-applier";
|
import { ResourceApplier } from "../resource-applier";
|
||||||
|
|
||||||
export function initIpcMainHandlers() {
|
export function initIpcMainHandlers() {
|
||||||
@ -73,6 +77,29 @@ export function initIpcMainHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
|
||||||
|
appEventBus.emit({ name: "cluster", action: "remove" });
|
||||||
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
|
if (!cluster) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClusterManager.getInstance().deleting.add(clusterId);
|
||||||
|
cluster.disconnect();
|
||||||
|
clusterFrameMap.delete(cluster.id);
|
||||||
|
const kubectlPath = bundledKubectlPath();
|
||||||
|
const args = ["config", "delete-context", cluster.contextName, "--kubeconfig", cluster.kubeConfigPath];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promiseExecFile(kubectlPath, args);
|
||||||
|
} catch ({ stderr }) {
|
||||||
|
logger.error(`[CLUSTER-REMOVE]: failed to remove cluster: ${stderr}`, { clusterId, context: cluster.contextName });
|
||||||
|
|
||||||
|
throw `Failed to remove cluster: ${stderr}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
|
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
|
||||||
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { UserStore } from "../common/user-store";
|
|||||||
import { customRequest } from "../common/request";
|
import { customRequest } from "../common/request";
|
||||||
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||||
|
import { SemVer } from "semver";
|
||||||
|
|
||||||
const bundledVersion = getBundledKubectlVersion();
|
const bundledVersion = getBundledKubectlVersion();
|
||||||
const kubectlMap: Map<string, string> = new Map([
|
const kubectlMap: Map<string, string> = new Map([
|
||||||
@ -92,14 +93,19 @@ export class Kubectl {
|
|||||||
|
|
||||||
// Returns the single bundled Kubectl instance
|
// Returns the single bundled Kubectl instance
|
||||||
public static bundled() {
|
public static bundled() {
|
||||||
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
|
return Kubectl.bundledInstance ??= new Kubectl(Kubectl.bundledKubectlVersion);
|
||||||
|
|
||||||
return Kubectl.bundledInstance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clusterVersion: string) {
|
constructor(clusterVersion: string) {
|
||||||
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
|
let version: SemVer;
|
||||||
const minorVersion = versionParts[1];
|
|
||||||
|
try {
|
||||||
|
version = new SemVer(clusterVersion, { includePrerelease: false });
|
||||||
|
} catch {
|
||||||
|
version = new SemVer(Kubectl.bundledKubectlVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minorVersion = `${version.major}.${version.minor}`;
|
||||||
|
|
||||||
/* minorVersion is the first two digits of kube server version
|
/* minorVersion is the first two digits of kube server version
|
||||||
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
||||||
@ -107,7 +113,7 @@ export class Kubectl {
|
|||||||
this.kubectlVersion = kubectlMap.get(minorVersion);
|
this.kubectlVersion = kubectlMap.get(minorVersion);
|
||||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
||||||
} else {
|
} else {
|
||||||
this.kubectlVersion = versionParts[1] + versionParts[2];
|
this.kubectlVersion = version.format();
|
||||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
|
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`);
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||||
case "memoryLimits":
|
case "memoryLimits":
|
||||||
@ -88,6 +90,8 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
|
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (instance)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
|
|||||||
@ -39,6 +39,8 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "memoryLimits":
|
case "memoryLimits":
|
||||||
@ -73,6 +75,8 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (node)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||||
case "memoryLimits":
|
case "memoryLimits":
|
||||||
@ -88,6 +90,8 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
||||||
|
case "workloadMemoryUsage":
|
||||||
|
return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (instance)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import { exec } from "child_process";
|
import { exec, execFile } from "child_process";
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
export const promiseExec = util.promisify(exec);
|
||||||
|
export const promiseExecFile = util.promisify(execFile);
|
||||||
|
|||||||
@ -28,9 +28,17 @@ import { LensExtension } from "../../../extensions/main-api";
|
|||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
||||||
import { LensProtocolRouterMain } from "../router";
|
import { LensProtocolRouterMain } from "../router";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("../../../common/ipc");
|
jest.mock("../../../common/ipc");
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function throwIfDefined(val: any): void {
|
function throwIfDefined(val: any): void {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
throw val;
|
throw val;
|
||||||
@ -39,12 +47,14 @@ function throwIfDefined(val: any): void {
|
|||||||
|
|
||||||
describe("protocol router tests", () => {
|
describe("protocol router tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockFs({
|
||||||
|
"tmp": {}
|
||||||
|
});
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
ExtensionLoader.createInstance();
|
ExtensionLoader.createInstance();
|
||||||
|
|
||||||
const lpr = LensProtocolRouterMain.createInstance();
|
const lpr = LensProtocolRouterMain.createInstance();
|
||||||
|
|
||||||
lpr.extensionsLoaded = true;
|
|
||||||
lpr.rendererLoaded = true;
|
lpr.rendererLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,23 +64,24 @@ describe("protocol router tests", () => {
|
|||||||
ExtensionsStore.resetInstance();
|
ExtensionsStore.resetInstance();
|
||||||
ExtensionLoader.resetInstance();
|
ExtensionLoader.resetInstance();
|
||||||
LensProtocolRouterMain.resetInstance();
|
LensProtocolRouterMain.resetInstance();
|
||||||
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on non-lens URLS", () => {
|
it("should throw on non-lens URLS", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(lpr.route("https://google.ca")).toBeUndefined();
|
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when host not internal or extension", () => {
|
it("should throw when host not internal or extension", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(lpr.route("lens://foobar")).toBeUndefined();
|
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
@ -103,13 +114,13 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app")).toBeUndefined();
|
expect(await lpr.route("lens://app")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -119,14 +130,14 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call handler if matches", () => {
|
it("should call handler if matches", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = true; });
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page")).toBeUndefined();
|
expect(await lpr.route("lens://app/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -135,7 +146,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler", () => {
|
it("should call most exact handler", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -143,7 +154,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -183,7 +194,7 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -251,7 +262,7 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -268,7 +279,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 3 found handlers", () => {
|
it("should call most exact handler with 3 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -278,7 +289,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -287,7 +298,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 2 found handlers", () => {
|
it("should call most exact handler with 2 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -296,7 +307,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,6 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||||
|
|
||||||
@observable rendererLoaded = false;
|
@observable rendererLoaded = false;
|
||||||
@observable extensionsLoaded = false;
|
|
||||||
|
|
||||||
protected disposers = disposer();
|
protected disposers = disposer();
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
* This will send an IPC message to the renderer router to do the same
|
* This will send an IPC message to the renderer router to do the same
|
||||||
* in the renderer.
|
* in the renderer.
|
||||||
*/
|
*/
|
||||||
public route(rawUrl: string) {
|
public async route(rawUrl: string) {
|
||||||
try {
|
try {
|
||||||
const url = new URLParse(rawUrl, true);
|
const url = new URLParse(rawUrl, true);
|
||||||
|
|
||||||
@ -82,15 +81,15 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
|
||||||
const routeInternally = checkHost(url);
|
const routeInternally = checkHost(url);
|
||||||
|
|
||||||
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
||||||
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
|
|
||||||
|
|
||||||
if (routeInternally) {
|
if (routeInternally) {
|
||||||
this._routeToInternal(url);
|
this._routeToInternal(url);
|
||||||
} else {
|
} else {
|
||||||
this.disposers.push(when(() => this.extensionsLoaded, () => this._routeToExtension(url)));
|
await this._routeToExtension(url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { Router } from "../router";
|
|||||||
import type { ContextHandler } from "../context-handler";
|
import type { ContextHandler } from "../context-handler";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { Singleton } from "../../common/utils";
|
import { Singleton } from "../../common/utils";
|
||||||
import { ClusterManager } from "../cluster-manager";
|
import type { Cluster } from "../cluster";
|
||||||
|
|
||||||
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ export class LensProxy extends Singleton {
|
|||||||
|
|
||||||
public port: number;
|
public port: number;
|
||||||
|
|
||||||
constructor(handleWsUpgrade: WSUpgradeHandler) {
|
constructor(handleWsUpgrade: WSUpgradeHandler, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster | undefined) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const proxy = this.createProxy();
|
const proxy = this.createProxy();
|
||||||
@ -104,7 +104,7 @@ export class LensProxy extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
const cluster = this.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
@ -220,7 +220,7 @@ export class LensProxy extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
const cluster = this.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||||
|
|||||||
@ -36,14 +36,14 @@ export class ResourceApplier {
|
|||||||
|
|
||||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||||
resource = this.sanitizeObject(resource);
|
resource = this.sanitizeObject(resource);
|
||||||
appEventBus.emit({name: "resource", action: "apply"});
|
appEventBus.emit({ name: "resource", action: "apply" });
|
||||||
|
|
||||||
return await this.kubectlApply(yaml.safeDump(resource));
|
return await this.kubectlApply(yaml.safeDump(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
protected async kubectlApply(content: string): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
const kubectlPath = await kubeCtl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
@ -82,8 +82,8 @@ export class ResourceApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise<string> {
|
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
const kubectlPath = await kubeCtl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { appEventBus } from "../common/event-bus";
|
|||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
import { initMenu } from "./menu";
|
import { initMenu } from "./menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
import { delay, Singleton } from "../common/utils";
|
import { delay, iter, Singleton } from "../common/utils";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -38,6 +38,12 @@ function isHideable(window: BrowserWindow | null): boolean {
|
|||||||
return Boolean(window && !window.isDestroyed());
|
return Boolean(window && !window.isDestroyed());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SendToViewArgs {
|
||||||
|
channel: string;
|
||||||
|
frameInfo?: ClusterFrameInfo;
|
||||||
|
data?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export class WindowManager extends Singleton {
|
export class WindowManager extends Singleton {
|
||||||
protected mainWindow: BrowserWindow;
|
protected mainWindow: BrowserWindow;
|
||||||
protected splashWindow: BrowserWindow;
|
protected splashWindow: BrowserWindow;
|
||||||
@ -175,7 +181,7 @@ export class WindowManager extends Singleton {
|
|||||||
return this.mainWindow;
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
|
private sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) {
|
||||||
if (frameInfo) {
|
if (frameInfo) {
|
||||||
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
||||||
} else {
|
} else {
|
||||||
@ -183,10 +189,22 @@ export class WindowManager extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async navigateExtension(extId: string, pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||||
|
await this.ensureMainWindow();
|
||||||
|
|
||||||
|
const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
|
||||||
|
|
||||||
|
this.sendToView({
|
||||||
|
channel: "extension:navigate",
|
||||||
|
frameInfo,
|
||||||
|
data: [extId, pageId, params],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async navigate(url: string, frameId?: number) {
|
async navigate(url: string, frameId?: number) {
|
||||||
await this.ensureMainWindow();
|
await this.ensureMainWindow();
|
||||||
|
|
||||||
const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId);
|
const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
|
||||||
const channel = frameInfo
|
const channel = frameInfo
|
||||||
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
|
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
|
||||||
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
|
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
|
||||||
|
|||||||
@ -19,12 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Early store format had the kubeconfig directly under context name, this moves
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
it under the kubeConfig key */
|
|
||||||
|
|
||||||
import { migration } from "../migration-wrapper";
|
/**
|
||||||
|
* Early store format had the kubeconfig directly under context name, this moves
|
||||||
|
* it under the kubeConfig key
|
||||||
|
*/
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: "2.0.0-beta.2",
|
version: "2.0.0-beta.2",
|
||||||
run(store) {
|
run(store) {
|
||||||
for (const value of store) {
|
for (const value of store) {
|
||||||
@ -35,4 +37,4 @@ export default migration({
|
|||||||
store.set(contextName, { kubeConfig: value[1] });
|
store.set(contextName, { kubeConfig: value[1] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -19,10 +19,11 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Cleans up a store that had the state related data stored
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
import { migration } from "../migration-wrapper";
|
|
||||||
|
|
||||||
export default migration({
|
// Cleans up a store that had the state related data stored
|
||||||
|
|
||||||
|
export default {
|
||||||
version: "2.4.1",
|
version: "2.4.1",
|
||||||
run(store) {
|
run(store) {
|
||||||
for (const value of store) {
|
for (const value of store) {
|
||||||
@ -34,4 +35,4 @@ export default migration({
|
|||||||
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
|
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -20,9 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Move cluster icon from root to preferences
|
// Move cluster icon from root to preferences
|
||||||
import { migration } from "../migration-wrapper";
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: "2.6.0-beta.2",
|
version: "2.6.0-beta.2",
|
||||||
run(store) {
|
run(store) {
|
||||||
for (const value of store) {
|
for (const value of store) {
|
||||||
@ -40,4 +40,4 @@ export default migration({
|
|||||||
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
|
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -19,12 +19,12 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migration } from "../migration-wrapper";
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: "2.6.0-beta.3",
|
version: "2.6.0-beta.3",
|
||||||
run(store, log) {
|
run(store) {
|
||||||
for (const value of store) {
|
for (const value of store) {
|
||||||
const clusterKey = value[0];
|
const clusterKey = value[0];
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export default migration({
|
|||||||
if (authConfig.expiry) {
|
if (authConfig.expiry) {
|
||||||
authConfig.expiry = `${authConfig.expiry}`;
|
authConfig.expiry = `${authConfig.expiry}`;
|
||||||
}
|
}
|
||||||
log(authConfig);
|
migrationLog(authConfig);
|
||||||
user["auth-provider"].config = authConfig;
|
user["auth-provider"].config = authConfig;
|
||||||
kubeConfig.users = [{
|
kubeConfig.users = [{
|
||||||
name: userObj.name,
|
name: userObj.name,
|
||||||
@ -62,4 +62,4 @@ export default migration({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -20,9 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Add existing clusters to "default" workspace
|
// Add existing clusters to "default" workspace
|
||||||
import { migration } from "../migration-wrapper";
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: "2.7.0-beta.0",
|
version: "2.7.0-beta.0",
|
||||||
run(store) {
|
run(store) {
|
||||||
for (const value of store) {
|
for (const value of store) {
|
||||||
@ -35,4 +35,4 @@ export default migration({
|
|||||||
store.set(clusterKey, cluster);
|
store.set(clusterKey, cluster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Add id for clusters and store them to array
|
// Add id for clusters and store them to array
|
||||||
import { migration } from "../migration-wrapper";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: "2.7.0-beta.1",
|
version: "2.7.0-beta.1",
|
||||||
run(store) {
|
run(store) {
|
||||||
const clusters: any[] = [];
|
const clusters: any[] = [];
|
||||||
@ -48,4 +48,4 @@ export default migration({
|
|||||||
store.set("clusters", clusters);
|
store.set("clusters", clusters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
@ -23,69 +23,68 @@
|
|||||||
// convert file path cluster icons to their base64 encoded versions
|
// convert file path cluster icons to their base64 encoded versions
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, remote } from "electron";
|
import { app } from "electron";
|
||||||
import { migration } from "../migration-wrapper";
|
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
|
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
|
||||||
import { loadConfigFromFileSync } from "../../common/kube-helpers";
|
import { loadConfigFromFileSync } from "../../common/kube-helpers";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
interface Pre360ClusterModel extends ClusterModel {
|
||||||
|
kubeConfig: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
version: "3.6.0-beta.1",
|
version: "3.6.0-beta.1",
|
||||||
run(store, printLog) {
|
run(store) {
|
||||||
const userDataPath = (app || remote.app).getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
|
const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? [];
|
||||||
const storedClusters: ClusterModel[] = store.get("clusters") || [];
|
const migratedClusters: ClusterModel[] = [];
|
||||||
|
|
||||||
if (!storedClusters.length) return;
|
fse.ensureDirSync(ClusterStore.storedKubeConfigFolder);
|
||||||
fse.ensureDirSync(kubeConfigBase);
|
|
||||||
|
|
||||||
printLog("Number of clusters to migrate: ", storedClusters.length);
|
migrationLog("Number of clusters to migrate: ", storedClusters.length);
|
||||||
const migratedClusters = storedClusters
|
|
||||||
.map(cluster => {
|
for (const clusterModel of storedClusters) {
|
||||||
/**
|
/**
|
||||||
* migrate kubeconfig
|
* migrate kubeconfig
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
const absPath = ClusterStore.getCustomKubeConfigPath(cluster.id);
|
const absPath = ClusterStore.getCustomKubeConfigPath(clusterModel.id);
|
||||||
|
|
||||||
fse.ensureDirSync(path.dirname(absPath));
|
|
||||||
fse.writeFileSync(absPath, cluster.kubeConfig, { encoding: "utf-8", mode: 0o600 });
|
|
||||||
// take the embedded kubeconfig and dump it into a file
|
// take the embedded kubeconfig and dump it into a file
|
||||||
cluster.kubeConfigPath = absPath;
|
fse.writeFileSync(absPath, clusterModel.kubeConfig, { encoding: "utf-8", mode: 0o600 });
|
||||||
cluster.contextName = loadConfigFromFileSync(cluster.kubeConfigPath).config.getCurrentContext();
|
|
||||||
delete cluster.kubeConfig;
|
clusterModel.kubeConfigPath = absPath;
|
||||||
|
clusterModel.contextName = loadConfigFromFileSync(clusterModel.kubeConfigPath).config.getCurrentContext();
|
||||||
|
delete clusterModel.kubeConfig;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
printLog(`Failed to migrate Kubeconfig for cluster "${cluster.id}", removing cluster...`, error);
|
migrationLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error);
|
||||||
|
|
||||||
return undefined;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* migrate cluster icon
|
* migrate cluster icon
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
if (cluster.preferences?.icon) {
|
if (clusterModel.preferences?.icon) {
|
||||||
printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`);
|
migrationLog(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`);
|
||||||
const iconPath = cluster.preferences.icon.replace("store://", "");
|
const iconPath = clusterModel.preferences.icon.replace("store://", "");
|
||||||
const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
|
const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
|
||||||
|
|
||||||
cluster.preferences.icon = `data:;base64,${fileData.toString("base64")}`;
|
clusterModel.preferences.icon = `data:;base64,${fileData.toString("base64")}`;
|
||||||
} else {
|
} else {
|
||||||
delete cluster.preferences?.icon;
|
delete clusterModel.preferences?.icon;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
|
migrationLog(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error);
|
||||||
delete cluster.preferences.icon;
|
delete clusterModel.preferences.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cluster;
|
migratedClusters.push(clusterModel);
|
||||||
})
|
}
|
||||||
.filter(c => c);
|
|
||||||
|
|
||||||
// "overwrite" the cluster configs
|
|
||||||
if (migratedClusters.length > 0) {
|
|
||||||
store.set("clusters", migratedClusters);
|
store.set("clusters", migratedClusters);
|
||||||
}
|
}
|
||||||
}
|
} as MigrationDeclaration;
|
||||||
});
|
|
||||||
|
|||||||
65
src/migrations/cluster-store/5.0.0-beta.10.ts
Normal file
65
src/migrations/cluster-store/5.0.0-beta.10.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
import { app } from "electron";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import type { ClusterModel } from "../../common/cluster-store";
|
||||||
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
|
interface Pre500WorkspaceStoreModel {
|
||||||
|
workspaces: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: "5.0.0-beta.10",
|
||||||
|
run(store) {
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
|
||||||
|
const workspaces = new Map<string, string>(); // mapping from WorkspaceId to name
|
||||||
|
|
||||||
|
for (const { id, name } of workspaceData.workspaces) {
|
||||||
|
workspaces.set(id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clusters: ClusterModel[] = store.get("clusters");
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
||||||
|
cluster.labels ??= {};
|
||||||
|
cluster.labels.workspace = workspaces.get(cluster.workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.set("clusters", clusters);
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
|
||||||
|
// ignore lens-workspace-store.json being missing
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as MigrationDeclaration;
|
||||||
120
src/migrations/cluster-store/5.0.0-beta.13.ts
Normal file
120
src/migrations/cluster-store/5.0.0-beta.13.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-store";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
import { generateNewIdFor } from "../utils";
|
||||||
|
import path from "path";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { moveSync, removeSync } from "fs-extra";
|
||||||
|
|
||||||
|
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
|
||||||
|
if (left.prometheus && left.prometheusProvider) {
|
||||||
|
return {
|
||||||
|
prometheus: left.prometheus,
|
||||||
|
prometheusProvider: left.prometheusProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right.prometheus && right.prometheusProvider) {
|
||||||
|
return {
|
||||||
|
prometheus: right.prometheus,
|
||||||
|
prometheusProvider: right.prometheusProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
|
||||||
|
return {
|
||||||
|
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||||
|
clusterName: left.clusterName || right.clusterName || undefined,
|
||||||
|
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||||
|
icon: left.icon || right.icon || undefined,
|
||||||
|
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||||
|
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||||
|
...mergePrometheusPreferences(left, right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
|
||||||
|
return {
|
||||||
|
...right,
|
||||||
|
...left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSet(left: Iterable<string>, right: Iterable<string>): string[] {
|
||||||
|
return [...new Set([...left, ...right])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel {
|
||||||
|
return {
|
||||||
|
id: prev.id,
|
||||||
|
kubeConfigPath: prev.id,
|
||||||
|
contextName: prev.contextName,
|
||||||
|
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||||
|
metadata: prev.metadata,
|
||||||
|
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
|
||||||
|
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: string, oldId: string }): void {
|
||||||
|
const oldPath = path.resolve(folder, `${oldId}.json`);
|
||||||
|
const newPath = path.resolve(folder, `${newId}.json`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
moveSync(oldPath, newPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (String(error).includes("dest already exists")) {
|
||||||
|
migrationLog(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`);
|
||||||
|
removeSync(oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: "5.0.0-beta.13",
|
||||||
|
run(store) {
|
||||||
|
const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
|
||||||
|
|
||||||
|
const oldClusters: ClusterModel[] = store.get("clusters");
|
||||||
|
const clusters = new Map<string, ClusterModel>();
|
||||||
|
|
||||||
|
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||||
|
const newId = generateNewIdFor(cluster);
|
||||||
|
|
||||||
|
if (clusters.has(newId)) {
|
||||||
|
clusters.set(newId, mergeClusterModel(clusters.get(newId), cluster));
|
||||||
|
} else {
|
||||||
|
clusters.set(newId, {
|
||||||
|
...cluster,
|
||||||
|
id: newId,
|
||||||
|
});
|
||||||
|
moveStorageFolder({ folder, newId, oldId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.set("clusters", [...clusters.values()]);
|
||||||
|
}
|
||||||
|
} as MigrationDeclaration;
|
||||||
@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
// Cluster store migrations
|
// Cluster store migrations
|
||||||
|
|
||||||
|
import { joinMigrations } from "../helpers";
|
||||||
|
|
||||||
import version200Beta2 from "./2.0.0-beta.2";
|
import version200Beta2 from "./2.0.0-beta.2";
|
||||||
import version241 from "./2.4.1";
|
import version241 from "./2.4.1";
|
||||||
import version260Beta2 from "./2.6.0-beta.2";
|
import version260Beta2 from "./2.6.0-beta.2";
|
||||||
@ -28,15 +30,19 @@ import version260Beta3 from "./2.6.0-beta.3";
|
|||||||
import version270Beta0 from "./2.7.0-beta.0";
|
import version270Beta0 from "./2.7.0-beta.0";
|
||||||
import version270Beta1 from "./2.7.0-beta.1";
|
import version270Beta1 from "./2.7.0-beta.1";
|
||||||
import version360Beta1 from "./3.6.0-beta.1";
|
import version360Beta1 from "./3.6.0-beta.1";
|
||||||
|
import version500Beta10 from "./5.0.0-beta.10";
|
||||||
|
import version500Beta13 from "./5.0.0-beta.13";
|
||||||
import snap from "./snap";
|
import snap from "./snap";
|
||||||
|
|
||||||
export default {
|
export default joinMigrations(
|
||||||
...version200Beta2,
|
version200Beta2,
|
||||||
...version241,
|
version241,
|
||||||
...version260Beta2,
|
version260Beta2,
|
||||||
...version260Beta3,
|
version260Beta3,
|
||||||
...version270Beta0,
|
version270Beta0,
|
||||||
...version270Beta1,
|
version270Beta1,
|
||||||
...version360Beta1,
|
version360Beta1,
|
||||||
...snap
|
version500Beta10,
|
||||||
};
|
version500Beta13,
|
||||||
|
snap,
|
||||||
|
);
|
||||||
|
|||||||
@ -21,22 +21,22 @@
|
|||||||
|
|
||||||
// Fix embedded kubeconfig paths under snap config
|
// Fix embedded kubeconfig paths under snap config
|
||||||
|
|
||||||
import { migration } from "../migration-wrapper";
|
|
||||||
import type { ClusterModel } from "../../common/cluster-store";
|
import type { ClusterModel } from "../../common/cluster-store";
|
||||||
import { getAppVersion } from "../../common/utils/app-version";
|
import { getAppVersion } from "../../common/utils/app-version";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
|
||||||
export default migration({
|
export default {
|
||||||
version: getAppVersion(), // Run always after upgrade
|
version: getAppVersion(), // Run always after upgrade
|
||||||
run(store, printLog) {
|
run(store) {
|
||||||
if (!process.env["SNAP"]) return;
|
if (!process.env["SNAP"]) return;
|
||||||
|
|
||||||
printLog("Migrating embedded kubeconfig paths");
|
migrationLog("Migrating embedded kubeconfig paths");
|
||||||
const storedClusters: ClusterModel[] = store.get("clusters") || [];
|
const storedClusters: ClusterModel[] = store.get("clusters") || [];
|
||||||
|
|
||||||
if (!storedClusters.length) return;
|
if (!storedClusters.length) return;
|
||||||
|
|
||||||
printLog("Number of clusters to migrate: ", storedClusters.length);
|
migrationLog("Number of clusters to migrate: ", storedClusters.length);
|
||||||
const migratedClusters = storedClusters
|
const migratedClusters = storedClusters
|
||||||
.map(cluster => {
|
.map(cluster => {
|
||||||
/**
|
/**
|
||||||
@ -54,4 +54,4 @@ export default migration({
|
|||||||
|
|
||||||
store.set("clusters", migratedClusters);
|
store.set("clusters", migratedClusters);
|
||||||
}
|
}
|
||||||
});
|
} as MigrationDeclaration;
|
||||||
|
|||||||
65
src/renderer/components/layout/login-layout.tsx → src/migrations/helpers.ts
Executable file → Normal file
65
src/renderer/components/layout/login-layout.tsx → src/migrations/helpers.ts
Executable file → Normal file
@ -19,40 +19,37 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./login-layout.scss";
|
import type Conf from "conf";
|
||||||
|
import type { Migrations } from "conf/dist/source/types";
|
||||||
|
import { ExtendedMap, iter } from "../common/utils";
|
||||||
|
import { isTestEnv } from "../common/vars";
|
||||||
|
|
||||||
import React from "react";
|
export function migrationLog(...args: any[]) {
|
||||||
import { Link } from "react-router-dom";
|
if (!isTestEnv) {
|
||||||
import { cssNames } from "../../utils";
|
console.log(...args);
|
||||||
import { Icon } from "../icon";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: any;
|
|
||||||
header?: any;
|
|
||||||
title?: any;
|
|
||||||
footer?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LoginLayout extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { className, header, title, footer, children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={cssNames("LoginLayout flex", className)}>
|
|
||||||
<div className="header">{header}</div>
|
|
||||||
<div className="box main">
|
|
||||||
<div className="title">
|
|
||||||
<Link to="/">
|
|
||||||
<Icon svg="logo" className="logo"/>
|
|
||||||
</Link>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div className="content">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="footer">{footer}</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MigrationDeclaration {
|
||||||
|
version: string,
|
||||||
|
run(store: Conf<any>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {
|
||||||
|
const migrations = new ExtendedMap<string, ((store: Conf<any>) => void)[]>();
|
||||||
|
|
||||||
|
for (const decl of declarations) {
|
||||||
|
migrations.getOrInsert(decl.version, () => []).push(decl.run);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
iter.map(
|
||||||
|
migrations,
|
||||||
|
([v, fns]) => [v, (store: Conf<any>) => {
|
||||||
|
for (const fn of fns) {
|
||||||
|
fn(store);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user