mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into allow-to-define-default-namespace-for-terminal
This commit is contained in:
commit
1fa6d000b0
@ -16,8 +16,8 @@ jobs:
|
|||||||
vmImage: windows-2019
|
vmImage: windows-2019
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_12.x:
|
node_14.x:
|
||||||
node_version: 12.x
|
node_version: 14.x
|
||||||
steps:
|
steps:
|
||||||
- powershell: |
|
- powershell: |
|
||||||
$CI_BUILD_TAG = git describe --tags
|
$CI_BUILD_TAG = git describe --tags
|
||||||
@ -48,12 +48,6 @@ jobs:
|
|||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
|
||||||
displayName: Install dependencies
|
|
||||||
|
|
||||||
- script: make build-npm
|
|
||||||
displayName: Generate npm package
|
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
@ -62,15 +56,16 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||||
BUILD_NUMBER: $(Build.BuildNumber)
|
BUILD_NUMBER: $(Build.BuildNumber)
|
||||||
|
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32"
|
||||||
displayName: Build
|
displayName: Build
|
||||||
|
|
||||||
- job: macOS
|
- job: macOS
|
||||||
pool:
|
pool:
|
||||||
vmImage: macOS-10.14
|
vmImage: macOS-11
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_12.x:
|
node_14.x:
|
||||||
node_version: 12.x
|
node_version: 14.x
|
||||||
steps:
|
steps:
|
||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
@ -99,12 +94,6 @@ jobs:
|
|||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
|
||||||
displayName: Install dependencies
|
|
||||||
|
|
||||||
- script: make build-npm
|
|
||||||
displayName: Generate npm package
|
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
@ -115,6 +104,7 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||||
BUILD_NUMBER: $(Build.BuildNumber)
|
BUILD_NUMBER: $(Build.BuildNumber)
|
||||||
|
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64"
|
||||||
displayName: Build
|
displayName: Build
|
||||||
|
|
||||||
- job: Linux
|
- job: Linux
|
||||||
@ -122,8 +112,8 @@ jobs:
|
|||||||
vmImage: ubuntu-16.04
|
vmImage: ubuntu-16.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_12.x:
|
node_14.x:
|
||||||
node_version: 12.x
|
node_version: 14.x
|
||||||
steps:
|
steps:
|
||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
@ -152,23 +142,6 @@ jobs:
|
|||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
|
||||||
displayName: Install dependencies
|
|
||||||
|
|
||||||
- script: make build-npm
|
|
||||||
displayName: Generate npm package
|
|
||||||
|
|
||||||
- bash: |
|
|
||||||
sudo chown root:root /
|
|
||||||
sudo apt-get update && sudo apt-get install -y snapd
|
|
||||||
sudo snap install snapcraft --classic
|
|
||||||
echo -n "${SNAP_LOGIN}" | base64 -i -d > snap_login
|
|
||||||
snapcraft login --with snap_login
|
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
|
||||||
env:
|
|
||||||
SNAP_LOGIN: $(SNAP_LOGIN)
|
|
||||||
displayName: Setup snapcraft
|
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -26,6 +26,7 @@ module.exports = {
|
|||||||
"**/node_modules/**/*",
|
"**/node_modules/**/*",
|
||||||
"**/dist/**/*",
|
"**/dist/**/*",
|
||||||
"**/static/**/*",
|
"**/static/**/*",
|
||||||
|
"**/site/**/*",
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
@ -114,7 +115,7 @@ module.exports = {
|
|||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warning",
|
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
|
||||||
"unused-imports/no-unused-vars-ts": [
|
"unused-imports/no-unused-vars-ts": [
|
||||||
"warn", {
|
"warn", {
|
||||||
"vars": "all",
|
"vars": "all",
|
||||||
@ -182,7 +183,7 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warning",
|
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
|
||||||
"unused-imports/no-unused-vars-ts": [
|
"unused-imports/no-unused-vars-ts": [
|
||||||
"warn", {
|
"warn", {
|
||||||
"vars": "all",
|
"vars": "all",
|
||||||
|
|||||||
2
.github/workflows/check-docs.yml
vendored
2
.github/workflows/check-docs.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }}
|
if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release from lens
|
- name: Checkout Release from lens
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release from lens
|
- name: Checkout Release from lens
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
needs: verify-docs
|
needs: verify-docs
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.7
|
||||||
|
|||||||
10
.github/workflows/mkdocs-manual.yml
vendored
10
.github/workflows/mkdocs-manual.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
@ -29,7 +29,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: '${{ github.event.inputs.version }}'
|
ref: '${{ github.event.inputs.version }}'
|
||||||
|
|
||||||
- name: Using Node.js ${{ matrix.node-version }}
|
- name: Using Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@ -53,15 +53,15 @@ jobs:
|
|||||||
rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting
|
rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting
|
||||||
sed -i '/Protocol Handlers/d' ./mkdocs.yml
|
sed -i '/Protocol Handlers/d' ./mkdocs.yml
|
||||||
sed -i '/IPC/d' ./mkdocs.yml
|
sed -i '/IPC/d' ./mkdocs.yml
|
||||||
sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md
|
sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md
|
||||||
sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md
|
sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md
|
||||||
sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md
|
sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md
|
||||||
|
|
||||||
- name: git config
|
- name: git config
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "action@github.com"
|
git config --local user.email "action@github.com"
|
||||||
git config --local user.name "GitHub Action"
|
git config --local user.name "GitHub Action"
|
||||||
|
|
||||||
- name: mkdocs deploy new release
|
- name: mkdocs deploy new release
|
||||||
run: |
|
run: |
|
||||||
mike deploy --push --force ${{ github.event.inputs.version }}
|
mike deploy --push --force ${{ github.event.inputs.version }}
|
||||||
|
|||||||
2
.github/workflows/publish-master-npm.yml
vendored
2
.github/workflows/publish-master-npm.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }}
|
${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release
|
- name: Checkout Release
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
2
.github/workflows/publish-release-npm.yml
vendored
2
.github/workflows/publish-release-npm.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release
|
- name: Checkout Release
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@ -12,8 +12,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-16.04, macos-10.15, windows-2019]
|
os: [ubuntu-16.04, macos-11, windows-2019]
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release from lens
|
- name: Checkout Release from lens
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -83,6 +83,15 @@ jobs:
|
|||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|
||||||
- run: make integration
|
- run: make integration
|
||||||
name: Run integration tests
|
name: Run macOS integration tests
|
||||||
shell: bash
|
shell: bash
|
||||||
if: runner.os != 'Linux'
|
env:
|
||||||
|
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64"
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
|
||||||
|
- run: make integration
|
||||||
|
name: Run Windows integration tests
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32"
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
|||||||
2
.yarnrc
2
.yarnrc
@ -1,3 +1,3 @@
|
|||||||
disturl "https://atom.io/download/electron"
|
disturl "https://atom.io/download/electron"
|
||||||
target "9.4.4"
|
target "12.0.15"
|
||||||
runtime "electron"
|
runtime "electron"
|
||||||
|
|||||||
5
Makefile
5
Makefile
@ -4,6 +4,7 @@ CMD_ARGS = $(filter-out $@,$(MAKECMDGOALS))
|
|||||||
@:
|
@:
|
||||||
|
|
||||||
NPM_RELEASE_TAG ?= latest
|
NPM_RELEASE_TAG ?= latest
|
||||||
|
ELECTRON_BUILDER_EXTRA_ARGS ?=
|
||||||
EXTENSIONS_DIR = ./extensions
|
EXTENSIONS_DIR = ./extensions
|
||||||
extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir})
|
extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir})
|
||||||
extension_node_modules = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/node_modules)
|
extension_node_modules = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/node_modules)
|
||||||
@ -62,10 +63,8 @@ build: node_modules binaries/client
|
|||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||||
rm -rf node_modules/win-ca/pem
|
rm -rf node_modules/win-ca/pem
|
||||||
yarn run electron-builder --publish onTag --x64 --ia32
|
|
||||||
else
|
|
||||||
yarn run electron-builder --publish onTag
|
|
||||||
endif
|
endif
|
||||||
|
yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS)
|
||||||
|
|
||||||
$(extension_node_modules): node_modules
|
$(extension_node_modules): node_modules
|
||||||
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund
|
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund
|
||||||
|
|||||||
@ -18,6 +18,18 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { helmCli } from "../src/main/helm/helm-cli";
|
import packageInfo from "../package.json";
|
||||||
|
import { isWindows } from "../src/common/vars";
|
||||||
|
import { HelmCli } from "../src/main/helm/helm-cli";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
helmCli.ensureBinary();
|
const helmVersion = packageInfo.config.bundledHelmVersion;
|
||||||
|
|
||||||
|
if (!isWindows) {
|
||||||
|
Promise.all([
|
||||||
|
new HelmCli(path.join(process.cwd(), "binaries", "client", "x64"), helmVersion).ensureBinary(),
|
||||||
|
new HelmCli(path.join(process.cwd(), "binaries", "client", "arm64"), helmVersion).ensureBinary()
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
new HelmCli(path.join(process.cwd(), "binaries", "client", "x64"), helmVersion).ensureBinary();
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,8 @@ import md5File from "md5-file";
|
|||||||
import requestPromise from "request-promise-native";
|
import requestPromise from "request-promise-native";
|
||||||
import { ensureDir, pathExists } from "fs-extra";
|
import { ensureDir, pathExists } from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { noop } from "../src/common/utils";
|
import { noop } from "lodash";
|
||||||
|
import { isLinux, isMac } from "../src/common/vars";
|
||||||
|
|
||||||
class KubectlDownloader {
|
class KubectlDownloader {
|
||||||
public kubectlVersion: string;
|
public kubectlVersion: string;
|
||||||
@ -115,15 +116,21 @@ class KubectlDownloader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadVersion = packageInfo.config.bundledKubectlVersion;
|
const downloadVersion = packageInfo.config.bundledKubectlVersion;
|
||||||
const baseDir = path.join(process.env.INIT_CWD, "binaries", "client");
|
const baseDir = path.join(__dirname, "..", "binaries", "client");
|
||||||
const downloads = [
|
|
||||||
{ platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "kubectl") },
|
const downloads = [];
|
||||||
{ platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "kubectl") },
|
|
||||||
{ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "kubectl.exe") },
|
if (isMac) {
|
||||||
{ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") }
|
downloads.push({ platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "kubectl") });
|
||||||
];
|
downloads.push({ platform: "darwin", arch: "arm64", target: path.join(baseDir, "darwin", "arm64", "kubectl") });
|
||||||
|
} else if (isLinux) {
|
||||||
|
downloads.push({ platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "kubectl") });
|
||||||
|
downloads.push({ platform: "linux", arch: "arm64", target: path.join(baseDir, "linux", "arm64", "kubectl") });
|
||||||
|
} else {
|
||||||
|
downloads.push({ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "kubectl.exe") });
|
||||||
|
downloads.push({ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") });
|
||||||
|
}
|
||||||
|
|
||||||
downloads.forEach((dlOpts) => {
|
downloads.forEach((dlOpts) => {
|
||||||
console.log(dlOpts);
|
console.log(dlOpts);
|
||||||
|
|||||||
74
integration/__tests__/app-preferences.tests.ts
Normal file
74
integration/__tests__/app-preferences.tests.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 type { Page } from "playwright";
|
||||||
|
import * as utils from "../helpers/utils";
|
||||||
|
|
||||||
|
describe("preferences page tests", () => {
|
||||||
|
let window: Page, cleanup: () => Promise<void>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ window, cleanup } = await utils.start());
|
||||||
|
await utils.clickWelcomeButton(window);
|
||||||
|
await window.keyboard.press("Meta+,");
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await cleanup();
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it('shows "preferences" and can navigate through the tabs', async () => {
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
id: "application",
|
||||||
|
header: "Application",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "proxy",
|
||||||
|
header: "Proxy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kubernetes",
|
||||||
|
header: "Kubernetes",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { id, header } of pages) {
|
||||||
|
await window.click(`[data-testid=${id}-tab]`);
|
||||||
|
await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`);
|
||||||
|
}
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
|
||||||
|
await window.click("[data-testid=kubernetes-tab]");
|
||||||
|
await window.waitForSelector("[data-testid=repository-name]", {
|
||||||
|
timeout: 100_000,
|
||||||
|
});
|
||||||
|
await window.click("#HelmRepoSelect");
|
||||||
|
await window.waitForSelector("div.Select__option");
|
||||||
|
}, 10*60*1000);
|
||||||
|
});
|
||||||
@ -1,87 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
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 type { Application } from "spectron";
|
|
||||||
import * as utils from "../helpers/utils";
|
|
||||||
import { listHelmRepositories } from "../helpers/utils";
|
|
||||||
import { fail } from "assert";
|
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron
|
|
||||||
|
|
||||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
|
||||||
describe("Lens integration tests", () => {
|
|
||||||
let app: Application;
|
|
||||||
|
|
||||||
describe("app start", () => {
|
|
||||||
utils.beforeAllWrapped(async () => {
|
|
||||||
app = await utils.setup();
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.afterAllWrapped(() => utils.tearDown(app));
|
|
||||||
|
|
||||||
it('shows "add cluster"', async () => {
|
|
||||||
await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster");
|
|
||||||
await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("preferences page", () => {
|
|
||||||
it('shows "preferences"', async () => {
|
|
||||||
const appName: string = process.platform === "darwin" ? "OpenLens" : "File";
|
|
||||||
|
|
||||||
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
|
||||||
await app.client.waitUntilTextExists("[data-testid=application-header]", "Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["application", "Application"],
|
|
||||||
["proxy", "Proxy"],
|
|
||||||
["kubernetes", "Kubernetes"],
|
|
||||||
])("Can click the %s tab and see the %s header", async (tab, header) => {
|
|
||||||
await app.client.click(`[data-testid=${tab}-tab]`);
|
|
||||||
await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ensures helm repos", async () => {
|
|
||||||
const repos = await listHelmRepositories();
|
|
||||||
|
|
||||||
if (repos.length === 0) {
|
|
||||||
fail("Lens failed to add any repositories");
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.client.click("[data-testid=kubernetes-tab]");
|
|
||||||
await app.client.waitUntilTextExists("[data-testid=repository-name]", 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)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('quits Lens"', async () => {
|
|
||||||
await app.client.keys(["Meta", "Q"]);
|
|
||||||
await app.client.keys("Meta");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -25,450 +25,439 @@
|
|||||||
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
||||||
cluster and vice versa.
|
cluster and vice versa.
|
||||||
*/
|
*/
|
||||||
import type { Application } from "spectron";
|
|
||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
import { minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube";
|
import { minikubeReady } from "../helpers/minikube";
|
||||||
import { exec } from "child_process";
|
import type { Frame, Page } from "playwright";
|
||||||
import * as util from "util";
|
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
const TEST_NAMESPACE = "integration-tests";
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron
|
function getSidebarSelectors(itemId: string) {
|
||||||
|
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
||||||
|
|
||||||
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
|
return {
|
||||||
describe("Lens cluster pages", () => {
|
expandSubMenu: `${root} .nav-item`,
|
||||||
const TEST_NAMESPACE = "integration-tests";
|
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
|
||||||
const BACKSPACE = "\uE003";
|
};
|
||||||
let app: Application;
|
}
|
||||||
const ready = minikubeReady(TEST_NAMESPACE);
|
|
||||||
|
|
||||||
utils.describeIf(ready)("test common pages", () => {
|
function getLoadedSelector(page: CommonPage): string {
|
||||||
let clusterAdded = false;
|
if (page.expectedText) {
|
||||||
const addCluster = async () => {
|
return `${page.expectedSelector} >> text='${page.expectedText}'`;
|
||||||
await app.client.waitUntilTextExists("div", "Catalog");
|
}
|
||||||
await waitForMinikubeDashboard(app);
|
|
||||||
await app.client.click('a[href="/nodes"]');
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("cluster add", () => {
|
return page.expectedSelector;
|
||||||
utils.beforeAllWrapped(async () => {
|
}
|
||||||
app = await utils.setup();
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.afterAllWrapped(() => utils.tearDown(app));
|
interface CommonPage {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
expectedSelector: string;
|
||||||
|
expectedText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
it("allows to add a cluster", async () => {
|
interface TopPageTest {
|
||||||
await addCluster();
|
page: CommonPage;
|
||||||
clusterAdded = true;
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const appStartAddCluster = async () => {
|
interface SubPageTest {
|
||||||
if (clusterAdded) {
|
drawerId: string;
|
||||||
app = await utils.setup();
|
pages: CommonPage[];
|
||||||
await addCluster();
|
}
|
||||||
|
|
||||||
|
type CommonPageTest = TopPageTest | SubPageTest;
|
||||||
|
|
||||||
|
function isTopPageTest(test: CommonPageTest): test is TopPageTest {
|
||||||
|
return typeof (test as any).page === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonPageTests: CommonPageTest[] = [{
|
||||||
|
page: {
|
||||||
|
name: "Cluster",
|
||||||
|
href: "cluster",
|
||||||
|
expectedSelector: "div.ClusterOverview div.label",
|
||||||
|
expectedText: "CPU"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: {
|
||||||
|
name: "Nodes",
|
||||||
|
href: "nodes",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Nodes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: {
|
||||||
|
name: "Namespaces",
|
||||||
|
href: "namespaces",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Namespaces"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
page: {
|
||||||
|
name: "Events",
|
||||||
|
href: "events",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawerId: "apps",
|
||||||
|
pages: [{
|
||||||
|
name: "Charts",
|
||||||
|
href: "apps/charts",
|
||||||
|
expectedSelector: "div.HelmCharts input",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Releases",
|
||||||
|
href: "apps/releases",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Releases"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawerId: "users",
|
||||||
|
pages: [{
|
||||||
|
name: "Service Accounts",
|
||||||
|
href: "service-accounts",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Service Accounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Roles",
|
||||||
|
href: "roles",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cluster Roles",
|
||||||
|
href: "cluster-roles",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Cluster Roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Role Bindings",
|
||||||
|
href: "role-bindings",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Role Bindings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cluster Role Bindings",
|
||||||
|
href: "cluster-role-bindings",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Cluster Role Bindings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod Security Policies",
|
||||||
|
href: "pod-security-policies",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Pod Security Policies"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drawerId: "custom-resources",
|
||||||
|
pages: [{
|
||||||
|
name: "Definitions",
|
||||||
|
href: "crd/definitions",
|
||||||
|
expectedSelector: "h5.title",
|
||||||
|
expectedText: "Custom Resources"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
||||||
|
let window: Page, cleanup: () => Promise<void>, frame: Frame;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ window, cleanup } = await utils.start());
|
||||||
|
await utils.clickWelcomeButton(window);
|
||||||
|
|
||||||
|
frame = await utils.lauchMinikubeClusterFromCatalog(window);
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await cleanup();
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it("should navigate around common cluster pages", async () => {
|
||||||
|
for (const test of commonPageTests) {
|
||||||
|
if (isTopPageTest(test)) {
|
||||||
|
const { href, expectedText, expectedSelector } = test.page;
|
||||||
|
const menuButton = await frame.waitForSelector(`a[href^="/${href}"]`);
|
||||||
|
|
||||||
|
await menuButton.click();
|
||||||
|
await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`);
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function getSidebarSelectors(itemId: string) {
|
const { drawerId, pages } = test;
|
||||||
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
const selectors = getSidebarSelectors(drawerId);
|
||||||
|
const mainPageSelector = `${selectors.subMenuLink(pages[0].href)} >> text='${pages[0].name}'`;
|
||||||
|
|
||||||
return {
|
await frame.click(selectors.expandSubMenu);
|
||||||
expandSubMenu: `${root} .nav-item`,
|
await frame.waitForSelector(mainPageSelector);
|
||||||
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
|
|
||||||
};
|
for (const page of pages) {
|
||||||
|
const subPageButton = await frame.waitForSelector(selectors.subMenuLink(page.href));
|
||||||
|
|
||||||
|
await subPageButton.click();
|
||||||
|
await frame.waitForSelector(getLoadedSelector(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
await frame.click(selectors.expandSubMenu);
|
||||||
|
await frame.waitForSelector(mainPageSelector, { state: "hidden" });
|
||||||
|
}
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it("show logs and highlight the log search entries", async () => {
|
||||||
|
await frame.click(`a[href="/workloads"]`);
|
||||||
|
await frame.click(`a[href="/pods"]`);
|
||||||
|
|
||||||
|
const namespacesSelector = await frame.waitForSelector(".NamespaceSelect");
|
||||||
|
|
||||||
|
await namespacesSelector.click();
|
||||||
|
await namespacesSelector.type("kube-system");
|
||||||
|
await namespacesSelector.press("Enter");
|
||||||
|
await namespacesSelector.click();
|
||||||
|
|
||||||
|
const kubeApiServerRow = await frame.waitForSelector("div.TableCell >> text=kube-apiserver");
|
||||||
|
|
||||||
|
await kubeApiServerRow.click();
|
||||||
|
await frame.waitForSelector(".Drawer", { state: "visible" });
|
||||||
|
|
||||||
|
const logButton = await frame.waitForSelector("ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']");
|
||||||
|
|
||||||
|
await logButton.click();
|
||||||
|
|
||||||
|
// Check if controls are available
|
||||||
|
await frame.waitForSelector(".LogList .VirtualList");
|
||||||
|
await frame.waitForSelector(".LogResourceSelector");
|
||||||
|
|
||||||
|
const logSearchInput = await frame.waitForSelector(".LogSearch .SearchInput input");
|
||||||
|
|
||||||
|
await logSearchInput.type(":");
|
||||||
|
await frame.waitForSelector(".LogList .list span.active");
|
||||||
|
|
||||||
|
const showTimestampsButton = await frame.waitForSelector(".LogControls .show-timestamps");
|
||||||
|
|
||||||
|
await showTimestampsButton.click();
|
||||||
|
|
||||||
|
const showPreviousButton = await frame.waitForSelector(".LogControls .show-previous");
|
||||||
|
|
||||||
|
await showPreviousButton.click();
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it("should show the default namespaces", async () => {
|
||||||
|
await frame.click('a[href="/namespaces"]');
|
||||||
|
await frame.waitForSelector("div.TableCell >> text='default'");
|
||||||
|
await frame.waitForSelector("div.TableCell >> text='kube-system'");
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => {
|
||||||
|
await frame.click('a[href="/namespaces"]');
|
||||||
|
await frame.click("button.add-button");
|
||||||
|
await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'");
|
||||||
|
|
||||||
|
const namespaceNameInput = await frame.waitForSelector(".AddNamespaceDialog input");
|
||||||
|
|
||||||
|
await namespaceNameInput.type(TEST_NAMESPACE);
|
||||||
|
await namespaceNameInput.press("Enter");
|
||||||
|
|
||||||
|
await frame.waitForSelector(`div.TableCell >> text=${TEST_NAMESPACE}`);
|
||||||
|
|
||||||
|
if ((await frame.innerText(`a[href^="/workloads"] .expand-icon`)) === "keyboard_arrow_down") {
|
||||||
|
await frame.click(`a[href^="/workloads"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("cluster pages", () => {
|
await frame.click(`a[href^="/pods"]`);
|
||||||
utils.beforeAllWrapped(appStartAddCluster);
|
|
||||||
utils.afterAllWrapped(() => utils.tearDown(app));
|
|
||||||
|
|
||||||
const tests: {
|
const namespacesSelector = await frame.waitForSelector(".NamespaceSelect");
|
||||||
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: "CPU"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 }) => {
|
await namespacesSelector.click();
|
||||||
const selectors = getSidebarSelectors(drawerId);
|
await namespacesSelector.type(TEST_NAMESPACE);
|
||||||
|
await namespacesSelector.press("Enter");
|
||||||
|
await namespacesSelector.click();
|
||||||
|
|
||||||
if (drawer !== "") {
|
await frame.click(".Icon.new-dock-tab");
|
||||||
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 }) => {
|
try {
|
||||||
it(`shows ${drawer}->${name} page`, async () => {
|
await frame.click("li.MenuItem.create-resource-tab", {
|
||||||
expect(clusterAdded).toBe(true);
|
// NOTE: the following shouldn't be required, but is because without it a TypeError is thrown
|
||||||
await app.client.click(selectors.subMenuLink(href));
|
// see: https://github.com/microsoft/playwright/issues/8229
|
||||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
position: {
|
||||||
});
|
y: 0,
|
||||||
});
|
x: 0,
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
await frame.waitForTimeout(100_000);
|
||||||
|
}
|
||||||
|
|
||||||
describe("viewing pod logs", () => {
|
const inputField = await frame.waitForSelector(".CreateResource div.react-monaco-editor-container");
|
||||||
utils.beforeEachWrapped(appStartAddCluster);
|
|
||||||
utils.afterEachWrapped(() => utils.tearDown(app));
|
|
||||||
|
|
||||||
it(`shows a log for a pod`, async () => {
|
await inputField.click();
|
||||||
expect(clusterAdded).toBe(true);
|
await inputField.type("apiVersion: v1", { delay: 10 });
|
||||||
// Go to Pods page
|
await inputField.press("Enter", { delay: 10 });
|
||||||
await app.client.click(getSidebarSelectors("workloads").expandSubMenu);
|
await inputField.type("kind: Pod", { delay: 10 });
|
||||||
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
|
await inputField.press("Enter", { delay: 10 });
|
||||||
await app.client.click('a[href^="/pods"]');
|
await inputField.type("metadata:", { delay: 10 });
|
||||||
await app.client.click(".NamespaceSelect");
|
await inputField.press("Enter", { delay: 10 });
|
||||||
await app.client.keys("kube-system");
|
await inputField.type(" name: nginx-create-pod-test", { delay: 10 });
|
||||||
await app.client.keys("Enter");// "\uE007"
|
await inputField.press("Enter", { delay: 10 });
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
|
await inputField.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 });
|
||||||
let podMenuItemEnabled = false;
|
await inputField.press("Enter", { delay: 10 });
|
||||||
|
await inputField.press("Backspace", { delay: 10 });
|
||||||
|
await inputField.type("spec:", { delay: 10 });
|
||||||
|
await inputField.press("Enter", { delay: 10 });
|
||||||
|
await inputField.type(" containers:", { delay: 10 });
|
||||||
|
await inputField.press("Enter", { delay: 10 });
|
||||||
|
await inputField.type("- name: nginx-create-pod-test", { delay: 10 });
|
||||||
|
await inputField.press("Enter", { delay: 10 });
|
||||||
|
await inputField.type(" image: nginx:alpine", { delay: 10 });
|
||||||
|
await inputField.press("Enter", { delay: 10 });
|
||||||
|
|
||||||
// Wait until extensions are enabled on renderer
|
await frame.click("button.Button >> text='Create & Close'");
|
||||||
while (!podMenuItemEnabled) {
|
await frame.click("div.TableCell >> text=nginx-create-pod-test");
|
||||||
const logs = await app.client.getRenderProcessLogs();
|
await frame.waitForSelector("div.drawer-title-text >> text='Pod: nginx-create-pod-test'");
|
||||||
|
}, 10*60*1000);
|
||||||
podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@"));
|
|
||||||
|
|
||||||
if (!podMenuItemEnabled) {
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open logs tab in dock
|
|
||||||
await app.client.click(".list .TableRow:first-child");
|
|
||||||
await app.client.waitForVisible(".Drawer");
|
|
||||||
const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']";
|
|
||||||
|
|
||||||
await app.client.waitForVisible(logsButton);
|
|
||||||
await app.client.click(logsButton);
|
|
||||||
|
|
||||||
// Check if controls are available
|
|
||||||
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(() => 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.react-monaco-editor-container");
|
|
||||||
// Write pod manifest to editor
|
|
||||||
await app.client.click(".CreateResource div.react-monaco-editor-container");
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,25 +19,25 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Application } from "spectron";
|
import type { Page } from "playwright";
|
||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron
|
|
||||||
|
|
||||||
describe("Lens command palette", () => {
|
describe("Lens command palette", () => {
|
||||||
let app: Application;
|
let window: Page, cleanup: () => Promise<void>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ window, cleanup } = await utils.start());
|
||||||
|
await utils.clickWelcomeButton(window);
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await cleanup();
|
||||||
|
}, 10*60*1000);
|
||||||
|
|
||||||
describe("menu", () => {
|
describe("menu", () => {
|
||||||
utils.beforeAllWrapped(async () => {
|
it("opens command dialog from keyboard shortcut", async () => {
|
||||||
app = await utils.setup();
|
await window.keyboard.press("Meta+Shift+p");
|
||||||
});
|
await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
|
||||||
|
}, 10*60*1000);
|
||||||
utils.afterAllWrapped(() => utils.tearDown(app));
|
|
||||||
|
|
||||||
it("opens command dialog from menu", async () => {
|
|
||||||
await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette...");
|
|
||||||
await app.client.waitUntilTextExists(".Select__option", "Hotbar: Switch");
|
|
||||||
await app.client.keys("Escape");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { spawnSync } from "child_process";
|
import { spawnSync } from "child_process";
|
||||||
import type { Application } from "spectron";
|
|
||||||
|
|
||||||
export function minikubeReady(testNamespace: string): boolean {
|
export function minikubeReady(testNamespace: string): boolean {
|
||||||
// determine if minikube is running
|
// determine if minikube is running
|
||||||
@ -57,18 +56,3 @@ export function minikubeReady(testNamespace: string): boolean {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForMinikubeDashboard(app: Application) {
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
|
||||||
await app.client.waitForExist(".Input.SearchInput input");
|
|
||||||
await app.client.setValue(".Input.SearchInput input", "minikube");
|
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
|
||||||
await app.client.click("div.TableRow");
|
|
||||||
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
|
||||||
await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
|
||||||
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
|
||||||
await app.client.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");
|
|
||||||
}
|
|
||||||
|
|||||||
@ -18,49 +18,20 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { Application } from "spectron";
|
import { createHash } from "crypto";
|
||||||
import * as util from "util";
|
import { mkdirp, remove } from "fs-extra";
|
||||||
import { exec } from "child_process";
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import { ElectronApplication, Frame, Page, _electron as electron } from "playwright";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
|
||||||
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
export const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||||
"win32": "./dist/win-unpacked/OpenLens.exe",
|
"win32": "./dist/win-unpacked/OpenLens.exe",
|
||||||
"linux": "./dist/linux-unpacked/open-lens",
|
"linux": "./dist/linux-unpacked/open-lens",
|
||||||
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
|
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
|
||||||
};
|
};
|
||||||
|
|
||||||
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> | void): (done: DoneCallback) => void {
|
|
||||||
return function (done: DoneCallback) {
|
|
||||||
(async () => fn())()
|
|
||||||
.then(() => done())
|
|
||||||
.catch(error => done.fail(error));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function beforeAllWrapped(fn: () => Promise<void> | void): void {
|
|
||||||
beforeAll(wrapJestLifecycle(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function beforeEachWrapped(fn: () => Promise<void> | void): void {
|
|
||||||
beforeEach(wrapJestLifecycle(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function afterAllWrapped(fn: () => Promise<void> | void): void {
|
|
||||||
afterAll(wrapJestLifecycle(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function afterEachWrapped(fn: () => Promise<void> | void): void {
|
|
||||||
afterEach(wrapJestLifecycle(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function itIf(condition: boolean) {
|
export function itIf(condition: boolean) {
|
||||||
return condition ? it : it.skip;
|
return condition ? it : it.skip;
|
||||||
}
|
}
|
||||||
@ -69,71 +40,80 @@ export function describeIf(condition: boolean) {
|
|||||||
return condition ? describe : describe.skip;
|
return condition ? describe : describe.skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const keys = {
|
async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> {
|
||||||
backspace: "\uE003"
|
const deadline = Date.now() + timeout;
|
||||||
};
|
|
||||||
|
|
||||||
export async function setup(): Promise<Application> {
|
for (; Date.now() < deadline;) {
|
||||||
const app = new Application({
|
for (const page of app.windows()) {
|
||||||
path: AppPaths[process.platform], // path to electron app
|
if (page.url().startsWith("http://localhost")) {
|
||||||
args: [],
|
return page;
|
||||||
startTimeout: 60000,
|
}
|
||||||
waitTimeout: 10000,
|
|
||||||
env: {
|
|
||||||
CICD: "true"
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
await app.start();
|
await new Promise(resolve => setTimeout(resolve, 2_000));
|
||||||
// Wait for splash screen to be closed
|
|
||||||
while (await app.client.getWindowCount() > 1);
|
|
||||||
await app.client.windowByIndex(0);
|
|
||||||
await app.client.waitUntilWindowLoaded();
|
|
||||||
await showCatalog(app);
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showCatalog(app: Application) {
|
|
||||||
await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon");
|
|
||||||
await app.client.click("#hotbarIcon-catalog-entity .Icon");
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncPidGetter = () => Promise<number>;
|
|
||||||
|
|
||||||
export async function tearDown(app?: Application) {
|
|
||||||
if (!app?.isRunning()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
throw new Error(`Lens did not open the main window within ${timeout}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
await app.stop();
|
export async function start() {
|
||||||
|
const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4());
|
||||||
|
|
||||||
|
// Make sure that the directory is clear
|
||||||
|
await remove(CICD).catch(noop);
|
||||||
|
await mkdirp(CICD);
|
||||||
|
|
||||||
|
const app = await electron.launch({
|
||||||
|
args: ["--integration-testing"], // this argument turns off the blocking of quit
|
||||||
|
executablePath: AppPaths[process.platform],
|
||||||
|
bypassCSP: true,
|
||||||
|
env: {
|
||||||
|
CICD,
|
||||||
|
...process.env
|
||||||
|
},
|
||||||
|
timeout: 100_000,
|
||||||
|
} as Parameters<typeof electron["launch"]>[0]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.kill(pid, "SIGKILL");
|
const window = await getMainWindow(app);
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
return {
|
||||||
|
app,
|
||||||
|
window,
|
||||||
|
cleanup: async () => {
|
||||||
|
await app.close();
|
||||||
|
await remove(CICD).catch(noop);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await app.close();
|
||||||
|
await remove(CICD).catch(noop);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
export async function clickWelcomeButton(window: Page) {
|
||||||
|
await window.click("#hotbarIcon-catalog-entity .Icon");
|
||||||
type HelmRepository = {
|
}
|
||||||
name: string;
|
|
||||||
url: string;
|
function minikubeEntityId() {
|
||||||
};
|
return createHash("md5").update(`${path.join(os.homedir(), ".kube", "config")}:minikube`).digest("hex");
|
||||||
|
}
|
||||||
export async function listHelmRepositories(): Promise<HelmRepository[]>{
|
|
||||||
for (let i = 0; i < 10; i += 1) {
|
/**
|
||||||
try {
|
* From the catalog, click the minikube entity and wait for it to connect, returning its frame
|
||||||
const { stdout } = await promiseExec("helm repo list -o json");
|
*/
|
||||||
|
export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Frame> {
|
||||||
return JSON.parse(stdout);
|
await window.waitForSelector("div.TableCell");
|
||||||
} catch {
|
await window.click("div.TableCell >> text='minikube'");
|
||||||
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
|
await window.waitForSelector("div.drawer-title-text >> text='KubernetesCluster: minikube'");
|
||||||
}
|
await window.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
}
|
|
||||||
|
const minikubeFrame = await window.waitForSelector(`#cluster-frame-${minikubeEntityId()}`);
|
||||||
return [];
|
|
||||||
|
const frame = await minikubeFrame.contentFrame();
|
||||||
|
|
||||||
|
await frame.waitForSelector("div.Sidebar");
|
||||||
|
|
||||||
|
return frame;
|
||||||
}
|
}
|
||||||
|
|||||||
56
package.json
56
package.json
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.2.0-beta.1",
|
"version": "5.2.0-beta.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -28,11 +28,9 @@
|
|||||||
"build:linux": "yarn run compile && electron-builder --linux --dir",
|
"build:linux": "yarn run compile && electron-builder --linux --dir",
|
||||||
"build:mac": "yarn run compile && electron-builder --mac --dir",
|
"build:mac": "yarn run compile && electron-builder --mac --dir",
|
||||||
"build:win": "yarn run compile && electron-builder --win --dir",
|
"build:win": "yarn run compile && electron-builder --win --dir",
|
||||||
"integration": "jest --runInBand integration",
|
"integration": "jest --runInBand --detectOpenHandles --forceExit integration",
|
||||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
|
||||||
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
||||||
"postinstall": "patch-package",
|
|
||||||
"download-bins": "concurrently yarn:download:*",
|
"download-bins": "concurrently yarn:download:*",
|
||||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||||
@ -49,11 +47,11 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.21.2",
|
"bundledKubectlVersion": "1.21.2",
|
||||||
"bundledHelmVersion": "3.5.4",
|
"bundledHelmVersion": "3.6.3",
|
||||||
"sentryDsn": ""
|
"sentryDsn": ""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12 <13"
|
"node": ">=14 <15"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"collectCoverage": false,
|
"collectCoverage": false,
|
||||||
@ -123,8 +121,8 @@
|
|||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "binaries/client/linux/x64/kubectl",
|
"from": "binaries/client/linux/${arch}/kubectl",
|
||||||
"to": "./x64/kubectl"
|
"to": "./${arch}/kubectl"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "binaries/client/helm3/helm3",
|
"from": "binaries/client/helm3/helm3",
|
||||||
@ -139,11 +137,11 @@
|
|||||||
"entitlementsInherit": "build/entitlements.mac.plist",
|
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "binaries/client/darwin/x64/kubectl",
|
"from": "binaries/client/darwin/${arch}/kubectl",
|
||||||
"to": "./x64/kubectl"
|
"to": "./${arch}/kubectl"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "binaries/client/helm3/helm3",
|
"from": "binaries/client/${arch}/helm3/helm3",
|
||||||
"to": "./helm3/helm3"
|
"to": "./helm3/helm3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -181,9 +179,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@electron/remote": "^1.2.1",
|
||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.14.3",
|
"@kubernetes/client-node": "^0.15.1",
|
||||||
"@sentry/electron": "^2.5.0",
|
"@sentry/electron": "^2.5.0",
|
||||||
"@sentry/integrations": "^6.10.0",
|
"@sentry/integrations": "^6.10.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
@ -196,7 +195,7 @@
|
|||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
"command-exists": "1.2.9",
|
"command-exists": "1.2.9",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.1.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-updater": "^4.3.9",
|
"electron-updater": "^4.3.9",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
@ -231,9 +230,10 @@
|
|||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-material-ui-carousel": "^2.3.1",
|
||||||
"react-monaco-editor": "^0.44.0",
|
"react-monaco-editor": "^0.44.0",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.5",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.9",
|
"request-promise-native": "^1.0.9",
|
||||||
@ -241,7 +241,7 @@
|
|||||||
"serializr": "^2.0.3",
|
"serializr": "^2.0.3",
|
||||||
"shell-env": "^3.0.1",
|
"shell-env": "^3.0.1",
|
||||||
"spdy": "^4.0.2",
|
"spdy": "^4.0.2",
|
||||||
"tar": "^6.1.4",
|
"tar": "^6.1.10",
|
||||||
"tcp-port-used": "^1.0.1",
|
"tcp-port-used": "^1.0.1",
|
||||||
"tempy": "^0.5.0",
|
"tempy": "^0.5.0",
|
||||||
"url-parse": "^1.5.1",
|
"url-parse": "^1.5.1",
|
||||||
@ -256,14 +256,14 @@
|
|||||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||||
"@material-ui/core": "^4.12.3",
|
"@material-ui/core": "^4.12.3",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@sentry/react": "^6.8.0",
|
"@sentry/react": "^6.8.0",
|
||||||
"@sentry/types": "^6.8.0",
|
"@sentry/types": "^6.8.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@types/byline": "^4.2.32",
|
"@types/byline": "^4.2.32",
|
||||||
"@types/chart.js": "^2.9.21",
|
"@types/chart.js": "^2.9.34",
|
||||||
"@types/color": "^3.0.2",
|
"@types/color": "^3.0.2",
|
||||||
"@types/crypto-js": "^3.1.47",
|
"@types/crypto-js": "^3.1.47",
|
||||||
"@types/dompurify": "^2.0.2",
|
"@types/dompurify": "^2.0.2",
|
||||||
@ -287,17 +287,17 @@
|
|||||||
"@types/module-alias": "^2.0.0",
|
"@types/module-alias": "^2.0.0",
|
||||||
"@types/node": "12.20",
|
"@types/node": "12.20",
|
||||||
"@types/node-fetch": "^2.5.12",
|
"@types/node-fetch": "^2.5.12",
|
||||||
"@types/npm": "^2.0.31",
|
"@types/npm": "^2.0.32",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"@types/randomcolor": "^0.5.6",
|
"@types/randomcolor": "^0.5.6",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-beautiful-dnd": "^13.1.1",
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
"@types/react-dom": "^17.0.6",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/react-select": "3.1.2",
|
"@types/react-select": "3.1.2",
|
||||||
"@types/react-table": "^7.7.0",
|
"@types/react-table": "^7.7.0",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@types/readable-stream": "^2.3.9",
|
||||||
"@types/request": "^2.48.7",
|
"@types/request": "^2.48.7",
|
||||||
@ -326,10 +326,10 @@
|
|||||||
"css-loader": "^5.2.6",
|
"css-loader": "^5.2.6",
|
||||||
"deepdash": "^5.3.5",
|
"deepdash": "^5.3.5",
|
||||||
"dompurify": "^2.0.17",
|
"dompurify": "^2.0.17",
|
||||||
"electron": "^9.4.4",
|
"electron": "^12.0.17",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
"esbuild": "^0.12.12",
|
"esbuild": "^0.12.24",
|
||||||
"esbuild-loader": "^2.13.1",
|
"esbuild-loader": "^2.13.1",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
@ -344,17 +344,18 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"include-media": "^1.4.9",
|
"include-media": "^1.4.9",
|
||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.1",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.16",
|
"jest-mock-extended": "^1.0.16",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^1.6.0",
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
|
"node-gyp": "7.1.2",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^1.0.3",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^2.0.12",
|
"nodemon": "^2.0.12",
|
||||||
"patch-package": "^6.2.2",
|
"playwright": "^1.14.0",
|
||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"postcss-loader": "4.0.3",
|
"postcss-loader": "4.3.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
@ -368,12 +369,11 @@
|
|||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.29.0",
|
"sharp": "^0.29.0",
|
||||||
"spectron": "11.0.0",
|
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.2.4",
|
"tailwindcss": "^2.2.4",
|
||||||
"ts-jest": "26.5.6",
|
"ts-jest": "26.5.6",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^10.1.0",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
"typed-emitter": "^1.3.1",
|
"typed-emitter": "^1.3.1",
|
||||||
"typedoc": "0.21.0-beta.2",
|
"typedoc": "0.21.0-beta.2",
|
||||||
@ -384,7 +384,7 @@
|
|||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"xterm": "^4.12.0",
|
"xterm": "^4.12.0",
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import { Console } from "console";
|
|||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
|
||||||
import { ThemeStore } from "../../renderer/theme.store";
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
import type { ClusterStoreModel } from "../cluster-store";
|
import type { ClusterStoreModel } from "../cluster-store";
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ console = new Console(stdout, stderr);
|
|||||||
|
|
||||||
describe("user store tests", () => {
|
describe("user store tests", () => {
|
||||||
describe("for an empty config", () => {
|
describe("for an empty config", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEach(() => {
|
||||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||||
|
|
||||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||||
@ -94,7 +93,7 @@ describe("user store tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("migrations", () => {
|
describe("migrations", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEach(() => {
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"config.json": JSON.stringify({
|
"config.json": JSON.stringify({
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import Config from "conf";
|
import Config from "conf";
|
||||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||||
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
import { ipcMain, ipcRenderer } from "electron";
|
||||||
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
|
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
|
||||||
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
@ -30,6 +30,7 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { isTestEnv } from "./vars";
|
import { isTestEnv } from "./vars";
|
||||||
import { kebabCase } from "lodash";
|
import { kebabCase } from "lodash";
|
||||||
|
import { getPath } from "./utils/getPath";
|
||||||
|
|
||||||
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
||||||
syncOptions?: IReactionOptions;
|
syncOptions?: IReactionOptions;
|
||||||
@ -88,7 +89,7 @@ export abstract class BaseStore<T> extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected cwd() {
|
protected cwd() {
|
||||||
return (app || remote.app).getPath("userData");
|
return getPath("userData");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async saveToFile(model: T) {
|
protected async saveToFile(model: T) {
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
||||||
|
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./invalid-kubeconfig";
|
export * from "./invalid-kubeconfig";
|
||||||
export * from "./update-available.ipc";
|
export * from "./update-available.ipc";
|
||||||
|
|||||||
@ -23,12 +23,14 @@
|
|||||||
// https://www.electronjs.org/docs/api/ipc-main
|
// https://www.electronjs.org/docs/api/ipc-main
|
||||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||||
|
|
||||||
import { ipcMain, ipcRenderer, remote, webContents } from "electron";
|
import { ipcMain, ipcRenderer, webContents } from "electron";
|
||||||
import { toJS } from "../utils/toJS";
|
import { toJS } from "../utils/toJS";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||||
import type { Disposer } from "../utils";
|
import type { Disposer } from "../utils";
|
||||||
|
|
||||||
|
const remote = ipcMain ? null : require("@electron/remote");
|
||||||
|
|
||||||
const subFramesChannel = "ipc:get-sub-frames";
|
const subFramesChannel = "ipc:get-sub-frames";
|
||||||
|
|
||||||
export async function requestMain(channel: string, ...args: any[]) {
|
export async function requestMain(channel: string, ...args: any[]) {
|
||||||
|
|||||||
290
src/common/k8s-api/__tests__/helm-charts.api.test.ts
Normal file
290
src/common/k8s-api/__tests__/helm-charts.api.test.ts
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyObject } from "jest-mock-extended";
|
||||||
|
import { HelmChart } from "../endpoints/helm-charts.api";
|
||||||
|
|
||||||
|
describe("HelmChart tests", () => {
|
||||||
|
describe("HelmChart.create() tests", () => {
|
||||||
|
it("should throw on non-object input", () => {
|
||||||
|
expect(() => HelmChart.create("" as any)).toThrowError('"value" must be of type object');
|
||||||
|
expect(() => HelmChart.create(1 as any)).toThrowError('"value" must be of type object');
|
||||||
|
expect(() => HelmChart.create(false as any)).toThrowError('"value" must be of type object');
|
||||||
|
expect(() => HelmChart.create([] as any)).toThrowError('"value" must be of type object');
|
||||||
|
expect(() => HelmChart.create(Symbol() as any)).toThrowError('"value" must be of type object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw on missing fields", () => {
|
||||||
|
expect(() => HelmChart.create({} as any)).toThrowError('"apiVersion" is required');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "!",
|
||||||
|
} as any)).toThrowError('"name" is required');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "!",
|
||||||
|
name: "!",
|
||||||
|
} as any)).toThrowError('"version" is required');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "!",
|
||||||
|
name: "!",
|
||||||
|
version: "!",
|
||||||
|
} as any)).toThrowError('"repo" is required');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "!",
|
||||||
|
name: "!",
|
||||||
|
version: "!",
|
||||||
|
repo: "!",
|
||||||
|
} as any)).toThrowError('"created" is required');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "!",
|
||||||
|
name: "!",
|
||||||
|
version: "!",
|
||||||
|
repo: "!",
|
||||||
|
created: "!",
|
||||||
|
} as any)).toThrowError('"digest" is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw on fields being wrong type", () => {
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: 1,
|
||||||
|
name: "!",
|
||||||
|
version: "!",
|
||||||
|
repo: "!",
|
||||||
|
created: "!",
|
||||||
|
digest: "!",
|
||||||
|
} as any)).toThrowError('"apiVersion" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: 1,
|
||||||
|
version: "!",
|
||||||
|
repo: "!",
|
||||||
|
created: "!",
|
||||||
|
digest: "!",
|
||||||
|
} as any)).toThrowError('"name" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "",
|
||||||
|
version: 1,
|
||||||
|
repo: "!",
|
||||||
|
created: "!",
|
||||||
|
digest: "!",
|
||||||
|
} as any)).toThrowError('"version" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: 1,
|
||||||
|
created: "!",
|
||||||
|
digest: "!",
|
||||||
|
} as any)).toThrowError('"repo" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
created: 1,
|
||||||
|
digest: "a",
|
||||||
|
} as any)).toThrowError('"created" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
created: "!",
|
||||||
|
digest: 1,
|
||||||
|
} as any)).toThrowError('"digest" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
kubeVersion: 1,
|
||||||
|
} as any)).toThrowError('"kubeVersion" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
description: 1,
|
||||||
|
} as any)).toThrowError('"description" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
home: 1,
|
||||||
|
} as any)).toThrowError('"home" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
engine: 1,
|
||||||
|
} as any)).toThrowError('"engine" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
icon: 1,
|
||||||
|
} as any)).toThrowError('"icon" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
appVersion: 1,
|
||||||
|
} as any)).toThrowError('"appVersion" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
tillerVersion: 1,
|
||||||
|
} as any)).toThrowError('"tillerVersion" must be a string');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
deprecated: 1,
|
||||||
|
} as any)).toThrowError('"deprecated" must be a boolean');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
keywords: 1,
|
||||||
|
} as any)).toThrowError('"keywords" must be an array');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
sources: 1,
|
||||||
|
} as any)).toThrowError('"sources" must be an array');
|
||||||
|
expect(() => HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
maintainers: 1,
|
||||||
|
} as any)).toThrowError('"maintainers" must be an array');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter non-string keywords", () => {
|
||||||
|
const chart = HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
keywords: [1, "a", false, {}, "b"] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chart.keywords).toStrictEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter non-string sources", () => {
|
||||||
|
const chart = HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
sources: [1, "a", false, {}, "b"] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chart.sources).toStrictEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter invalid maintainers", () => {
|
||||||
|
const chart = HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
maintainers: [{
|
||||||
|
name: "a",
|
||||||
|
email: "b",
|
||||||
|
url: "c",
|
||||||
|
}] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chart.maintainers).toStrictEqual([{
|
||||||
|
name: "a",
|
||||||
|
email: "b",
|
||||||
|
url: "c",
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn on unknown fields", () => {
|
||||||
|
const { warn } = console;
|
||||||
|
const warnFn = console.warn = jest.fn();
|
||||||
|
|
||||||
|
HelmChart.create({
|
||||||
|
apiVersion: "1",
|
||||||
|
name: "1",
|
||||||
|
version: "1",
|
||||||
|
repo: "1",
|
||||||
|
digest: "1",
|
||||||
|
created: "!",
|
||||||
|
maintainers: [{
|
||||||
|
name: "a",
|
||||||
|
email: "b",
|
||||||
|
url: "c",
|
||||||
|
}] as any,
|
||||||
|
"asdjhajksdhadjks": 1,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
|
||||||
|
original: anyObject(),
|
||||||
|
unknownFields: ["asdjhajksdhadjks"]
|
||||||
|
});
|
||||||
|
console.warn = warn;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -22,11 +22,11 @@
|
|||||||
import { compile } from "path-to-regexp";
|
import { compile } from "path-to-regexp";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { autoBind } from "../../utils";
|
|
||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
|
import { autoBind, bifurcateArray } from "../../utils";
|
||||||
|
import Joi from "joi";
|
||||||
|
|
||||||
export type RepoHelmChartList = Record<string, HelmChart[]>;
|
export type RepoHelmChartList = Record<string, RawHelmChart[]>;
|
||||||
export type HelmChartList = Record<string, RepoHelmChartList>;
|
|
||||||
|
|
||||||
export interface IHelmChartDetails {
|
export interface IHelmChartDetails {
|
||||||
readme: string;
|
readme: string;
|
||||||
@ -42,12 +42,13 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
|||||||
* Get a list of all helm charts from all saved helm repos
|
* Get a list of all helm charts from all saved helm repos
|
||||||
*/
|
*/
|
||||||
export async function listCharts(): Promise<HelmChart[]> {
|
export async function listCharts(): Promise<HelmChart[]> {
|
||||||
const data = await apiBase.get<HelmChartList>(endpoint());
|
const data = await apiBase.get<Record<string, RepoHelmChartList>>(endpoint());
|
||||||
|
|
||||||
return Object
|
return Object
|
||||||
.values(data)
|
.values(data)
|
||||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
||||||
.map(([chart]) => HelmChart.create(chart));
|
.map(([chart]) => HelmChart.create(chart, { onError: "log" }))
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetChartDetailsOptions {
|
export interface GetChartDetailsOptions {
|
||||||
@ -66,7 +67,7 @@ export async function getChartDetails(repo: string, name: string, { version, req
|
|||||||
const path = endpoint({ repo, name });
|
const path = endpoint({ repo, name });
|
||||||
|
|
||||||
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit);
|
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit);
|
||||||
const versions = data.versions.map(HelmChart.create);
|
const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(Boolean);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readme,
|
readme,
|
||||||
@ -84,6 +85,179 @@ export async function getChartValues(repo: string, name: string, version: string
|
|||||||
return apiBase.get<string>(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`);
|
return apiBase.get<string>(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawHelmChart {
|
||||||
|
apiVersion: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
repo: string;
|
||||||
|
created: string;
|
||||||
|
digest: string;
|
||||||
|
kubeVersion?: string;
|
||||||
|
description?: string;
|
||||||
|
home?: string;
|
||||||
|
engine?: string;
|
||||||
|
icon?: string;
|
||||||
|
appVersion?: string;
|
||||||
|
type?: string;
|
||||||
|
tillerVersion?: string;
|
||||||
|
deprecated?: boolean;
|
||||||
|
keywords?: string[];
|
||||||
|
sources?: string[];
|
||||||
|
urls?: string[];
|
||||||
|
maintainers?: HelmChartMaintainer[];
|
||||||
|
dependencies?: RawHelmChartDependency[];
|
||||||
|
annotations?: Record<string, string>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const helmChartMaintainerValidator = Joi.object<HelmChartMaintainer>({
|
||||||
|
name: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
email: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
url: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const helmChartDependencyValidator = Joi.object<RawHelmChartDependency>({
|
||||||
|
name: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
repository: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
condition: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
version: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
tags: Joi
|
||||||
|
.array()
|
||||||
|
.items(Joi.string())
|
||||||
|
.default(() => ([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
const helmChartValidator = Joi.object<RawHelmChart>({
|
||||||
|
apiVersion: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
name: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
version: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
repo: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
created: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
digest: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
kubeVersion: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
description: Joi
|
||||||
|
.string()
|
||||||
|
.default(""),
|
||||||
|
home: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
engine: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
icon: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
appVersion: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
tillerVersion: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
type: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
deprecated: Joi
|
||||||
|
.boolean()
|
||||||
|
.default(false),
|
||||||
|
keywords: Joi
|
||||||
|
.array()
|
||||||
|
.items(Joi.string())
|
||||||
|
.options({
|
||||||
|
stripUnknown: {
|
||||||
|
arrays: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default(() => ([])),
|
||||||
|
sources: Joi
|
||||||
|
.array()
|
||||||
|
.items(Joi.string())
|
||||||
|
.options({
|
||||||
|
stripUnknown: {
|
||||||
|
arrays: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default(() => ([])),
|
||||||
|
urls: Joi
|
||||||
|
.array()
|
||||||
|
.items(Joi.string())
|
||||||
|
.options({
|
||||||
|
stripUnknown: {
|
||||||
|
arrays: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default(() => ([])),
|
||||||
|
maintainers: Joi
|
||||||
|
.array()
|
||||||
|
.items(helmChartMaintainerValidator)
|
||||||
|
.options({
|
||||||
|
stripUnknown: {
|
||||||
|
arrays: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default(() => ([])),
|
||||||
|
dependencies: Joi
|
||||||
|
.array()
|
||||||
|
.items(helmChartDependencyValidator)
|
||||||
|
.options({
|
||||||
|
stripUnknown: {
|
||||||
|
arrays: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default(() => ([])),
|
||||||
|
annotations: Joi
|
||||||
|
.object({})
|
||||||
|
.pattern(/.*/, Joi.string())
|
||||||
|
.default(() => ({})),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface HelmChartCreateOpts {
|
||||||
|
onError?: "throw" | "log";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HelmChartMaintainer {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawHelmChartDependency {
|
||||||
|
name: string;
|
||||||
|
repository: string;
|
||||||
|
condition?: string;
|
||||||
|
version: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">>
|
||||||
|
& Pick<RawHelmChartDependency, "condition">;
|
||||||
|
|
||||||
export interface HelmChart {
|
export interface HelmChart {
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -91,74 +265,121 @@ export interface HelmChart {
|
|||||||
repo: string;
|
repo: string;
|
||||||
kubeVersion?: string;
|
kubeVersion?: string;
|
||||||
created: string;
|
created: string;
|
||||||
description?: string;
|
description: string;
|
||||||
digest: string;
|
digest: string;
|
||||||
keywords?: string[];
|
keywords: string[];
|
||||||
home?: string;
|
home?: string;
|
||||||
sources?: string[];
|
sources: string[];
|
||||||
maintainers?: {
|
urls: string[];
|
||||||
name: string;
|
annotations: Record<string, string>;
|
||||||
email: string;
|
dependencies: HelmChartDependency[];
|
||||||
url: string;
|
maintainers: HelmChartMaintainer[];
|
||||||
}[];
|
|
||||||
engine?: string;
|
engine?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
appVersion?: string;
|
appVersion?: string;
|
||||||
deprecated?: boolean;
|
type?: string;
|
||||||
|
deprecated: boolean;
|
||||||
tillerVersion?: string;
|
tillerVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HelmChart {
|
export class HelmChart {
|
||||||
constructor(data: HelmChart) {
|
private constructor(value: HelmChart) {
|
||||||
Object.assign(this, data);
|
this.apiVersion = value.apiVersion;
|
||||||
|
this.name = value.name;
|
||||||
|
this.version = value.version;
|
||||||
|
this.repo = value.repo;
|
||||||
|
this.kubeVersion = value.kubeVersion;
|
||||||
|
this.created = value.created;
|
||||||
|
this.description = value.description;
|
||||||
|
this.digest = value.digest;
|
||||||
|
this.keywords = value.keywords;
|
||||||
|
this.home = value.home;
|
||||||
|
this.sources = value.sources;
|
||||||
|
this.maintainers = value.maintainers;
|
||||||
|
this.engine = value.engine;
|
||||||
|
this.icon = value.icon;
|
||||||
|
this.apiVersion = value.apiVersion;
|
||||||
|
this.deprecated = value.deprecated;
|
||||||
|
this.tillerVersion = value.tillerVersion;
|
||||||
|
this.annotations = value.annotations;
|
||||||
|
this.urls = value.urls;
|
||||||
|
this.dependencies = value.dependencies;
|
||||||
|
this.type = value.type;
|
||||||
|
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(data: any) {
|
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
|
||||||
return new HelmChart(data);
|
const { value, error } = helmChartValidator.validate(data, {
|
||||||
|
abortEarly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return new HelmChart(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [actualErrors, unknownDetails] = bifurcateArray(error.details, ({ type }) => type === "object.unknown");
|
||||||
|
|
||||||
|
if (unknownDetails.length > 0) {
|
||||||
|
console.warn("HelmChart data has unexpected fields", { original: data, unknownFields: unknownDetails.flatMap(d => d.path) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualErrors.length === 0) {
|
||||||
|
return new HelmChart(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original);
|
||||||
|
|
||||||
|
if (onError === "throw") {
|
||||||
|
throw validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[HELM-CHART]: failed to validate data", data, validationError);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId(): string {
|
||||||
return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`;
|
return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName(): string {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullName(splitter = "/") {
|
getFullName(seperator = "/"): string {
|
||||||
return [this.getRepository(), this.getName()].join(splitter);
|
return [this.getRepository(), this.getName()].join(seperator);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription() {
|
getDescription(): string {
|
||||||
return this.description;
|
return this.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIcon() {
|
getIcon(): string | undefined {
|
||||||
return this.icon;
|
return this.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHome() {
|
getHome(): string {
|
||||||
return this.home;
|
return this.home;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaintainers() {
|
getMaintainers(): HelmChartMaintainer[] {
|
||||||
return this.maintainers || [];
|
return this.maintainers;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion(): string {
|
||||||
return this.version;
|
return this.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRepository() {
|
getRepository(): string {
|
||||||
return this.repo;
|
return this.repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppVersion() {
|
getAppVersion(): string | undefined {
|
||||||
return this.appVersion || "";
|
return this.appVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeywords() {
|
getKeywords(): string[] {
|
||||||
return this.keywords || [];
|
return this.keywords;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,19 +19,22 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, ipcMain, remote } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import winston, { format } from "winston";
|
import winston, { format } from "winston";
|
||||||
import type Transport from "winston-transport";
|
import type Transport from "winston-transport";
|
||||||
import { consoleFormat } from "winston-console-format";
|
import { consoleFormat } from "winston-console-format";
|
||||||
import { isDebugging, isTestEnv } from "./vars";
|
import { isDebugging, isTestEnv } from "./vars";
|
||||||
import BrowserConsole from "winston-transport-browserconsole";
|
import BrowserConsole from "winston-transport-browserconsole";
|
||||||
import { SentryTransport } from "./logger-transports";
|
import { SentryTransport } from "./logger-transports";
|
||||||
|
import { getPath } from "./utils/getPath";
|
||||||
|
|
||||||
const logLevel = process.env.LOG_LEVEL
|
const logLevel = process.env.LOG_LEVEL
|
||||||
? process.env.LOG_LEVEL
|
? process.env.LOG_LEVEL
|
||||||
: isDebugging
|
: isDebugging
|
||||||
? "debug"
|
? "debug"
|
||||||
: "info";
|
: isTestEnv
|
||||||
|
? "error"
|
||||||
|
: "info";
|
||||||
|
|
||||||
const transports: Transport[] = [
|
const transports: Transport[] = [
|
||||||
new SentryTransport("error")
|
new SentryTransport("error")
|
||||||
@ -69,7 +72,7 @@ if (!isTestEnv) {
|
|||||||
handleExceptions: false,
|
handleExceptions: false,
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
filename: "lens.log",
|
filename: "lens.log",
|
||||||
dirname: (app ?? remote?.app)?.getPath("logs"),
|
dirname: getPath("logs"),
|
||||||
maxsize: 16 * 1024,
|
maxsize: 16 * 1024,
|
||||||
maxFiles: 16,
|
maxFiles: 16,
|
||||||
tailable: true,
|
tailable: true,
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, remote } from "electron";
|
import { app } from "electron";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
import { BaseStore } from "../base-store";
|
import { BaseStore } from "../base-store";
|
||||||
@ -32,7 +32,8 @@ import { fileNameMigration } from "../../migrations/user-store";
|
|||||||
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||||
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers";
|
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type {monaco} from "react-monaco-editor";
|
import type { monaco } from "react-monaco-editor";
|
||||||
|
import { getPath } from "../utils/getPath";
|
||||||
|
|
||||||
export interface UserStoreModel {
|
export interface UserStoreModel {
|
||||||
lastSeenAppVersion: string;
|
lastSeenAppVersion: string;
|
||||||
@ -69,7 +70,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
@observable shell?: string;
|
@observable shell?: string;
|
||||||
@observable downloadBinariesPath?: string;
|
@observable downloadBinariesPath?: string;
|
||||||
@observable kubectlBinariesPath?: string;
|
@observable kubectlBinariesPath?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download kubectl binaries matching cluster version
|
* Download kubectl binaries matching cluster version
|
||||||
*/
|
*/
|
||||||
@ -86,7 +87,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
* Monaco editor configs
|
* Monaco editor configs
|
||||||
*/
|
*/
|
||||||
@observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null};
|
@observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set of file/folder paths to be synced
|
* The set of file/folder paths to be synced
|
||||||
*/
|
*/
|
||||||
@ -115,7 +116,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted)
|
// Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted)
|
||||||
@ -244,5 +245,5 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
* @returns string
|
* @returns string
|
||||||
*/
|
*/
|
||||||
export function getDefaultKubectlDownloadPath(): string {
|
export function getDefaultKubectlDownloadPath(): string {
|
||||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
return path.join(getPath("userData"), "binaries");
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/common/utils/__tests__/iter.test.ts
Normal file
34
src/common/utils/__tests__/iter.test.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { reduce } from "../iter";
|
||||||
|
|
||||||
|
describe("iter", () => {
|
||||||
|
describe("reduce", () => {
|
||||||
|
it("can reduce a value", () => {
|
||||||
|
expect(reduce([1, 2, 3], (acc: number[], current: number) => [current, ...acc], [0])).toEqual([3, 2, 1, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can reduce an empty iterable", () => {
|
||||||
|
expect(reduce([], (acc: number[], current: number) => [acc[0] + current], [])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -19,23 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import { app, ipcMain } from "electron";
|
||||||
import { welcomeURL } from "../../../common/routes";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
|
|
||||||
export function CatalogTopbar() {
|
const remote = ipcMain ? null : require("@electron/remote");
|
||||||
return (
|
|
||||||
<TopBar label="Catalog">
|
/**
|
||||||
<div>
|
* calls getPath either on app or on the remote's app
|
||||||
<Icon
|
*
|
||||||
style={{ cursor: "default" }}
|
* @deprecated Use a different method for accessing the getPath function
|
||||||
material="close"
|
*/
|
||||||
onClick={() => navigate(welcomeURL())}
|
export function getPath(name: Parameters<typeof app["getPath"]>[0]): string {
|
||||||
tooltip="Close Catalog"
|
if (app) {
|
||||||
/>
|
return app.getPath(name);
|
||||||
</div>
|
}
|
||||||
</TopBar>
|
|
||||||
);
|
return remote.app.getPath(name);
|
||||||
}
|
}
|
||||||
@ -40,6 +40,7 @@ export * from "./downloadFile";
|
|||||||
export * from "./formatDuration";
|
export * from "./formatDuration";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./extended-map";
|
export * from "./extended-map";
|
||||||
|
export * from "./getPath";
|
||||||
export * from "./getRandId";
|
export * from "./getRandId";
|
||||||
export * from "./hash-set";
|
export * from "./hash-set";
|
||||||
export * from "./local-kubeconfig";
|
export * from "./local-kubeconfig";
|
||||||
|
|||||||
@ -156,3 +156,22 @@ export function find<T>(src: Iterable<T>, match: (i: T) => any): T | undefined {
|
|||||||
|
|
||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over `src` calling `reducer` with the previous produced value and the current
|
||||||
|
* yielded value until `src` is exausted. Then return the final value.
|
||||||
|
* @param src The value to iterate over
|
||||||
|
* @param reducer A function for producing the next item from an accumilation and the current item
|
||||||
|
* @param initial The initial value for the iteration
|
||||||
|
*/
|
||||||
|
export function reduce<T, R>(src: Iterable<T>, reducer: (acc: Iterable<R>, cur: T) => Iterable<R>, initial: Iterable<R>): Iterable<R>;
|
||||||
|
|
||||||
|
export function reduce<T, R = T>(src: Iterable<T>, reducer: (acc: R, cur: T) => R, initial: R): R {
|
||||||
|
let acc = initial;
|
||||||
|
|
||||||
|
for (const item of src) {
|
||||||
|
acc = reducer(acc, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|||||||
@ -19,13 +19,13 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, remote } from "electron";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as uuid from "uuid";
|
import * as uuid from "uuid";
|
||||||
import type { ClusterId } from "../cluster-types";
|
import type { ClusterId } from "../cluster-types";
|
||||||
|
import { getPath } from "./getPath";
|
||||||
|
|
||||||
export function storedKubeConfigFolder(): string {
|
export function storedKubeConfigFolder(): string {
|
||||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
|
return path.resolve(getPath("userData"), "kubeconfigs");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
|
export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
|
||||||
|
|||||||
@ -19,7 +19,10 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import semver, { SemVer } from "semver";
|
import semver, { coerce, SemVer } from "semver";
|
||||||
|
import * as iter from "./iter";
|
||||||
|
import type { RawHelmChart } from "../k8s-api/endpoints/helm-charts.api";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
|
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
|
||||||
if (left < right) {
|
if (left < right) {
|
||||||
@ -53,3 +56,32 @@ export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion
|
|||||||
|
|
||||||
return sortCompare(left.version, right.version);
|
return sortCompare(left.version, right.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function sortCharts(charts: RawHelmChart[]) {
|
||||||
|
interface ExtendedHelmChart extends RawHelmChart {
|
||||||
|
__version: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartsWithVersion = Array.from(
|
||||||
|
iter.map(
|
||||||
|
charts,
|
||||||
|
(chart => {
|
||||||
|
const __version = coerce(chart.version, { includePrerelease: true, loose: true });
|
||||||
|
|
||||||
|
if (!__version) {
|
||||||
|
logger.warn(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
||||||
|
}
|
||||||
|
|
||||||
|
(chart as ExtendedHelmChart).__version = __version;
|
||||||
|
|
||||||
|
return chart as ExtendedHelmChart;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return chartsWithVersion
|
||||||
|
.sort(sortCompareChartVersions)
|
||||||
|
.map(chart => (delete chart.__version, chart));
|
||||||
|
}
|
||||||
|
|||||||
@ -35,6 +35,9 @@ export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
|||||||
export const isDevelopment = !isTestEnv && !isProduction;
|
export const isDevelopment = !isTestEnv && !isProduction;
|
||||||
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
|
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
|
||||||
|
|
||||||
|
export const integrationTestingArg = "--integration-testing";
|
||||||
|
export const isIntegrationTesting = process.argv.includes(integrationTestingArg);
|
||||||
|
|
||||||
export const productName = packageInfo.productName;
|
export const productName = packageInfo.productName;
|
||||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||||
export const publicPath = "/build/" as string;
|
export const publicPath = "/build/" as string;
|
||||||
|
|||||||
@ -19,14 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, ipcRenderer, remote } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ClusterStore } from "../common/cluster-store";
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
|
||||||
import { Disposer, getHostedClusterId, Singleton, toJS } from "../common/utils";
|
import { Disposer, getHostedClusterId, Singleton, toJS, getPath } from "../common/utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery";
|
||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
@ -36,7 +36,7 @@ import type { LensRendererExtension } from "./lens-renderer-extension";
|
|||||||
import * as registries from "./registries";
|
import * as registries from "./registries";
|
||||||
|
|
||||||
export function extensionPackagesRoot() {
|
export function extensionPackagesRoot() {
|
||||||
return path.join((app || remote.app).getPath("userData"));
|
return path.join(getPath("userData"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const logModule = "[EXTENSIONS-LOADER]";
|
const logModule = "[EXTENSIONS-LOADER]";
|
||||||
@ -278,6 +278,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
registries.CommandRegistry.getInstance().add(extension.commands),
|
||||||
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
|
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
|
||||||
|
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
|
||||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||||
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
|
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
import type * as registries from "./registries";
|
import type * as registries from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { Disposers, LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
import type { CatalogEntity } from "../common/catalog";
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
|
import type { Disposer } from "../common/utils";
|
||||||
|
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -38,6 +40,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||||
|
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: registries.TopBarRegistration[] = [];
|
||||||
|
|
||||||
@ -58,4 +61,17 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
||||||
return (void cluster) || true;
|
return (void cluster) || true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a filtering function for the catalog. This will be removed if the extension is disabled.
|
||||||
|
* @param fn The function which should return a truthy value for those entities which should be kepted
|
||||||
|
* @returns A function to clean up the filter
|
||||||
|
*/
|
||||||
|
addCatalogFilter(fn: EntityFilter): Disposer {
|
||||||
|
const dispose = catalogEntityRegistry.addCatalogFilter(fn);
|
||||||
|
|
||||||
|
this[Disposers].push(dispose);
|
||||||
|
|
||||||
|
return dispose;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export * from "./kube-object-status-registry";
|
|||||||
export * from "./command-registry";
|
export * from "./command-registry";
|
||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
export * from "./welcome-menu-registry";
|
||||||
|
export * from "./welcome-banner-registry";
|
||||||
export * from "./catalog-entity-detail-registry";
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./workloads-overview-detail-registry";
|
export * from "./workloads-overview-detail-registry";
|
||||||
export * from "./topbar-registry";
|
export * from "./topbar-registry";
|
||||||
|
|||||||
39
src/extensions/registries/welcome-banner-registry.ts
Normal file
39
src/extensions/registries/welcome-banner-registry.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WelcomeBannerRegistration is for an extension to register
|
||||||
|
* Provide a Banner component to be renderered in the welcome screen.
|
||||||
|
*/
|
||||||
|
export interface WelcomeBannerRegistration {
|
||||||
|
/**
|
||||||
|
* The banner component to be shown on the welcome screen.
|
||||||
|
*/
|
||||||
|
Banner?: React.ComponentType
|
||||||
|
/**
|
||||||
|
* The banner width in px.
|
||||||
|
*/
|
||||||
|
width?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }
|
||||||
@ -43,24 +43,20 @@ export class CatalogEntityRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get items(): CatalogEntity[] {
|
@computed get items(): CatalogEntity[] {
|
||||||
const allItems = Array.from(iter.flatMap(this.sources.values(), source => source.get()));
|
return Array.from(
|
||||||
|
iter.filter(
|
||||||
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
|
iter.flatMap(this.sources.values(), source => source.get()),
|
||||||
|
entity => this.categoryRegistry.getCategoryForEntity(entity)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getById<T extends CatalogEntity>(id: string): T | undefined {
|
getById<T extends CatalogEntity>(id: string): T | undefined {
|
||||||
const item = this.items.find((entity) => entity.metadata.uid === id);
|
return this.items.find((entity) => entity.metadata.uid === id) as T | undefined;
|
||||||
|
|
||||||
if (item) return item as T;
|
|
||||||
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
||||||
|
|
||||||
return items as T[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
||||||
|
|||||||
@ -33,7 +33,8 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
|
|||||||
import { DetectorRegistry } from "./cluster-detectors/detector-registry";
|
import { DetectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
import { initialNodeShellImage, ClusterState, ClusterMetadataKey, ClusterRefreshOptions, ClusterStatus, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types";
|
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types";
|
||||||
|
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cluster
|
* Cluster
|
||||||
|
|||||||
@ -26,14 +26,9 @@ import { ClusterManager } from "./cluster-manager";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export function exitApp() {
|
export function exitApp() {
|
||||||
console.log("before windowManager");
|
|
||||||
const windowManager = WindowManager.getInstance(false);
|
const windowManager = WindowManager.getInstance(false);
|
||||||
|
|
||||||
console.log("before clusterManager");
|
|
||||||
const clusterManager = ClusterManager.getInstance(false);
|
const clusterManager = ClusterManager.getInstance(false);
|
||||||
|
|
||||||
console.log("after clusterManager");
|
|
||||||
|
|
||||||
appEventBus.emit({ name: "service", action: "close" });
|
appEventBus.emit({ name: "service", action: "close" });
|
||||||
windowManager?.hide();
|
windowManager?.hide();
|
||||||
clusterManager?.stop();
|
clusterManager?.stop();
|
||||||
|
|||||||
@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { SHA256 } from "crypto-js";
|
import { SHA256 } from "crypto-js";
|
||||||
import { app, remote } from "electron";
|
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { action, makeObservable, observable } from "mobx";
|
import { action, makeObservable, observable } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { BaseStore } from "../common/base-store";
|
import { BaseStore } from "../common/base-store";
|
||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
|
import { getPath } from "../common/utils/getPath";
|
||||||
|
|
||||||
interface FSProvisionModel {
|
interface FSProvisionModel {
|
||||||
extensions: Record<string, string>; // extension names to paths
|
extensions: Record<string, string>; // extension names to paths
|
||||||
@ -55,7 +55,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
if (!this.registeredExtensions.has(extensionName)) {
|
if (!this.registeredExtensions.has(extensionName)) {
|
||||||
const salt = randomBytes(32).toString("hex");
|
const salt = randomBytes(32).toString("hex");
|
||||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||||
const dirPath = path.resolve((app || remote.app).getPath("userData"), "extension_data", hashedName);
|
const dirPath = path.resolve(getPath("userData"), "extension_data", hashedName);
|
||||||
|
|
||||||
this.registeredExtensions.set(extensionName, dirPath);
|
this.registeredExtensions.set(extensionName, dirPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,141 +19,151 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HelmRepo, HelmRepoManager } from "../helm-repo-manager";
|
import { sortCharts } from "../../../common/utils";
|
||||||
|
import type { HelmRepo } from "../helm-repo-manager";
|
||||||
|
|
||||||
|
const charts = new Map([
|
||||||
|
["stable", {
|
||||||
|
"invalid-semver": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.3.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver but more",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.4.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
"apm-server": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "apm-server",
|
||||||
|
version: "2.1.7",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "apm-server",
|
||||||
|
version: "2.1.6",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
"redis": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "apm-server",
|
||||||
|
version: "1.0.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "apm-server",
|
||||||
|
version: "0.0.9",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
}],
|
||||||
|
["experiment", {
|
||||||
|
"fairwind": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "fairwind",
|
||||||
|
version: "0.0.1",
|
||||||
|
repo: "experiment",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "fairwind",
|
||||||
|
version: "0.0.2",
|
||||||
|
repo: "experiment",
|
||||||
|
digest: "test",
|
||||||
|
deprecated: true,
|
||||||
|
created: "now",
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
}],
|
||||||
|
["bitnami", {
|
||||||
|
"hotdog": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "hotdog",
|
||||||
|
version: "1.0.1",
|
||||||
|
repo: "bitnami",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "hotdog",
|
||||||
|
version: "1.0.2",
|
||||||
|
repo: "bitnami",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
"pretzel": sortCharts([
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "pretzel",
|
||||||
|
version: "1.0",
|
||||||
|
repo: "bitnami",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "pretzel",
|
||||||
|
version: "1.0.1",
|
||||||
|
repo: "bitnami",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
export class HelmChartManager {
|
export class HelmChartManager {
|
||||||
cache: any = {};
|
constructor(private repo: HelmRepo){ }
|
||||||
private repo: HelmRepo;
|
|
||||||
|
|
||||||
constructor(repo: HelmRepo){
|
static forRepo(repo: HelmRepo) {
|
||||||
this.cache = HelmRepoManager.cache;
|
return new this(repo);
|
||||||
this.repo = repo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async charts(): Promise<any> {
|
public async charts(): Promise<any> {
|
||||||
switch (this.repo.name) {
|
return charts.get(this.repo.name) ?? {};
|
||||||
case "stable":
|
|
||||||
return Promise.resolve({
|
|
||||||
"invalid-semver": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "weird-versioning",
|
|
||||||
version: "I am not semver",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "weird-versioning",
|
|
||||||
version: "v4.3.0",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "weird-versioning",
|
|
||||||
version: "I am not semver but more",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "weird-versioning",
|
|
||||||
version: "v4.4.0",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"apm-server": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "apm-server",
|
|
||||||
version: "2.1.7",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "apm-server",
|
|
||||||
version: "2.1.6",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"redis": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "apm-server",
|
|
||||||
version: "1.0.0",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "apm-server",
|
|
||||||
version: "0.0.9",
|
|
||||||
repo: "stable",
|
|
||||||
digest: "test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
case "experiment":
|
|
||||||
return Promise.resolve({
|
|
||||||
"fairwind": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "fairwind",
|
|
||||||
version: "0.0.1",
|
|
||||||
repo: "experiment",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "fairwind",
|
|
||||||
version: "0.0.2",
|
|
||||||
repo: "experiment",
|
|
||||||
digest: "test",
|
|
||||||
deprecated: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
case "bitnami":
|
|
||||||
return Promise.resolve({
|
|
||||||
"hotdog": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "hotdog",
|
|
||||||
version: "1.0.1",
|
|
||||||
repo: "bitnami",
|
|
||||||
digest: "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "hotdog",
|
|
||||||
version: "1.0.2",
|
|
||||||
repo: "bitnami",
|
|
||||||
digest: "test",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pretzel": [
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "pretzel",
|
|
||||||
version: "1.0",
|
|
||||||
repo: "bitnami",
|
|
||||||
digest: "test",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
apiVersion: "3.0.0",
|
|
||||||
name: "pretzel",
|
|
||||||
version: "1.0.1",
|
|
||||||
repo: "bitnami",
|
|
||||||
digest: "test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ describe("Helm Service tests", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("list charts without deprecated ones", async () => {
|
it("list charts with deprecated entries", async () => {
|
||||||
mockHelmRepoManager.mockReturnValue({
|
mockHelmRepoManager.mockReturnValue({
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
repositories: jest.fn().mockImplementation(async () => {
|
repositories: jest.fn().mockImplementation(async () => {
|
||||||
@ -52,14 +52,16 @@ describe("Helm Service tests", () => {
|
|||||||
name: "apm-server",
|
name: "apm-server",
|
||||||
version: "2.1.7",
|
version: "2.1.7",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "apm-server",
|
name: "apm-server",
|
||||||
version: "2.1.6",
|
version: "2.1.6",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"invalid-semver": [
|
"invalid-semver": [
|
||||||
@ -68,28 +70,32 @@ describe("Helm Service tests", () => {
|
|||||||
name: "weird-versioning",
|
name: "weird-versioning",
|
||||||
version: "v4.4.0",
|
version: "v4.4.0",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "weird-versioning",
|
name: "weird-versioning",
|
||||||
version: "v4.3.0",
|
version: "v4.3.0",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "weird-versioning",
|
name: "weird-versioning",
|
||||||
version: "I am not semver",
|
version: "I am not semver",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "weird-versioning",
|
name: "weird-versioning",
|
||||||
version: "I am not semver but more",
|
version: "I am not semver but more",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"redis": [
|
"redis": [
|
||||||
@ -98,18 +104,40 @@ describe("Helm Service tests", () => {
|
|||||||
name: "apm-server",
|
name: "apm-server",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "apm-server",
|
name: "apm-server",
|
||||||
version: "0.0.9",
|
version: "0.0.9",
|
||||||
repo: "stable",
|
repo: "stable",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
experiment: {}
|
experiment: {
|
||||||
|
"fairwind": [
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "fairwind",
|
||||||
|
version: "0.0.2",
|
||||||
|
repo: "experiment",
|
||||||
|
digest: "test",
|
||||||
|
deprecated: true,
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "fairwind",
|
||||||
|
version: "0.0.1",
|
||||||
|
repo: "experiment",
|
||||||
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -134,13 +162,15 @@ describe("Helm Service tests", () => {
|
|||||||
version: "1.0.2",
|
version: "1.0.2",
|
||||||
repo: "bitnami",
|
repo: "bitnami",
|
||||||
digest: "test",
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "hotdog",
|
name: "hotdog",
|
||||||
version: "1.0.1",
|
version: "1.0.1",
|
||||||
repo: "bitnami",
|
repo: "bitnami",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"pretzel": [
|
"pretzel": [
|
||||||
@ -150,13 +180,15 @@ describe("Helm Service tests", () => {
|
|||||||
version: "1.0.1",
|
version: "1.0.1",
|
||||||
repo: "bitnami",
|
repo: "bitnami",
|
||||||
digest: "test",
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
name: "pretzel",
|
name: "pretzel",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
repo: "bitnami",
|
repo: "bitnami",
|
||||||
digest: "test"
|
digest: "test",
|
||||||
|
created: "now",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,27 +20,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import v8 from "v8";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
|
import type { HelmRepo } from "./helm-repo-manager";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { promiseExec } from "../promise-exec";
|
import { promiseExec } from "../promise-exec";
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
|
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
|
||||||
|
import { sortCharts } from "../../common/utils";
|
||||||
type CachedYaml = {
|
|
||||||
entries: RepoHelmChartList
|
|
||||||
};
|
|
||||||
|
|
||||||
export class HelmChartManager {
|
export class HelmChartManager {
|
||||||
protected cache: any = {};
|
static #cache = new Map<string, Buffer>();
|
||||||
protected repo: HelmRepo;
|
|
||||||
|
|
||||||
constructor(repo: HelmRepo){
|
private constructor(protected repo: HelmRepo) {}
|
||||||
this.cache = HelmRepoManager.cache;
|
|
||||||
this.repo = repo;
|
static forRepo(repo: HelmRepo) {
|
||||||
|
return new this(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async chart(name: string) {
|
public async chartVersions(name: string) {
|
||||||
const charts = await this.charts();
|
const charts = await this.charts();
|
||||||
|
|
||||||
return charts[name];
|
return charts[name];
|
||||||
@ -48,9 +46,7 @@ export class HelmChartManager {
|
|||||||
|
|
||||||
public async charts(): Promise<RepoHelmChartList> {
|
public async charts(): Promise<RepoHelmChartList> {
|
||||||
try {
|
try {
|
||||||
const cachedYaml = await this.cachedYaml();
|
return await this.cachedYaml();
|
||||||
|
|
||||||
return cachedYaml["entries"];
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
|
logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
|
||||||
|
|
||||||
@ -58,48 +54,61 @@ export class HelmChartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReadme(name: string, version = "") {
|
private async executeCommand(action: string, name: string, version?: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
const cmd = [`"${helm}" ${action} ${this.repo.name}/${name}`];
|
||||||
|
|
||||||
if(version && version != "") {
|
if (version) {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
cmd.push("--version", version);
|
||||||
|
}
|
||||||
return stdout;
|
|
||||||
} else {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(cmd.join(" "));
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getValues(name: string, version = "") {
|
public async getReadme(name: string, version?: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
return this.executeCommand("show readme", name, version);
|
||||||
|
|
||||||
if(version && version != "") {
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
|
||||||
|
|
||||||
return stdout;
|
|
||||||
} else {
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
|
||||||
|
|
||||||
return stdout;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async cachedYaml(): Promise<CachedYaml> {
|
public async getValues(name: string, version?: string) {
|
||||||
if (!(this.repo.name in this.cache)) {
|
return this.executeCommand("show values", name, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async cachedYaml(): Promise<RepoHelmChartList> {
|
||||||
|
if (!HelmChartManager.#cache.has(this.repo.name)) {
|
||||||
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
||||||
const data = yaml.safeLoad(cacheFile);
|
const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList };
|
||||||
|
|
||||||
for(const key in data["entries"]) {
|
/**
|
||||||
data["entries"][key].forEach((version: any) => {
|
* Do some initial preprocessing on the data, so as to avoid needing to do it later
|
||||||
version["repo"] = this.repo.name;
|
* 1. Set the repo name
|
||||||
version["created"] = Date.parse(version.created).toString();
|
* 2. Normalize the created date
|
||||||
});
|
* 3. Filter out deprecated items
|
||||||
}
|
*/
|
||||||
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
|
|
||||||
|
const normalized = Object.fromEntries(
|
||||||
|
Object.entries(entries)
|
||||||
|
.map(([name, charts]) => [
|
||||||
|
name,
|
||||||
|
sortCharts(
|
||||||
|
charts.map(chart => ({
|
||||||
|
...chart,
|
||||||
|
created: Date.parse(chart.created).toString(),
|
||||||
|
repo: this.repo.name,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
] as const)
|
||||||
|
.filter(([, charts]) => !charts.every(chart => chart.deprecated))
|
||||||
|
);
|
||||||
|
|
||||||
|
HelmChartManager.#cache.set(this.repo.name, v8.serialize(normalized));
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(this.cache[this.repo.name].toString());
|
return v8.deserialize(HelmChartManager.#cache.get(this.repo.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const helmVersion = packageInfo.config.bundledHelmVersion;
|
|||||||
let baseDir = process.resourcesPath;
|
let baseDir = process.resourcesPath;
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
baseDir = path.join(process.cwd(), "binaries", "client");
|
baseDir = path.join(process.cwd(), "binaries", "client", process.arch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helmCli = new HelmCli(baseDir, helmVersion);
|
export const helmCli = new HelmCli(baseDir, helmVersion);
|
||||||
|
|||||||
@ -50,8 +50,6 @@ export interface HelmRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class HelmRepoManager extends Singleton {
|
export class HelmRepoManager extends Singleton {
|
||||||
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
|
|
||||||
|
|
||||||
protected repos: HelmRepo[];
|
protected repos: HelmRepo[];
|
||||||
protected helmEnv: HelmEnv;
|
protected helmEnv: HelmEnv;
|
||||||
protected initialized: boolean;
|
protected initialized: boolean;
|
||||||
@ -97,6 +95,12 @@ export class HelmRepoManager extends Singleton {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async repo(name: string): Promise<HelmRepo> {
|
||||||
|
const repos = await this.repositories();
|
||||||
|
|
||||||
|
return repos.find(repo => repo.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
public async repositories(): Promise<HelmRepo[]> {
|
public async repositories(): Promise<HelmRepo[]> {
|
||||||
try {
|
try {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
|
|||||||
@ -19,14 +19,11 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import semver, { SemVer } from "semver";
|
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { HelmRepoManager } from "./helm-repo-manager";
|
import { HelmRepoManager } from "./helm-repo-manager";
|
||||||
import { HelmChartManager } from "./helm-chart-manager";
|
import { HelmChartManager } from "./helm-chart-manager";
|
||||||
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
|
|
||||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||||
import { iter, sortCompareChartVersions } from "../../common/utils";
|
|
||||||
|
|
||||||
interface GetReleaseValuesArgs {
|
interface GetReleaseValuesArgs {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -42,43 +39,27 @@ class HelmService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async listCharts() {
|
public async listCharts() {
|
||||||
const charts: HelmChartList = {};
|
|
||||||
const repositories = await HelmRepoManager.getInstance().repositories();
|
const repositories = await HelmRepoManager.getInstance().repositories();
|
||||||
|
|
||||||
for (const repo of repositories) {
|
return Object.fromEntries(
|
||||||
charts[repo.name] = {};
|
await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()]))
|
||||||
const manager = new HelmChartManager(repo);
|
);
|
||||||
const sortedCharts = this.sortChartsByVersion(await manager.charts());
|
|
||||||
const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts);
|
|
||||||
|
|
||||||
charts[repo.name] = enabledCharts;
|
|
||||||
}
|
|
||||||
|
|
||||||
return charts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChart(repoName: string, chartName: string, version = "") {
|
public async getChart(repoName: string, chartName: string, version = "") {
|
||||||
const result = {
|
const repo = await HelmRepoManager.getInstance().repo(repoName);
|
||||||
readme: "",
|
const chartManager = HelmChartManager.forRepo(repo);
|
||||||
versions: {}
|
|
||||||
|
return {
|
||||||
|
readme: await chartManager.getReadme(chartName, version),
|
||||||
|
versions: await chartManager.chartVersions(chartName),
|
||||||
};
|
};
|
||||||
const repos = await HelmRepoManager.getInstance().repositories();
|
|
||||||
const repo = repos.find(repo => repo.name === repoName);
|
|
||||||
const chartManager = new HelmChartManager(repo);
|
|
||||||
const chart = await chartManager.chart(chartName);
|
|
||||||
|
|
||||||
result.readme = await chartManager.getReadme(chartName, version);
|
|
||||||
result.versions = chart;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChartValues(repoName: string, chartName: string, version = "") {
|
public async getChartValues(repoName: string, chartName: string, version = "") {
|
||||||
const repos = await HelmRepoManager.getInstance().repositories();
|
const repo = await HelmRepoManager.getInstance().repo(repoName);
|
||||||
const repo = repos.find(repo => repo.name === repoName);
|
|
||||||
const chartManager = new HelmChartManager(repo);
|
|
||||||
|
|
||||||
return chartManager.getValues(chartName, version);
|
return HelmChartManager.forRepo(repo).getValues(chartName, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||||
@ -131,58 +112,6 @@ class HelmService {
|
|||||||
|
|
||||||
return { message: output };
|
return { message: output };
|
||||||
}
|
}
|
||||||
|
|
||||||
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
iter.filterMap(
|
|
||||||
Object.entries(chartGroups),
|
|
||||||
([name, charts]) => {
|
|
||||||
for (const chart of charts) {
|
|
||||||
if (chart.deprecated) {
|
|
||||||
// ignore chart group if any chart is deprecated
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [name, charts];
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sortCharts(charts: HelmChart[]) {
|
|
||||||
interface ExtendedHelmChart extends HelmChart {
|
|
||||||
__version: SemVer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartsWithVersion = Array.from(
|
|
||||||
iter.map(
|
|
||||||
charts,
|
|
||||||
(chart => {
|
|
||||||
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
|
|
||||||
|
|
||||||
if (!__version) {
|
|
||||||
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
|
||||||
}
|
|
||||||
|
|
||||||
(chart as ExtendedHelmChart).__version = __version;
|
|
||||||
|
|
||||||
return chart as ExtendedHelmChart;
|
|
||||||
})
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return chartsWithVersion
|
|
||||||
.sort(sortCompareChartVersions)
|
|
||||||
.map(chart => (delete chart.__version, chart as HelmChart));
|
|
||||||
}
|
|
||||||
|
|
||||||
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(chartGroups)
|
|
||||||
.map(([name, charts]) => [name, this.sortCharts(charts)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helmService = new HelmService();
|
export const helmService = new HelmService();
|
||||||
|
|||||||
@ -22,12 +22,12 @@
|
|||||||
// Main process
|
// Main process
|
||||||
|
|
||||||
import "../common/system-ca";
|
import "../common/system-ca";
|
||||||
|
import { initialize as initializeRemote } from "@electron/remote/main";
|
||||||
import * as Mobx from "mobx";
|
import * as Mobx from "mobx";
|
||||||
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
||||||
import * as LensExtensionsMainApi from "../extensions/main-api";
|
import * as LensExtensionsMainApi from "../extensions/main-api";
|
||||||
import { app, autoUpdater, dialog, powerMonitor } from "electron";
|
import { app, autoUpdater, dialog, powerMonitor } from "electron";
|
||||||
import { appName, isMac, productName } from "../common/vars";
|
import { appName, isIntegrationTesting, isMac, productName } from "../common/vars";
|
||||||
import path from "path";
|
|
||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { ClusterManager } from "./cluster-manager";
|
import { ClusterManager } from "./cluster-manager";
|
||||||
@ -63,13 +63,13 @@ import { ensureDir } from "fs-extra";
|
|||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import { initMenu } from "./menu";
|
import { initMenu } from "./menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
|
import * as path from "path";
|
||||||
import { kubeApiRequest, shellApiRequest } from "./proxy-functions";
|
import { kubeApiRequest, shellApiRequest } from "./proxy-functions";
|
||||||
|
|
||||||
|
const onCloseCleanup = disposer();
|
||||||
|
const onQuitCleanup = disposer();
|
||||||
|
|
||||||
SentryInit();
|
SentryInit();
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
|
||||||
const cleanup = disposer();
|
|
||||||
|
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||||
@ -80,14 +80,16 @@ if (app.setAsDefaultProtocolClient("lens")) {
|
|||||||
logger.info("📟 Protocol client register failed ❗");
|
logger.info("📟 Protocol client register failed ❗");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.CICD) {
|
if (process.env.CICD) {
|
||||||
app.setPath("userData", workingDir);
|
app.setPath("appData", process.env.CICD);
|
||||||
|
app.setPath("userData", path.join(process.env.CICD, appName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.LENS_DISABLE_GPU) {
|
if (process.env.LENS_DISABLE_GPU) {
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeRemote();
|
||||||
configurePackages();
|
configurePackages();
|
||||||
mangleProxyEnv();
|
mangleProxyEnv();
|
||||||
initializers.initIpcMainHandlers();
|
initializers.initIpcMainHandlers();
|
||||||
@ -121,7 +123,7 @@ app.on("second-instance", (event, argv) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logger.info(`🚀 Starting ${productName} from "${workingDir}"`);
|
logger.info(`🚀 Starting ${productName} from "${app.getPath("exe")}"`);
|
||||||
logger.info("🐚 Syncing shell environment");
|
logger.info("🐚 Syncing shell environment");
|
||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
@ -209,7 +211,7 @@ app.on("ready", async () => {
|
|||||||
logger.info("🖥️ Starting WindowManager");
|
logger.info("🖥️ Starting WindowManager");
|
||||||
const windowManager = WindowManager.createInstance();
|
const windowManager = WindowManager.createInstance();
|
||||||
|
|
||||||
cleanup.push(
|
onQuitCleanup.push(
|
||||||
initMenu(windowManager),
|
initMenu(windowManager),
|
||||||
initTray(windowManager),
|
initTray(windowManager),
|
||||||
);
|
);
|
||||||
@ -221,7 +223,7 @@ app.on("ready", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
||||||
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
|
onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
|
||||||
await ensureDir(storedKubeConfigFolder());
|
await ensureDir(storedKubeConfigFolder());
|
||||||
KubeconfigSyncManager.getInstance().startSync();
|
KubeconfigSyncManager.getInstance().startSync();
|
||||||
startUpdateChecking();
|
startUpdateChecking();
|
||||||
@ -269,7 +271,7 @@ app.on("activate", (event, hasVisibleWindows) => {
|
|||||||
/**
|
/**
|
||||||
* This variable should is used so that `autoUpdater.installAndQuit()` works
|
* This variable should is used so that `autoUpdater.installAndQuit()` works
|
||||||
*/
|
*/
|
||||||
let blockQuit = true;
|
let blockQuit = !isIntegrationTesting;
|
||||||
|
|
||||||
autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
||||||
|
|
||||||
@ -282,7 +284,7 @@ app.on("will-quit", (event) => {
|
|||||||
appEventBus.emit({ name: "app", action: "close" });
|
appEventBus.emit({ name: "app", action: "close" });
|
||||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||||
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||||
cleanup();
|
onCloseCleanup();
|
||||||
|
|
||||||
if (lprm) {
|
if (lprm) {
|
||||||
// This is set to false here so that LPRM can wait to send future lens://
|
// This is set to false here so that LPRM can wait to send future lens://
|
||||||
@ -298,7 +300,8 @@ app.on("will-quit", (event) => {
|
|||||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||||
}
|
}
|
||||||
|
|
||||||
LensProtocolRouterMain.getInstance(false)?.cleanup();
|
lprm?.cleanup();
|
||||||
|
onQuitCleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("open-url", (event, rawUrl) => {
|
app.on("open-url", (event, rawUrl) => {
|
||||||
|
|||||||
@ -19,20 +19,21 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IpcMainInvokeEvent } from "electron";
|
import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
|
||||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { ipcMainHandle } from "../../common/ipc";
|
import { dialogShowOpenDialogHandler, ipcMainHandle } from "../../common/ipc";
|
||||||
import { catalogEntityRegistry } from "../catalog";
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
import { ClusterManager } from "../cluster-manager";
|
import { ClusterManager } from "../cluster-manager";
|
||||||
import { bundledKubectlPath } from "../kubectl";
|
import { bundledKubectlPath } from "../kubectl";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { promiseExecFile } from "../promise-exec";
|
import { promiseExecFile } from "../promise-exec";
|
||||||
import { ResourceApplier } from "../resource-applier";
|
import { ResourceApplier } from "../resource-applier";
|
||||||
|
import { WindowManager } from "../window-manager";
|
||||||
|
|
||||||
export function initIpcMainHandlers() {
|
export function initIpcMainHandlers() {
|
||||||
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||||
@ -138,4 +139,10 @@ export function initIpcMainHandlers() {
|
|||||||
throw `${clusterId} is not a valid cluster id`;
|
throw `${clusterId} is not a valid cluster id`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => {
|
||||||
|
await WindowManager.getInstance().ensureMainWindow();
|
||||||
|
|
||||||
|
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, remote } from "electron";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { promiseExec } from "./promise-exec";
|
import { promiseExec } from "./promise-exec";
|
||||||
@ -32,6 +31,7 @@ import { customRequest } from "../common/request";
|
|||||||
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
|
import { getPath } from "../common/utils/getPath";
|
||||||
|
|
||||||
const bundledVersion = getBundledKubectlVersion();
|
const bundledVersion = getBundledKubectlVersion();
|
||||||
const kubectlMap: Map<string, string> = new Map([
|
const kubectlMap: Map<string, string> = new Map([
|
||||||
@ -84,7 +84,7 @@ export class Kubectl {
|
|||||||
protected dirname: string;
|
protected dirname: string;
|
||||||
|
|
||||||
static get kubectlDir() {
|
static get kubectlDir() {
|
||||||
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl");
|
return path.join(getPath("userData"), "binaries", "kubectl");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly bundledKubectlVersion: string = bundledVersion;
|
public static readonly bundledKubectlVersion: string = bundledVersion;
|
||||||
|
|||||||
@ -60,13 +60,13 @@ export class LensBinary {
|
|||||||
this.logger = console;
|
this.logger = console;
|
||||||
let arch = null;
|
let arch = null;
|
||||||
|
|
||||||
if (process.arch == "x64") {
|
if (process.env.BINARY_ARCH) {
|
||||||
|
arch = process.env.BINARY_ARCH;
|
||||||
|
} else if (process.arch == "x64") {
|
||||||
arch = "amd64";
|
arch = "amd64";
|
||||||
}
|
} else if (process.arch == "x86" || process.arch == "ia32") {
|
||||||
else if (process.arch == "x86" || process.arch == "ia32") {
|
|
||||||
arch = "386";
|
arch = "386";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
arch = process.arch;
|
arch = process.arch;
|
||||||
}
|
}
|
||||||
this.arch = arch;
|
this.arch = arch;
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
accelerator: "CmdOrCtrl+,",
|
accelerator: "CmdOrCtrl+,",
|
||||||
click() {
|
click() {
|
||||||
navigate(preferencesURL());
|
navigate(preferencesURL());
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Extensions",
|
label: "Extensions",
|
||||||
@ -106,7 +106,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
exitApp();
|
exitApp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
const fileMenu: MenuItemConstructorOptions = {
|
const fileMenu: MenuItemConstructorOptions = {
|
||||||
label: "File",
|
label: "File",
|
||||||
@ -150,7 +150,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
const editMenu: MenuItemConstructorOptions = {
|
const editMenu: MenuItemConstructorOptions = {
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
|
|||||||
@ -89,6 +89,8 @@ export class WindowManager extends Singleton {
|
|||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
enableRemoteModule: true,
|
enableRemoteModule: true,
|
||||||
|
webviewTag: true,
|
||||||
|
contextIsolation: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.windowState.manage(this.mainWindow);
|
this.windowState.manage(this.mainWindow);
|
||||||
@ -121,6 +123,35 @@ export class WindowManager extends Singleton {
|
|||||||
})
|
})
|
||||||
.on("did-finish-load", () => {
|
.on("did-finish-load", () => {
|
||||||
logger.info("[WINDOW-MANAGER]: Main window loaded");
|
logger.info("[WINDOW-MANAGER]: Main window loaded");
|
||||||
|
})
|
||||||
|
.on("will-attach-webview", (event, webPreferences, params) => {
|
||||||
|
logger.info("[WINDOW-MANAGER]: Attaching webview");
|
||||||
|
// Following is security recommendations because we allow webview tag (webviewTag: true)
|
||||||
|
// suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
|
||||||
|
// and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups
|
||||||
|
|
||||||
|
if (webPreferences.preload) {
|
||||||
|
logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview");
|
||||||
|
delete webPreferences.preload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload
|
||||||
|
if (webPreferences.preloadURL) {
|
||||||
|
logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview");
|
||||||
|
delete webPreferences.preload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.allowpopups) {
|
||||||
|
logger.warn("[WINDOW-MANAGER]: We do not allow allowpopups props, stop webview from renderer");
|
||||||
|
|
||||||
|
// event.preventDefault() will destroy the guest page.
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always disable Node.js integration for all webviews
|
||||||
|
webPreferences.nodeIntegration = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,6 +244,7 @@ export class WindowManager extends Singleton {
|
|||||||
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||||
} else {
|
} else {
|
||||||
webContents.getFocusedWebContents()?.reload();
|
webContents.getFocusedWebContents()?.reload();
|
||||||
|
webContents.getFocusedWebContents()?.clearHistory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +259,10 @@ export class WindowManager extends Singleton {
|
|||||||
resizable: false,
|
resizable: false,
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
nodeIntegrationInSubFrames: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await this.splashWindow.loadURL("static://splash.html");
|
await this.splashWindow.loadURL("static://splash.html");
|
||||||
|
|||||||
@ -20,11 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { app, remote } from "electron";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { getPath } from "../../common/utils/getPath";
|
||||||
|
|
||||||
export function fileNameMigration() {
|
export function fileNameMigration() {
|
||||||
const userDataPath = (app || remote.app).getPath("userData");
|
const userDataPath = getPath("userData");
|
||||||
const configJsonPath = path.join(userDataPath, "config.json");
|
const configJsonPath = path.join(userDataPath, "config.json");
|
||||||
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
|
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
|||||||
import "../../../common/catalog-entities";
|
import "../../../common/catalog-entities";
|
||||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
||||||
import { WebLink } from "../../../common/catalog-entities";
|
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
|
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
|
||||||
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
|
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
|
||||||
@ -51,6 +52,49 @@ class FooBarCategory extends CatalogCategory {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const entity = new WebLink({
|
||||||
|
metadata: {
|
||||||
|
uid: "test",
|
||||||
|
name: "test-link",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
url: "https://k8slens.dev"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "available"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const entity2 = new WebLink({
|
||||||
|
metadata: {
|
||||||
|
uid: "test2",
|
||||||
|
name: "test-link",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
url: "https://k8slens.dev"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "available"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const entitykc = new KubernetesCluster({
|
||||||
|
metadata: {
|
||||||
|
uid: "test3",
|
||||||
|
name: "test-link",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
kubeconfigPath: "",
|
||||||
|
kubeconfigContext: "",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "connected"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("CatalogEntityRegistry", () => {
|
describe("CatalogEntityRegistry", () => {
|
||||||
describe("updateItems", () => {
|
describe("updateItems", () => {
|
||||||
@ -250,4 +294,25 @@ describe("CatalogEntityRegistry", () => {
|
|||||||
catalogCategoryRegistry.add(new FooBarCategory());
|
catalogCategoryRegistry.add(new FooBarCategory());
|
||||||
expect(catalog.items.length).toBe(1);
|
expect(catalog.items.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not return items that are filtered out", () => {
|
||||||
|
const source = observable.array([entity, entity2, entitykc]);
|
||||||
|
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
|
||||||
|
catalog.replaceItems(source);
|
||||||
|
|
||||||
|
expect(catalog.items.length).toBe(3);
|
||||||
|
expect(catalog.filteredItems.length).toBe(3);
|
||||||
|
|
||||||
|
const d = catalog.addCatalogFilter(entity => entity.kind === KubernetesCluster.kind);
|
||||||
|
|
||||||
|
expect(catalog.items.length).toBe(3);
|
||||||
|
expect(catalog.filteredItems.length).toBe(1);
|
||||||
|
|
||||||
|
// Remove filter
|
||||||
|
d();
|
||||||
|
|
||||||
|
expect(catalog.items.length).toBe(3);
|
||||||
|
expect(catalog.filteredItems.length).toBe(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,10 +25,17 @@ import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegis
|
|||||||
import "../../common/catalog-entities";
|
import "../../common/catalog-entities";
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
|
import { Disposer, iter } from "../utils";
|
||||||
|
import { once } from "lodash";
|
||||||
|
|
||||||
|
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@observable.ref activeEntity: CatalogEntity;
|
@observable.ref activeEntity: CatalogEntity;
|
||||||
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||||
|
protected filters = observable.set<EntityFilter>([], {
|
||||||
|
deep: false,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
||||||
@ -95,27 +102,56 @@ export class CatalogEntityRegistry {
|
|||||||
return Array.from(this._entities.values());
|
return Array.from(this._entities.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get entities(): Map<string, CatalogEntity> {
|
@computed get filteredItems() {
|
||||||
this.processRawEntities();
|
return Array.from(
|
||||||
|
iter.reduce(
|
||||||
|
this.filters,
|
||||||
|
iter.filter,
|
||||||
|
this.items,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this._entities;
|
@computed get entities(): Map<string, CatalogEntity> {
|
||||||
|
return new Map(
|
||||||
|
this.items.map(entity => [entity.getId(), entity])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get filteredEntities(): Map<string, CatalogEntity> {
|
||||||
|
return new Map(
|
||||||
|
this.filteredItems.map(entity => [entity.getId(), entity])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getById<T extends CatalogEntity>(id: string) {
|
getById<T extends CatalogEntity>(id: string) {
|
||||||
return this.entities.get(id) as T;
|
return this.entities.get(id) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string, { filtered = false } = {}): T[] {
|
||||||
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
const byApiKind = (item: CatalogEntity) => item.apiVersion === apiVersion && item.kind === kind;
|
||||||
|
const entities = filtered ? this.filteredItems : this.items;
|
||||||
|
|
||||||
return items as T[];
|
return entities.filter(byApiKind) as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory, { filtered = false } = {}): T[] {
|
||||||
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`);
|
const supportedVersions = new Set(category.spec.versions.map((v) => `${category.spec.group}/${v.name}`));
|
||||||
const items = this.items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
|
const byApiVersionKind = (item: CatalogEntity) => supportedVersions.has(item.apiVersion) && item.kind === category.spec.names.kind;
|
||||||
|
const entities = filtered ? this.filteredItems : this.items;
|
||||||
|
|
||||||
return items as T[];
|
return entities.filter(byApiVersionKind) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new filter to the set of item filters
|
||||||
|
* @param fn The function that should return a truthy value if that entity should be sent currently "active"
|
||||||
|
* @returns A function to remove that filter
|
||||||
|
*/
|
||||||
|
addCatalogFilter(fn: EntityFilter): Disposer {
|
||||||
|
this.filters.add(fn);
|
||||||
|
|
||||||
|
return once(() => void this.filters.delete(fn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,12 @@
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Select__option {
|
||||||
|
span.deprecated {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.intro-contents {
|
.intro-contents {
|
||||||
.description {
|
.description {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@ -33,12 +33,19 @@ import { Button } from "../button";
|
|||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { createInstallChartTab } from "../dock/install-chart.store";
|
import { createInstallChartTab } from "../dock/install-chart.store";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
|
import { Tooltip, withStyles } from "@material-ui/core";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chart: HelmChart;
|
chart: HelmChart;
|
||||||
hideDetails(): void;
|
hideDetails(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LargeTooltip = withStyles({
|
||||||
|
tooltip: {
|
||||||
|
fontSize: "var(--font-size-small)",
|
||||||
|
}
|
||||||
|
})(Tooltip);
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class HelmChartDetails extends Component<Props> {
|
export class HelmChartDetails extends Component<Props> {
|
||||||
@observable chartVersions: HelmChart[];
|
@observable chartVersions: HelmChart[];
|
||||||
@ -73,15 +80,15 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async onVersionChange({ value: version }: SelectOption<string>) {
|
async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
|
||||||
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
this.selectedChart = chart;
|
||||||
this.readme = null;
|
this.readme = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.abortController?.abort();
|
this.abortController?.abort();
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
const { chart: { name, repo } } = this.props;
|
const { chart: { name, repo } } = this.props;
|
||||||
const { readme } = await getChartDetails(repo, name, { version, reqInit: { signal: this.abortController.signal }});
|
const { readme } = await getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }});
|
||||||
|
|
||||||
this.readme = readme;
|
this.readme = readme;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -115,7 +122,19 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
<Select
|
<Select
|
||||||
themeName="outlined"
|
themeName="outlined"
|
||||||
menuPortalTarget={null}
|
menuPortalTarget={null}
|
||||||
options={chartVersions.map(chart => chart.version)}
|
options={chartVersions.map(chart => ({
|
||||||
|
label: (
|
||||||
|
chart.deprecated
|
||||||
|
? (
|
||||||
|
<LargeTooltip title="Deprecated" placement="left">
|
||||||
|
<span className="deprecated">{chart.version}</span>
|
||||||
|
</LargeTooltip>
|
||||||
|
)
|
||||||
|
: chart.version
|
||||||
|
),
|
||||||
|
value: chart,
|
||||||
|
}))}
|
||||||
|
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
||||||
value={selectedChart.getVersion()}
|
value={selectedChart.getVersion()}
|
||||||
onChange={onVersionChange}
|
onChange={onVersionChange}
|
||||||
/>
|
/>
|
||||||
@ -173,7 +192,7 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { chart, hideDetails } = this.props;
|
const { chart, hideDetails } = this.props;
|
||||||
const title = chart ? <>Chart: {chart.getFullName()}</> : "";
|
const title = chart ? `Chart: ${chart.getFullName()}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export class HelmCharts extends Component<Props> {
|
|||||||
searchProps: {
|
searchProps: {
|
||||||
...searchProps,
|
...searchProps,
|
||||||
placeholder: "Search Helm Charts...",
|
placeholder: "Search Helm Charts...",
|
||||||
},
|
}
|
||||||
})}
|
})}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ className: "icon", showWithColumn: columnId.name },
|
{ className: "icon", showWithColumn: columnId.name },
|
||||||
|
|||||||
@ -288,7 +288,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { release, hideDetails } = this.props;
|
const { release, hideDetails } = this.props;
|
||||||
const title = release ? <>Release: {release.getName()}</> : "";
|
const title = release ? `Release: ${release.getName()}` : "";
|
||||||
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
|
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -129,10 +129,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
|
|||||||
|
|
||||||
@computed get entities() {
|
@computed get entities() {
|
||||||
if (!this.activeCategory) {
|
if (!this.activeCategory) {
|
||||||
return catalogEntityRegistry.items.map(entity => new CatalogEntityItem(entity));
|
return catalogEntityRegistry.filteredItems.map(entity => new CatalogEntityItem(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
|
return catalogEntityRegistry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity));
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get selectedItem() {
|
@computed get selectedItem() {
|
||||||
|
|||||||
@ -27,4 +27,9 @@
|
|||||||
@apply uppercase font-bold;
|
@apply uppercase font-bold;
|
||||||
color: var(--textColorAccent);
|
color: var(--textColorAccent);
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog {
|
||||||
|
@apply p-5 font-bold text-2xl;
|
||||||
|
color: var(--textColorAccent);
|
||||||
}
|
}
|
||||||
@ -58,6 +58,7 @@ export function CatalogMenu(props: Props) {
|
|||||||
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
|
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
|
||||||
<StylesProvider injectFirst>
|
<StylesProvider injectFirst>
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
|
<div className={styles.catalog}>Catalog</div>
|
||||||
<TreeView
|
<TreeView
|
||||||
defaultExpanded={["catalog"]}
|
defaultExpanded={["catalog"]}
|
||||||
defaultCollapseIcon={<Icon material="expand_more"/>}
|
defaultCollapseIcon={<Icon material="expand_more"/>}
|
||||||
|
|||||||
@ -25,11 +25,10 @@
|
|||||||
|
|
||||||
.root {
|
.root {
|
||||||
color: var(--textColorTertiary);
|
color: var(--textColorTertiary);
|
||||||
min-height: 26px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: var(--font-size);
|
font-size: var(--font-size)!important;
|
||||||
background-color: transparent!important;
|
background-color: transparent!important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
|||||||
import { CatalogMenu } from "./catalog-menu";
|
import { CatalogMenu } from "./catalog-menu";
|
||||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
import { RenderDelay } from "../render-delay/render-delay";
|
import { RenderDelay } from "../render-delay/render-delay";
|
||||||
import { CatalogTopbar } from "../cluster-manager/catalog-topbar";
|
import { TopBar } from "../layout/topbar";
|
||||||
|
|
||||||
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
|
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
|
||||||
|
|
||||||
@ -247,7 +247,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CatalogTopbar/>
|
<TopBar/>
|
||||||
<MainLayout sidebar={this.renderNavigation()}>
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
<div className="p-6 h-full">
|
<div className="p-6 h-full">
|
||||||
{ this.renderList() }
|
{ this.renderList() }
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import "./extensions.scss";
|
import "./extensions.scss";
|
||||||
|
|
||||||
import { remote, shell } from "electron";
|
import { shell } from "electron";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { makeObservable, observable, reaction, when } from "mobx";
|
import { makeObservable, observable, reaction, when } from "mobx";
|
||||||
@ -46,6 +46,8 @@ import { InstalledExtensions } from "./installed-extensions";
|
|||||||
import { Notice } from "./notice";
|
import { Notice } from "./notice";
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
|
import { dialog } from "../../remote-helpers";
|
||||||
|
import { getPath } from "../../../common/utils/getPath";
|
||||||
|
|
||||||
function getMessageFromError(error: any): string {
|
function getMessageFromError(error: any): string {
|
||||||
if (!error || typeof error !== "object") {
|
if (!error || typeof error !== "object") {
|
||||||
@ -466,9 +468,8 @@ async function installFromInput(input: string) {
|
|||||||
const supportedFormats = ["tar", "tgz"];
|
const supportedFormats = ["tar", "tgz"];
|
||||||
|
|
||||||
async function installFromSelectFileDialog() {
|
async function installFromSelectFileDialog() {
|
||||||
const { dialog, BrowserWindow, app } = remote;
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
defaultPath: getPath("downloads"),
|
||||||
defaultPath: app.getPath("downloads"),
|
|
||||||
properties: ["openFile", "multiSelections"],
|
properties: ["openFile", "multiSelections"],
|
||||||
message: `Select extensions to install (formats: ${supportedFormats.join(", ")}), `,
|
message: `Select extensions to install (formats: ${supportedFormats.join(", ")}), `,
|
||||||
buttonLabel: "Use configuration",
|
buttonLabel: "Use configuration",
|
||||||
|
|||||||
@ -45,3 +45,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.NamespaceSelectFilterMenu {
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
.Select {
|
||||||
|
&__menu-list {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
margin-right: $margin / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -180,6 +180,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
onBlur={this.reset}
|
onBlur={this.reset}
|
||||||
formatOptionLabel={this.formatOptionLabel}
|
formatOptionLabel={this.formatOptionLabel}
|
||||||
className="NamespaceSelectFilter"
|
className="NamespaceSelectFilter"
|
||||||
|
menuClass="NamespaceSelectFilterMenu"
|
||||||
sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)}
|
sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "./ingress-details.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { observable, reaction } from "mobx";
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import type { ILoadBalancerIngress, Ingress } from "../../../common/k8s-api/endpoints";
|
import type { ILoadBalancerIngress, Ingress } from "../../../common/k8s-api/endpoints";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
@ -34,6 +34,7 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
|||||||
import { getBackendServiceNamePort, getMetricsForIngress, IIngressMetrics } from "../../../common/k8s-api/endpoints/ingress.api";
|
import { getBackendServiceNamePort, getMetricsForIngress, IIngressMetrics } from "../../../common/k8s-api/endpoints/ingress.api";
|
||||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
|
import { boundMethod } from "../../utils";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||||
}
|
}
|
||||||
@ -42,11 +43,17 @@ interface Props extends KubeObjectDetailsProps<Ingress> {
|
|||||||
export class IngressDetails extends React.Component<Props> {
|
export class IngressDetails extends React.Component<Props> {
|
||||||
@observable metrics: IIngressMetrics = null;
|
@observable metrics: IIngressMetrics = null;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
clean = reaction(() => this.props.object, () => {
|
clean = reaction(() => this.props.object, () => {
|
||||||
this.metrics = null;
|
this.metrics = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
async loadMetrics() {
|
async loadMetrics() {
|
||||||
const { object: ingress } = this.props;
|
const { object: ingress } = this.props;
|
||||||
|
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
<DrawerItem name="Taints" labelsOnly>
|
<DrawerItem name="Taints" labelsOnly>
|
||||||
{
|
{
|
||||||
taints.map(({ key, effect, value }) => (
|
taints.map(({ key, effect, value }) => (
|
||||||
<Badge key={key} label={`${key}: ${effect}`} tooltip={value}/>
|
<Badge key={key} label={`${key}=${value}:${effect}`} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import "./add-helm-repo-dialog.scss";
|
import "./add-helm-repo-dialog.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { remote, FileFilter } from "electron";
|
import type { FileFilter } from "electron";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Dialog, DialogProps } from "../dialog";
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
@ -35,6 +35,7 @@ import { SubTitle } from "../layout/sub-title";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
||||||
|
import { dialog } from "../../remote-helpers";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
onAddRepo: Function
|
onAddRepo: Function
|
||||||
@ -88,8 +89,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
|
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
|
||||||
const { dialog, BrowserWindow } = remote;
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
|
||||||
defaultPath: this.getFilePath(type),
|
defaultPath: this.getFilePath(type),
|
||||||
properties: ["openFile", "showHiddenFiles"],
|
properties: ["openFile", "showHiddenFiles"],
|
||||||
message: `Select file`,
|
message: `Select file`,
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import "./volume-claim-details.scss";
|
import "./volume-claim-details.scss";
|
||||||
|
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { action, observable, reaction } from "mobx";
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
@ -36,6 +36,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
|||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
import { KubeObjectMeta } from "../kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object-meta";
|
||||||
import { getDetailsUrl } from "../kube-detail-params";
|
import { getDetailsUrl } from "../kube-detail-params";
|
||||||
|
import { boundMethod } from "../../utils";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||||
}
|
}
|
||||||
@ -44,12 +45,17 @@ interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
|||||||
export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
||||||
@observable metrics: IPvcMetrics = null;
|
@observable metrics: IPvcMetrics = null;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
clean = reaction(() => this.props.object, () => {
|
clean = reaction(() => this.props.object, () => {
|
||||||
this.metrics = null;
|
this.metrics = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@action
|
@boundMethod
|
||||||
async loadMetrics() {
|
async loadMetrics() {
|
||||||
const { object: volumeClaim } = this.props;
|
const { object: volumeClaim } = this.props;
|
||||||
|
|
||||||
|
|||||||
108
src/renderer/components/+welcome/__test__/welcome.test.tsx
Normal file
108
src/renderer/components/+welcome/__test__/welcome.test.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { Welcome } from "../welcome";
|
||||||
|
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
|
||||||
|
import { defaultWidth } from "../welcome";
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
"electron",
|
||||||
|
() => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("<Welcome/>", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TopBarRegistry.createInstance();
|
||||||
|
WelcomeMenuRegistry.createInstance();
|
||||||
|
WelcomeBannerRegistry.createInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
TopBarRegistry.resetInstance();
|
||||||
|
WelcomeMenuRegistry.resetInstance();
|
||||||
|
WelcomeBannerRegistry.resetInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders items in the top bar", async () => {
|
||||||
|
const testId = "testId";
|
||||||
|
const text = "topBarItem";
|
||||||
|
|
||||||
|
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Item: () => <span data-testid={testId}>{text}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<Welcome />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId(testId)).toHaveTextContent(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
||||||
|
const testId = "testId";
|
||||||
|
|
||||||
|
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||||
|
{
|
||||||
|
Banner: () => <div data-testid={testId} />
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { container } = render(<Welcome />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId(testId)).toBeInTheDocument();
|
||||||
|
expect(container.getElementsByClassName("logo").length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
||||||
|
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||||
|
{
|
||||||
|
width: 100,
|
||||||
|
Banner: () => <div />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 800,
|
||||||
|
Banner: () => <div />
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<Welcome />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("welcome-banner-container")).toHaveStyle({
|
||||||
|
// should take the max width of the banners (if > defaultWidth)
|
||||||
|
width: `800px`,
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId("welcome-text-container")).toHaveStyle({
|
||||||
|
width: `${defaultWidth}px`
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId("welcome-menu-container")).toHaveStyle({
|
||||||
|
width: `${defaultWidth}px`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -25,10 +25,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
.box {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: var(--textColorAccent);
|
color: var(--textColorAccent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -41,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
width: 200px;
|
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
|||||||
@ -22,35 +22,71 @@
|
|||||||
import "./welcome.scss";
|
import "./welcome.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import Carousel from "react-material-ui-carousel";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { productName, slackUrl } from "../../../common/vars";
|
import { productName, slackUrl } from "../../../common/vars";
|
||||||
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
||||||
|
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
||||||
|
import { TopBar } from "../layout/topbar";
|
||||||
|
|
||||||
|
export const defaultWidth = 320;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Welcome extends React.Component {
|
export class Welcome extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
|
||||||
|
|
||||||
|
// if there is banner with specified width, use it to calculate the width of the container
|
||||||
|
const maxWidth = welcomeBanner.reduce((acc, curr) => {
|
||||||
|
const currWidth = curr.width ?? 0;
|
||||||
|
|
||||||
|
if (acc > currWidth) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currWidth;
|
||||||
|
}, defaultWidth);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Welcome flex justify-center align-center">
|
<>
|
||||||
<div className="box">
|
<TopBar/>
|
||||||
<Icon svg="logo-lens" className="logo" />
|
<div className="flex justify-center Welcome align-center">
|
||||||
|
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
||||||
|
{welcomeBanner.length > 0 ? (
|
||||||
|
<Carousel
|
||||||
|
stopAutoPlayOnHover={true}
|
||||||
|
indicators={welcomeBanner.length > 1}
|
||||||
|
autoPlay={true}
|
||||||
|
navButtonsAlwaysInvisible={true}
|
||||||
|
>
|
||||||
|
{welcomeBanner.map((item, index) =>
|
||||||
|
<item.Banner key={index} />
|
||||||
|
)}
|
||||||
|
</Carousel>
|
||||||
|
) : <Icon svg="logo-lens" className="logo" />}
|
||||||
|
|
||||||
<h2>Welcome to {productName} 5!</h2>
|
<div className="flex justify-center">
|
||||||
|
<div style={{ width: `${defaultWidth}px` }} data-testid="welcome-text-container">
|
||||||
|
<h2>Welcome to {productName} 5!</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
|
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
|
||||||
<br/><br/>
|
<br /><br />
|
||||||
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>.
|
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="box">
|
<ul className="block" style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container">
|
||||||
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
|
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
|
||||||
<li key={index} className="flex grid-12" onClick={() => item.click()}>
|
<li key={index} className="flex grid-12" onClick={() => item.click()}>
|
||||||
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
|
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
|||||||
<div className="StatefulSetDetails">
|
<div className="StatefulSetDetails">
|
||||||
{!isMetricHidden && podsStore.isLoaded && (
|
{!isMetricHidden && podsStore.isLoaded && (
|
||||||
<ResourceMetrics
|
<ResourceMetrics
|
||||||
loader={() => this.loadMetrics}
|
loader={this.loadMetrics}
|
||||||
tabs={podMetricTabs} object={statefulSet} params={{ metrics: this.metrics }}
|
tabs={podMetricTabs} object={statefulSet} params={{ metrics: this.metrics }}
|
||||||
>
|
>
|
||||||
<PodCharts/>
|
<PodCharts/>
|
||||||
|
|||||||
@ -72,6 +72,7 @@ import { catalogEntityRegistry } from "../api/catalog-entity-registry";
|
|||||||
import { getHostedClusterId } from "../utils";
|
import { getHostedClusterId } from "../utils";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
|
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -128,7 +129,9 @@ export class App extends React.Component {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], {
|
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], {
|
||||||
preload: true,
|
preload: true,
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
watchHistoryState()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { previousActiveTab } from "../+catalog";
|
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
|
||||||
import { catalogURL } from "../../../common/routes";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
|
|
||||||
import type { RouteComponentProps } from "react-router";
|
|
||||||
import type { ClusterViewRouteParams } from "../../../common/routes";
|
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { TooltipPosition } from "../tooltip";
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClusterTopbar = observer((props: Props) => {
|
|
||||||
const getCluster = (): Cluster | undefined => {
|
|
||||||
return ClusterStore.getInstance().getById(props.match.params.clusterId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TopBar label={getCluster()?.name}>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
style={{ cursor: "default" }}
|
|
||||||
material="close"
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`${catalogURL()}/${previousActiveTab.get()}`);
|
|
||||||
}}
|
|
||||||
tooltip={{
|
|
||||||
preferredPositions: TooltipPosition.BOTTOM_RIGHT,
|
|
||||||
children: "Back to Catalog"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TopBar>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -34,7 +34,7 @@ import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
|||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
||||||
import { previousActiveTab } from "../+catalog";
|
import { previousActiveTab } from "../+catalog";
|
||||||
import { ClusterTopbar } from "./cluster-topbar";
|
import { TopBar } from "../layout/topbar";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ClusterView flex column align-center">
|
<div className="ClusterView flex column align-center">
|
||||||
<ClusterTopbar {...this.props}/>
|
<TopBar/>
|
||||||
{this.renderStatus()}
|
{this.renderStatus()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export async function initView(clusterId: ClusterId) {
|
|||||||
const parentElem = document.getElementById("lens-views");
|
const parentElem = document.getElementById("lens-views");
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
|
|
||||||
|
iframe.id = `cluster-frame-${cluster.id}`;
|
||||||
iframe.name = cluster.contextName;
|
iframe.name = cluster.contextName;
|
||||||
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
||||||
iframe.addEventListener("load", () => {
|
iframe.addEventListener("load", () => {
|
||||||
|
|||||||
@ -163,7 +163,9 @@ export class Drawer extends React.Component<DrawerProps> {
|
|||||||
<div className="drawer-title flex align-center">
|
<div className="drawer-title flex align-center">
|
||||||
<div className="drawer-title-text flex gaps align-center">
|
<div className="drawer-title-text flex gaps align-center">
|
||||||
{title}
|
{title}
|
||||||
<Icon material={copyIcon} tooltip={copyTooltip} onClick={this.copyK8sObjName}/>
|
{typeof title == "string" && (
|
||||||
|
<Icon material={copyIcon} tooltip={copyTooltip} onClick={this.copyK8sObjName}/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{toolbar}
|
{toolbar}
|
||||||
<Icon material="close" onClick={this.close}/>
|
<Icon material="close" onClick={this.close}/>
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item ? catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
|
return catalogEntityRegistry.getById(item?.entity.uid) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragEnd(result: DropResult) {
|
onDragEnd(result: DropResult) {
|
||||||
|
|||||||
@ -20,11 +20,46 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render, fireEvent } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { TopBar } from "../topbar";
|
import { TopBar } from "../topbar";
|
||||||
import { TopBarRegistry } from "../../../../extensions/registries";
|
import { TopBarRegistry } from "../../../../extensions/registries";
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
"electron",
|
||||||
|
() => ({
|
||||||
|
ipcRenderer: {
|
||||||
|
on: jest.fn(
|
||||||
|
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||||
|
if (channel === "history:can-go-back") {
|
||||||
|
listener({}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel === "history:can-go-forward") {
|
||||||
|
listener({}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const goBack = jest.fn();
|
||||||
|
const goForward = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("@electron/remote", () => {
|
||||||
|
return {
|
||||||
|
webContents: {
|
||||||
|
getFocusedWebContents: () => {
|
||||||
|
return {
|
||||||
|
goBack,
|
||||||
|
goForward
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("<TopBar/>", () => {
|
describe("<TopBar/>", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TopBarRegistry.createInstance();
|
TopBarRegistry.createInstance();
|
||||||
@ -35,15 +70,38 @@ describe("<TopBar/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
const { container } = render(<TopBar label="test bar" />);
|
const { container } = render(<TopBar/>);
|
||||||
|
|
||||||
expect(container).toBeInstanceOf(HTMLElement);
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders title", async () => {
|
it("renders history arrows", async () => {
|
||||||
const { getByTestId } = render(<TopBar label="topbar" />);
|
const { getByTestId } = render(<TopBar/>);
|
||||||
|
|
||||||
expect(await getByTestId("topbarLabel")).toHaveTextContent("topbar");
|
expect(await getByTestId("history-back")).toBeInTheDocument();
|
||||||
|
expect(await getByTestId("history-forward")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables arrow by ipc event", async () => {
|
||||||
|
const { getByTestId } = render(<TopBar/>);
|
||||||
|
|
||||||
|
expect(await getByTestId("history-back")).not.toHaveClass("disabled");
|
||||||
|
expect(await getByTestId("history-forward")).not.toHaveClass("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers browser history back and forward", async () => {
|
||||||
|
const { getByTestId } = render(<TopBar/>);
|
||||||
|
|
||||||
|
const prevButton = await getByTestId("history-back");
|
||||||
|
const nextButton = await getByTestId("history-forward");
|
||||||
|
|
||||||
|
fireEvent.click(prevButton);
|
||||||
|
|
||||||
|
expect(goBack).toBeCalled();
|
||||||
|
|
||||||
|
fireEvent.click(nextButton);
|
||||||
|
|
||||||
|
expect(goForward).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders items", async () => {
|
it("renders items", async () => {
|
||||||
@ -58,7 +116,7 @@ describe("<TopBar/>", () => {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { getByTestId } = render(<TopBar label="topbar" />);
|
const { getByTestId } = render(<TopBar/>);
|
||||||
|
|
||||||
expect(await getByTestId(testId)).toHaveTextContent(text);
|
expect(await getByTestId(testId)).toHaveTextContent(text);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,4 +45,12 @@
|
|||||||
padding: $padding;
|
padding: $padding;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cluster-name {
|
||||||
|
padding: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
word-break: break-all;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,8 @@ import { SidebarItem } from "./sidebar-item";
|
|||||||
import { Apps } from "../+apps";
|
import { Apps } from "../+apps";
|
||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
import { Config } from "../+config";
|
import { Config } from "../+config";
|
||||||
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
|
import { App } from "../app";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -181,6 +183,9 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames(Sidebar.displayName, "flex column", className)}>
|
<div className={cssNames(Sidebar.displayName, "flex column", className)}>
|
||||||
|
<div className="cluster-name">
|
||||||
|
{ClusterStore.getInstance().getById(App.clusterId)?.name}
|
||||||
|
</div>
|
||||||
<div className={cssNames("sidebar-nav flex column box grow-fixed")}>
|
<div className={cssNames("sidebar-nav flex column box grow-fixed")}>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
|
|||||||
@ -30,11 +30,8 @@
|
|||||||
grid-area: topbar;
|
grid-area: topbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.history {
|
||||||
@apply font-bold px-6;
|
@apply flex items-center;
|
||||||
color: var(--textColorAccent);
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
|||||||
@ -20,15 +20,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import styles from "./topbar.module.css";
|
import styles from "./topbar.module.css";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { TopBarRegistry } from "../../../extensions/registries";
|
import { TopBarRegistry } from "../../../extensions/registries";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { webContents } from "@electron/remote";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { ipcRendererOn } from "../../../common/ipc";
|
||||||
|
import { watchHistoryState } from "../../remote-helpers/history-updater";
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<any> {
|
interface Props extends React.HTMLAttributes<any> {
|
||||||
label: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopBar = observer(({ label, children, ...rest }: Props) => {
|
const prevEnabled = observable.box(false);
|
||||||
|
const nextEnabled = observable.box(false);
|
||||||
|
|
||||||
|
ipcRendererOn("history:can-go-back", (event, state: boolean) => {
|
||||||
|
prevEnabled.set(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
|
||||||
|
nextEnabled.set(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TopBar = observer(({ children, ...rest }: Props) => {
|
||||||
const renderRegisteredItems = () => {
|
const renderRegisteredItems = () => {
|
||||||
const items = TopBarRegistry.getInstance().getItems();
|
const items = TopBarRegistry.getInstance().getItems();
|
||||||
|
|
||||||
@ -37,7 +52,7 @@ export const TopBar = observer(({ label, children, ...rest }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6">
|
<div>
|
||||||
{items.map((registration, index) => {
|
{items.map((registration, index) => {
|
||||||
if (!registration?.components?.Item) {
|
if (!registration?.components?.Item) {
|
||||||
return null;
|
return null;
|
||||||
@ -53,9 +68,38 @@ export const TopBar = observer(({ label, children, ...rest }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
webContents.getFocusedWebContents()?.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goForward = () => {
|
||||||
|
webContents.getFocusedWebContents()?.goForward();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const disposer = watchHistoryState();
|
||||||
|
|
||||||
|
return () => disposer();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.topBar} {...rest}>
|
<div className={styles.topBar} {...rest}>
|
||||||
<div className={styles.title} data-testid="topbarLabel">{label}</div>
|
<div className={styles.history}>
|
||||||
|
<Icon
|
||||||
|
data-testid="history-back"
|
||||||
|
material="arrow_back"
|
||||||
|
className="ml-5"
|
||||||
|
onClick={goBack}
|
||||||
|
disabled={!prevEnabled.get()}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
data-testid="history-forward"
|
||||||
|
material="arrow_forward"
|
||||||
|
className="ml-5"
|
||||||
|
onClick={goForward}
|
||||||
|
disabled={!nextEnabled.get()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
{renderRegisteredItems()}
|
{renderRegisteredItems()}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
22
src/renderer/components/path-picker/index.ts
Normal file
22
src/renderer/components/path-picker/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./path-picker";
|
||||||
@ -27,7 +27,7 @@ import { UserStore } from "../../common/user-store";
|
|||||||
import { getAllEntries } from "../components/+preferences/kubeconfig-syncs";
|
import { getAllEntries } from "../components/+preferences/kubeconfig-syncs";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import { isWindows } from "../../common/vars";
|
import { isWindows } from "../../common/vars";
|
||||||
import { PathPicker } from "../components/path-picker/path-picker";
|
import { PathPicker } from "../components/path-picker";
|
||||||
import { Notifications } from "../components/notifications";
|
import { Notifications } from "../components/notifications";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export function initRegistries() {
|
|||||||
registries.KubeObjectStatusRegistry.createInstance();
|
registries.KubeObjectStatusRegistry.createInstance();
|
||||||
registries.StatusBarRegistry.createInstance();
|
registries.StatusBarRegistry.createInstance();
|
||||||
registries.WelcomeMenuRegistry.createInstance();
|
registries.WelcomeMenuRegistry.createInstance();
|
||||||
|
registries.WelcomeBannerRegistry.createInstance();
|
||||||
registries.WorkloadsOverviewDetailRegistry.createInstance();
|
registries.WorkloadsOverviewDetailRegistry.createInstance();
|
||||||
registries.TopBarRegistry.createInstance();
|
registries.TopBarRegistry.createInstance();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,7 +117,7 @@ function HotbarTooManyItemsHandler(): void {
|
|||||||
Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`);
|
Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcListeners() {
|
||||||
onCorrect({
|
onCorrect({
|
||||||
source: ipcRenderer,
|
source: ipcRenderer,
|
||||||
channel: UpdateAvailableChannel,
|
channel: UpdateAvailableChannel,
|
||||||
|
|||||||
88
src/renderer/keyboard-shortcuts/index.ts
Normal file
88
src/renderer/keyboard-shortcuts/index.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import { navigate } from "../navigation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The definition of a keyboard shortcut
|
||||||
|
*/
|
||||||
|
interface Shortcut {
|
||||||
|
code?: string;
|
||||||
|
key?: string;
|
||||||
|
metaKey?: boolean;
|
||||||
|
altKey?: boolean;
|
||||||
|
shiftKey?: boolean;
|
||||||
|
ctrlKey?: boolean;
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts: Shortcut[] = [
|
||||||
|
{
|
||||||
|
key: "p",
|
||||||
|
metaKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
action: () => ipcRenderer.emit("command-palette:open"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "Comma",
|
||||||
|
metaKey: true,
|
||||||
|
action: () => navigate("/preferences"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function shortcutMatches(shortcut: Shortcut, event: KeyboardEvent): boolean {
|
||||||
|
if (typeof shortcut.metaKey === "boolean" && shortcut.metaKey !== event.metaKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof shortcut.altKey === "boolean" && shortcut.altKey !== event.altKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof shortcut.shiftKey === "boolean" && shortcut.shiftKey !== event.shiftKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof shortcut.ctrlKey === "boolean" && shortcut.ctrlKey !== event.ctrlKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof shortcut.code === "string" && shortcut.code !== event.code) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof shortcut.key === "string" && shortcut.key !== event.key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerKeyboardShortcuts() {
|
||||||
|
window.addEventListener("keydown", event => {
|
||||||
|
for (const shortcut of shortcuts) {
|
||||||
|
if (shortcutMatches(shortcut, event)) {
|
||||||
|
shortcut.action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -32,10 +32,11 @@ import { ExtensionLoader } from "../extensions/extension-loader";
|
|||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import { CommandContainer } from "./components/command-palette/command-container";
|
import { CommandContainer } from "./components/command-palette/command-container";
|
||||||
import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler";
|
import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler";
|
||||||
import { registerIpcHandlers } from "./ipc";
|
import { registerIpcListeners } from "./ipc";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||||
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||||
|
import { registerKeyboardShortcuts } from "./keyboard-shortcuts";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
@ -48,7 +49,8 @@ export class LensApp extends React.Component {
|
|||||||
window.addEventListener("offline", () => broadcastMessage("network:offline"));
|
window.addEventListener("offline", () => broadcastMessage("network:offline"));
|
||||||
window.addEventListener("online", () => broadcastMessage("network:online"));
|
window.addEventListener("online", () => broadcastMessage("network:online"));
|
||||||
|
|
||||||
registerIpcHandlers();
|
registerKeyboardShortcuts();
|
||||||
|
registerIpcListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
26
src/renderer/remote-helpers/dialog.ts
Normal file
26
src/renderer/remote-helpers/dialog.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dialogShowOpenDialogHandler, requestMain } from "../../common/ipc";
|
||||||
|
|
||||||
|
export async function showOpenDialog(options: Electron.OpenDialogOptions): Promise<Electron.OpenDialogReturnValue> {
|
||||||
|
return requestMain(dialogShowOpenDialogHandler, options);
|
||||||
|
}
|
||||||
32
src/renderer/remote-helpers/history-updater.ts
Normal file
32
src/renderer/remote-helpers/history-updater.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { webContents } from "@electron/remote";
|
||||||
|
import { reaction } from "mobx";
|
||||||
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
|
import { navigation } from "../navigation";
|
||||||
|
|
||||||
|
export function watchHistoryState() {
|
||||||
|
return reaction(() => navigation.location, () => {
|
||||||
|
broadcastMessage("history:can-go-back", webContents.getFocusedWebContents()?.canGoBack());
|
||||||
|
broadcastMessage("history:can-go-forward", webContents.getFocusedWebContents()?.canGoForward());
|
||||||
|
});
|
||||||
|
}
|
||||||
24
src/renderer/remote-helpers/index.ts
Normal file
24
src/renderer/remote-helpers/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as dialog from "./dialog";
|
||||||
|
|
||||||
|
export { dialog };
|
||||||
@ -22,13 +22,12 @@
|
|||||||
// Keeps window.localStorage state in external JSON-files.
|
// Keeps window.localStorage state in external JSON-files.
|
||||||
// Because app creates random port between restarts => storage session wiped out each time.
|
// Because app creates random port between restarts => storage session wiped out each time.
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, remote } from "electron";
|
|
||||||
import { comparer, observable, reaction, toJS, when } from "mobx";
|
import { comparer, observable, reaction, toJS, when } from "mobx";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { StorageHelper } from "./storageHelper";
|
import { StorageHelper } from "./storageHelper";
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { getHostedClusterId } from "../../common/utils";
|
import { getHostedClusterId, getPath } from "../../common/utils";
|
||||||
|
|
||||||
const storage = observable({
|
const storage = observable({
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@ -47,7 +46,7 @@ export function createStorage<T>(key: string, defaultValue: T) {
|
|||||||
|
|
||||||
export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: string | undefined) {
|
export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: string | undefined) {
|
||||||
const { logPrefix } = StorageHelper;
|
const { logPrefix } = StorageHelper;
|
||||||
const folder = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage");
|
const folder = path.resolve(getPath("userData"), "lens-local-storage");
|
||||||
const fileName = `${clusterId ?? "app"}.json`;
|
const fileName = `${clusterId ?? "app"}.json`;
|
||||||
const filePath = path.resolve(folder, fileName);
|
const filePath = path.resolve(folder, fileName);
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,6 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
|||||||
optimization: {
|
optimization: {
|
||||||
minimize: false
|
minimize: false
|
||||||
},
|
},
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user