mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into improve-metrics-queries
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
a1babe7b2a
@ -1,19 +1,7 @@
|
||||
variables:
|
||||
YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- releases/*
|
||||
paths:
|
||||
exclude:
|
||||
- .github/*
|
||||
- docs/*
|
||||
- mkdocs/*
|
||||
pr: none
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- "*"
|
||||
@ -57,11 +45,6 @@ jobs:
|
||||
displayName: Run tests
|
||||
- script: make test-extensions
|
||||
displayName: Run In-tree Extension tests
|
||||
- bash: |
|
||||
rm -rf extensions/telemetry
|
||||
make integration-win
|
||||
git checkout extensions/telemetry
|
||||
displayName: Run integration tests
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Build
|
||||
@ -69,6 +52,8 @@ jobs:
|
||||
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
||||
WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- job: macOS
|
||||
pool:
|
||||
vmImage: macOS-10.14
|
||||
@ -101,13 +86,6 @@ jobs:
|
||||
displayName: Run tests
|
||||
- script: make test-extensions
|
||||
displayName: Run In-tree Extension tests
|
||||
- bash: |
|
||||
rm -rf extensions/telemetry
|
||||
make integration-mac
|
||||
git checkout extensions/telemetry
|
||||
displayName: Run integration tests
|
||||
- script: make test-extensions
|
||||
displayName: Run In-tree Extension tests
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Build
|
||||
@ -117,6 +95,8 @@ jobs:
|
||||
CSC_LINK: $(CSC_LINK)
|
||||
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
@ -149,20 +129,6 @@ jobs:
|
||||
displayName: Run tests
|
||||
- script: make test-extensions
|
||||
displayName: Run In-tree Extension tests
|
||||
- bash: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgconf-2-4 conntrack -y
|
||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
||||
sudo minikube start --driver=none
|
||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||
displayName: Install integration test dependencies
|
||||
- bash: |
|
||||
rm -rf extensions/telemetry
|
||||
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||
git checkout extensions/telemetry
|
||||
displayName: Run integration tests
|
||||
- bash: |
|
||||
sudo chown root:root /
|
||||
sudo apt-get update && sudo apt-get install -y snapd
|
||||
@ -178,6 +144,8 @@ jobs:
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- script: make publish-npm
|
||||
displayName: Publish npm package
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
|
||||
17
.dependabot/config.yml
Normal file
17
.dependabot/config.yml
Normal file
@ -0,0 +1,17 @@
|
||||
# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# for config options
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 4
|
||||
reviewers:
|
||||
- "lensapp/lens-maintainers"
|
||||
labels:
|
||||
- "dependencies"
|
||||
versioning-strategy:
|
||||
lockfile-only: false
|
||||
increase: true
|
||||
@ -46,6 +46,8 @@ module.exports = {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true,
|
||||
}],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"eol-last": ["error", "always"],
|
||||
"semi": ["error", "always"],
|
||||
"object-shorthand": "error",
|
||||
"prefer-template": "error",
|
||||
@ -101,6 +103,8 @@ module.exports = {
|
||||
}],
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"eol-last": ["error", "always"],
|
||||
"object-shorthand": "error",
|
||||
"prefer-template": "error",
|
||||
"template-curly-spacing": "error",
|
||||
@ -162,6 +166,8 @@ module.exports = {
|
||||
}],
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"eol-last": ["error", "always"],
|
||||
"object-shorthand": "error",
|
||||
"prefer-template": "error",
|
||||
"template-curly-spacing": "error",
|
||||
|
||||
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# This is the default code owners for the whole Lens repo
|
||||
* @lensapp/lens-ide
|
||||
30
.github/release-drafter.yml
vendored
Normal file
30
.github/release-drafter.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
exclude-labels:
|
||||
- 'skip-changelog'
|
||||
categories:
|
||||
- title: '🚀 Features'
|
||||
labels:
|
||||
- 'enhancement'
|
||||
- title: '🐛 Bug Fixes'
|
||||
labels:
|
||||
- 'bug'
|
||||
- title: '🧰 Maintenance'
|
||||
labels:
|
||||
- 'chore'
|
||||
- 'area/ci'
|
||||
- 'area/tests'
|
||||
- 'dependencies'
|
||||
|
||||
template: |
|
||||
## Changes since $PREVIOUS_TAG
|
||||
|
||||
$CHANGES
|
||||
|
||||
### Download
|
||||
|
||||
- Lens v$RESOLVED_VERSION - Linux
|
||||
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
|
||||
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
|
||||
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
|
||||
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
||||
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
|
||||
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
||||
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
@ -83,12 +83,12 @@ jobs:
|
||||
mike deploy --push master
|
||||
|
||||
- name: Get the release version
|
||||
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
|
||||
- name: mkdocs deploy new release
|
||||
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
run: |
|
||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
22
.github/workflows/maintenance.yml
vendored
Normal file
22
.github/workflows/maintenance.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: "Maintenance"
|
||||
on:
|
||||
# So that PRs touching the same files as the push are updated
|
||||
push:
|
||||
# So that the `dirtyLabel` is removed if conflicts are resolve
|
||||
# We recommend `pull_request_target` so that github secrets are available.
|
||||
# In `pull_request` we wouldn't be able to change labels of fork PRs
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: check if prs are dirty
|
||||
uses: eps1lon/actions-label-merge-conflict@releases/2.x
|
||||
with:
|
||||
dirtyLabel: "PR: needs rebase"
|
||||
removeOnDirtyLabel: "PR: ready to ship"
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
|
||||
commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly."
|
||||
16
.github/workflows/release-drafter.yml
vendored
Normal file
16
.github/workflows/release-drafter.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
83
.github/workflows/test.yml
vendored
Normal file
83
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
name: Test
|
||||
on:
|
||||
- pull_request
|
||||
jobs:
|
||||
build:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-16.04, macos-10.15, windows-2019]
|
||||
node-version: [12.x]
|
||||
steps:
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Using Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- run: make node_modules
|
||||
name: Install dependencies
|
||||
|
||||
- run: make build-npm
|
||||
name: Generate npm package
|
||||
|
||||
- run: make -j2 build-extensions
|
||||
name: Build bundled extensions
|
||||
|
||||
- run: make test
|
||||
name: Run tests
|
||||
|
||||
- run: make test-extensions
|
||||
name: Run In-tree Extension tests
|
||||
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgconf-2-4 conntrack -y
|
||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
||||
sudo minikube start --driver=none
|
||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||
name: Install integration test dependencies
|
||||
if: runner.os == 'Linux'
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||
git checkout extensions/telemetry
|
||||
name: Run Linux integration tests
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
make integration-win
|
||||
git checkout extensions/telemetry
|
||||
name: Run Windows integration tests
|
||||
shell: bash
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
make integration-mac
|
||||
git checkout extensions/telemetry
|
||||
name: Run macOS integration tests
|
||||
if: runner.os == 'macOS'
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,4 +17,3 @@ types/extension-renderer-api.d.ts
|
||||
extensions/*/dist
|
||||
docs/extensions/api
|
||||
site/
|
||||
.vscode/
|
||||
|
||||
57
.vscode/launch.json
vendored
Normal file
57
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "compile-dev",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9223",
|
||||
"--inspect",
|
||||
"."
|
||||
],
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"name": "Renderer Process",
|
||||
"type": "pwa-chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"timeout": 30000
|
||||
},
|
||||
{
|
||||
"name": "Integration Tests",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"console": "integratedTerminal",
|
||||
"runtimeArgs": [
|
||||
"${workspaceFolder}/node_modules/.bin/jest",
|
||||
"--runInBand",
|
||||
"integration"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Unit Tests",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
|
||||
"args": [
|
||||
"--env=jsdom",
|
||||
"-i",
|
||||
"src"
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
18
.vscode/tasks.json
vendored
Normal file
18
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "yarn",
|
||||
"args": [
|
||||
"debug-build"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"label": "compile-dev",
|
||||
"detail": "Compiles main and extension types"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.yarnrc
2
.yarnrc
@ -1,3 +1,3 @@
|
||||
disturl "https://atom.io/download/electron"
|
||||
target "9.1.0"
|
||||
target "9.4.4"
|
||||
runtime "electron"
|
||||
|
||||
8
LICENSE
8
LICENSE
@ -1,12 +1,12 @@
|
||||
Copyright (c) 2020 Mirantis, Inc.
|
||||
Copyright (c) 2021 Mirantis, Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content residing under the "docs/" directory of this repository, if that
|
||||
* All content residing under the "docs/" directory of this repository, if that
|
||||
directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license".
|
||||
* All third party components incorporated into the Lens Software are licensed
|
||||
* All third party components incorporated into the Lens Software are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the "MIT" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
36
Makefile
36
Makefile
@ -1,3 +1,8 @@
|
||||
CMD_ARGS = $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
%:
|
||||
@:
|
||||
|
||||
EXTENSIONS_DIR = ./extensions
|
||||
extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir})
|
||||
extension_node_modules = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/node_modules)
|
||||
@ -9,23 +14,23 @@ else
|
||||
DETECTED_OS := $(shell uname)
|
||||
endif
|
||||
|
||||
binaries/client:
|
||||
binaries/client: node_modules
|
||||
yarn download-bins
|
||||
|
||||
node_modules:
|
||||
node_modules: yarn.lock
|
||||
yarn install --frozen-lockfile
|
||||
yarn check --verify-tree --integrity
|
||||
|
||||
static/build/LensDev.html:
|
||||
static/build/LensDev.html: node_modules
|
||||
yarn compile:renderer
|
||||
|
||||
.PHONY: compile-dev
|
||||
compile-dev:
|
||||
compile-dev: node_modules
|
||||
yarn compile:main --cache
|
||||
yarn compile:renderer --cache
|
||||
|
||||
.PHONY: dev
|
||||
dev: node_modules binaries/client build-extensions static/build/LensDev.html
|
||||
dev: binaries/client build-extensions static/build/LensDev.html
|
||||
yarn dev
|
||||
|
||||
.PHONY: lint
|
||||
@ -34,27 +39,30 @@ lint:
|
||||
|
||||
.PHONY: test
|
||||
test: binaries/client
|
||||
yarn test
|
||||
yarn run jest $(or $(CMD_ARGS), "src")
|
||||
|
||||
.PHONY: integration-linux
|
||||
integration-linux: build-extension-types build-extensions
|
||||
integration-linux: binaries/client build-extension-types build-extensions
|
||||
# ifdef XDF_CONFIG_HOME
|
||||
# rm -rf ${XDG_CONFIG_HOME}/.config/Lens
|
||||
# else
|
||||
# rm -rf ${HOME}/.config/Lens
|
||||
# endif
|
||||
yarn build:linux
|
||||
yarn integration
|
||||
|
||||
.PHONY: integration-mac
|
||||
integration-mac: build-extension-types build-extensions
|
||||
integration-mac: binaries/client build-extension-types build-extensions
|
||||
# rm ${HOME}/Library/Application\ Support/Lens
|
||||
yarn build:mac
|
||||
yarn integration
|
||||
|
||||
.PHONY: integration-win
|
||||
integration-win: build-extension-types build-extensions
|
||||
integration-win: binaries/client build-extension-types build-extensions
|
||||
# rm %APPDATA%/Lens
|
||||
yarn build:win
|
||||
yarn integration
|
||||
|
||||
.PHONY: test-app
|
||||
test-app:
|
||||
yarn test
|
||||
|
||||
.PHONY: build
|
||||
build: node_modules binaries/client build-extensions
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
@ -70,7 +78,7 @@ $(extension_dists): src/extensions/npm/extensions/dist
|
||||
cd $(@:/dist=) && npm run build
|
||||
|
||||
.PHONY: build-extensions
|
||||
build-extensions: $(extension_node_modules) $(extension_dists)
|
||||
build-extensions: node_modules $(extension_node_modules) $(extension_dists)
|
||||
|
||||
.PHONY: test-extensions
|
||||
test-extensions: $(extension_node_modules)
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Team Lens encourages users who become aware of a security vulnerability in Lens to contact Team Lens with details of the vulnerability. Team Lens has established an email address that should be used for reporting a vulnerability. Please send descriptions of any vulnerabilities found to security@k8slens.dev. Please include details on the software and hardware configuration of your system so that we can duplicate the issue being reported.
|
||||
|
||||
Team Lens hopes that users encountering a new vulnerability will contact us privately as it is in the best interests of our users that Team Lens has an opportunity to investigate and confirm a suspected vulnerability before it becomes public knowledge.
|
||||
|
||||
In the case of vulnerabilities found in third-party software components used in Lens, please also notify Team Lens as described above.
|
||||
@ -1 +1 @@
|
||||
module.exports = {};
|
||||
module.exports = {};
|
||||
|
||||
@ -1 +1 @@
|
||||
module.exports = {};
|
||||
module.exports = {};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 23 KiB |
BIN
build/icons/512x512@2x.png
Normal file
BIN
build/icons/512x512@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@ -2,7 +2,7 @@
|
||||
|
||||
Add clusters by clicking the **Add Cluster** button in the left-side menu.
|
||||
|
||||
1. Click the **Add Cluster** button (indicated with a '+' icon).
|
||||
1. Click the **Add Cluster** button (indicated with a '+' icon). Or [click here](lens://app/cluster).
|
||||
2. Enter the path to your kubeconfig file. You'll need to have a kubeconfig file for the cluster you want to add. You can either browse for the path from the file system or or enter it directly.
|
||||
|
||||
Selected [cluster contexts](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) are added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
# Lens Extension API
|
||||
|
||||
Customize and enhance the Lens experience with the Lens Extension API. Use the extension API to create menus or page content. The same extension API was used to create many of Lens's core features.
|
||||
Customize and enhance the Lens experience with the Lens Extension API.
|
||||
Use the extension API to create menus or page content.
|
||||
The same extension API was used to create many of Lens's core features.
|
||||
To install your first extension you should goto the [extension page](lens://app/extensions) in lens.
|
||||
|
||||
This documentation describes:
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ You can use theme-based CSS Variables to style an extension according to the act
|
||||
## Button Colors
|
||||
- `--buttonPrimaryBackground`: button background color for primary actions.
|
||||
- `--buttonDefaultBackground`: default button background color.
|
||||
- `--buttonLightBackground`: light button background color.
|
||||
- `--buttonAccentBackground`: accent button background color.
|
||||
- `--buttonDisabledBackground`: disabled button background color.
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ This extension can register custom app menus that will be displayed on OS native
|
||||
|
||||
Example:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensMainExtension, windowManager } from "@k8slens/extensions"
|
||||
|
||||
export default class ExampleMainExtension extends LensMainExtension {
|
||||
@ -92,7 +92,7 @@ export default class ExampleMainExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom global pages (views) to Lens's main window. The global page is a full-screen page that hides all other content from a window.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { Component, LensRendererExtension } from "@k8slens/extensions"
|
||||
import { ExamplePage } from "./src/example-page"
|
||||
@ -123,7 +123,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom app preferences. It is responsible for storing a state for custom preferences.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions"
|
||||
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
||||
@ -147,7 +147,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom cluster pages. These pages are visible in a cluster menu when a cluster is opened.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./src/page"
|
||||
@ -180,7 +180,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register installable features for a cluster. These features are visible in the "Cluster Settings" page.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions"
|
||||
import { MyCustomFeature } from "./src/my-custom-feature"
|
||||
@ -209,18 +209,20 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom icons and text to a status bar area.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
|
||||
|
||||
export default class ExampleExtension extends LensRendererExtension {
|
||||
statusBarItems = [
|
||||
{
|
||||
item: (
|
||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||
<Component.Icon material="favorite" />
|
||||
</div>
|
||||
)
|
||||
components: {
|
||||
Item: (
|
||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||
<Component.Icon material="favorite" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -231,7 +233,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
||||
@ -254,7 +256,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
|
||||
This extension can register custom details (content) for specified Kubernetes kinds/apiVersions.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
||||
|
||||
@ -21,12 +21,13 @@ Each guide or code sample includes the following:
|
||||
| [Components](components.md) | |
|
||||
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
||||
| [Working with mobx](working-with-mobx.md) | |
|
||||
| [Protocol Handlers](protocol-handlers.md) | |
|
||||
|
||||
## Samples
|
||||
|
||||
| Sample | APIs |
|
||||
| ----- | ----- |
|
||||
[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |
|
||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
|
||||
@ -42,7 +42,7 @@ Next, you'll try changing the way the new menu item appears in the UI. You'll ch
|
||||
|
||||
Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`:
|
||||
|
||||
```tsx
|
||||
```typescript
|
||||
clusterPageMenus = [
|
||||
{
|
||||
target: { pageId: "hello" },
|
||||
|
||||
BIN
docs/extensions/guides/images/clusterpagemenus.png
Normal file
BIN
docs/extensions/guides/images/clusterpagemenus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
BIN
docs/extensions/guides/images/globalpagemenus.png
Normal file
BIN
docs/extensions/guides/images/globalpagemenus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
docs/extensions/guides/images/routing-diag.png
Normal file
BIN
docs/extensions/guides/images/routing-diag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
83
docs/extensions/guides/protocol-handlers.md
Normal file
83
docs/extensions/guides/protocol-handlers.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Lens Protocol Handlers
|
||||
|
||||
Lens has a file association with the `lens://` protocol.
|
||||
This means that Lens can be opened by external programs by providing a link that has `lens` as its protocol.
|
||||
Lens provides a routing mechanism that extensions can use to register custom handlers.
|
||||
|
||||
## Registering A Protocol Handler
|
||||
|
||||
The field `protocolHandlers` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#protocolhandlers) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#protocolhandlers).
|
||||
This field will be iterated through every time a `lens://` request gets sent to the application.
|
||||
The `pathSchema` argument must comply with the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package's `compileToRegex` function.
|
||||
|
||||
Once you have registered a handler it will be called when a user opens a link on their computer.
|
||||
Handlers will be run in both `main` and `renderer` in parallel with no synchronization between the two processes.
|
||||
Furthermore, both `main` and `renderer` are routed separately.
|
||||
In other words, which handler is selected in either process is independent from the list of possible handlers in the other.
|
||||
|
||||
Example of registering a handler:
|
||||
|
||||
```typescript
|
||||
import { LensMainExtension, Interface } from "@k8slens/extensions";
|
||||
|
||||
function rootHandler(params: Iterface.ProtocolRouteParams) {
|
||||
console.log("routed to ExampleExtension", params);
|
||||
}
|
||||
|
||||
export default class ExampleExtensionMain extends LensMainExtension {
|
||||
protocolHandlers = [
|
||||
pathSchema: "/",
|
||||
handler: rootHandler,
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For testing the routing of URIs the `open` (on macOS) or `xdg-open` (on most linux) CLI utilities can be used.
|
||||
For the above handler, the following URI would be always routed to it:
|
||||
|
||||
```
|
||||
open lens://extension/example-extension/
|
||||
```
|
||||
|
||||
## Deregistering A Protocol Handler
|
||||
|
||||
All that is needed to deregister a handler is to remove it from the array of handlers.
|
||||
|
||||
## Routing Algorithm
|
||||
|
||||
The routing mechanism for extensions is quite straight forward.
|
||||
For example consider an extension `example-extension` which is published by the `@mirantis` org.
|
||||
If it were to register a handler with `"/display/:type"` as its corresponding link then we would match the following URI like this:
|
||||
|
||||

|
||||
|
||||
Once matched, the handler would be called with the following argument (note both `"search"` and `"pathname"` will always be defined):
|
||||
|
||||
```json
|
||||
{
|
||||
"search": {
|
||||
"text": "Hello"
|
||||
},
|
||||
"pathname": {
|
||||
"type": "notification"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As the diagram above shows, the search (or query) params are not considered as part of the handler resolution.
|
||||
If the URI had instead been `lens://extension/@mirantis/example-extension/display/notification/green` then a third (and optional) field will have the rest of the path.
|
||||
The `tail` field would be filled with `"/green"`.
|
||||
If multiple `pathSchema`'s match a given URI then the most specific handler will be called.
|
||||
|
||||
For example consider the following `pathSchema`'s:
|
||||
|
||||
1. `"/"`
|
||||
1. `"/display"`
|
||||
1. `"/display/:type"`
|
||||
1. `"/show/:id"`
|
||||
|
||||
The URI sub-path `"/display"` would be routed to #2 since it is an exact match.
|
||||
On the other hand, the subpath `"/display/notification"` would be routed to #3.
|
||||
|
||||
The URI is routed to the most specific matching `pathSchema`.
|
||||
This way the `"/"` (root) `pathSchema` acts as a sort of catch all or default route if no other route matches.
|
||||
@ -1,15 +1,28 @@
|
||||
# Renderer Extension
|
||||
|
||||
The renderer extension api is the interface to Lens's renderer process (Lens runs in main and renderer processes).
|
||||
It allows you to access, configure, and customize Lens data, add custom Lens UI elements, and generally run custom code in Lens's renderer process.
|
||||
The custom Lens UI elements that can be added include global pages, cluster pages, cluster page menus, cluster features, app preferences, status bar items, KubeObject menu items, and KubeObject details items.
|
||||
These UI elements are based on React components.
|
||||
The Renderer Extension API is the interface to Lens's renderer process. Lens runs in both the main and renderer processes. The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, and run custom code in Lens's renderer process.
|
||||
|
||||
The custom Lens UI elements that you can add include:
|
||||
|
||||
* [Cluster pages](#clusterpages)
|
||||
* [Cluster page menus](#clusterpagemenus)
|
||||
* [Global pages](#globalpages)
|
||||
* [Global page menus](#globalpagemenus)
|
||||
* [Cluster features](#clusterfeatures)
|
||||
* [App preferences](#apppreferences)
|
||||
* [Status bar items](#statusbaritems)
|
||||
* [KubeObject menu items](#kubeobjectmenuitems)
|
||||
* [KubeObject detail items](#kubeobjectdetailitems)
|
||||
|
||||
All UI elements are based on React components.
|
||||
|
||||
## `LensRendererExtension` Class
|
||||
|
||||
To create a renderer extension simply extend the `LensRendererExtension` class:
|
||||
### `onActivate()` and `onDeactivate()` Methods
|
||||
|
||||
``` typescript
|
||||
To create a renderer extension, extend the `LensRendererExtension` class:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
|
||||
export default class ExampleExtensionMain extends LensRendererExtension {
|
||||
@ -23,23 +36,23 @@ export default class ExampleExtensionMain extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
There are two methods that you can implement to facilitate running your custom code.
|
||||
`onActivate()` is called when your extension has been successfully enabled.
|
||||
By implementing `onActivate()` you can initiate your custom code.
|
||||
`onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary.
|
||||
The example above simply logs messages when the extension is enabled and disabled.
|
||||
Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. You can initiate custom code by implementing `onActivate()`. Implementing `onDeactivate()` gives you the opportunity to clean up after your extension.
|
||||
|
||||
!!! info
|
||||
Disable extensions from the Lens Extensions page:
|
||||
|
||||
1. Navigate to **File** > **Extensions** in the top menu bar. (On Mac, it is **Lens** > **Extensions**.)
|
||||
2. Click **Disable** on the extension you want to disable.
|
||||
|
||||
The example above logs messages when the extension is enabled and disabled.
|
||||
|
||||
### `clusterPages`
|
||||
|
||||
Cluster pages appear as part of the cluster dashboard.
|
||||
They are accessible from the side bar, and are shown in the menu list after *Custom Resources*.
|
||||
It is conventional to use a cluster page to show information or provide functionality pertaining to the active cluster, along with custom data and functionality your extension may have.
|
||||
However, it is not limited to the active cluster.
|
||||
Also, your extension can gain access to the Kubernetes resources in the active cluster in a straightforward manner using the [`clusterStore`](../stores#clusterstore).
|
||||
Cluster pages appear in the cluster dashboard. Use cluster pages to display information about or add functionality to the active cluster. It is also possible to include custom details from other clusters. Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore).
|
||||
|
||||
The following example adds a cluster page definition to a `LensRendererExtension` subclass:
|
||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
@ -56,14 +69,15 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Cluster pages are objects matching the `PageRegistration` interface.
|
||||
The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above.
|
||||
The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()).
|
||||
The `components` field matches the `PageComponents` interface for wich there is one field, `Page`.
|
||||
`Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page.
|
||||
For the example above `ExamplePage` can be defined in `page.tsx`:
|
||||
`clusterPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `clusterPages` array objects are defined as follows:
|
||||
|
||||
``` typescript
|
||||
* `id` is a string that identifies the page.
|
||||
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
* `Page` is of type ` React.ComponentType<any>`. It offers flexibility in defining the appearance and behavior of your page.
|
||||
|
||||
`ExamplePage` in the example above can be defined in `page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
|
||||
@ -78,16 +92,17 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `ExamplePage` class defines a property named `extension`.
|
||||
This allows the `ExampleExtension` object to be passed in React-style in the cluster page definition, so that `ExamplePage` can access any `ExampleExtension` subclass data.
|
||||
Note that the `ExamplePage` class defines the `extension` property. This allows the `ExampleExtension` object to be passed in the cluster page definition in the React style. This way, `ExamplePage` can access all `ExampleExtension` subclass data.
|
||||
|
||||
The above example shows how to create a cluster page, but not how to make that page available to the Lens user. Use `clusterPageMenus`, covered in the next section, to add cluster pages to the Lens UI.
|
||||
|
||||
### `clusterPageMenus`
|
||||
|
||||
The above example code shows how to create a cluster page but not how to make it available to the Lens user.
|
||||
Cluster pages are typically made available through a menu item in the cluster dashboard sidebar.
|
||||
Expanding on the above example a cluster page menu is added to the `ExampleExtension` definition:
|
||||
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav.
|
||||
|
||||
``` typescript
|
||||
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
@ -114,16 +129,18 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Cluster page menus are objects matching the `ClusterPageMenuRegistration` interface.
|
||||
They define the appearance of the cluster page menu item in the cluster dashboard sidebar and the behaviour when the cluster page menu item is activated (typically by a mouse click).
|
||||
The example above uses the `target` field to set the behaviour as a link to the cluster page with `id` of `"hello"`.
|
||||
This is done by setting `target`'s `pageId` field to `"hello"`.
|
||||
The cluster page menu item's appearance is defined by setting the `title` field to the text that is to be displayed in the cluster dashboard sidebar.
|
||||
The `components` field is used to set an icon that appears to the left of the `title` text in the sidebar.
|
||||
Thus when the `"Hello World"` menu item is activated the cluster dashboard will show the contents of `ExamplePage`.
|
||||
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`:
|
||||
`clusterPageMenus` is an array of objects that satisfy the `ClusterPageMenuRegistration` interface. This element defines how the cluster page menu item will appear and what it will do when you click it. The properties of the `clusterPageMenus` array objects are defined as follows:
|
||||
|
||||
``` typescript
|
||||
* `target` links to the relevant cluster page using `pageId`.
|
||||
* `pageId` takes the value of the relevant cluster page's `id` property.
|
||||
* `title` sets the name of the cluster page menu item that will appear in the left side menu.
|
||||
* `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
|
||||
|
||||
The above example creates a menu item that reads **Hello World**. When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`.
|
||||
|
||||
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
|
||||
@ -142,16 +159,15 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
||||
}
|
||||
```
|
||||
|
||||
`ExampleIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`.
|
||||
Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/).
|
||||
One can be selected by name via the `material` field.
|
||||
`ExampleIcon` also sets a tooltip, shown when the Lens user hovers over the icon with a mouse, by setting the `tooltip` field.
|
||||
Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The properties that `Component.Icon` uses are defined as follows:
|
||||
|
||||
A cluster page menu can also be used to define a foldout submenu in the cluster dashboard sidebar.
|
||||
This enables the grouping of cluster pages.
|
||||
The following example shows how to specify a submenu having two menu items:
|
||||
* `material` takes the name of the icon you want to use.
|
||||
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||
|
||||
``` typescript
|
||||
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. The following example groups two sub menu items under one parent menu item:
|
||||
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
@ -201,7 +217,8 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
```
|
||||
|
||||
The above defines two cluster pages and three cluster page menu objects.
|
||||
The cluster page definitons are straightforward.
|
||||
The cluster page definitions are straightforward.
|
||||
The three cluster page menu objects include one parent menu item and two sub menu items.
|
||||
The first cluster page menu object defines the parent of a foldout submenu.
|
||||
Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu.
|
||||
Also note that the `target` field is not specified (it is ignored if the `id` field is specified).
|
||||
@ -209,19 +226,19 @@ This cluster page menu object specifies the `title` and `components` fields, whi
|
||||
Initially the submenu is hidden.
|
||||
Activating this menu item toggles on and off the appearance of the submenu below it.
|
||||
The remaining two cluster page menu objects define the contents of the submenu.
|
||||
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case
|
||||
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:
|
||||
|
||||
### `globalPages`
|
||||
|
||||
Global pages appear independently of the cluster dashboard and they fill the Lens UI space.
|
||||
A global page is typically triggered from the cluster menu using a [global page menu](#globalpagemenus).
|
||||
They can also be triggered by a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems).
|
||||
Global pages can appear even when there is no active cluster, unlike cluster pages.
|
||||
It is conventional to use a global page to show information and provide functionality relevant across clusters, along with custom data and functionality that your extension may have.
|
||||
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.
|
||||
|
||||
Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page. You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). Unlike cluster pages, users can trigger global pages even when there is no active cluster.
|
||||
|
||||
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensRendererExtension } from '@k8slens/extensions';
|
||||
import { HelpPage } from './page';
|
||||
import React from 'react';
|
||||
@ -238,14 +255,15 @@ export default class HelpExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Global pages are objects matching the `PageRegistration` interface.
|
||||
The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above.
|
||||
The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()).
|
||||
The `components` field matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
`Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page.
|
||||
For the example above `HelpPage` can be defined in `page.tsx`:
|
||||
`globalPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `globalPages` array objects are defined as follows:
|
||||
|
||||
``` typescript
|
||||
* `id` is a string that identifies the page.
|
||||
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
* `Page` is of type `React.ComponentType<any>`. It offers flexibility in defining the appearance and behavior of your page.
|
||||
|
||||
`HelpPage` in the example above can be defined in `page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
|
||||
@ -260,22 +278,21 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `HelpPage` class defines a property named `extension`.
|
||||
This allows the `HelpExtension` object to be passed in React-style in the global page definition, so that `HelpPage` can access any `HelpExtension` subclass data.
|
||||
Note that the `HelpPage` class defines the `extension` property. This allows the `HelpExtension` object to be passed in the global page definition in the React-style. This way, `HelpPage` can access all `HelpExtension` subclass data.
|
||||
|
||||
This example code shows how to create a global page but not how to make it available to the Lens user.
|
||||
Global pages are typically made available through a number of ways.
|
||||
Menu items can be added to the Lens app menu system and set to open a global page when activated (See [`appMenus` in the Main Extension guide](../main-extension#appmenus)).
|
||||
Interactive elements can be placed on the status bar (the blue strip along the bottom of the Lens UI) and can be configured to link to a global page when activated (See [`statusBarItems`](#statusbaritems)).
|
||||
As well, global pages can be made accessible from the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon.
|
||||
Global page menu icons that are defined using [`globalPageMenus`](#globalpagemenus) appear below the Add Cluster icon.
|
||||
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:
|
||||
|
||||
* 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 to the left side menu, see [`globalPageMenus`](#globalpagemenus).
|
||||
|
||||
### `globalPageMenus`
|
||||
|
||||
Global page menus connect a global page to the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon.
|
||||
Expanding on the example from [`globalPages`](#globalPages) a global page menu is added to the `HelpExtension` definition:
|
||||
`globalPageMenus` allows you to add global page menu items to the left nav.
|
||||
|
||||
``` typescript
|
||||
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
||||
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { HelpIcon, HelpPage } from "./page"
|
||||
import React from "react"
|
||||
@ -302,16 +319,18 @@ export default class HelpExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Global page menus are objects matching the `PageMenuRegistration` interface.
|
||||
They define the appearance of the global page menu item in the cluster menu and the behaviour when the global page menu item is activated (typically by a mouse click).
|
||||
The example above uses the `target` field to set the behaviour as a link to the global page with `id` of `"help"`.
|
||||
This is done by setting `target`'s `pageId` field to `"help"`.
|
||||
The global page menu item's appearance is defined by setting the `title` field to the text that is to be displayed as a tooltip in the cluster menu.
|
||||
The `components` field is used to set an icon that appears in the cluster menu.
|
||||
Thus when the `"Help"` icon is activated the contents of `ExamplePage` will be shown.
|
||||
This example requires the definition of another React-based component, `HelpIcon`, which has been added to `page.tsx`:
|
||||
`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:
|
||||
|
||||
``` typescript
|
||||
* `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 { LensRendererExtension, Component } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
|
||||
@ -330,18 +349,25 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
||||
}
|
||||
```
|
||||
|
||||
`HelpIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`.
|
||||
Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/).
|
||||
One can be selected by name via the `material` field.
|
||||
Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The property that `Component.Icon` uses is defined as follows:
|
||||
|
||||
* `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/uninstalled by the Lens user from the [cluster settings page]().
|
||||
They can be installed and uninstalled by the Lens user from the cluster **Settings** page.
|
||||
|
||||
!!! info
|
||||
To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**.
|
||||
|
||||
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions"
|
||||
import { ExampleFeature } from "./src/example-feature"
|
||||
import React from "react"
|
||||
@ -364,34 +390,44 @@ export default class ExampleFeatureExtension extends LensRendererExtension {
|
||||
];
|
||||
}
|
||||
```
|
||||
The `title` and `components.Description` fields provide content that appears on the cluster settings page, in the **Features** section.
|
||||
The `feature` field must specify an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implement the following methods:
|
||||
|
||||
``` typescript
|
||||
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 `install()` method is typically called by Lens when a user has indicated that this feature is to be installed (i.e. clicked **Install** for the feature on the cluster settings page).
|
||||
The implementation of this method should install kubernetes resources using the `applyResources()` method, or by directly accessing the kubernetes api ([`K8sApi`](tbd)).
|
||||
The four methods listed above are defined as follows:
|
||||
|
||||
The `upgrade()` method is typically called by Lens when a user has indicated that this feature is to be upgraded (i.e. clicked **Upgrade** for the feature on the cluster settings page).
|
||||
The implementation of this method should upgrade the kubernetes resources already installed, if relevant to the feature.
|
||||
* 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 `uninstall()` method is typically called by Lens when a user has indicated that this feature is to be uninstalled (i.e. clicked **Uninstall** for the feature on the cluster settings page).
|
||||
The implementation of this method should uninstall kubernetes resources using the kubernetes api (`K8sApi`)
|
||||
* 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 `updateStatus()` method is called periodically by Lens to determine details about the feature's current status.
|
||||
The implementation of this method should provide the current status information in the `status` field of the `ClusterFeature.Feature` parent class.
|
||||
The `status.currentVersion` and `status.latestVersion` fields may be displayed by Lens in describing the feature.
|
||||
The `status.installed` field should be set to true if the feature is currently installed, otherwise false.
|
||||
Also, Lens relies on the `status.canUpgrade` field to determine if the feature can be upgraded (i.e a new version could be available) so the implementation should set the `status.canUpgrade` field according to specific rules for the feature, if relevant.
|
||||
* 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
|
||||
```typescript
|
||||
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
|
||||
import * as path from "path";
|
||||
|
||||
@ -435,9 +471,9 @@ export class ExampleFeature extends ClusterFeature.Feature {
|
||||
}
|
||||
```
|
||||
|
||||
This example implements the `install()` method by simply invoking the helper `applyResources()` method.
|
||||
This example implements the `install()` method by invoking the helper `applyResources()` method.
|
||||
`applyResources()` tries to apply all resources read from all files found in the folder path provided.
|
||||
In this case this folder path is the `../resources` subfolder relative to current source code's folder.
|
||||
In this case the folder path is the `../resources` subfolder relative to the current source code's folder.
|
||||
The file `../resources/example-pod.yml` could contain:
|
||||
|
||||
``` yaml
|
||||
@ -451,21 +487,21 @@ spec:
|
||||
image: nginx
|
||||
```
|
||||
|
||||
The `upgrade()` method in the example above is implemented by simply invoking the `install()` method.
|
||||
Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps.
|
||||
The example above implements the four methods as follows:
|
||||
|
||||
The `uninstall()` method is implemented in the example above by utilizing the [`K8sApi`](tbd) provided by Lens to simply delete the `example-pod` pod applied by the `install()` method.
|
||||
* It implements `upgrade()` by invoking the `install()` method. Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps.
|
||||
|
||||
The `updateStatus()` method is implemented above by using the [`K8sApi`](tbd) as well, this time to get information from the `example-pod` pod, in particular to determine if it is installed, what version is associated with it, and if it can be upgraded.
|
||||
How the status is updated for a specific cluster feature is up to the implementation.
|
||||
* 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.
|
||||
|
||||
### `appPreferences`
|
||||
|
||||
The Preferences page is a built-in global page.
|
||||
Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global options, for Lens and extensions alike.
|
||||
The Lens **Preferences** page is a built-in global page. You can use Lens extensions to add custom preferences to the Preferences page, providing a single location for users to configure global options.
|
||||
|
||||
The following example demonstrates adding a custom preference:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||
import { observable } from "mobx";
|
||||
@ -487,15 +523,22 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
App preferences are objects matching the `AppPreferenceRegistration` interface.
|
||||
The `title` field specifies the text to show as the heading on the Preferences page.
|
||||
The `components` field specifies two `React.Component` objects defining the interface for the preference.
|
||||
`Input` should specify an interactive input element for your preference and `Hint` should provide descriptive information for the preference, which is shown below the `Input` element.
|
||||
`ExamplePreferenceInput` expects its React props set to an `ExamplePreferenceProps` instance, which is how `ExampleRendererExtension` handles the state of the preference input.
|
||||
`ExampleRendererExtension` has the field `preference`, which is provided to `ExamplePreferenceInput` when it is created.
|
||||
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx`:
|
||||
`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface. The properties of the `appPreferences` array objects are defined as follows:
|
||||
|
||||
``` typescript
|
||||
* `title` sets the heading text displayed on the Preferences page.
|
||||
* `components` specifies two `React.Component` objects that define the interface for the preference.
|
||||
* `Input` specifies an interactive input element for the preference.
|
||||
* `Hint` provides descriptive information for the preference, shown below the `Input` element.
|
||||
|
||||
!!! note
|
||||
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
|
||||
|
||||
`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance. This is how `ExampleRendererExtension` handles the state of the preference input.
|
||||
`ExampleRendererExtension` has a `preference` field, which you will add to `ExamplePreferenceInput`.
|
||||
|
||||
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
||||
|
||||
```typescript
|
||||
import { Component } from "@k8slens/extensions";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
@ -530,30 +573,28 @@ export class ExamplePreferenceHint extends React.Component {
|
||||
}
|
||||
```
|
||||
|
||||
`ExamplePreferenceInput` implements a simple checkbox (using Lens's `Component.Checkbox`).
|
||||
It provides `label` as the text to display next to the checkbox and an `onChange` function, which reacts to the checkbox state change.
|
||||
The checkbox's `value` is initially set to `preference.enabled`.
|
||||
`ExamplePreferenceInput` is defined with React props of `ExamplePreferenceProps` type, which is an object with a single field, `enabled`.
|
||||
This is used to indicate the state of the preference, and is bound to the checkbox state in `onChange`.
|
||||
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties:
|
||||
|
||||
* `label` sets the text that displays next to the checkbox.
|
||||
* `value` is initially set to `preference.enabled`.
|
||||
* `onChange` is a function that responds when the state of the checkbox changes.
|
||||
|
||||
`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props. This is an object with the single `enabled` property. It is used to indicate the state of the preference, and it is bound to the checkbox state in `onChange`.
|
||||
|
||||
`ExamplePreferenceHint` is a simple text span.
|
||||
Note that the input and the hint could comprise of more sophisticated elements, according to the needs of the extension.
|
||||
|
||||
Note that the above example introduces decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
||||
`mobx` simplifies state management and without it this example would not visually update the checkbox properly when the user activates it.
|
||||
[Lens uses `mobx` extensively for state management](../working-with-mobx) of its own UI elements and it is recommended that extensions rely on it too.
|
||||
Alternatively, React's state management can be used, though `mobx` is typically simpler to use.
|
||||
The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. `mobx` simplifies state management. Without it, this example would not visually update the checkbox properly when the user activates it. [Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements. We recommend that extensions rely on it, as well.
|
||||
Alternatively, you can use React's state management, though `mobx` is typically simpler to use.
|
||||
|
||||
Also note that an extension's state data can be managed using an `ExtensionStore` object, which conveniently handles persistence and synchronization.
|
||||
The example above defined a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state primarily to simplify the code for this guide, but it is recommended to manage your extension's state data using [`ExtensionStore`](../stores#extensionstore)
|
||||
Note that you can manage an extension's state data using an `ExtensionStore` object, which conveniently handles persistence and synchronization. To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state. However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore).
|
||||
|
||||
### `statusBarItems`
|
||||
|
||||
The Status bar is the blue strip along the bottom of the Lens UI.
|
||||
Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page, or even an external page.
|
||||
The status bar is the blue strip along the bottom of the Lens UI. `statusBarItems` are `React.ReactNode` types. They can be used to display status information, or act as links to global pages as well as external pages.
|
||||
|
||||
The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||
The following example adds a `statusBarItems` definition and a `globalPages` definition to a `LensRendererExtension` subclass. It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { LensRendererExtension } from '@k8slens/extensions';
|
||||
import { HelpIcon, HelpPage } from "./page"
|
||||
import React from 'react';
|
||||
@ -570,39 +611,41 @@ export default class HelpExtension extends LensRendererExtension {
|
||||
|
||||
statusBarItems = [
|
||||
{
|
||||
item: (
|
||||
<div
|
||||
className="flex align-center gaps"
|
||||
onClick={() => this.navigate("help")}
|
||||
>
|
||||
<HelpIcon />
|
||||
My Status Bar Item
|
||||
</div>
|
||||
),
|
||||
components: {
|
||||
Item: (
|
||||
<div
|
||||
className="flex align-center gaps"
|
||||
onClick={() => this.navigate("help")}
|
||||
>
|
||||
<HelpIcon />
|
||||
My Status Bar Item
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The `item` field of a status bar item specifies the `React.Component` to be shown on the status bar.
|
||||
By default items are added starting from the right side of the status bar.
|
||||
Typically, `item` would specify an icon and/or a short string of text, considering the limited space on the status bar.
|
||||
In the example above the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus) is reused.
|
||||
Also, the `item` provides a link to the global page by setting the `onClick` property to a function that calls the `LensRendererExtension` `navigate()` method.
|
||||
`navigate()` takes as a parameter the id of the global page, which is shown when `navigate()` is called.
|
||||
The properties of the `statusBarItems` array objects are defined as follows:
|
||||
|
||||
* `Item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
||||
* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages.
|
||||
|
||||
### `kubeObjectMenuItems`
|
||||
|
||||
An extension can add custom menu items (including actions) for specific Kubernetes resource kinds/apiVersions.
|
||||
These menu items appear under the `...` for each listed resource in the cluster dashboard, and on the title bar of the details page for a specific resource:
|
||||
An extension can add custom menu items (`kubeObjectMenuItems`) for specific Kubernetes resource kinds and apiVersions.
|
||||
`kubeObjectMenuItems` appear under the vertical ellipsis for each listed resource in the cluster dashboard:
|
||||
|
||||

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

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

|
||||
|
||||
The following example shows how to add a tabulated list of pods to the Namespace resource details page:
|
||||
The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { NamespaceDetailsItem } from "./src/namespace-details-item"
|
||||
@ -697,12 +737,13 @@ export default class ExampleExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Kube object detail items are objects matching the `KubeObjectDetailRegistration` interface.
|
||||
The `kind` field specifies the kubernetes resource type to apply this detail item to, and the `apiVersion` field specifies the kubernetes api to use in relation to this resource type.
|
||||
This example adds a detail item for namespaces in the cluster dashboard.
|
||||
The `components` field defines the detail item's appearance and behaviour.
|
||||
The `Details` field provides a function that returns a `React.Component` given a set of detail item properties.
|
||||
In this example a `NamespaceDetailsItem` object is returned.
|
||||
`kubeObjectDetailItems` is an array of objects matching the `KubeObjectDetailRegistration` interface. This example above adds a detail item for namespaces in the cluster dashboard. The properties of the `kubeObjectDetailItems` array objects are defined as follows:
|
||||
|
||||
* `kind` specifies the Kubernetes resource type the detail item will apply to.
|
||||
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||
* `components` defines the detail item's appearance and behavior.
|
||||
* `Details` provides a function that returns a `React.Component` given a set of detail item properties. In this example a `NamespaceDetailsItem` object is returned.
|
||||
|
||||
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
||||
|
||||
``` typescript
|
||||
@ -732,21 +773,22 @@ export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDe
|
||||
}
|
||||
```
|
||||
|
||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>` it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
||||
This object can be queried for many details about the current namespace.
|
||||
In this example the namespace's name is obtained in `componentDidMount()` using the `K8sApi.Namespace` `getName()` method.
|
||||
The namespace's name is needed to limit the list of pods to only those in this namespace.
|
||||
To get the list of pods this example uses the kubernetes pods api, specifically the `K8sApi.podsApi.list()` method.
|
||||
The `K8sApi.podsApi` is automatically configured for the currently active cluster.
|
||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
||||
You can query this object for many details about the current namespace.
|
||||
In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method.
|
||||
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
||||
To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method.
|
||||
The `K8sApi.podsApi` is automatically configured for the active cluster.
|
||||
|
||||
Note that `K8sApi.podsApi.list()` is an asynchronous method, and ideally getting the pods list should be done before rendering the `NamespaceDetailsItem`.
|
||||
Note that `K8sApi.podsApi.list()` is an asynchronous method.
|
||||
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
||||
It is a common technique in React development to await async calls in `componentDidMount()`.
|
||||
However, `componentDidMount()` is called right after the first call to `render()`.
|
||||
In order to effect a subsequent `render()` call React must be made aware of a state change.
|
||||
Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list is updated.
|
||||
In order to effect a subsequent `render()` call, React must be made aware of a state change.
|
||||
Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates.
|
||||
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
||||
|
||||
Finally, the `NamespaceDetailsItem` is rendered using the `render()` method.
|
||||
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
||||
Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one.
|
||||
Multiple details in a drawer can be placed in `<Component.DrawerItem>` elements for further separation, if desired.
|
||||
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
||||
@ -800,6 +842,9 @@ export class PodsDetailsList extends React.Component<Props> {
|
||||
|
||||

|
||||
|
||||
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
||||
Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods. Construct the table using the `Component.Table` and related elements.
|
||||
|
||||
|
||||
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
||||
The table is constructed using the `Component.Table` and related elements.
|
||||
See [`Component` documentation](url?) for further details.
|
||||
See [`Component` documentation](https://docs.k8slens.dev/master/extensions/api/modules/_renderer_api_components_/) for further details.
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
# Stores
|
||||
|
||||
Stores are components that persist and synchronize state data. Lens utilizes a number of stores for maintaining a variety of state information.
|
||||
A few of these are exposed by the extensions api for use by the extension developer.
|
||||
Stores are components that persist and synchronize state data. Lens uses a number of stores to maintain various kinds of state information, including:
|
||||
|
||||
- The `ClusterStore` manages cluster state data such as cluster details, and which cluster is active.
|
||||
- The `WorkspaceStore` similarly manages workspace state data, such as workspace name, and which clusters belong to a given workspace.
|
||||
- The `ExtensionStore` is a store for managing custom extension state data.
|
||||
* The `ClusterStore` manages cluster state data (such as cluster details), and it tracks which cluster is active.
|
||||
* The `WorkspaceStore` manages workspace state data (such as the workspace name), and and it tracks which clusters belong to a given workspace.
|
||||
* The `ExtensionStore` manages custom extension state data.
|
||||
|
||||
This guide focuses on the `ExtensionStore`.
|
||||
|
||||
## ExtensionStore
|
||||
|
||||
Extension developers can create their own store for managing state data by extending the `ExtensionStore` class.
|
||||
This guide shows how to create a store for the [`appPreferences` guide example](../renderer-extension#apppreferences), which demonstrates how to add a custom preference to the Preferences page.
|
||||
The preference is a simple boolean that indicates whether something is enabled or not.
|
||||
The problem with that example is that the enabled state is not stored anywhere, and reverts to the default the next time Lens is started.
|
||||
Extension developers can create their own store for managing state data by extending the `ExtensionStore` class. This guide shows how to create a store for the [`appPreferences`](../renderer-extension#apppreferences) guide example, which demonstrates how to add a custom preference to the **Preferences** page. The preference is a simple boolean that indicates whether or not something is enabled. However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted.
|
||||
|
||||
The following example code creates a store for the `appPreferences` guide example:
|
||||
|
||||
@ -53,25 +51,14 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
|
||||
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
|
||||
```
|
||||
|
||||
First the extension's data model is defined using a simple type, `ExamplePreferencesModel`, which has a single field, `enabled`, representing the preference's state.
|
||||
`ExamplePreferencesStore` extends `Store.ExtensionStore`, based on the `ExamplePreferencesModel`.
|
||||
The field `enabled` is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference.
|
||||
Note the use of the `observer` decorator on the `enabled` field.
|
||||
As for the [`appPreferences` guide example](../renderer-extension#apppreferences), [`mobx`](https://mobx.js.org/README.html) is used for the UI state management, ensuring the checkbox updates when activated by the user.
|
||||
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. This has a single field, `enabled`, which represents the preference's state. `ExamplePreferencesStore` extends `Store.ExtensionStore`, which is based on the `ExamplePreferencesModel`. The `enabled` field is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference. Note the use of the `observable` decorator on the `enabled` field. The [`appPreferences`](../renderer-extension#apppreferences) guide example uses [MobX](https://mobx.js.org/README.html) for the UI state management, ensuring the checkbox updates when it's activated by the user.
|
||||
|
||||
Then the constructor and two abstract methods are implemented.
|
||||
In the constructor, the name of the store (`"example-preferences-store"`), and the default (initial) value for the preference state (`enabled: false`) are specified.
|
||||
The `fromStore()` method is called by Lens internals when the store is loaded, and gives the extension the opportunity to retrieve the stored state data values based on the defined data model.
|
||||
Here, the `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked.
|
||||
The `toJSON()` method is complementary to `fromStore()`, and is called when the store is being saved.
|
||||
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
|
||||
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
|
||||
Next, our example implements the constructor and two abstract methods. The constructor specifies the name of the store (`"example-preferences-store"`) and the default (initial) value for the preference state (`enabled: false`). Lens internals call the `fromStore()` method when the store loads. It gives the extension the opportunity to retrieve the stored state data values based on the defined data model. The `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked. The `toJSON()` method is complementary to `fromStore()`. It is called when the store is being saved.
|
||||
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
|
||||
|
||||
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension.
|
||||
Note that `examplePreferencesStore` is a singleton, calling this function again will not create a new store.
|
||||
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension. Note that `examplePreferencesStore` is a singleton. Calling this function again will not create a new store.
|
||||
|
||||
The following example code, modified from the [`appPreferences` guide example](../renderer-extension#apppreferences) demonstrates how to use the extension store.
|
||||
`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`:
|
||||
The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store. `examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`:
|
||||
|
||||
``` typescript
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
@ -84,8 +71,8 @@ export default class ExampleMainExtension extends LensMainExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Here, `examplePreferencesStore` is loaded with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
||||
Similarly, `examplePreferencesStore` must be loaded in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`:
|
||||
Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
||||
Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`:
|
||||
|
||||
``` typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
@ -111,8 +98,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
|
||||
Also, there is no longer the need for the `preference` field in the `ExampleRendererExtension` class, as the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
|
||||
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
|
||||
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
||||
|
||||
``` typescript
|
||||
@ -151,5 +137,5 @@ export class ExamplePreferenceHint extends React.Component {
|
||||
```
|
||||
|
||||
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
|
||||
Everything else works as before except now the `enabled` state persists across Lens restarts because it is managed by the
|
||||
Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
|
||||
`examplePreferencesStore`.
|
||||
@ -1,23 +1,26 @@
|
||||
# Working with mobx
|
||||
# Working with MobX
|
||||
|
||||
## Introduction
|
||||
|
||||
Lens uses `mobx` as its state manager on top of React's state management system.
|
||||
This helps with having a more declarative style of managing state, as opposed to `React`'s native `setState` mechanism.
|
||||
You should already have a basic understanding of how `React` handles state ([read here](https://reactjs.org/docs/faq-state.html) for more information).
|
||||
However, if you do not, here is a quick overview.
|
||||
Lens uses MobX on top of React's state management system.
|
||||
The result is a more declarative state management style, rather than React's native `setState` mechanism.
|
||||
|
||||
- A `React.Component` is generic over both `Props` and `State` (with default empty object types).
|
||||
- `Props` should be considered read-only from the point of view of the component and is the mechanism for passing in "arguments" to a component.
|
||||
- `State` is a component's internal state and can be read by accessing the parent field `state`.
|
||||
- `State` **must** be updated using the `setState` parent method which merges the new data with the old state.
|
||||
- `React` does do some optimizations around re-rendering components after quick successions of `setState` calls.
|
||||
You can review how React handles state management [here](https://reactjs.org/docs/faq-state.html).
|
||||
|
||||
## How mobx works:
|
||||
The following is a quick overview:
|
||||
|
||||
`mobx` is a package that provides an abstraction over `React`'s state management. The three main concepts are:
|
||||
- `observable`: data stored in the component's `state`
|
||||
- `action`: a function that modifies any `observable` data
|
||||
- `computed`: data that is derived from `observable` data but is not actually stored. Think of this as computing `isEmpty` vs an `observable` field called `count`.
|
||||
* `React.Component` is generic with respect to both `props` and `state` (which default to the empty object type).
|
||||
* `props` should be considered read-only from the point of view of the component, and it is the mechanism for passing in arguments to a component.
|
||||
* `state` is a component's internal state, and can be read by accessing the super-class field `state`.
|
||||
* `state` **must** be updated using the `setState` parent method which merges the new data with the old state.
|
||||
* React does some optimizations around re-rendering components after quick successions of `setState` calls.
|
||||
|
||||
Further reading is available from `mobx`'s [website](https://mobx.js.org/the-gist-of-mobx.html).
|
||||
## How MobX Works:
|
||||
|
||||
MobX is a package that provides an abstraction over React's state management system. The three main concepts are:
|
||||
|
||||
* `observable` is a marker for data stored in the component's `state`.
|
||||
* `action` is a function that modifies any `observable` data.
|
||||
* `computed` is a marker for data that is derived from `observable` data, but that is not actually stored. Think of this as computing `isEmpty` rather than an observable field called `count`.
|
||||
|
||||
Further reading is available on the [MobX website](https://mobx.js.org/the-gist-of-mobx.html).
|
||||
|
||||
@ -10,7 +10,7 @@ For example, I have a component `GlobalPageMenuIcon` and want to test if `props.
|
||||
|
||||
My component `GlobalPageMenuIcon`
|
||||
|
||||
```tsx
|
||||
```typescript
|
||||
import React from "react"
|
||||
import { Component: { Icon } } from "@k8slens/extensions";
|
||||
|
||||
@ -61,7 +61,7 @@ In the Renderer process, `console.log()` is printed in the Console in Developer
|
||||
|
||||
### Main Process Logs
|
||||
|
||||
Viewing the logs from the Main process is a little trickier, since they cannot be printed using Developer Tools.
|
||||
Viewing the logs from the Main process is a little trickier, since they cannot be printed using Developer Tools.
|
||||
|
||||
#### macOS
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ Lens is lightweight and simple to install. You'll be up and running in just a fe
|
||||
|
||||
## System Requirements
|
||||
|
||||
Review the [System Requirements](/supporting/requirements/) to check if your computer configuration is supported.
|
||||
Review the [System Requirements](../supporting/requirements.md) to check if your computer configuration is supported.
|
||||
|
||||
|
||||
## macOS
|
||||
@ -28,6 +28,28 @@ Review the [System Requirements](/supporting/requirements/) to check if your com
|
||||
|
||||
See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options.
|
||||
|
||||
After installing Lens manually (not using a package manager file such as `.deb` or `.rpm`) the following will need to be done to allow protocol handling.
|
||||
This assumes that your linux distribution uses `xdg-open` and the `xdg-*` suite of programs for determining which application can handle custom URIs.
|
||||
|
||||
1. Create a file called `lens.desktop` in either `~/.local/share/applications/` or `/usr/share/applications` (if you have permissions and are installing Lens for all users).
|
||||
1. That file should have the following contents, with `<path/to/executable>` being the absolute path to where you have installed the unpacked `Lens` executable:
|
||||
```
|
||||
[Desktop Entry]
|
||||
Name=Lens
|
||||
Exec=<path/to/executable> %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=lens
|
||||
StartupWMClass=Lens
|
||||
Comment=Lens - The Kubernetes IDE
|
||||
MimeType=x-scheme-handler/lens;
|
||||
Categories=Network;
|
||||
```
|
||||
1. Then run the following command:
|
||||
```
|
||||
xdg-settings set default-url-scheme-handler lens lens.desktop
|
||||
```
|
||||
1. If that succeeds (exits with code `0`) then your Lens install should be set up to handle `lens://` URIs.
|
||||
|
||||
### Snap
|
||||
|
||||
@ -50,6 +72,6 @@ To stay current with the Lens features, you can review the [release notes](https
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Launch Lens](lens://app/landing)
|
||||
- [Add clusters](../clusters/adding-clusters.md)
|
||||
- [Watch introductory videos](./introductory-videos.md)
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
# Preferences
|
||||
|
||||
|
||||
## Color Themes
|
||||
|
||||
The Color Themes option in Lens preferences lets you set the colors in the Lens user interface to suit your liking.
|
||||
|
||||
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac).
|
||||
2. Select your preferred theme from the **Color Theme** dropdown.
|
||||
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=appearance).
|
||||
2. Select your preferred theme from the **Color Theme** dropdown.
|
||||

|
||||
|
||||
|
||||
@ -19,10 +18,9 @@ Lens collects telemetry data, which is used to help us understand how to improve
|
||||
|
||||
If you don't wish to send usage data to Mirantis, you can disable the "Telemetry & Usage Tracking" in the Lens preferences.
|
||||
|
||||
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac).
|
||||
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=telemetry-tracking).
|
||||
2. Scroll down to **Telemetry & Usage Tracking**
|
||||
3. Uncheck **Allow Telemetry & Usage Tracking**.
|
||||
3. Uncheck **Allow Telemetry & Usage Tracking**.
|
||||
|
||||
This will silence all telemetry events from Lens going forward. Telemetry information may have been collected and sent up until the point when you disable this setting.
|
||||

|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ Lens has integration to Helm making it easy to install and manage Helm charts an
|
||||
|
||||

|
||||
|
||||
## Managing Helm Reporistories
|
||||
## Managing Helm Repositories
|
||||
|
||||
Used Helm repositories are possible to configure in the [Preferences](/getting-started/preferences). Lens app will fetch available Helm repositories from the [Artifact HUB](https://artifacthub.io/) and automatically add `bitnami` repository by default if no other repositories are already configured. If any other repositories are needed to add, those can be added manually via command line. **Note!** Configured Helm repositories are added globally to user's computer, so other processes can see those as well.
|
||||
|
||||
@ -18,4 +18,4 @@ Lens will list all charts from configured Helm repositries on Apps section. To i
|
||||
To update a Helm release, you can open the release details and modify the release values and click "Save" button. To upgrade or downgrade the release, click "Upgrade" button in the release details. In the release editor you can select a new chart version and edit the release values if needed and then click "Upgrade" or "Upgrade and Close" button.
|
||||
|
||||
## Deleting a Helm Release
|
||||
To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually!
|
||||
To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually!
|
||||
|
||||
47
extensions/example-extension/package-lock.json
generated
47
extensions/example-extension/package-lock.json
generated
@ -2159,24 +2159,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2796,7 +2796,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3226,6 +3227,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4367,6 +4369,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4381,6 +4384,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@ -4390,6 +4394,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@ -4399,6 +4404,7 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -4407,7 +4413,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5398,7 +5405,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -6275,7 +6283,8 @@
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "7.0.0",
|
||||
@ -6870,9 +6879,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@ -986,24 +986,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -3497,9 +3497,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@ -56,4 +56,4 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
|
||||
text: `${event.message}`,
|
||||
timestamp: event.metadata.creationTimestamp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
47
extensions/license-menu-item/package-lock.json
generated
47
extensions/license-menu-item/package-lock.json
generated
@ -2231,24 +2231,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2868,7 +2868,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3298,6 +3299,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4460,6 +4462,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4474,6 +4477,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@ -4483,6 +4487,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@ -4492,6 +4497,7 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -4500,7 +4506,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5516,7 +5523,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -6406,7 +6414,8 @@
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "7.0.0",
|
||||
@ -7001,9 +7010,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
42
extensions/metrics-cluster-feature/package-lock.json
generated
42
extensions/metrics-cluster-feature/package-lock.json
generated
@ -2173,24 +2173,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2816,7 +2816,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3246,6 +3247,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4394,6 +4396,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4408,6 +4411,7 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -5434,7 +5438,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -6311,7 +6316,8 @@
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "7.0.0",
|
||||
@ -6906,9 +6912,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
"dev": "npm run build --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
|
||||
@ -1,21 +1,55 @@
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { LensRendererExtension, Store, Interface, Component } from "@k8slens/extensions";
|
||||
import { MetricsFeature } from "./src/metrics-feature";
|
||||
import React from "react";
|
||||
|
||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||
clusterFeatures = [
|
||||
{
|
||||
title: "Metrics Stack",
|
||||
components: {
|
||||
Description: () => (
|
||||
<span>
|
||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
||||
Install this only if you don't have existing Prometheus stack installed.
|
||||
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/metrics-cluster-feature/resources" rel="noreferrer" target="_blank">here</a>.
|
||||
</span>
|
||||
)
|
||||
},
|
||||
feature: new MetricsFeature()
|
||||
onActivate() {
|
||||
const category = Store.catalogCategories.getForGroupKind<Store.KubernetesClusterCategory>("entity.k8slens.dev", "KubernetesCluster");
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
];
|
||||
|
||||
category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this));
|
||||
}
|
||||
|
||||
async clusterContextMenuOpen(cluster: Store.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) {
|
||||
if (!cluster.status.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metricsFeature = new MetricsFeature();
|
||||
|
||||
await metricsFeature.updateStatus(cluster);
|
||||
|
||||
if (metricsFeature.status.installed) {
|
||||
if (metricsFeature.status.canUpgrade) {
|
||||
ctx.menuItems.unshift({
|
||||
icon: "refresh",
|
||||
title: "Upgrade Lens Metrics stack",
|
||||
onClick: async () => {
|
||||
metricsFeature.upgrade(cluster);
|
||||
}
|
||||
});
|
||||
}
|
||||
ctx.menuItems.unshift({
|
||||
icon: "toggle_off",
|
||||
title: "Uninstall Lens Metrics stack",
|
||||
onClick: async () => {
|
||||
await metricsFeature.uninstall(cluster);
|
||||
|
||||
Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ctx.menuItems.unshift({
|
||||
icon: "toggle_on",
|
||||
title: "Install Lens Metrics stack",
|
||||
onClick: async () => {
|
||||
metricsFeature.install(cluster);
|
||||
|
||||
Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
storageClass: null,
|
||||
};
|
||||
|
||||
async install(cluster: Store.Cluster): Promise<void> {
|
||||
async install(cluster: Store.KubernetesCluster): Promise<void> {
|
||||
// Check if there are storageclasses
|
||||
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
||||
const scs = await storageClassApi.list();
|
||||
@ -62,11 +62,11 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||
}
|
||||
|
||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
||||
async upgrade(cluster: Store.KubernetesCluster): Promise<void> {
|
||||
return this.install(cluster);
|
||||
}
|
||||
|
||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
||||
async updateStatus(cluster: Store.KubernetesCluster): Promise<ClusterFeature.FeatureStatus> {
|
||||
try {
|
||||
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
||||
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||
@ -87,12 +87,13 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
async uninstall(cluster: Store.Cluster): Promise<void> {
|
||||
async uninstall(cluster: Store.KubernetesCluster): Promise<void> {
|
||||
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
|
||||
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
|
||||
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
|
||||
|
||||
await namespaceApi.delete({name: "lens-metrics"});
|
||||
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
|
||||
await clusterRoleApi.delete({name: "lens-prometheus"}); }
|
||||
await clusterRoleApi.delete({name: "lens-prometheus"});
|
||||
}
|
||||
}
|
||||
|
||||
47
extensions/node-menu/package-lock.json
generated
47
extensions/node-menu/package-lock.json
generated
@ -2159,24 +2159,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2796,7 +2796,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3226,6 +3227,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4382,6 +4384,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4396,6 +4399,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@ -4405,6 +4409,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@ -4414,6 +4419,7 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -4422,7 +4428,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5438,7 +5445,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -6315,7 +6323,8 @@
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "7.0.0",
|
||||
@ -6910,9 +6919,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
686
extensions/pod-menu/package-lock.json
generated
686
extensions/pod-menu/package-lock.json
generated
@ -626,7 +626,644 @@
|
||||
},
|
||||
"@k8slens/extensions": {
|
||||
"version": "file:../../src/extensions/npm/extensions",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@material-ui/core": "*",
|
||||
"@types/node": "*",
|
||||
"@types/react-select": "*",
|
||||
"conf": "^7.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"dev": true
|
||||
},
|
||||
"@material-ui/core": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz",
|
||||
"integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/styles": "^4.11.2",
|
||||
"@material-ui/system": "^4.11.2",
|
||||
"@material-ui/types": "^5.1.0",
|
||||
"@material-ui/utils": "^4.11.2",
|
||||
"@types/react-transition-group": "^4.2.0",
|
||||
"clsx": "^1.0.4",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"popper.js": "1.16.1-lts",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0",
|
||||
"react-transition-group": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"@material-ui/styles": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz",
|
||||
"integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@emotion/hash": "^0.8.0",
|
||||
"@material-ui/types": "^5.1.0",
|
||||
"@material-ui/utils": "^4.11.2",
|
||||
"clsx": "^1.0.4",
|
||||
"csstype": "^2.5.2",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"jss": "^10.0.3",
|
||||
"jss-plugin-camel-case": "^10.0.3",
|
||||
"jss-plugin-default-unit": "^10.0.3",
|
||||
"jss-plugin-global": "^10.0.3",
|
||||
"jss-plugin-nested": "^10.0.3",
|
||||
"jss-plugin-props-sort": "^10.0.3",
|
||||
"jss-plugin-rule-value-function": "^10.0.3",
|
||||
"jss-plugin-vendor-prefixer": "^10.0.3",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@material-ui/system": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz",
|
||||
"integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/utils": "^4.11.2",
|
||||
"csstype": "^2.5.2",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@material-ui/types": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
|
||||
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
|
||||
"dev": true
|
||||
},
|
||||
"@material-ui/utils": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
|
||||
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
|
||||
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz",
|
||||
"integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz",
|
||||
"integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-select": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz",
|
||||
"integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@types/react-transition-group": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz",
|
||||
"integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"atomically": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
|
||||
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
|
||||
"dev": true
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
|
||||
"dev": true
|
||||
},
|
||||
"conf": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz",
|
||||
"integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.2",
|
||||
"atomically": "^1.3.1",
|
||||
"debounce-fn": "^4.0.0",
|
||||
"dot-prop": "^5.2.0",
|
||||
"env-paths": "^2.2.0",
|
||||
"json-schema-typed": "^7.0.3",
|
||||
"make-dir": "^3.1.0",
|
||||
"onetime": "^5.1.0",
|
||||
"pkg-up": "^3.1.0",
|
||||
"semver": "^7.3.2"
|
||||
}
|
||||
},
|
||||
"css-vendor": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
|
||||
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"is-in-browser": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz",
|
||||
"integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==",
|
||||
"dev": true
|
||||
},
|
||||
"debounce-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mimic-fn": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
|
||||
"integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
||||
"integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"env-paths": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
|
||||
"integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
},
|
||||
"find-up": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"hyphenate-style-name": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
|
||||
"dev": true
|
||||
},
|
||||
"indefinite-observable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz",
|
||||
"integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"symbol-observable": "1.2.0"
|
||||
}
|
||||
},
|
||||
"is-in-browser": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
|
||||
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=",
|
||||
"dev": true
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"json-schema-typed": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
|
||||
"integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
|
||||
"dev": true
|
||||
},
|
||||
"jss": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz",
|
||||
"integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"csstype": "^3.0.2",
|
||||
"indefinite-observable": "^2.0.1",
|
||||
"is-in-browser": "^1.1.3",
|
||||
"tiny-warning": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jss-plugin-camel-case": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz",
|
||||
"integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"hyphenate-style-name": "^1.0.3",
|
||||
"jss": "10.5.0"
|
||||
}
|
||||
},
|
||||
"jss-plugin-default-unit": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz",
|
||||
"integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "10.5.0"
|
||||
}
|
||||
},
|
||||
"jss-plugin-global": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz",
|
||||
"integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "10.5.0"
|
||||
}
|
||||
},
|
||||
"jss-plugin-nested": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz",
|
||||
"integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "10.5.0",
|
||||
"tiny-warning": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"jss-plugin-props-sort": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz",
|
||||
"integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "10.5.0"
|
||||
}
|
||||
},
|
||||
"jss-plugin-rule-value-function": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz",
|
||||
"integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "10.5.0",
|
||||
"tiny-warning": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"jss-plugin-vendor-prefixer": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz",
|
||||
"integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"css-vendor": "^2.0.8",
|
||||
"jss": "10.5.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^3.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"dev": true
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
|
||||
"dev": true
|
||||
},
|
||||
"pkg-up": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
|
||||
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.16.1-lts",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
|
||||
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==",
|
||||
"dev": true
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
|
||||
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==",
|
||||
"dev": true
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"dev": true
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.8.1",
|
||||
@ -2159,24 +2796,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2796,7 +3433,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3226,6 +3864,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4382,6 +5021,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4396,6 +5036,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@ -4405,6 +5046,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@ -4414,6 +5056,7 @@
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -4422,7 +5065,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5438,7 +6082,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -6315,7 +6960,8 @@
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "7.0.0",
|
||||
@ -6910,9 +7556,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@ -9,13 +9,9 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
||||
Navigation.hideDetails();
|
||||
const pod = this.props.object;
|
||||
|
||||
Component.createPodLogsTab({
|
||||
pod,
|
||||
containers: pod.getContainers(),
|
||||
initContainers: pod.getInitContainers(),
|
||||
Component.logTabStore.createPodTab({
|
||||
selectedPod: pod,
|
||||
selectedContainer: container,
|
||||
showTimestamps: false,
|
||||
previous: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
9
extensions/survey/main.ts
Normal file
9
extensions/survey/main.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||
|
||||
export default class SurveyMainExtension extends LensMainExtension {
|
||||
|
||||
async onActivate() {
|
||||
await surveyPreferencesStore.loadExtension(this);
|
||||
}
|
||||
}
|
||||
7928
extensions/survey/package-lock.json
generated
Normal file
7928
extensions/survey/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
extensions/survey/package.json
Normal file
28
extensions/survey/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "lens-survey",
|
||||
"version": "0.1.0",
|
||||
"description": "Lens survey",
|
||||
"main": "dist/main.js",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
"metadata": {},
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -p",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"got": "^11.8.1",
|
||||
"jest": "^26.6.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"react": "^16.13.1",
|
||||
"refiner-js": "^1.0.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
25
extensions/survey/renderer.tsx
Normal file
25
extensions/survey/renderer.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { survey } from "./src/survey";
|
||||
import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference";
|
||||
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||
import React from "react";
|
||||
|
||||
export default class SurveyRendererExtension extends LensRendererExtension {
|
||||
appPreferences = [
|
||||
{
|
||||
title: "In-App Surveys",
|
||||
showInPreferencesTab: "telemetry",
|
||||
components: {
|
||||
Hint: () => <SurveyPreferenceHint/>,
|
||||
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>
|
||||
}
|
||||
}
|
||||
];
|
||||
async onActivate() {
|
||||
// Activate extension only on main renderer
|
||||
if (window.location.hostname === "localhost") {
|
||||
await surveyPreferencesStore.loadExtension(this);
|
||||
survey.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
3
extensions/survey/src/refiner-js.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module "refiner-js" {
|
||||
export default function Refiner(key: string, value: string|object|number|Boolean|Array): void;
|
||||
}
|
||||
27
extensions/survey/src/survey-preference.tsx
Normal file
27
extensions/survey/src/survey-preference.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Component } from "@k8slens/extensions";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SurveyPreferencesStore } from "./survey-preferences-store";
|
||||
|
||||
@observer
|
||||
export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> {
|
||||
render() {
|
||||
const { survey } = this.props;
|
||||
|
||||
return (
|
||||
<Component.Checkbox
|
||||
label="Allow in-app surveys"
|
||||
value={survey.enabled}
|
||||
onChange={v => survey.enabled = v }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SurveyPreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>This will allow you to participate in surveys to improve the Lens experience.</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
36
extensions/survey/src/survey-preferences-store.ts
Normal file
36
extensions/survey/src/survey-preferences-store.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Store } from "@k8slens/extensions";
|
||||
import { observable, toJS, when } from "mobx";
|
||||
|
||||
export type SurveyPreferencesModel = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export class SurveyPreferencesStore extends Store.ExtensionStore<SurveyPreferencesModel> {
|
||||
|
||||
@observable enabled = true;
|
||||
|
||||
whenEnabled = when(() => this.enabled);
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "preferences-store",
|
||||
defaults: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected fromStore({ enabled }: SurveyPreferencesModel): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
toJSON(): SurveyPreferencesModel {
|
||||
return toJS({
|
||||
enabled: this.enabled
|
||||
}, {
|
||||
recurseEverything: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const surveyPreferencesStore = SurveyPreferencesStore.getInstance<SurveyPreferencesStore>();
|
||||
46
extensions/survey/src/survey.ts
Normal file
46
extensions/survey/src/survey.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Util } from "@k8slens/extensions";
|
||||
import { machineId } from "node-machine-id";
|
||||
import Refiner from "refiner-js";
|
||||
import got from "got";
|
||||
import { surveyPreferencesStore } from "./survey-preferences-store";
|
||||
|
||||
type SurveyIdResponse = {
|
||||
surveyId: string;
|
||||
};
|
||||
export class Survey extends Util.Singleton {
|
||||
static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43";
|
||||
protected anonymousId: string;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async start() {
|
||||
await surveyPreferencesStore.whenEnabled;
|
||||
|
||||
const surveyId = await this.fetchSurveyId();
|
||||
|
||||
if (surveyId) {
|
||||
Refiner("setProject", Survey.PROJECT_ID);
|
||||
Refiner("identifyUser", {
|
||||
id: surveyId,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSurveyId() {
|
||||
try {
|
||||
const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev";
|
||||
const anonymousId = await machineId();
|
||||
const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"});
|
||||
|
||||
return (body as SurveyIdResponse).surveyId;
|
||||
} catch(error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const survey = Survey.getInstance<Survey>();
|
||||
29
extensions/survey/tsconfig.json
Normal file
29
extensions/survey/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"module": "CommonJS",
|
||||
"target": "ES2017",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"../../types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"renderer.ts",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
67
extensions/survey/webpack.config.js
Normal file
67
extensions/survey/webpack.config.js
Normal file
@ -0,0 +1,67 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: "./main.ts",
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: "./renderer.tsx",
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "renderer.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
];
|
||||
47
extensions/telemetry/package-lock.json
generated
47
extensions/telemetry/package-lock.json
generated
@ -2255,24 +2255,24 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -2901,7 +2901,8 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
@ -3337,6 +3338,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@ -4533,6 +4535,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
@ -4547,6 +4550,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
@ -4556,6 +4560,7 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
@ -4564,13 +4569,15 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -4579,7 +4586,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5595,7 +5603,8 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@ -7089,9 +7098,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"@types/analytics-node": "^3.1.3",
|
||||
|
||||
@ -8,6 +8,8 @@ export default class TelemetryRendererExtension extends LensRendererExtension {
|
||||
appPreferences = [
|
||||
{
|
||||
title: "Telemetry & Usage Tracking",
|
||||
showInPreferencesTab: "telemetry",
|
||||
id: "telemetry-tracking",
|
||||
components: {
|
||||
Hint: () => <TelemetryPreferenceHint/>,
|
||||
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore}/>
|
||||
|
||||
@ -102,13 +102,12 @@ export class Tracker extends Util.Singleton {
|
||||
}
|
||||
|
||||
protected reportData() {
|
||||
const clustersList = Store.clusterStore.enabledClustersList;
|
||||
const clustersList = Store.catalogEntities.getItemsForApiKind<Store.KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
|
||||
|
||||
this.event("generic-data", "report", {
|
||||
appVersion: App.version,
|
||||
os: this.os,
|
||||
clustersCount: clustersList.length,
|
||||
workspacesCount: Store.workspaceStore.enabledWorkspacesList.length,
|
||||
extensions: App.getEnabledExtensions()
|
||||
});
|
||||
|
||||
@ -118,10 +117,10 @@ export class Tracker extends Util.Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
protected reportClusterData(cluster: Store.ClusterModel) {
|
||||
protected reportClusterData(cluster: Store.KubernetesCluster) {
|
||||
this.event("cluster-data", "report", {
|
||||
id: cluster.metadata.id,
|
||||
managed: !!cluster.ownerRef,
|
||||
managed: cluster.metadata.source !== "local",
|
||||
kubernetesVersion: cluster.metadata.version,
|
||||
distribution: cluster.metadata.distribution,
|
||||
nodesCount: cluster.metadata.nodes,
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
/*
|
||||
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
|
||||
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
|
||||
@ -5,83 +9,34 @@
|
||||
cluster and vice versa.
|
||||
*/
|
||||
import { Application } from "spectron";
|
||||
import * as util from "../helpers/utils";
|
||||
import { spawnSync } from "child_process";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { listHelmRepositories } from "../helpers/utils";
|
||||
import { fail } from "assert";
|
||||
|
||||
jest.setTimeout(60000);
|
||||
|
||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
||||
describe("Lens integration tests", () => {
|
||||
const TEST_NAMESPACE = "integration-tests";
|
||||
const BACKSPACE = "\uE003";
|
||||
let app: Application;
|
||||
const appStart = async () => {
|
||||
app = util.setup();
|
||||
await app.start();
|
||||
// Wait for splash screen to be closed
|
||||
while (await app.client.getWindowCount() > 1);
|
||||
await app.client.windowByIndex(0);
|
||||
await app.client.waitUntilWindowLoaded();
|
||||
};
|
||||
const clickWhatsNew = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||
await app.client.click("button.primary");
|
||||
await app.client.waitUntilTextExists("h1", "Welcome");
|
||||
};
|
||||
const minikubeReady = (): boolean => {
|
||||
// determine if minikube is running
|
||||
{
|
||||
const { status } = spawnSync("minikube status", { shell: true });
|
||||
|
||||
if (status !== 0) {
|
||||
console.warn("minikube not running");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove TEST_NAMESPACE if it already exists
|
||||
{
|
||||
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
||||
|
||||
if (status === 0) {
|
||||
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
||||
|
||||
const { status, stdout, stderr } = spawnSync(
|
||||
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
||||
{ shell: true },
|
||||
);
|
||||
|
||||
if (status !== 0) {
|
||||
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
const ready = minikubeReady();
|
||||
|
||||
describe("app start", () => {
|
||||
beforeAll(appStart, 20000);
|
||||
utils.beforeAllWrapped(async () => {
|
||||
app = await utils.appStart();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
utils.afterAllWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
await util.tearDown(app);
|
||||
await utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows "whats new"', async () => {
|
||||
await clickWhatsNew(app);
|
||||
await utils.clickWhatsNew(app);
|
||||
});
|
||||
|
||||
it('shows "add cluster"', async () => {
|
||||
await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster");
|
||||
await app.client.waitUntilTextExists("h2", "Add Cluster");
|
||||
await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig");
|
||||
});
|
||||
|
||||
describe("preferences page", () => {
|
||||
@ -89,11 +44,28 @@ describe("Lens integration tests", () => {
|
||||
const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
||||
|
||||
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
||||
await app.client.waitUntilTextExists("h2", "Preferences");
|
||||
await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION");
|
||||
});
|
||||
|
||||
it("shows all tabs and their contents", async () => {
|
||||
await app.client.click("[data-testid=application-tab]");
|
||||
await app.client.click("[data-testid=proxy-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=proxy-header]", "PROXY");
|
||||
await app.client.click("[data-testid=kube-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "KUBERNETES");
|
||||
await app.client.click("[data-testid=telemetry-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "TELEMETRY");
|
||||
});
|
||||
|
||||
it("ensures helm repos", async () => {
|
||||
await app.client.waitUntilTextExists("div.repos #message-bitnami", "bitnami"); // wait for the helm-cli to fetch the bitnami repo
|
||||
const repos = await listHelmRepositories();
|
||||
|
||||
if (!repos[0]) {
|
||||
fail("Lens failed to add Bitnami repository");
|
||||
}
|
||||
|
||||
await app.client.click("[data-testid=kube-tab]");
|
||||
await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s)
|
||||
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
|
||||
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
|
||||
});
|
||||
@ -104,470 +76,4 @@ describe("Lens integration tests", () => {
|
||||
await app.client.keys("Meta");
|
||||
});
|
||||
});
|
||||
|
||||
util.describeIf(ready)("workspaces", () => {
|
||||
beforeAll(appStart, 20000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("creates new workspace", async () => {
|
||||
await clickWhatsNew(app);
|
||||
await app.client.click("#current-workspace .Icon");
|
||||
await app.client.click('a[href="/workspaces"]');
|
||||
await app.client.click(".Workspaces button.Button");
|
||||
await app.client.keys("test-workspace");
|
||||
await app.client.click(".Workspaces .Input.description input");
|
||||
await app.client.keys("test description");
|
||||
await app.client.click(".Workspaces .workspace.editing .Icon");
|
||||
await app.client.waitUntilTextExists(".workspace .name a", "test-workspace");
|
||||
});
|
||||
|
||||
it("adds cluster in default workspace", async () => {
|
||||
await addMinikubeCluster(app);
|
||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
||||
});
|
||||
|
||||
it("adds cluster in test-workspace", async () => {
|
||||
await app.client.click("#current-workspace .Icon");
|
||||
await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]');
|
||||
await app.client.click('.WorkspaceMenu li[title="test description"]');
|
||||
await addMinikubeCluster(app);
|
||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||
});
|
||||
|
||||
it("checks if default workspace has active cluster", async () => {
|
||||
await app.client.click("#current-workspace .Icon");
|
||||
await app.client.waitForVisible(".WorkspaceMenu > li:first-of-type");
|
||||
await app.client.click(".WorkspaceMenu > li:first-of-type");
|
||||
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
|
||||
});
|
||||
});
|
||||
|
||||
const addMinikubeCluster = async (app: Application) => {
|
||||
await app.client.click("div.add-cluster");
|
||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
||||
await app.client.click("div.Select__control"); // show the context drop-down list
|
||||
await app.client.waitUntilTextExists("div", "minikube");
|
||||
|
||||
if (!await app.client.$("button.primary").isEnabled()) {
|
||||
await app.client.click("div.minikube"); // select minikube context
|
||||
} // else the only context, which must be 'minikube', is automatically selected
|
||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||
await app.client.click("button.primary"); // add minikube cluster
|
||||
};
|
||||
const waitForMinikubeDashboard = async (app: Application) => {
|
||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||
await app.client.frame("minikube");
|
||||
await app.client.waitUntilTextExists("span.link-text", "Cluster");
|
||||
};
|
||||
|
||||
util.describeIf(ready)("cluster tests", () => {
|
||||
let clusterAdded = false;
|
||||
const addCluster = async () => {
|
||||
await clickWhatsNew(app);
|
||||
await addMinikubeCluster(app);
|
||||
await waitForMinikubeDashboard(app);
|
||||
await app.client.click('a[href="/nodes"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
||||
};
|
||||
|
||||
describe("cluster add", () => {
|
||||
beforeAll(appStart, 20000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows to add a cluster", async () => {
|
||||
await addCluster();
|
||||
clusterAdded = true;
|
||||
});
|
||||
});
|
||||
|
||||
const appStartAddCluster = async () => {
|
||||
if (clusterAdded) {
|
||||
await appStart();
|
||||
await addCluster();
|
||||
}
|
||||
};
|
||||
|
||||
describe("cluster pages", () => {
|
||||
|
||||
beforeAll(appStartAddCluster, 40000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
const tests: {
|
||||
drawer?: string
|
||||
drawerId?: string
|
||||
pages: {
|
||||
name: string,
|
||||
href: string,
|
||||
expectedSelector: string,
|
||||
expectedText: string
|
||||
}[]
|
||||
}[] = [{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Cluster",
|
||||
href: "cluster",
|
||||
expectedSelector: "div.ClusterOverview div.label",
|
||||
expectedText: "Master"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Nodes",
|
||||
href: "nodes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Nodes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Workloads",
|
||||
drawerId: "workloads",
|
||||
pages: [{
|
||||
name: "Overview",
|
||||
href: "workloads",
|
||||
expectedSelector: "h5.box",
|
||||
expectedText: "Overview"
|
||||
},
|
||||
{
|
||||
name: "Pods",
|
||||
href: "pods",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pods"
|
||||
},
|
||||
{
|
||||
name: "Deployments",
|
||||
href: "deployments",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Deployments"
|
||||
},
|
||||
{
|
||||
name: "DaemonSets",
|
||||
href: "daemonsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Daemon Sets"
|
||||
},
|
||||
{
|
||||
name: "StatefulSets",
|
||||
href: "statefulsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Stateful Sets"
|
||||
},
|
||||
{
|
||||
name: "ReplicaSets",
|
||||
href: "replicasets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Replica Sets"
|
||||
},
|
||||
{
|
||||
name: "Jobs",
|
||||
href: "jobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Jobs"
|
||||
},
|
||||
{
|
||||
name: "CronJobs",
|
||||
href: "cronjobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Cron Jobs"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Configuration",
|
||||
drawerId: "config",
|
||||
pages: [{
|
||||
name: "ConfigMaps",
|
||||
href: "configmaps",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Config Maps"
|
||||
},
|
||||
{
|
||||
name: "Secrets",
|
||||
href: "secrets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Secrets"
|
||||
},
|
||||
{
|
||||
name: "Resource Quotas",
|
||||
href: "resourcequotas",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Resource Quotas"
|
||||
},
|
||||
{
|
||||
name: "HPA",
|
||||
href: "hpa",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Horizontal Pod Autoscalers"
|
||||
},
|
||||
{
|
||||
name: "Pod Disruption Budgets",
|
||||
href: "poddisruptionbudgets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Disruption Budgets"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Network",
|
||||
drawerId: "networks",
|
||||
pages: [{
|
||||
name: "Services",
|
||||
href: "services",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Services"
|
||||
},
|
||||
{
|
||||
name: "Endpoints",
|
||||
href: "endpoints",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Endpoints"
|
||||
},
|
||||
{
|
||||
name: "Ingresses",
|
||||
href: "ingresses",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Ingresses"
|
||||
},
|
||||
{
|
||||
name: "Network Policies",
|
||||
href: "network-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Network Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Storage",
|
||||
drawerId: "storage",
|
||||
pages: [{
|
||||
name: "Persistent Volume Claims",
|
||||
href: "persistent-volume-claims",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volume Claims"
|
||||
},
|
||||
{
|
||||
name: "Persistent Volumes",
|
||||
href: "persistent-volumes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volumes"
|
||||
},
|
||||
{
|
||||
name: "Storage Classes",
|
||||
href: "storage-classes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Storage Classes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Namespaces",
|
||||
href: "namespaces",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Namespaces"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Events",
|
||||
href: "events",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Events"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Apps",
|
||||
drawerId: "apps",
|
||||
pages: [{
|
||||
name: "Charts",
|
||||
href: "apps/charts",
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
expectedText: ""
|
||||
},
|
||||
{
|
||||
name: "Releases",
|
||||
href: "apps/releases",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Releases"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Access Control",
|
||||
drawerId: "users",
|
||||
pages: [{
|
||||
name: "Service Accounts",
|
||||
href: "service-accounts",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Service Accounts"
|
||||
},
|
||||
{
|
||||
name: "Role Bindings",
|
||||
href: "role-bindings",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Role Bindings"
|
||||
},
|
||||
{
|
||||
name: "Roles",
|
||||
href: "roles",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Roles"
|
||||
},
|
||||
{
|
||||
name: "Pod Security Policies",
|
||||
href: "pod-security-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Security Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Custom Resources",
|
||||
drawerId: "custom-resources",
|
||||
pages: [{
|
||||
name: "Definitions",
|
||||
href: "crd/definitions",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Custom Resources"
|
||||
}]
|
||||
}];
|
||||
|
||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||
if (drawer !== "") {
|
||||
it(`shows ${drawer} drawer`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
||||
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name);
|
||||
});
|
||||
}
|
||||
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
|
||||
it(`shows ${drawer}->${name} page`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(`a[href^="/${href}"]`);
|
||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
if (drawer !== "") {
|
||||
// hide the drawer
|
||||
it(`hides ${drawer} drawer`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
|
||||
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("viewing pod logs", () => {
|
||||
beforeEach(appStartAddCluster, 40000);
|
||||
|
||||
afterEach(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it(`shows a logs for a pod`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
// Go to Pods page
|
||||
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||
await app.client.click('a[href^="/pods"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
||||
// Open logs tab in dock
|
||||
await app.client.click(".list .TableRow:first-child");
|
||||
await app.client.waitForVisible(".Drawer");
|
||||
await app.client.click(".drawer-title .Menu li:nth-child(2)");
|
||||
// Check if controls are available
|
||||
await app.client.waitForVisible(".PodLogs .VirtualList");
|
||||
await app.client.waitForVisible(".PodLogControls");
|
||||
await app.client.waitForVisible(".PodLogControls .SearchInput");
|
||||
await app.client.waitForVisible(".PodLogControls .SearchInput input");
|
||||
// Search for semicolon
|
||||
await app.client.keys(":");
|
||||
await app.client.waitForVisible(".PodLogs .list span.active");
|
||||
// Click through controls
|
||||
await app.client.click(".PodLogControls .timestamps-icon");
|
||||
await app.client.click(".PodLogControls .undo-icon");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cluster operations", () => {
|
||||
beforeEach(appStartAddCluster, 40000);
|
||||
|
||||
afterEach(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
return util.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows default namespace", async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click('a[href="/namespaces"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||
});
|
||||
|
||||
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click('a[href="/namespaces"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||
await app.client.click("button.add-button");
|
||||
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
|
||||
await app.client.keys(`${TEST_NAMESPACE}\n`);
|
||||
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
|
||||
});
|
||||
|
||||
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
|
||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||
await app.client.click('a[href^="/pods"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
||||
await app.client.click(".Icon.new-dock-tab");
|
||||
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
|
||||
await app.client.click("li.MenuItem.create-resource-tab");
|
||||
await app.client.waitForVisible(".CreateResource div.ace_content");
|
||||
// Write pod manifest to editor
|
||||
await app.client.keys("apiVersion: v1\n");
|
||||
await app.client.keys("kind: Pod\n");
|
||||
await app.client.keys("metadata:\n");
|
||||
await app.client.keys(" name: nginx-create-pod-test\n");
|
||||
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
|
||||
await app.client.keys(`${BACKSPACE}spec:\n`);
|
||||
await app.client.keys(" containers:\n");
|
||||
await app.client.keys("- name: nginx-create-pod-test\n");
|
||||
await app.client.keys(" image: nginx:alpine\n");
|
||||
// Create deployment
|
||||
await app.client.waitForEnabled("button.Button=Create & Close");
|
||||
await app.client.click("button.Button=Create & Close");
|
||||
// Wait until first bits of pod appears on dashboard
|
||||
await app.client.waitForExist(".name=nginx-create-pod-test");
|
||||
// Open pod details
|
||||
await app.client.click(".name=nginx-create-pod-test");
|
||||
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
470
integration/__tests__/cluster-pages.tests.ts
Normal file
470
integration/__tests__/cluster-pages.tests.ts
Normal file
@ -0,0 +1,470 @@
|
||||
/*
|
||||
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
|
||||
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
|
||||
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
||||
cluster and vice versa.
|
||||
*/
|
||||
import { Application } from "spectron";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube";
|
||||
import { exec } from "child_process";
|
||||
import * as util from "util";
|
||||
|
||||
export const promiseExec = util.promisify(exec);
|
||||
|
||||
jest.setTimeout(60000);
|
||||
|
||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
||||
describe("Lens cluster pages", () => {
|
||||
const TEST_NAMESPACE = "integration-tests";
|
||||
const BACKSPACE = "\uE003";
|
||||
let app: Application;
|
||||
const ready = minikubeReady(TEST_NAMESPACE);
|
||||
|
||||
utils.describeIf(ready)("test common pages", () => {
|
||||
let clusterAdded = false;
|
||||
const addCluster = async () => {
|
||||
await utils.clickWhatsNew(app);
|
||||
await utils.clickWelcomeNotification(app);
|
||||
await app.client.waitUntilTextExists("div", "Catalog");
|
||||
await addMinikubeCluster(app);
|
||||
await waitForMinikubeDashboard(app);
|
||||
await app.client.click('a[href="/nodes"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
||||
};
|
||||
|
||||
describe("cluster add", () => {
|
||||
utils.beforeAllWrapped(async () => {
|
||||
app = await utils.appStart();
|
||||
});
|
||||
|
||||
utils.afterAllWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
return utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows to add a cluster", async () => {
|
||||
await addCluster();
|
||||
clusterAdded = true;
|
||||
});
|
||||
});
|
||||
|
||||
const appStartAddCluster = async () => {
|
||||
if (clusterAdded) {
|
||||
app = await utils.appStart();
|
||||
await addCluster();
|
||||
}
|
||||
};
|
||||
|
||||
function getSidebarSelectors(itemId: string) {
|
||||
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
||||
|
||||
return {
|
||||
expandSubMenu: `${root} .nav-item`,
|
||||
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
|
||||
};
|
||||
}
|
||||
|
||||
describe("cluster pages", () => {
|
||||
utils.beforeAllWrapped(appStartAddCluster);
|
||||
|
||||
utils.afterAllWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
return utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
const tests: {
|
||||
drawer?: string
|
||||
drawerId?: string
|
||||
pages: {
|
||||
name: string,
|
||||
href: string,
|
||||
expectedSelector: string,
|
||||
expectedText: string
|
||||
}[]
|
||||
}[] = [{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Cluster",
|
||||
href: "cluster",
|
||||
expectedSelector: "div.ClusterOverview div.label",
|
||||
expectedText: "Master"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Nodes",
|
||||
href: "nodes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Nodes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Workloads",
|
||||
drawerId: "workloads",
|
||||
pages: [{
|
||||
name: "Overview",
|
||||
href: "workloads",
|
||||
expectedSelector: "h5.box",
|
||||
expectedText: "Overview"
|
||||
},
|
||||
{
|
||||
name: "Pods",
|
||||
href: "pods",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pods"
|
||||
},
|
||||
{
|
||||
name: "Deployments",
|
||||
href: "deployments",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Deployments"
|
||||
},
|
||||
{
|
||||
name: "DaemonSets",
|
||||
href: "daemonsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Daemon Sets"
|
||||
},
|
||||
{
|
||||
name: "StatefulSets",
|
||||
href: "statefulsets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Stateful Sets"
|
||||
},
|
||||
{
|
||||
name: "ReplicaSets",
|
||||
href: "replicasets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Replica Sets"
|
||||
},
|
||||
{
|
||||
name: "Jobs",
|
||||
href: "jobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Jobs"
|
||||
},
|
||||
{
|
||||
name: "CronJobs",
|
||||
href: "cronjobs",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Cron Jobs"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Configuration",
|
||||
drawerId: "config",
|
||||
pages: [{
|
||||
name: "ConfigMaps",
|
||||
href: "configmaps",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Config Maps"
|
||||
},
|
||||
{
|
||||
name: "Secrets",
|
||||
href: "secrets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Secrets"
|
||||
},
|
||||
{
|
||||
name: "Resource Quotas",
|
||||
href: "resourcequotas",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Resource Quotas"
|
||||
},
|
||||
{
|
||||
name: "Limit Ranges",
|
||||
href: "limitranges",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Limit Ranges"
|
||||
},
|
||||
{
|
||||
name: "HPA",
|
||||
href: "hpa",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Horizontal Pod Autoscalers"
|
||||
},
|
||||
{
|
||||
name: "Pod Disruption Budgets",
|
||||
href: "poddisruptionbudgets",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Disruption Budgets"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Network",
|
||||
drawerId: "networks",
|
||||
pages: [{
|
||||
name: "Services",
|
||||
href: "services",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Services"
|
||||
},
|
||||
{
|
||||
name: "Endpoints",
|
||||
href: "endpoints",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Endpoints"
|
||||
},
|
||||
{
|
||||
name: "Ingresses",
|
||||
href: "ingresses",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Ingresses"
|
||||
},
|
||||
{
|
||||
name: "Network Policies",
|
||||
href: "network-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Network Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Storage",
|
||||
drawerId: "storage",
|
||||
pages: [{
|
||||
name: "Persistent Volume Claims",
|
||||
href: "persistent-volume-claims",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volume Claims"
|
||||
},
|
||||
{
|
||||
name: "Persistent Volumes",
|
||||
href: "persistent-volumes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Persistent Volumes"
|
||||
},
|
||||
{
|
||||
name: "Storage Classes",
|
||||
href: "storage-classes",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Storage Classes"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Namespaces",
|
||||
href: "namespaces",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Namespaces"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "",
|
||||
drawerId: "",
|
||||
pages: [{
|
||||
name: "Events",
|
||||
href: "events",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Events"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Apps",
|
||||
drawerId: "apps",
|
||||
pages: [{
|
||||
name: "Charts",
|
||||
href: "apps/charts",
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
expectedText: ""
|
||||
},
|
||||
{
|
||||
name: "Releases",
|
||||
href: "apps/releases",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Releases"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Access Control",
|
||||
drawerId: "users",
|
||||
pages: [{
|
||||
name: "Service Accounts",
|
||||
href: "service-accounts",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Service Accounts"
|
||||
},
|
||||
{
|
||||
name: "Role Bindings",
|
||||
href: "role-bindings",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Role Bindings"
|
||||
},
|
||||
{
|
||||
name: "Roles",
|
||||
href: "roles",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Roles"
|
||||
},
|
||||
{
|
||||
name: "Pod Security Policies",
|
||||
href: "pod-security-policies",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Pod Security Policies"
|
||||
}]
|
||||
},
|
||||
{
|
||||
drawer: "Custom Resources",
|
||||
drawerId: "custom-resources",
|
||||
pages: [{
|
||||
name: "Definitions",
|
||||
href: "crd/definitions",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Custom Resources"
|
||||
}]
|
||||
}];
|
||||
|
||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||
const selectors = getSidebarSelectors(drawerId);
|
||||
|
||||
if (drawer !== "") {
|
||||
it(`shows ${drawer} drawer`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(selectors.expandSubMenu);
|
||||
await app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name);
|
||||
});
|
||||
|
||||
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
|
||||
it(`shows ${drawer}->${name} page`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(selectors.subMenuLink(href));
|
||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
it(`hides ${drawer} drawer`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(selectors.expandSubMenu);
|
||||
await expect(app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name, 100)).rejects.toThrow();
|
||||
});
|
||||
} else {
|
||||
const { href, name, expectedText, expectedSelector } = pages[0];
|
||||
|
||||
it(`shows page ${name}`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(`a[href^="/${href}"]`);
|
||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("viewing pod logs", () => {
|
||||
utils.beforeEachWrapped(appStartAddCluster);
|
||||
|
||||
utils.afterEachWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
return utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it(`shows a log for a pod`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
// Go to Pods page
|
||||
await app.client.click(getSidebarSelectors("workloads").expandSubMenu);
|
||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||
await app.client.click('a[href^="/pods"]');
|
||||
await app.client.click(".NamespaceSelect");
|
||||
await app.client.keys("kube-system");
|
||||
await app.client.keys("Enter");// "\uE007"
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
||||
let podMenuItemEnabled = false;
|
||||
|
||||
// Wait until extensions are enabled on renderer
|
||||
while (!podMenuItemEnabled) {
|
||||
const logs = await app.client.getRenderProcessLogs();
|
||||
|
||||
podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@"));
|
||||
|
||||
if (!podMenuItemEnabled) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions
|
||||
// Open logs tab in dock
|
||||
await app.client.click(".list .TableRow:first-child");
|
||||
await app.client.waitForVisible(".Drawer");
|
||||
await app.client.click(".drawer-title .Menu li:nth-child(2)");
|
||||
// Check if controls are available
|
||||
await app.client.waitForVisible(".LogList .VirtualList");
|
||||
await app.client.waitForVisible(".LogResourceSelector");
|
||||
//await app.client.waitForVisible(".LogSearch .SearchInput");
|
||||
await app.client.waitForVisible(".LogSearch .SearchInput input");
|
||||
// Search for semicolon
|
||||
await app.client.keys(":");
|
||||
await app.client.waitForVisible(".LogList .list span.active");
|
||||
// Click through controls
|
||||
await app.client.click(".LogControls .show-timestamps");
|
||||
await app.client.click(".LogControls .show-previous");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cluster operations", () => {
|
||||
utils.beforeEachWrapped(appStartAddCluster);
|
||||
|
||||
utils.afterEachWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
return utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows default namespace", async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click('a[href="/namespaces"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||
});
|
||||
|
||||
it(`creates ${TEST_NAMESPACE} namespace`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click('a[href="/namespaces"]');
|
||||
await app.client.waitUntilTextExists("div.TableCell", "default");
|
||||
await app.client.waitUntilTextExists("div.TableCell", "kube-system");
|
||||
await app.client.click("button.add-button");
|
||||
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
|
||||
await app.client.keys(`${TEST_NAMESPACE}\n`);
|
||||
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
|
||||
});
|
||||
|
||||
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
|
||||
expect(clusterAdded).toBe(true);
|
||||
await app.client.click(getSidebarSelectors("workloads").expandSubMenu);
|
||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
||||
await app.client.click('a[href^="/pods"]');
|
||||
|
||||
await app.client.click(".NamespaceSelect");
|
||||
await app.client.keys(TEST_NAMESPACE);
|
||||
await app.client.keys("Enter");// "\uE007"
|
||||
await app.client.click(".Icon.new-dock-tab");
|
||||
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
|
||||
await app.client.click("li.MenuItem.create-resource-tab");
|
||||
await app.client.waitForVisible(".CreateResource div.ace_content");
|
||||
// Write pod manifest to editor
|
||||
await app.client.keys("apiVersion: v1\n");
|
||||
await app.client.keys("kind: Pod\n");
|
||||
await app.client.keys("metadata:\n");
|
||||
await app.client.keys(" name: nginx-create-pod-test\n");
|
||||
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
|
||||
await app.client.keys(`${BACKSPACE}spec:\n`);
|
||||
await app.client.keys(" containers:\n");
|
||||
await app.client.keys("- name: nginx-create-pod-test\n");
|
||||
await app.client.keys(" image: nginx:alpine\n");
|
||||
// Create deployment
|
||||
await app.client.waitForEnabled("button.Button=Create & Close");
|
||||
await app.client.click("button.Button=Create & Close");
|
||||
// Wait until first bits of pod appears on dashboard
|
||||
await app.client.waitForExist(".name=nginx-create-pod-test");
|
||||
// Open pod details
|
||||
await app.client.click(".name=nginx-create-pod-test");
|
||||
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
27
integration/__tests__/command-palette.tests.ts
Normal file
27
integration/__tests__/command-palette.tests.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Application } from "spectron";
|
||||
import * as utils from "../helpers/utils";
|
||||
|
||||
jest.setTimeout(60000);
|
||||
|
||||
describe("Lens command palette", () => {
|
||||
let app: Application;
|
||||
|
||||
describe("menu", () => {
|
||||
utils.beforeAllWrapped(async () => {
|
||||
app = await utils.appStart();
|
||||
});
|
||||
|
||||
utils.afterAllWrapped(async () => {
|
||||
if (app?.isRunning()) {
|
||||
await utils.tearDown(app);
|
||||
}
|
||||
});
|
||||
|
||||
it("opens command dialog from menu", async () => {
|
||||
await utils.clickWhatsNew(app);
|
||||
await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette...");
|
||||
await app.client.waitUntilTextExists(".Select__option", "Preferences: Open");
|
||||
await app.client.keys("Escape");
|
||||
});
|
||||
});
|
||||
});
|
||||
61
integration/helpers/minikube.ts
Normal file
61
integration/helpers/minikube.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import { Application } from "spectron";
|
||||
|
||||
export function minikubeReady(testNamespace: string): boolean {
|
||||
// determine if minikube is running
|
||||
{
|
||||
const { status } = spawnSync("minikube status", { shell: true });
|
||||
|
||||
if (status !== 0) {
|
||||
console.warn("minikube not running");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove TEST_NAMESPACE if it already exists
|
||||
{
|
||||
const { status } = spawnSync(`minikube kubectl -- get namespace ${testNamespace}`, { shell: true });
|
||||
|
||||
if (status === 0) {
|
||||
console.warn(`Removing existing ${testNamespace} namespace`);
|
||||
|
||||
const { status, stdout, stderr } = spawnSync(
|
||||
`minikube kubectl -- delete namespace ${testNamespace}`,
|
||||
{ shell: true },
|
||||
);
|
||||
|
||||
if (status !== 0) {
|
||||
console.warn(`Error removing ${testNamespace} namespace: ${stderr.toString()}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addMinikubeCluster(app: Application) {
|
||||
await app.client.click("button.add-button");
|
||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
||||
await app.client.click("div.Select__control"); // show the context drop-down list
|
||||
await app.client.waitUntilTextExists("div", "minikube");
|
||||
|
||||
if (!await app.client.$("button.primary").isEnabled()) {
|
||||
await app.client.click("div.minikube"); // select minikube context
|
||||
} // else the only context, which must be 'minikube', is automatically selected
|
||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||
await app.client.click("button.primary"); // add minikube cluster
|
||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||
await app.client.click("div.TableRow");
|
||||
}
|
||||
|
||||
export async function waitForMinikubeDashboard(app: Application) {
|
||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||
await app.client.frame("minikube");
|
||||
await app.client.waitUntilTextExists("span.link-text", "Cluster");
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
import { Application } from "spectron";
|
||||
import * as util from "util";
|
||||
import { exec } from "child_process";
|
||||
|
||||
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||
"win32": "./dist/win-unpacked/Lens.exe",
|
||||
@ -6,6 +8,39 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
||||
};
|
||||
|
||||
interface DoneCallback {
|
||||
(...args: any[]): any;
|
||||
fail(error?: string | { message: string }): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is necessary because Jest doesn't do this correctly.
|
||||
* @param fn The function to call
|
||||
*/
|
||||
export function wrapJestLifecycle(fn: () => Promise<void>): (done: DoneCallback) => void {
|
||||
return function (done: DoneCallback) {
|
||||
fn()
|
||||
.then(() => done())
|
||||
.catch(error => done.fail(error));
|
||||
};
|
||||
}
|
||||
|
||||
export function beforeAllWrapped(fn: () => Promise<void>): void {
|
||||
beforeAll(wrapJestLifecycle(fn));
|
||||
}
|
||||
|
||||
export function beforeEachWrapped(fn: () => Promise<void>): void {
|
||||
beforeEach(wrapJestLifecycle(fn));
|
||||
}
|
||||
|
||||
export function afterAllWrapped(fn: () => Promise<void>): void {
|
||||
afterAll(wrapJestLifecycle(fn));
|
||||
}
|
||||
|
||||
export function afterEachWrapped(fn: () => Promise<void>): void {
|
||||
afterEach(wrapJestLifecycle(fn));
|
||||
}
|
||||
|
||||
export function itIf(condition: boolean) {
|
||||
return condition ? it : it.skip;
|
||||
}
|
||||
@ -26,6 +61,38 @@ export function setup(): Application {
|
||||
});
|
||||
}
|
||||
|
||||
export const keys = {
|
||||
backspace: "\uE003"
|
||||
};
|
||||
|
||||
export async function appStart() {
|
||||
const app = setup();
|
||||
|
||||
await app.start();
|
||||
// Wait for splash screen to be closed
|
||||
while (await app.client.getWindowCount() > 1);
|
||||
await app.client.windowByIndex(0);
|
||||
await app.client.waitUntilWindowLoaded();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function clickWhatsNew(app: Application) {
|
||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||
await app.client.click("button.primary");
|
||||
await app.client.waitUntilTextExists("div", "Catalog");
|
||||
}
|
||||
|
||||
export async function clickWelcomeNotification(app: Application) {
|
||||
const itemsText = await app.client.$("div.info-panel").getText();
|
||||
|
||||
if (itemsText === "0 item") {
|
||||
// welcome notification should be present, dismiss it
|
||||
await app.client.waitUntilTextExists("div.message", "Welcome!");
|
||||
await app.client.click(".notification i.Icon.close");
|
||||
}
|
||||
}
|
||||
|
||||
type AsyncPidGetter = () => Promise<number>;
|
||||
|
||||
export async function tearDown(app: Application) {
|
||||
@ -39,3 +106,26 @@ export async function tearDown(app: Application) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export const promiseExec = util.promisify(exec);
|
||||
|
||||
type HelmRepository = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export async function listHelmRepositories(retries = 0): Promise<HelmRepository[]>{
|
||||
if (retries < 5) {
|
||||
try {
|
||||
const { stdout: reposJson } = await promiseExec("helm repo list -o json");
|
||||
|
||||
return JSON.parse(reposJson);
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
|
||||
|
||||
return await listHelmRepositories((retries + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ site_url: https://docs.k8slens.dev
|
||||
docs_dir: docs/
|
||||
repo_name: GitHub
|
||||
repo_url: https://github.com/lensapp/lens
|
||||
copyright: Copyright © 2020 <a href="https://mirantis.com/">Mirantis Inc.</a> - All rights reserved.
|
||||
copyright: Copyright © 2021 <a href="https://mirantis.com/">Mirantis Inc.</a> - All rights reserved.
|
||||
edit_uri: ""
|
||||
nav:
|
||||
- Overview: README.md
|
||||
@ -34,7 +34,7 @@ nav:
|
||||
- Main Extension: extensions/guides/main-extension.md
|
||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||
- Stores: extensions/guides/stores.md
|
||||
- Working with mobx: extensions/guides/working-with-mobx.md
|
||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||
- Testing and Publishing:
|
||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
|
||||
@ -81,6 +81,8 @@ markdown_extensions:
|
||||
- toc:
|
||||
permalink: "#"
|
||||
toc_depth: 3
|
||||
- admonition: {}
|
||||
- pymdownx.details: {}
|
||||
|
||||
extra:
|
||||
generator: false
|
||||
|
||||
79
package.json
79
package.json
@ -2,9 +2,9 @@
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "4.1.0-alpha.0",
|
||||
"version": "5.0.0-alpha.1",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"copyright": "© 2021, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
@ -13,7 +13,8 @@
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||
"dev-build": "concurrently yarn:compile:*",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
|
||||
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||
"dev:main": "yarn run compile:main --watch",
|
||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||
@ -25,8 +26,7 @@
|
||||
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
||||
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
||||
"test": "jest --env=jsdom src $@",
|
||||
"integration": "jest --coverage integration $@",
|
||||
"integration": "jest --runInBand integration",
|
||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
||||
@ -35,14 +35,14 @@
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 .",
|
||||
"lint": "yarn run eslint --ext js,ts,tsx --max-warnings=0 .",
|
||||
"lint:fix": "yarn run lint --fix",
|
||||
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.17.15",
|
||||
"bundledKubectlVersion": "1.18.15",
|
||||
"bundledHelmVersion": "3.4.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -51,7 +51,6 @@
|
||||
"jest": {
|
||||
"collectCoverage": false,
|
||||
"verbose": true,
|
||||
"testEnvironment": "node",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
@ -70,6 +69,7 @@
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"files": [
|
||||
"static/build/main.js"
|
||||
],
|
||||
@ -103,7 +103,10 @@
|
||||
],
|
||||
"linux": {
|
||||
"category": "Network",
|
||||
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
||||
"target": [
|
||||
"deb",
|
||||
"rpm",
|
||||
"snap",
|
||||
"AppImage"
|
||||
],
|
||||
@ -154,17 +157,26 @@
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"include": "build/installer.nsh"
|
||||
"include": "build/installer.nsh",
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"snap": {
|
||||
"confinement": "classic"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"repo": "lens",
|
||||
"owner": "lensapp"
|
||||
"provider": "s3",
|
||||
"bucket": "lens-binaries",
|
||||
"path": "/ide"
|
||||
}
|
||||
],
|
||||
"snap": {
|
||||
"confinement": "classic"
|
||||
"protocols": {
|
||||
"name": "Lens Protocol Handler",
|
||||
"schemes": [
|
||||
"lens"
|
||||
],
|
||||
"role": "Viewer"
|
||||
}
|
||||
},
|
||||
"lens": {
|
||||
@ -174,15 +186,18 @@
|
||||
"node-menu",
|
||||
"metrics-cluster-feature",
|
||||
"license-menu-item",
|
||||
"kube-object-event-status"
|
||||
"kube-object-event-status",
|
||||
"survey"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/call": "^8.0.0",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.12.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"array-move": "^3.0.0",
|
||||
"await-lock": "^2.1.0",
|
||||
"byline": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"command-exists": "1.2.9",
|
||||
@ -195,17 +210,19 @@
|
||||
"fs-extra": "^9.0.1",
|
||||
"handlebars": "^4.7.6",
|
||||
"http-proxy": "^1.18.1",
|
||||
"immer": "^8.0.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsdom": "^16.4.0",
|
||||
"jsonpath": "^1.0.2",
|
||||
"lodash": "^4.17.15",
|
||||
"mac-ca": "^1.0.4",
|
||||
"marked": "^1.1.0",
|
||||
"marked": "^1.2.7",
|
||||
"md5-file": "^5.0.0",
|
||||
"mobx": "^5.15.7",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mobx-react": "^6.2.2",
|
||||
"mock-fs": "^4.12.0",
|
||||
"moment": "^2.26.0",
|
||||
"node-pty": "^0.9.0",
|
||||
"npm": "^6.14.8",
|
||||
"openid-client": "^3.15.2",
|
||||
@ -215,15 +232,17 @@
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router": "^5.2.0",
|
||||
"readable-stream": "^3.6.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"semver": "^7.3.2",
|
||||
"serializr": "^2.0.3",
|
||||
"shell-env": "^3.0.0",
|
||||
"shell-env": "^3.0.1",
|
||||
"spdy": "^4.0.2",
|
||||
"tar": "^6.0.5",
|
||||
"tcp-port-used": "^1.0.1",
|
||||
"tempy": "^0.5.0",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^8.3.2",
|
||||
"win-ca": "^3.2.0",
|
||||
"winston": "^3.2.1",
|
||||
@ -233,9 +252,11 @@
|
||||
"devDependencies": {
|
||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||
"@material-ui/core": "^4.10.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@testing-library/jest-dom": "^5.11.5",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@types/byline": "^4.2.32",
|
||||
"@types/chart.js": "^2.9.21",
|
||||
"@types/circular-dependency-plugin": "^5.0.1",
|
||||
"@types/color": "^3.0.1",
|
||||
@ -244,7 +265,7 @@
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/electron-window-state": "^2.0.34",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/hapi": "^18.0.3",
|
||||
"@types/hapi": "^18.0.5",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/html-webpack-plugin": "^3.2.3",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
@ -268,6 +289,7 @@
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/react-select": "^3.0.13",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/readable-stream": "^2.3.9",
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/request-promise-native": "^1.0.17",
|
||||
"@types/semver": "^7.2.0",
|
||||
@ -279,25 +301,27 @@
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
"@types/universal-analytics": "^0.4.4",
|
||||
"@types/url-parse": "^1.4.3",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/webdriverio": "^4.13.0",
|
||||
"@types/webpack": "^4.41.17",
|
||||
"@types/webpack-dev-server": "^3.11.1",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"ace-builds": "^1.4.11",
|
||||
"ansi_up": "^4.0.4",
|
||||
"ace-builds": "^1.4.12",
|
||||
"ansi_up": "^5.0.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"color": "^3.1.2",
|
||||
"concurrently": "^5.2.0",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"css-loader": "^3.5.3",
|
||||
"deepdash": "^5.3.5",
|
||||
"dompurify": "^2.0.11",
|
||||
"electron": "^9.4.0",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron": "^9.4.4",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
@ -313,12 +337,12 @@
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"make-plural": "^6.2.1",
|
||||
"make-plural": "^6.2.2",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"moment": "^2.26.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemon": "^2.0.4",
|
||||
"open": "^7.3.1",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.2.0",
|
||||
@ -328,6 +352,7 @@
|
||||
"react-refresh": "^0.9.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^3.1.0",
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-window": "^1.8.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sharp": "^0.26.1",
|
||||
@ -348,7 +373,7 @@
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"what-input": "^5.2.10",
|
||||
"xterm": "^4.6.0",
|
||||
"xterm": "^4.10.0",
|
||||
"xterm-addon-fit": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
63
src/common/__tests__/catalog-entity-registry.test.ts
Normal file
63
src/common/__tests__/catalog-entity-registry.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { observable, reaction } from "mobx";
|
||||
import { WebLink } from "../catalog-entities";
|
||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||
|
||||
describe("CatalogEntityRegistry", () => {
|
||||
let registry: CatalogEntityRegistry;
|
||||
const entity = new WebLink({
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "WebLink",
|
||||
metadata: {
|
||||
uid: "test",
|
||||
name: "test-link",
|
||||
source: "test",
|
||||
labels: {}
|
||||
},
|
||||
spec: {
|
||||
url: "https://k8slens.dev"
|
||||
},
|
||||
status: {
|
||||
phase: "ok"
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new CatalogEntityRegistry();
|
||||
});
|
||||
|
||||
describe("addSource", () => {
|
||||
it ("allows to add an observable source", () => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
expect(registry.items.length).toEqual(0);
|
||||
|
||||
source.push(entity);
|
||||
|
||||
expect(registry.items.length).toEqual(1);
|
||||
});
|
||||
|
||||
it ("added source change triggers reaction", (done) => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
reaction(() => registry.items, () => {
|
||||
done();
|
||||
});
|
||||
|
||||
source.push(entity);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeSource", () => {
|
||||
it ("removes source", () => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
source.push(entity);
|
||||
registry.removeSource("test");
|
||||
|
||||
expect(registry.items.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,17 +2,40 @@ import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import yaml from "js-yaml";
|
||||
import { Cluster } from "../../main/cluster";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { workspaceStore } from "../workspace-store";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||
|
||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||
const kubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost
|
||||
name: test
|
||||
contexts:
|
||||
- context:
|
||||
cluster: test
|
||||
user: test
|
||||
name: foo
|
||||
- context:
|
||||
cluster: test
|
||||
user: test
|
||||
name: foo2
|
||||
current-context: test
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: test
|
||||
user:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en"
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
@ -47,14 +70,13 @@ describe("empty config", () => {
|
||||
clusterStore.addCluster(
|
||||
new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
contextName: "foo",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp",
|
||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||
clusterName: "minikube"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"),
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -68,21 +90,14 @@ describe("empty config", () => {
|
||||
expect(storedCluster.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("adds cluster to default workspace", () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
|
||||
expect(storedCluster.workspace).toBe("default");
|
||||
});
|
||||
|
||||
it("removes cluster from store", async () => {
|
||||
await clusterStore.removeById("foo");
|
||||
expect(clusterStore.getById("foo")).toBeUndefined();
|
||||
expect(clusterStore.getById("foo")).toBeNull();
|
||||
});
|
||||
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
||||
});
|
||||
});
|
||||
|
||||
@ -91,21 +106,19 @@ describe("empty config", () => {
|
||||
clusterStore.addClusters(
|
||||
new Cluster({
|
||||
id: "prod",
|
||||
contextName: "prod",
|
||||
contextName: "foo",
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||
}),
|
||||
new Cluster({
|
||||
id: "dev",
|
||||
contextName: "dev",
|
||||
contextName: "foo2",
|
||||
preferences: {
|
||||
clusterName: "dev"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -115,51 +128,11 @@ describe("empty config", () => {
|
||||
expect(clusterStore.clusters.size).toBe(2);
|
||||
});
|
||||
|
||||
it("gets clusters by workspaces", () => {
|
||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
||||
|
||||
expect(defaultClusters.length).toBe(0);
|
||||
expect(wsClusters.length).toBe(2);
|
||||
expect(wsClusters[0].id).toBe("prod");
|
||||
expect(wsClusters[1].id).toBe("dev");
|
||||
});
|
||||
|
||||
it("check if cluster's kubeconfig file saved", () => {
|
||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||
|
||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||
});
|
||||
|
||||
it("check if reorderring works for same from and to", () => {
|
||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
||||
|
||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||
|
||||
expect(clusters[0].id).toBe("prod");
|
||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||
expect(clusters[1].id).toBe("dev");
|
||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
||||
});
|
||||
|
||||
it("check if reorderring works for different from and to", () => {
|
||||
clusterStore.swapIconOrders("workstation", 0, 1);
|
||||
|
||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||
|
||||
expect(clusters[0].id).toBe("dev");
|
||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||
expect(clusters[1].id).toBe("prod");
|
||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
||||
});
|
||||
|
||||
it("check if after icon reordering, changing workspaces still works", () => {
|
||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
||||
clusterStore.getById("prod").workspace = "default";
|
||||
|
||||
expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1);
|
||||
expect(clusterStore.getByWorkspaceId("default").length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -177,20 +150,20 @@ describe("config with existing clusters", () => {
|
||||
clusters: [
|
||||
{
|
||||
id: "cluster1",
|
||||
kubeConfig: "foo",
|
||||
kubeConfigPath: kubeconfig,
|
||||
contextName: "foo",
|
||||
preferences: { terminalCWD: "/foo" },
|
||||
workspace: "default"
|
||||
},
|
||||
{
|
||||
id: "cluster2",
|
||||
kubeConfig: "foo2",
|
||||
kubeConfigPath: kubeconfig,
|
||||
contextName: "foo2",
|
||||
preferences: { terminalCWD: "/foo2" }
|
||||
},
|
||||
{
|
||||
id: "cluster3",
|
||||
kubeConfig: "foo",
|
||||
kubeConfigPath: kubeconfig,
|
||||
contextName: "foo",
|
||||
preferences: { terminalCWD: "/foo" },
|
||||
workspace: "foo",
|
||||
@ -225,7 +198,7 @@ describe("config with existing clusters", () => {
|
||||
expect(storedCluster).toBeTruthy();
|
||||
const storedCluster2 = clusterStore.getById("cluster2");
|
||||
|
||||
expect(storedCluster2).toBeUndefined();
|
||||
expect(storedCluster2).toBeNull();
|
||||
});
|
||||
|
||||
it("allows getting all of the clusters", async () => {
|
||||
@ -247,6 +220,78 @@ describe("config with existing clusters", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("config with invalid cluster kubeconfig", () => {
|
||||
beforeEach(() => {
|
||||
const invalidKubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost
|
||||
name: test2
|
||||
contexts:
|
||||
- context:
|
||||
cluster: test
|
||||
user: test
|
||||
name: test
|
||||
current-context: test
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: test
|
||||
user:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
"tmp": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "99.99.99"
|
||||
}
|
||||
},
|
||||
clusters: [
|
||||
{
|
||||
id: "cluster1",
|
||||
kubeConfigPath: invalidKubeconfig,
|
||||
contextName: "test",
|
||||
preferences: { terminalCWD: "/foo" },
|
||||
workspace: "foo",
|
||||
},
|
||||
{
|
||||
id: "cluster2",
|
||||
kubeConfigPath: kubeconfig,
|
||||
contextName: "foo",
|
||||
preferences: { terminalCWD: "/foo" },
|
||||
workspace: "default"
|
||||
},
|
||||
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("does not enable clusters with invalid kubeconfig", () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
|
||||
expect(storedClusters.length).toBe(2);
|
||||
expect(storedClusters[0].enabled).toBeFalsy;
|
||||
expect(storedClusters[1].id).toBe("cluster2");
|
||||
expect(storedClusters[1].enabled).toBeTruthy;
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 2.0 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
@ -390,12 +435,6 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("adds cluster to default workspace", async () => {
|
||||
const storedClusterData = clusterStore.clustersList[0];
|
||||
|
||||
expect(storedClusterData.workspace).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
@ -446,3 +485,27 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getClusterIdFromHost", () => {
|
||||
const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a";
|
||||
|
||||
it("should return undefined for non cluster frame hosts", () => {
|
||||
expect(getClusterIdFromHost("localhost:45345")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return ClusterId for cluster frame hosts", () => {
|
||||
expect(getClusterIdFromHost(`${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
});
|
||||
|
||||
it("should return ClusterId for cluster frame hosts with additional subdomains", () => {
|
||||
expect(getClusterIdFromHost(`abc.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId);
|
||||
});
|
||||
});
|
||||
|
||||
101
src/common/__tests__/kube-helpers.test.ts
Normal file
101
src/common/__tests__/kube-helpers.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { validateKubeConfig } from "../kube-helpers";
|
||||
|
||||
const kubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost
|
||||
name: test
|
||||
contexts:
|
||||
- context:
|
||||
cluster: test
|
||||
user: test
|
||||
name: valid
|
||||
- context:
|
||||
cluster: test2
|
||||
user: test
|
||||
name: invalidCluster
|
||||
- context:
|
||||
cluster: test
|
||||
user: test2
|
||||
name: invalidUser
|
||||
- context:
|
||||
cluster: test
|
||||
user: invalidExec
|
||||
name: invalidExec
|
||||
current-context: test
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: test
|
||||
user:
|
||||
exec:
|
||||
command: echo
|
||||
- name: invalidExec
|
||||
user:
|
||||
exec:
|
||||
command: foo
|
||||
`;
|
||||
|
||||
const kc = new KubeConfig();
|
||||
|
||||
describe("validateKubeconfig", () => {
|
||||
beforeAll(() => {
|
||||
kc.loadFromString(kubeconfig);
|
||||
});
|
||||
describe("with default validation options", () => {
|
||||
describe("with valid kubeconfig", () => {
|
||||
it("does not raise exceptions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
|
||||
});
|
||||
});
|
||||
describe("with invalid context object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid cluster object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid user object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid exec command", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateCluster as false", () => {
|
||||
describe("with invalid cluster object", () => {
|
||||
it("does not raise exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateUser as false", () => {
|
||||
describe("with invalid user object", () => {
|
||||
it("does not raise excpetions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateExec as false", () => {
|
||||
describe("with invalid exec object", () => {
|
||||
it("does not raise excpetions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { SearchStore } from "../search-store";
|
||||
|
||||
let searchStore: SearchStore = null;
|
||||
@ -29,28 +25,28 @@ describe("search store tests", () => {
|
||||
expect(searchStore.occurrences).toEqual([]);
|
||||
});
|
||||
|
||||
it("find 3 occurences across 3 lines", () => {
|
||||
it("find 3 occurrences across 3 lines", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
expect(searchStore.occurrences).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it("find occurences within 1 line (case-insensitive)", () => {
|
||||
it("find occurrences within 1 line (case-insensitive)", () => {
|
||||
searchStore.onSearch(logs, "Starting");
|
||||
expect(searchStore.occurrences).toEqual([2, 2]);
|
||||
});
|
||||
|
||||
it("sets overlay index equal to first occurence", () => {
|
||||
it("sets overlay index equal to first occurrence", () => {
|
||||
searchStore.onSearch(logs, "Replica");
|
||||
expect(searchStore.activeOverlayIndex).toBe(0);
|
||||
});
|
||||
|
||||
it("set overlay index to next occurence", () => {
|
||||
it("set overlay index to next occurrence", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
searchStore.setNextOverlayActive();
|
||||
expect(searchStore.activeOverlayIndex).toBe(1);
|
||||
});
|
||||
|
||||
it("sets overlay to last occurence", () => {
|
||||
it("sets overlay to last occurrence", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
searchStore.setPrevOverlayActive();
|
||||
expect(searchStore.activeOverlayIndex).toBe(2);
|
||||
@ -62,7 +58,7 @@ describe("search store tests", () => {
|
||||
});
|
||||
|
||||
it("escapes string for using in regex", () => {
|
||||
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||
const regex = SearchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||
|
||||
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
||||
});
|
||||
@ -77,4 +73,4 @@ describe("search store tests", () => {
|
||||
searchStore.onSearch(logs, "Starting");
|
||||
expect(searchStore.totalFinds).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import { Console } from "console";
|
||||
|
||||
console = new Console(process.stdout, process.stderr);
|
||||
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.mock("electron", () => {
|
||||
@ -5,7 +9,8 @@ jest.mock("electron", () => {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en"
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -101,4 +106,4 @@ describe("user store tests", () => {
|
||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en"
|
||||
},
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
on: jest.fn()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Workspace, WorkspaceStore } from "../workspace-store";
|
||||
|
||||
describe("workspace store tests", () => {
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(async () => {
|
||||
WorkspaceStore.resetInstance();
|
||||
mockFs({ tmp: { "lens-workspace-store.json": "{}" } });
|
||||
|
||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("default workspace should always exist", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
expect(ws.workspaces.size).toBe(1);
|
||||
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
|
||||
});
|
||||
|
||||
it("cannot remove the default workspace", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
|
||||
});
|
||||
|
||||
it("can update workspace description", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
const workspace = ws.addWorkspace(new Workspace({
|
||||
id: "foobar",
|
||||
name: "foobar",
|
||||
}));
|
||||
|
||||
workspace.description = "Foobar description";
|
||||
ws.updateWorkspace(workspace);
|
||||
|
||||
expect(ws.getById("foobar").description).toBe("Foobar description");
|
||||
});
|
||||
|
||||
it("can add workspaces", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "123",
|
||||
name: "foobar",
|
||||
}));
|
||||
|
||||
const workspace = ws.getById("123");
|
||||
|
||||
expect(workspace.name).toBe("foobar");
|
||||
expect(workspace.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot set a non-existent workspace to be active", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
expect(() => ws.setActive("abc")).toThrow("doesn't exist");
|
||||
});
|
||||
|
||||
it("can set a existent workspace to be active", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "abc",
|
||||
name: "foobar",
|
||||
}));
|
||||
|
||||
expect(() => ws.setActive("abc")).not.toThrowError();
|
||||
});
|
||||
|
||||
it("can remove a workspace", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "123",
|
||||
name: "foobar",
|
||||
}));
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "1234",
|
||||
name: "foobar 1",
|
||||
}));
|
||||
ws.removeWorkspaceById("123");
|
||||
|
||||
expect(ws.workspaces.size).toBe(2);
|
||||
});
|
||||
|
||||
it("cannot create workspace with existent name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "someid",
|
||||
name: "default",
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
});
|
||||
|
||||
it("cannot create workspace with empty name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: "",
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
});
|
||||
|
||||
it("cannot create workspace with ' ' name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: " ",
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
});
|
||||
|
||||
it("trim workspace name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: "default ",
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a non-empty config", () => {
|
||||
beforeEach(async () => {
|
||||
WorkspaceStore.resetInstance();
|
||||
mockFs({
|
||||
tmp: {
|
||||
"lens-workspace-store.json": JSON.stringify({
|
||||
currentWorkspace: "abc",
|
||||
workspaces: [{
|
||||
id: "abc",
|
||||
name: "test"
|
||||
}, {
|
||||
id: "default",
|
||||
name: "default"
|
||||
}]
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("doesn't revert to default workspace", async () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
expect(ws.currentWorkspaceId).toBe("abc");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -19,7 +19,7 @@ export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
||||
* Note: T should only contain base JSON serializable types.
|
||||
*/
|
||||
export abstract class BaseStore<T = any> extends Singleton {
|
||||
protected storeConfig: Config<T>;
|
||||
protected storeConfig?: Config<T>;
|
||||
protected syncDisposers: Function[] = [];
|
||||
|
||||
whenLoaded = when(() => this.isLoaded);
|
||||
@ -36,7 +36,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
||||
}
|
||||
|
||||
get name() {
|
||||
return path.basename(this.storeConfig.path);
|
||||
return path.basename(this.path);
|
||||
}
|
||||
|
||||
protected get syncRendererChannel() {
|
||||
@ -48,7 +48,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.storeConfig.path;
|
||||
return this.storeConfig?.path || "";
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
@ -82,10 +82,13 @@ export abstract class BaseStore<T = any> extends Singleton {
|
||||
|
||||
protected async saveToFile(model: T) {
|
||||
logger.info(`[STORE]: SAVING ${this.path}`);
|
||||
|
||||
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
||||
Object.entries(model).forEach(([key, value]) => {
|
||||
this.storeConfig.set(key, value);
|
||||
});
|
||||
if (this.storeConfig) {
|
||||
for (const [key, value] of Object.entries(model)) {
|
||||
this.storeConfig.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableSync() {
|
||||
|
||||
56
src/common/catalog-category-registry.ts
Normal file
56
src/common/catalog-category-registry.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { CatalogCategory, CatalogEntityData } from "./catalog-entity";
|
||||
|
||||
export class CatalogCategoryRegistry {
|
||||
@observable protected categories: CatalogCategory[] = [];
|
||||
|
||||
@action add(category: CatalogCategory) {
|
||||
this.categories.push(category);
|
||||
}
|
||||
|
||||
@action remove(category: CatalogCategory) {
|
||||
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
return toJS(this.categories);
|
||||
}
|
||||
|
||||
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
|
||||
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
|
||||
}
|
||||
|
||||
getEntityForData(data: CatalogEntityData) {
|
||||
const category = this.getCategoryForEntity(data);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const version = splitApiVersion[1];
|
||||
|
||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||
|
||||
if (!specVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new specVersion.entityClass(data);
|
||||
}
|
||||
|
||||
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData) {
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
const category = this.categories.find((category) => {
|
||||
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||
});
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
return category as T;
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
2
src/common/catalog-entities/index.ts
Normal file
2
src/common/catalog-entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./kubernetes-cluster";
|
||||
export * from "./web-link";
|
||||
105
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
105
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { observable } from "mobx";
|
||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||
import { clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { clusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
|
||||
export type KubernetesClusterSpec = {
|
||||
kubeconfigPath: string;
|
||||
kubeconfigContext: string;
|
||||
};
|
||||
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
phase: "connected" | "disconnected";
|
||||
}
|
||||
|
||||
export class KubernetesCluster implements CatalogEntity {
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "KubernetesCluster";
|
||||
@observable public metadata: CatalogEntityMetadata;
|
||||
@observable public status: KubernetesClusterStatus;
|
||||
@observable public spec: KubernetesClusterSpec;
|
||||
|
||||
constructor(data: CatalogEntityData) {
|
||||
this.metadata = data.metadata;
|
||||
this.status = data.status as KubernetesClusterStatus;
|
||||
this.spec = data.spec as KubernetesClusterSpec;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.metadata.uid;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
async onRun(context: CatalogEntityActionContext) {
|
||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||
}
|
||||
|
||||
async onDetailsOpen() {
|
||||
//
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
context.menuItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
title: "Settings",
|
||||
onClick: async () => context.navigate(`/cluster/${this.metadata.uid}/settings`)
|
||||
},
|
||||
{
|
||||
icon: "delete",
|
||||
title: "Delete",
|
||||
onClick: async () => clusterStore.removeById(this.metadata.uid),
|
||||
confirm: {
|
||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (this.status.active) {
|
||||
context.menuItems.unshift({
|
||||
icon: "link_off",
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
clusterStore.deactivate(this.metadata.uid);
|
||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||
|
||||
if (category) category.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KubernetesClusterCategory extends EventEmitter implements CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Kubernetes Clusters"
|
||||
};
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: KubernetesCluster
|
||||
}
|
||||
],
|
||||
names: {
|
||||
kind: "KubernetesCluster"
|
||||
}
|
||||
};
|
||||
|
||||
getId() {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||
71
src/common/catalog-entities/web-link.ts
Normal file
71
src/common/catalog-entities/web-link.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { observable } from "mobx";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||
|
||||
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||
phase: "valid" | "invalid";
|
||||
}
|
||||
|
||||
export type WebLinkSpec = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export class WebLink implements CatalogEntity {
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "KubernetesCluster";
|
||||
@observable public metadata: CatalogEntityMetadata;
|
||||
@observable public status: WebLinkStatus;
|
||||
@observable public spec: WebLinkSpec;
|
||||
|
||||
constructor(data: CatalogEntityData) {
|
||||
this.metadata = data.metadata;
|
||||
this.status = data.status as WebLinkStatus;
|
||||
this.spec = data.spec as WebLinkSpec;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.metadata.uid;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
async onRun() {
|
||||
window.open(this.spec.url, "_blank");
|
||||
}
|
||||
|
||||
async onDetailsOpen() {
|
||||
//
|
||||
}
|
||||
|
||||
async onContextMenuOpen() {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export class WebLinkCategory implements CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Web Links"
|
||||
};
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink
|
||||
}
|
||||
],
|
||||
names: {
|
||||
kind: "WebLink"
|
||||
}
|
||||
};
|
||||
|
||||
getId() {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||
26
src/common/catalog-entity-registry.ts
Normal file
26
src/common/catalog-entity-registry.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { action, computed, observable, IObservableArray } from "mobx";
|
||||
import { CatalogEntity } from "./catalog-entity";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected sources = observable.map<string, IObservableArray<CatalogEntity>>([], { deep: true });
|
||||
|
||||
@action addSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
this.sources.set(id, source);
|
||||
}
|
||||
|
||||
@action removeSource(id: string) {
|
||||
this.sources.delete(id);
|
||||
}
|
||||
|
||||
@computed get items(): CatalogEntity[] {
|
||||
return Array.from(this.sources.values()).flat();
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||
|
||||
return items as T[];
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry();
|
||||
75
src/common/catalog-entity.ts
Normal file
75
src/common/catalog-entity.ts
Normal file
@ -0,0 +1,75 @@
|
||||
export interface CatalogCategoryVersion {
|
||||
name: string;
|
||||
entityClass: { new(data: CatalogEntityData): CatalogEntity };
|
||||
}
|
||||
|
||||
export interface CatalogCategory {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
}
|
||||
spec: {
|
||||
group: string;
|
||||
versions: CatalogCategoryVersion[];
|
||||
names: {
|
||||
kind: string;
|
||||
}
|
||||
}
|
||||
getId: () => string;
|
||||
}
|
||||
|
||||
export type CatalogEntityMetadata = {
|
||||
uid: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
labels: {
|
||||
[key: string]: string;
|
||||
}
|
||||
[key: string]: string | object;
|
||||
};
|
||||
|
||||
export type CatalogEntityStatus = {
|
||||
phase: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export interface CatalogEntityActionContext {
|
||||
navigate: (url: string) => void;
|
||||
setCommandPaletteContext: (context?: CatalogEntity) => void;
|
||||
}
|
||||
|
||||
export type CatalogEntityContextMenu = {
|
||||
icon: string;
|
||||
title: string;
|
||||
onClick: () => Promise<void>;
|
||||
confirm?: {
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CatalogEntityContextMenuContext {
|
||||
navigate: (url: string) => void;
|
||||
menuItems: CatalogEntityContextMenu[];
|
||||
}
|
||||
|
||||
export type CatalogEntityData = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: CatalogEntityMetadata;
|
||||
status: CatalogEntityStatus;
|
||||
spec: {
|
||||
[key: string]: any;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CatalogEntity extends CatalogEntityData {
|
||||
getId: () => string;
|
||||
getName: () => string;
|
||||
onRun: (context: CatalogEntityActionContext) => Promise<void>;
|
||||
onDetailsOpen: (context: CatalogEntityActionContext) => Promise<void>;
|
||||
onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise<void>;
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import { workspaceStore } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
@ -12,9 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||
import _ from "lodash";
|
||||
import move from "array-move";
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
clusterId: string;
|
||||
@ -34,7 +31,7 @@ export type ClusterPrometheusMetadata = {
|
||||
|
||||
export interface ClusterStoreModel {
|
||||
activeCluster?: ClusterId; // last opened cluster
|
||||
clusters?: ClusterModel[]
|
||||
clusters?: ClusterModel[];
|
||||
}
|
||||
|
||||
export type ClusterId = string;
|
||||
@ -46,8 +43,12 @@ export interface ClusterModel {
|
||||
/** Path to cluster kubeconfig */
|
||||
kubeConfigPath: string;
|
||||
|
||||
/** Workspace id */
|
||||
workspace?: WorkspaceId;
|
||||
/**
|
||||
* Workspace id
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
workspace?: string;
|
||||
|
||||
/** User context in kubeconfig */
|
||||
contextName?: string;
|
||||
@ -70,12 +71,13 @@ export interface ClusterModel {
|
||||
kubeConfig?: string; // yaml
|
||||
}
|
||||
|
||||
export interface ClusterPreferences extends ClusterPrometheusPreferences{
|
||||
export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
||||
terminalCWD?: string;
|
||||
clusterName?: string;
|
||||
iconOrder?: number;
|
||||
icon?: string;
|
||||
httpsProxy?: string;
|
||||
hiddenMetrics?: string[];
|
||||
}
|
||||
|
||||
export interface ClusterPrometheusPreferences {
|
||||
@ -211,27 +213,24 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.activeCluster === id;
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id: ClusterId) {
|
||||
const clusterId = this.clusters.has(id) ? id : null;
|
||||
|
||||
this.activeCluster = clusterId;
|
||||
workspaceStore.setLastActiveClusterId(clusterId);
|
||||
isMetricHidden(resource: ResourceType) {
|
||||
return Boolean(this.active?.preferences.hiddenMetrics?.includes(resource));
|
||||
}
|
||||
|
||||
@action
|
||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||
const clusters = this.getByWorkspaceId(workspace);
|
||||
setActive(clusterId: ClusterId) {
|
||||
const cluster = this.clusters.get(clusterId);
|
||||
|
||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||
throw new Error(`invalid from<->to arguments`);
|
||||
if (!cluster?.enabled) {
|
||||
clusterId = null;
|
||||
}
|
||||
|
||||
move.mutate(clusters, from, to);
|
||||
this.activeCluster = clusterId;
|
||||
}
|
||||
|
||||
for (const i in clusters) {
|
||||
// This resets the iconOrder to the current display order
|
||||
clusters[i].preferences.iconOrder = +i;
|
||||
deactivate(id: ClusterId) {
|
||||
if (this.isActive(id)) {
|
||||
this.setActive(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,15 +238,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
|
||||
getById(id: ClusterId): Cluster {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
|
||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
||||
const clusters = Array.from(this.clusters.values())
|
||||
.filter(cluster => cluster.workspace === workspaceId);
|
||||
|
||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
||||
getById(id: ClusterId): Cluster | null {
|
||||
return this.clusters.get(id) ?? null;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -301,13 +293,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeByWorkspaceId(workspaceId: string) {
|
||||
this.getByWorkspaceId(workspaceId).forEach(cluster => {
|
||||
this.removeById(cluster.id);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||
const currentClusters = this.clusters.toJS();
|
||||
@ -323,7 +308,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
} else {
|
||||
cluster = new Cluster(clusterModel);
|
||||
|
||||
if (!cluster.isManaged) {
|
||||
if (!cluster.isManaged && cluster.apiUrl) {
|
||||
cluster.enabled = true;
|
||||
}
|
||||
}
|
||||
@ -337,7 +322,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
});
|
||||
|
||||
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
|
||||
this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null;
|
||||
this.clusters.replace(newClusters);
|
||||
this.removedClusters.replace(removedClusters);
|
||||
}
|
||||
@ -354,10 +339,11 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
export function getClusterIdFromHost(hostname: string): ClusterId {
|
||||
const subDomains = hostname.split(":")[0].split(".");
|
||||
export function getClusterIdFromHost(host: string): ClusterId | undefined {
|
||||
// e.g host == "%clusterId.localhost:45345"
|
||||
const subDomains = host.split(":")[0].split(".");
|
||||
|
||||
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
|
||||
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
|
||||
}
|
||||
|
||||
export function getClusterFrameUrl(clusterId: ClusterId) {
|
||||
@ -365,7 +351,7 @@ export function getClusterFrameUrl(clusterId: ClusterId) {
|
||||
}
|
||||
|
||||
export function getHostedClusterId() {
|
||||
return getClusterIdFromHost(location.hostname);
|
||||
return getClusterIdFromHost(location.host);
|
||||
}
|
||||
|
||||
export function getHostedCluster(): Cluster {
|
||||
|
||||
@ -10,4 +10,4 @@ export class ExecValidationNotFoundError extends Error {
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
src/common/hotbar-store.ts
Normal file
67
src/common/hotbar-store.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { action, comparer, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
|
||||
export interface HotbarItem {
|
||||
entity: {
|
||||
uid: string;
|
||||
};
|
||||
params?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Hotbar {
|
||||
name: string;
|
||||
items: HotbarItem[];
|
||||
}
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
}
|
||||
|
||||
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||
this.hotbars = data.hotbars || [{
|
||||
name: "default",
|
||||
items: []
|
||||
}];
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
add(hotbar: Hotbar) {
|
||||
this.hotbars.push(hotbar);
|
||||
}
|
||||
|
||||
remove(hotbar: Hotbar) {
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
hotbars: this.hotbars
|
||||
};
|
||||
|
||||
return toJS(model, {
|
||||
recurseEverything: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
|
||||
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { onCorrect, onceCorrect } from "../type-enforced-ipc";
|
||||
|
||||
describe("type enforced ipc tests", () => {
|
||||
describe("onCorrect tests", () => {
|
||||
it("should call the handler if the args are valid", () => {
|
||||
let called = false;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called = true;
|
||||
const verifier = (args: unknown[]): args is [] => true;
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("should not call the handler if the args are not valid", () => {
|
||||
let called = false;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called = true;
|
||||
const verifier = (args: unknown[]): args is [] => false;
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
it("should call the handler twice if the args are valid on two emits", () => {
|
||||
let called = 0;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called += 1;
|
||||
const verifier = (args: unknown[]): args is [] => true;
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
source.emit(channel);
|
||||
expect(called).toBe(2);
|
||||
});
|
||||
|
||||
it("should call the handler twice if the args are [valid, invalid, valid]", () => {
|
||||
let called = 0;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called += 1;
|
||||
const results = [true, false, true];
|
||||
const verifier = (args: unknown[]): args is [] => results.pop();
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
source.emit(channel);
|
||||
source.emit(channel);
|
||||
expect(called).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onceCorrect tests", () => {
|
||||
it("should call the handler if the args are valid", () => {
|
||||
let called = false;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called = true;
|
||||
const verifier = (args: unknown[]): args is [] => true;
|
||||
const channel = "foobar";
|
||||
|
||||
onceCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("should not call the handler if the args are not valid", () => {
|
||||
let called = false;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called = true;
|
||||
const verifier = (args: unknown[]): args is [] => false;
|
||||
const channel = "foobar";
|
||||
|
||||
onceCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
it("should call the handler only once even if args are valid multiple times", () => {
|
||||
let called = 0;
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called += 1;
|
||||
const verifier = (args: unknown[]): args is [] => true;
|
||||
const channel = "foobar";
|
||||
|
||||
onceCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel);
|
||||
source.emit(channel);
|
||||
expect(called).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the handler on only the first valid set of args", () => {
|
||||
let called = "";
|
||||
let verifierCalled = 0;
|
||||
const source = new EventEmitter();
|
||||
const listener = (info: any, arg: string) => called = arg;
|
||||
const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0;
|
||||
const channel = "foobar";
|
||||
|
||||
onceCorrect({ source, listener, verifier, channel });
|
||||
|
||||
source.emit(channel, {}, "a");
|
||||
source.emit(channel, {}, "b");
|
||||
source.emit(channel, {}, "c");
|
||||
source.emit(channel, {}, "d");
|
||||
source.emit(channel, {}, "e");
|
||||
source.emit(channel, {}, "f");
|
||||
source.emit(channel, {}, "g");
|
||||
source.emit(channel, {}, "h");
|
||||
source.emit(channel, {}, "i");
|
||||
expect(called).toBe("c");
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/common/ipc/cluster.ipc.ts
Normal file
11
src/common/ipc/cluster.ipc.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This channel is broadcast on whenever the cluster fails to list namespaces
|
||||
* during a refresh and no `accessibleNamespaces` have been set.
|
||||
*/
|
||||
export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
|
||||
|
||||
export type ListNamespaceForbiddenArgs = [clusterId: string];
|
||||
|
||||
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
|
||||
return args.length === 1 && typeof args[0] === "string";
|
||||
}
|
||||
5
src/common/ipc/index.ts
Normal file
5
src/common/ipc/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./ipc";
|
||||
export * from "./invalid-kubeconfig";
|
||||
export * from "./update-available.ipc";
|
||||
export * from "./cluster.ipc";
|
||||
export * from "./type-enforced-ipc";
|
||||
3
src/common/ipc/invalid-kubeconfig/index.ts
Normal file
3
src/common/ipc/invalid-kubeconfig/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const InvalidKubeconfigChannel = "invalid-kubeconfig";
|
||||
|
||||
export type InvalidKubeConfigArgs = [clusterId: string];
|
||||
@ -3,10 +3,13 @@
|
||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||
|
||||
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
||||
import logger from "../main/logger";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames";
|
||||
import { toJS } from "mobx";
|
||||
import logger from "../../main/logger";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||
|
||||
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
||||
const subFramesChannel = "ipc:get-sub-frames";
|
||||
|
||||
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
|
||||
ipcMain.handle(channel, listener);
|
||||
}
|
||||
|
||||
@ -14,38 +17,39 @@ export async function requestMain(channel: string, ...args: any[]) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
|
||||
async function getSubFrames(): Promise<ClusterFrameInfo[]> {
|
||||
const subFrames: ClusterFrameInfo[] = [];
|
||||
|
||||
clusterFrameMap.forEach(frameInfo => {
|
||||
subFrames.push(frameInfo);
|
||||
});
|
||||
|
||||
return subFrames;
|
||||
function getSubFrames(): ClusterFrameInfo[] {
|
||||
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
|
||||
}
|
||||
|
||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||
export async function broadcastMessage(channel: string, ...args: any[]) {
|
||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||
|
||||
if (!views) return;
|
||||
|
||||
views.forEach(webContent => {
|
||||
const type = webContent.getType();
|
||||
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||
webContent.send(channel, ...args);
|
||||
getSubFrames().then((frames) => {
|
||||
frames.map((frameInfo) => {
|
||||
webContent.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||
});
|
||||
}).catch((e) => e);
|
||||
});
|
||||
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
} else {
|
||||
ipcMain.emit(channel, ...args);
|
||||
}
|
||||
|
||||
for (const view of views) {
|
||||
const type = view.getType();
|
||||
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args });
|
||||
view.send(channel, ...args);
|
||||
|
||||
try {
|
||||
const subFrames: ClusterFrameInfo[] = ipcRenderer
|
||||
? await requestMain(subFramesChannel)
|
||||
: getSubFrames();
|
||||
|
||||
for (const frameInfo of subFrames) {
|
||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[IPC]: failed to send IPC message", { error: String(error) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
|
||||
@ -73,3 +77,9 @@ export function unsubscribeAllFromBroadcast(channel: string) {
|
||||
ipcMain.removeAllListeners(channel);
|
||||
}
|
||||
}
|
||||
|
||||
export function bindBroadcastHandlers() {
|
||||
handleRequest(subFramesChannel, () => {
|
||||
return getSubFrames();
|
||||
});
|
||||
}
|
||||
71
src/common/ipc/type-enforced-ipc.ts
Normal file
71
src/common/ipc/type-enforced-ipc.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { EventEmitter } from "events";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
||||
export type Rest<T> = T extends [any, ...infer R] ? R : [];
|
||||
|
||||
/**
|
||||
* Adds a listener to `source` that waits for the first IPC message with the correct
|
||||
* argument data is sent.
|
||||
* @param channel The channel to be listened on
|
||||
* @param listener The function for the channel to be called if the args of the correct type
|
||||
* @param verifier The function to be called to verify that the args are the correct type
|
||||
*/
|
||||
export function onceCorrect<
|
||||
EM extends EventEmitter,
|
||||
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): void {
|
||||
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
|
||||
if (verifier(args)) {
|
||||
source.removeListener(channel, handler); // remove immediately
|
||||
|
||||
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
|
||||
.catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error }));
|
||||
} else {
|
||||
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
|
||||
}
|
||||
}
|
||||
|
||||
source.on(channel, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to `source` that checks to verify the arguments before calling the handler.
|
||||
* @param channel The channel to be listened on
|
||||
* @param listener The function for the channel to be called if the args of the correct type
|
||||
* @param verifier The function to be called to verify that the args are the correct type
|
||||
*/
|
||||
export function onCorrect<
|
||||
EM extends EventEmitter,
|
||||
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): void {
|
||||
source.on(channel, (event, ...args: unknown[]) => {
|
||||
if (verifier(args)) {
|
||||
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
|
||||
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
|
||||
} else {
|
||||
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
|
||||
}
|
||||
});
|
||||
}
|
||||
45
src/common/ipc/update-available.ipc.ts
Normal file
45
src/common/ipc/update-available.ipc.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { UpdateInfo } from "electron-updater";
|
||||
|
||||
export const UpdateAvailableChannel = "update-available";
|
||||
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||
|
||||
export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
|
||||
|
||||
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
|
||||
if (args.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof args[0] !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof args[1] !== "object" || args[1] === null) {
|
||||
// TODO: improve this checking
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type BackchannelArg = {
|
||||
doUpdate: false;
|
||||
} | {
|
||||
doUpdate: true;
|
||||
now: boolean;
|
||||
};
|
||||
|
||||
export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
|
||||
|
||||
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
||||
if (args.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof args[0] !== "object" || args[0] === null) {
|
||||
// TODO: improve this checking
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7,6 +7,12 @@ import logger from "../main/logger";
|
||||
import commandExists from "command-exists";
|
||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||
|
||||
export type KubeConfigValidationOpts = {
|
||||
validateCluster?: boolean;
|
||||
validateUser?: boolean;
|
||||
validateExec?: boolean;
|
||||
};
|
||||
|
||||
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
||||
|
||||
function resolveTilde(filePath: string) {
|
||||
@ -151,28 +157,43 @@ export function getNodeWarningConditions(node: V1Node) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates kubeconfig supplied in the add clusters screen. At present this will just validate
|
||||
* the User struct, specifically the command passed to the exec substructure.
|
||||
*/
|
||||
export function validateKubeConfig (config: KubeConfig) {
|
||||
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||
*/
|
||||
export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) {
|
||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
||||
// will be called when we add a new cluster to Lens
|
||||
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
|
||||
|
||||
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
||||
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
|
||||
// Validate the Context Object
|
||||
if (!contextObject) {
|
||||
throw new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate the Cluster Object
|
||||
if (validateCluster && !config.getCluster(contextObject.cluster)) {
|
||||
throw new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
const user = config.getUser(contextObject.user);
|
||||
|
||||
// Validate the User Object
|
||||
const user = config.getCurrentUser();
|
||||
|
||||
if (user.exec) {
|
||||
if (validateUser && !user) {
|
||||
throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate exec command if present
|
||||
if (validateExec && user?.exec) {
|
||||
const execCommand = user.exec["command"];
|
||||
// check if the command is absolute or not
|
||||
const isAbsolute = path.isAbsolute(execCommand);
|
||||
|
||||
// validate the exec struct in the user object, start with the command field
|
||||
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
||||
|
||||
if (!commandExists.sync(execCommand)) {
|
||||
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`);
|
||||
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`);
|
||||
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,4 +10,4 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry
|
||||
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
||||
});
|
||||
|
||||
export const prometheusProviders = PrometheusProviderRegistry.getProviders();
|
||||
export const prometheusProviders = PrometheusProviderRegistry.getProviders();
|
||||
|
||||
36
src/common/protocol-handler/error.ts
Normal file
36
src/common/protocol-handler/error.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import Url from "url-parse";
|
||||
|
||||
export enum RoutingErrorType {
|
||||
INVALID_PROTOCOL = "invalid-protocol",
|
||||
INVALID_HOST = "invalid-host",
|
||||
INVALID_PATHNAME = "invalid-pathname",
|
||||
NO_HANDLER = "no-handler",
|
||||
NO_EXTENSION_ID = "no-ext-id",
|
||||
MISSING_EXTENSION = "missing-ext",
|
||||
}
|
||||
|
||||
export class RoutingError extends Error {
|
||||
/**
|
||||
* Will be set if the routing error originated in an extension route table
|
||||
*/
|
||||
public extensionName?: string;
|
||||
|
||||
constructor(public type: RoutingErrorType, public url: Url) {
|
||||
super("routing error");
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
switch (this.type) {
|
||||
case RoutingErrorType.INVALID_HOST:
|
||||
return "invalid host";
|
||||
case RoutingErrorType.INVALID_PROTOCOL:
|
||||
return "invalid protocol";
|
||||
case RoutingErrorType.INVALID_PATHNAME:
|
||||
return "invalid pathname";
|
||||
case RoutingErrorType.NO_EXTENSION_ID:
|
||||
return "no extension ID";
|
||||
case RoutingErrorType.MISSING_EXTENSION:
|
||||
return "extension not found";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/common/protocol-handler/index.ts
Normal file
2
src/common/protocol-handler/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./error";
|
||||
export * from "./router";
|
||||
220
src/common/protocol-handler/router.ts
Normal file
220
src/common/protocol-handler/router.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { match, matchPath } from "react-router";
|
||||
import { countBy } from "lodash";
|
||||
import { Singleton } from "../utils";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
import logger from "../../main/logger";
|
||||
import Url from "url-parse";
|
||||
import { RoutingError, RoutingErrorType } from "./error";
|
||||
import { extensionsStore } from "../../extensions/extensions-store";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { LensExtension } from "../../extensions/lens-extension";
|
||||
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
|
||||
|
||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||
|
||||
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
||||
export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
||||
|
||||
/**
|
||||
* These two names are long and cumbersome by design so as to decrease the chances
|
||||
* of an extension using the same names.
|
||||
*
|
||||
* Though under the current (2021/01/18) implementation, these are never matched
|
||||
* against in the final matching so their names are less of a concern.
|
||||
*/
|
||||
const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
||||
const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
||||
|
||||
export abstract class LensProtocolRouter extends Singleton {
|
||||
// Map between path schemas and the handlers
|
||||
protected internalRoutes = new Map<string, RouteHandler>();
|
||||
|
||||
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||
|
||||
protected static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url the parsed URL that initiated the `lens://` protocol
|
||||
*/
|
||||
protected _routeToInternal(url: Url): void {
|
||||
this._route(Array.from(this.internalRoutes.entries()), url);
|
||||
}
|
||||
|
||||
/**
|
||||
* match against all matched URIs, returning either the first exact match or
|
||||
* the most specific match if none are exact.
|
||||
* @param routes the array of path schemas, handler pairs to match against
|
||||
* @param url the url (in its current state)
|
||||
*/
|
||||
protected _findMatchingRoute(routes: [string, RouteHandler][], url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
||||
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
||||
|
||||
for (const [schema, handler] of routes) {
|
||||
const match = matchPath(url.pathname, { path: schema });
|
||||
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// prefer an exact match
|
||||
if (match.isExact) {
|
||||
return [match, handler];
|
||||
}
|
||||
|
||||
matches.push([match, handler]);
|
||||
}
|
||||
|
||||
// if no exact match pick the one that is the most specific
|
||||
return matches.sort(([a], [b]) => compareMatches(a, b))[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* find the most specific matching handler and call it
|
||||
* @param routes the array of (path schemas, handler) paris to match against
|
||||
* @param url the url (in its current state)
|
||||
*/
|
||||
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
||||
const route = this._findMatchingRoute(routes, url);
|
||||
|
||||
if (!route) {
|
||||
const data: Record<string, string> = { url: url.toString() };
|
||||
|
||||
if (extensionName) {
|
||||
data.extensionName = extensionName;
|
||||
}
|
||||
|
||||
return void logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||
}
|
||||
|
||||
const [match, handler] = route;
|
||||
|
||||
const params: RouteParams = {
|
||||
pathname: match.params,
|
||||
search: url.query,
|
||||
};
|
||||
|
||||
if (!match.isExact) {
|
||||
params.tail = url.pathname.slice(match.url.length);
|
||||
}
|
||||
|
||||
handler(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the matching LensExtension instance
|
||||
*
|
||||
* Note: this needs to be async so that `main`'s overloaded version can also be async
|
||||
* @param url the protocol request URI that was "open"-ed
|
||||
* @returns either the found name or the instance of `LensExtension`
|
||||
*/
|
||||
protected async _findMatchingExtensionByName(url: Url): Promise<LensExtension | string> {
|
||||
interface ExtensionUrlMatch {
|
||||
[EXTENSION_PUBLISHER_MATCH]: string;
|
||||
[EXTENSION_NAME_MATCH]: string;
|
||||
}
|
||||
|
||||
const match = matchPath<ExtensionUrlMatch>(url.pathname, LensProtocolRouter.ExtensionUrlSchema);
|
||||
|
||||
if (!match) {
|
||||
throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url);
|
||||
}
|
||||
|
||||
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||
|
||||
const extension = extensionLoader.userExtensionsByName.get(name);
|
||||
|
||||
if (!extension) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!extensionsStore.isEnabled(extension.id)) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a matching extension by the first one or two path segments of `url` and then try to `_route`
|
||||
* its correspondingly registered handlers.
|
||||
*
|
||||
* If no handlers are found or the extension is not enabled then `_missingHandlers` is called before
|
||||
* checking if more handlers have been added.
|
||||
*
|
||||
* Note: this function modifies its argument, do not reuse
|
||||
* @param url the protocol request URI that was "open"-ed
|
||||
*/
|
||||
protected async _routeToExtension(url: Url): Promise<void> {
|
||||
const extension = await this._findMatchingExtensionByName(url);
|
||||
|
||||
if (typeof extension === "string") {
|
||||
// failed to find an extension, it returned its name
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the extension name from the path name so we don't need to match on it anymore
|
||||
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||
|
||||
const handlers = extension
|
||||
.protocolHandlers
|
||||
.map<[string, RouteHandler]>(({ pathSchema, handler }) => [pathSchema, handler]);
|
||||
|
||||
try {
|
||||
this._route(handlers, url, extension.name);
|
||||
} catch (error) {
|
||||
if (error instanceof RoutingError) {
|
||||
error.extensionName = extension.name;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a handler under the `lens://app` tree of routing.
|
||||
* @param pathSchema the URI path schema to match against for this handler
|
||||
* @param handler a function that will be called if a protocol path matches
|
||||
*/
|
||||
public addInternalHandler(urlSchema: string, handler: RouteHandler): this {
|
||||
pathToRegexp(urlSchema); // verify now that the schema is valid
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`);
|
||||
this.internalRoutes.set(urlSchema, handler);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an internal protocol handler.
|
||||
* @param pathSchema the path schema that the handler was registered under
|
||||
*/
|
||||
public removeInternalHandler(urlSchema: string): void {
|
||||
this.internalRoutes.delete(urlSchema);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* a comparison function for `array.sort(...)`. Sort order should be most path
|
||||
* parts to least path parts.
|
||||
* @param a the left side to compare
|
||||
* @param b the right side to compare
|
||||
*/
|
||||
function compareMatches<T>(a: match<T>, b: match<T>): number {
|
||||
if (a.path === "/") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.path === "/") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return countBy(b.path)["/"] - countBy(a.path)["/"];
|
||||
}
|
||||
@ -1,42 +1,44 @@
|
||||
import { getHostedCluster } from "./cluster-store";
|
||||
|
||||
export type KubeResource =
|
||||
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" |
|
||||
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
|
||||
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
|
||||
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
|
||||
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
|
||||
|
||||
export interface KubeApiResource {
|
||||
resource: KubeResource; // valid resource name
|
||||
kind: string; // resource type (e.g. "Namespace")
|
||||
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
|
||||
group?: string; // api-group
|
||||
}
|
||||
|
||||
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
|
||||
export const apiResources: KubeApiResource[] = [
|
||||
{ resource: "configmaps" },
|
||||
{ resource: "cronjobs", group: "batch" },
|
||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||
{ resource: "daemonsets", group: "apps" },
|
||||
{ resource: "deployments", group: "apps" },
|
||||
{ resource: "endpoints" },
|
||||
{ resource: "events" },
|
||||
{ resource: "horizontalpodautoscalers" },
|
||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
||||
{ resource: "jobs", group: "batch" },
|
||||
{ resource: "namespaces" },
|
||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
||||
{ resource: "nodes" },
|
||||
{ resource: "persistentvolumes" },
|
||||
{ resource: "persistentvolumeclaims" },
|
||||
{ resource: "pods" },
|
||||
{ resource: "poddisruptionbudgets" },
|
||||
{ resource: "podsecuritypolicies" },
|
||||
{ resource: "resourcequotas" },
|
||||
{ resource: "replicasets", group: "apps" },
|
||||
{ resource: "secrets" },
|
||||
{ resource: "services" },
|
||||
{ resource: "statefulsets", group: "apps" },
|
||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
||||
{ kind: "ConfigMap", apiName: "configmaps" },
|
||||
{ kind: "CronJob", apiName: "cronjobs", group: "batch" },
|
||||
{ kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||
{ kind: "DaemonSet", apiName: "daemonsets", group: "apps" },
|
||||
{ kind: "Deployment", apiName: "deployments", group: "apps" },
|
||||
{ kind: "Endpoint", apiName: "endpoints" },
|
||||
{ kind: "Event", apiName: "events" },
|
||||
{ kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" },
|
||||
{ kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" },
|
||||
{ kind: "Job", apiName: "jobs", group: "batch" },
|
||||
{ kind: "Namespace", apiName: "namespaces" },
|
||||
{ kind: "LimitRange", apiName: "limitranges" },
|
||||
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
|
||||
{ kind: "Node", apiName: "nodes" },
|
||||
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
||||
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
||||
{ kind: "Pod", apiName: "pods" },
|
||||
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets", group: "policy" },
|
||||
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
||||
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
||||
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
||||
{ kind: "Secret", apiName: "secrets" },
|
||||
{ kind: "Service", apiName: "services" },
|
||||
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
|
||||
{ kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" },
|
||||
];
|
||||
|
||||
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
||||
|
||||
@ -1,10 +1,42 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { action, computed, observable,reaction } from "mobx";
|
||||
import { dockStore } from "../renderer/components/dock/dock.store";
|
||||
import { autobind } from "../renderer/utils";
|
||||
|
||||
export class SearchStore {
|
||||
@observable searchQuery = ""; // Text in the search input
|
||||
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
|
||||
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
|
||||
/**
|
||||
* An utility methods escaping user string to safely pass it into new Regex(variable)
|
||||
* @param value Unescaped string
|
||||
*/
|
||||
public static escapeRegex(value?: string): string {
|
||||
return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Text in the search input
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable searchQuery = "";
|
||||
|
||||
/**
|
||||
* Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable occurrences: number[] = [];
|
||||
|
||||
/**
|
||||
* Index within the occurrences array. Showing where is activeOverlay currently located
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable activeOverlayIndex = -1;
|
||||
|
||||
constructor() {
|
||||
reaction(() => dockStore.selectedTabId, () => {
|
||||
searchStore.reset();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default activeOverlayIndex
|
||||
@ -12,49 +44,45 @@ export class SearchStore {
|
||||
* @param query Search query from input
|
||||
*/
|
||||
@action
|
||||
onSearch(text: string[], query = this.searchQuery) {
|
||||
public onSearch(text?: string[] | null, query = this.searchQuery): void {
|
||||
this.searchQuery = query;
|
||||
|
||||
if (!query) {
|
||||
this.reset();
|
||||
return this.reset();
|
||||
}
|
||||
|
||||
this.occurrences = this.findOccurrences(text ?? [], query);
|
||||
|
||||
if (!this.occurrences.length) {
|
||||
return;
|
||||
}
|
||||
this.occurrences = this.findOccurences(text, query);
|
||||
if (!this.occurrences.length) return;
|
||||
|
||||
// If new highlighted keyword in exact same place as previous one, then no changing in active overlay
|
||||
if (this.occurrences[this.activeOverlayIndex] !== undefined) return;
|
||||
this.activeOverlayIndex = this.getNextOverlay(true);
|
||||
if (this.occurrences[this.activeOverlayIndex] === undefined) {
|
||||
this.activeOverlayIndex = this.getNextOverlay(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does searching within text array, create a list of search keyword occurences.
|
||||
* Each keyword "occurency" is saved as index of the the line where keyword founded
|
||||
* @param text An array of any textual data (logs, for example)
|
||||
* Does searching within text array, create a list of search keyword occurrences.
|
||||
* Each keyword "occurrence" is saved as index of the line where keyword was found
|
||||
* @param lines An array of any textual data (logs, for example)
|
||||
* @param query Search query from input
|
||||
* @returns {Array} Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
|
||||
* @returns Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
|
||||
*/
|
||||
findOccurences(text: string[], query: string) {
|
||||
if (!text) return [];
|
||||
const occurences: number[] = [];
|
||||
private findOccurrences(lines: string[], query?: string): number[] {
|
||||
const regex = new RegExp(SearchStore.escapeRegex(query), "gi");
|
||||
|
||||
text.forEach((line, index) => {
|
||||
const regex = new RegExp(this.escapeRegex(query), "gi");
|
||||
const matches = [...line.matchAll(regex)];
|
||||
|
||||
matches.forEach(() => occurences.push(index));
|
||||
});
|
||||
|
||||
return occurences;
|
||||
return lines
|
||||
.flatMap((line, index) => Array.from(line.matchAll(regex), () => index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting next overlay index within the occurences array
|
||||
* Getting next overlay index within the occurrences array
|
||||
* @param loopOver Allows to jump from last element to first
|
||||
* @returns {number} next overlay index
|
||||
* @returns next overlay index
|
||||
*/
|
||||
getNextOverlay(loopOver = false) {
|
||||
private getNextOverlay(loopOver = false): number {
|
||||
const next = this.activeOverlayIndex + 1;
|
||||
|
||||
if (next > this.occurrences.length - 1) {
|
||||
@ -65,11 +93,11 @@ export class SearchStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting previous overlay index within the occurences array of occurences
|
||||
* Getting previous overlay index within the occurrences array of occurrences
|
||||
* @param loopOver Allows to jump from first element to last one
|
||||
* @returns {number} prev overlay index
|
||||
* @returns previous overlay index
|
||||
*/
|
||||
getPrevOverlay(loopOver = false) {
|
||||
private getPrevOverlay(loopOver = false): number {
|
||||
const prev = this.activeOverlayIndex - 1;
|
||||
|
||||
if (prev < 0) {
|
||||
@ -80,18 +108,18 @@ export class SearchStore {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
setNextOverlayActive() {
|
||||
public setNextOverlayActive(): void {
|
||||
this.activeOverlayIndex = this.getNextOverlay(true);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
setPrevOverlayActive() {
|
||||
public setPrevOverlayActive(): void {
|
||||
this.activeOverlayIndex = this.getPrevOverlay(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets line index of where active overlay is located
|
||||
* @returns {number} A line index within the text/logs array
|
||||
* @returns A line index within the text/logs array
|
||||
*/
|
||||
@computed get activeOverlayLine(): number {
|
||||
return this.occurrences[this.activeOverlayIndex];
|
||||
@ -108,29 +136,21 @@ export class SearchStore {
|
||||
/**
|
||||
* Checks if overlay is active (to highlight it with orange background usually)
|
||||
* @param line Index of the line where overlay is located
|
||||
* @param occurence Number of the overlay within one line
|
||||
* @param occurrence Number of the overlay within one line
|
||||
*/
|
||||
@autobind()
|
||||
isActiveOverlay(line: number, occurence: number) {
|
||||
public isActiveOverlay(line: number, occurrence: number): boolean {
|
||||
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
||||
|
||||
return firstLineIndex + occurence === this.activeOverlayIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* An utility methods escaping user string to safely pass it into new Regex(variable)
|
||||
* @param value Unescaped string
|
||||
*/
|
||||
escapeRegex(value: string) {
|
||||
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
|
||||
return firstLineIndex + occurrence === this.activeOverlayIndex;
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
private reset(): void {
|
||||
this.searchQuery = "";
|
||||
this.activeOverlayIndex = -1;
|
||||
this.occurrences = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const searchStore = new SearchStore;
|
||||
export const searchStore = new SearchStore;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user