Merge branch 'master' into fix/consistent-inputs
6
.adr.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"language": "en",
|
||||
"path": "docs/architecture/decisions/",
|
||||
"prefix": "",
|
||||
"digits": 4
|
||||
}
|
||||
@ -16,8 +16,8 @@ jobs:
|
||||
vmImage: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node:
|
||||
node_version: 16.x
|
||||
steps:
|
||||
- powershell: |
|
||||
$CI_BUILD_TAG = git describe --tags
|
||||
@ -60,12 +60,13 @@ jobs:
|
||||
displayName: Build
|
||||
|
||||
- job: macOS
|
||||
timeoutInMinutes: 90
|
||||
pool:
|
||||
vmImage: macOS-11
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node:
|
||||
node_version: 16.x
|
||||
steps:
|
||||
- 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/'))"
|
||||
@ -94,9 +95,25 @@ jobs:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
displayName: Customize config
|
||||
|
||||
- script: make build
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
echo "Importing codesign certificate ..."
|
||||
echo $CSC_LINK | base64 -D > certificate.p12
|
||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security set-keychain-settings -lut 21600 build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security import certificate.p12 -k build.keychain -P $CSC_KEY_PASSWORD -T /usr/bin/codesign -T /usr/bin/security -A
|
||||
security set-key-partition-list -S apple-tool:,apple: -k $KEYCHAIN_PASSWORD build.keychain
|
||||
|
||||
rm certificate.p12
|
||||
echo "Codesign certificate imported!"
|
||||
|
||||
make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: secretz
|
||||
APPLEID: $(APPLEID)
|
||||
APPLEIDPASS: $(APPLEIDPASS)
|
||||
CSC_LINK: $(CSC_LINK)
|
||||
@ -112,8 +129,8 @@ jobs:
|
||||
vmImage: ubuntu-18.04
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node:
|
||||
node_version: 16.x
|
||||
steps:
|
||||
- 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/'))"
|
||||
|
||||
97
.eslintrc.js
@ -22,15 +22,16 @@ module.exports = {
|
||||
{
|
||||
files: [
|
||||
"**/*.js",
|
||||
"**/*.mjs",
|
||||
],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: [
|
||||
@ -129,6 +130,46 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [
|
||||
{
|
||||
"name": ".",
|
||||
"message": "No importing from local index.ts(x?) file. A common way to make circular dependencies.",
|
||||
},
|
||||
],
|
||||
}],
|
||||
"@typescript-eslint/member-delimiter-style": ["error", {
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true,
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false,
|
||||
},
|
||||
}],
|
||||
"react/jsx-max-props-per-line": ["error", {
|
||||
"maximum": {
|
||||
"single": 2,
|
||||
"multi": 1,
|
||||
},
|
||||
}],
|
||||
"react/jsx-first-prop-new-line": ["error", "multiline"],
|
||||
"react/jsx-one-expression-per-line": ["error", {
|
||||
"allow": "single-child",
|
||||
}],
|
||||
"react/jsx-indent": ["error", 2],
|
||||
"react/jsx-indent-props": ["error", 2],
|
||||
"react/jsx-closing-tag-location": "error",
|
||||
"react/jsx-wrap-multilines": ["error", {
|
||||
"declaration": "parens-new-line",
|
||||
"assignment": "parens-new-line",
|
||||
"return": "parens-new-line",
|
||||
"arrow": "parens-new-line",
|
||||
"condition": "parens-new-line",
|
||||
"logical": "parens-new-line",
|
||||
"prop": "parens-new-line",
|
||||
}],
|
||||
"react/display-name": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"@typescript-eslint/space-before-function-paren": ["error", {
|
||||
@ -136,6 +177,29 @@ module.exports = {
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"@typescript-eslint/naming-convention": ["error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^Props$",
|
||||
"match": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"selector": "typeAlias",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^(Props|State)$",
|
||||
"match": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
|
||||
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
|
||||
"unused-imports/no-unused-vars-ts": [
|
||||
"warn", {
|
||||
@ -181,6 +245,37 @@ module.exports = {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"no-template-curly-in-string": "error",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"src/{common,main,renderer}/**/*.ts",
|
||||
"src/{common,main,renderer}/**/*.tsx",
|
||||
],
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [
|
||||
{
|
||||
"name": ".",
|
||||
"message": "No importing from local index.ts(x?) file. A common way to make circular dependencies.",
|
||||
},
|
||||
{
|
||||
"name": "..",
|
||||
"message": "No importing from parent index.ts(x?) file. A common way to make circular dependencies.",
|
||||
},
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/extensions/renderer-api/**/*",
|
||||
"**/extensions/main-api/**/*",
|
||||
"**/extensions/common-api/**/*",
|
||||
],
|
||||
message: "No importing from the extension api definitions in application code",
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
6
.github/workflows/check-docs.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
@ -23,8 +23,8 @@ jobs:
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: Verify that the markdown is valid
|
||||
run: |
|
||||
|
||||
28
.github/workflows/electronegativity.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Electronegativity
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- uses: doyensec/electronegativity-action@v1.1
|
||||
with:
|
||||
input: src/
|
||||
electron-version: "19.0.4"
|
||||
severity: medium
|
||||
|
||||
- name: Upload sarif
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: ../results
|
||||
4
.github/workflows/license-header.yml
vendored
@ -15,11 +15,11 @@ jobs:
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.15.1'
|
||||
go-version: '^1.18.0'
|
||||
- name: Install addlicense
|
||||
run: |
|
||||
export PATH=${PATH}:`go env GOPATH`/bin
|
||||
go get -v -u github.com/google/addlicense
|
||||
go install github.com/google/addlicense@v1.0.0
|
||||
- name: Check license headers
|
||||
run: |
|
||||
set -e
|
||||
|
||||
2
.github/workflows/linter.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
|
||||
29
.github/workflows/main.yml
vendored
@ -6,18 +6,21 @@ on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
concurrency:
|
||||
group: publish-docs
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
verify-docs:
|
||||
name: Verify docs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
@ -31,8 +34,8 @@ jobs:
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: Verify that the markdown is valid
|
||||
run: |
|
||||
@ -43,13 +46,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
needs: verify-docs
|
||||
steps:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@ -64,8 +67,8 @@ jobs:
|
||||
|
||||
- name: git config
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: Using Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
@ -74,13 +77,13 @@ jobs:
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: mkdocs deploy master
|
||||
if: contains(github.ref, 'refs/heads/master')
|
||||
run: |
|
||||
mike deploy --push master
|
||||
mike deploy --push master
|
||||
|
||||
- name: Get the release version
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
@ -90,5 +93,5 @@ jobs:
|
||||
- name: mkdocs deploy new release
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
run: |
|
||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
10
.github/workflows/mkdocs-manual.yml
vendored
@ -11,12 +11,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: '${{ github.event.inputs.version }}'
|
||||
ref: "${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Using Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
@ -43,8 +43,8 @@ jobs:
|
||||
- name: Checkout master branch from lens
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'master'
|
||||
ref: 'master'
|
||||
path: "master"
|
||||
ref: "master"
|
||||
|
||||
- name: Bring in latest mkdocs.yml from master
|
||||
run: |
|
||||
|
||||
9
.github/workflows/publish-master-npm.yml
vendored
@ -3,6 +3,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
concurrency:
|
||||
group: publish-master-npm
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish NPM Package `master`
|
||||
@ -11,7 +14,7 @@ jobs:
|
||||
${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Checkout Release
|
||||
uses: actions/checkout@v2
|
||||
@ -25,11 +28,11 @@ jobs:
|
||||
|
||||
- name: Generate NPM package
|
||||
run: |
|
||||
make build-npm
|
||||
make build-npm
|
||||
|
||||
- name: publish new release
|
||||
run: |
|
||||
make publish-npm
|
||||
make publish-npm
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_RELEASE_TAG: master
|
||||
|
||||
6
.github/workflows/publish-release-npm.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Checkout Release
|
||||
uses: actions/checkout@v2
|
||||
@ -23,11 +23,11 @@ jobs:
|
||||
|
||||
- name: Generate NPM package
|
||||
run: |
|
||||
make build-npm
|
||||
make build-npm
|
||||
|
||||
- name: publish new release
|
||||
if: contains(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
make publish-npm
|
||||
make publish-npm
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
14
.github/workflows/test.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, macos-11, windows-2019]
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
@ -42,7 +42,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- uses: nick-invision/retry@v2
|
||||
- uses: nick-fields/retry@v2
|
||||
name: Install dependencies
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
- run: make build-npm
|
||||
name: Generate npm package
|
||||
|
||||
- uses: nick-invision/retry@v2
|
||||
- uses: nick-fields/retry@v2
|
||||
name: Build bundled extensions
|
||||
with:
|
||||
timeout_minutes: 15
|
||||
@ -67,9 +67,15 @@ jobs:
|
||||
- run: make test-extensions
|
||||
name: Run In-tree Extension tests
|
||||
|
||||
- run: make ci-validate-dev
|
||||
if: contains(github.event.pull_request.labels.*.name, 'dependencies')
|
||||
name: Validate dev mode will work
|
||||
|
||||
- name: Install integration test dependencies
|
||||
id: minikube
|
||||
uses: medyagh/setup-minikube@5a9a7104d7322fa40424de8855c84685e89cefd7
|
||||
uses: medyagh/setup-minikube@master
|
||||
with:
|
||||
minikube-version: latest
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
|
||||
|
||||
18
.swcrc
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"decorators": true,
|
||||
"dynamicImport": false
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2019"
|
||||
}
|
||||
}
|
||||
4
.yarnrc
@ -1,3 +1,3 @@
|
||||
disturl "https://atom.io/download/electron"
|
||||
target "14.2.4"
|
||||
disturl "https://electronjs.org/headers"
|
||||
target "19.0.4"
|
||||
runtime "electron"
|
||||
|
||||
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2021 OpenLens Authors.
|
||||
Copyright (c) 2022 OpenLens Authors.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
40
Makefile
@ -17,22 +17,22 @@ else
|
||||
endif
|
||||
|
||||
node_modules: yarn.lock
|
||||
yarn install --frozen-lockfile --network-timeout=100000
|
||||
yarn check --verify-tree --integrity
|
||||
yarn install --check-files --frozen-lockfile --network-timeout=100000
|
||||
|
||||
binaries/client: node_modules
|
||||
yarn download-bins
|
||||
|
||||
static/build/LensDev.html: node_modules
|
||||
yarn compile:renderer
|
||||
yarn download:binaries
|
||||
|
||||
.PHONY: compile-dev
|
||||
compile-dev: node_modules
|
||||
yarn compile:main --cache
|
||||
yarn compile:renderer --cache
|
||||
|
||||
.PHONY: validate-dev
|
||||
ci-validate-dev: binaries/client build-extensions compile-dev
|
||||
|
||||
.PHONY: dev
|
||||
dev: binaries/client build-extensions static/build/LensDev.html
|
||||
dev: binaries/client build-extensions
|
||||
rm -rf static/build/
|
||||
yarn dev
|
||||
|
||||
.PHONY: lint
|
||||
@ -55,6 +55,7 @@ integration: build
|
||||
build: node_modules binaries/client
|
||||
yarn run npm:fix-build-version
|
||||
$(MAKE) build-extensions -B
|
||||
yarn run build:tray-icons
|
||||
yarn run compile
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||
@ -62,10 +63,15 @@ ifeq "$(DETECTED_OS)" "Windows"
|
||||
endif
|
||||
yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS)
|
||||
|
||||
$(extension_node_modules): node_modules
|
||||
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund
|
||||
.PHONY: update-extension-locks
|
||||
update-extension-locks:
|
||||
$(foreach dir, $(extensions), (cd $(dir) && rm package-lock.json && ../../node_modules/.bin/npm install --package-lock-only);)
|
||||
|
||||
$(extension_dists): src/extensions/npm/extensions/dist
|
||||
.NOTPARALLEL: $(extension_node_modules)
|
||||
$(extension_node_modules): node_modules
|
||||
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund --no-save
|
||||
|
||||
$(extension_dists): src/extensions/npm/extensions/dist $(extension_node_modules)
|
||||
cd $(@:/dist=) && ../../node_modules/.bin/npm run build
|
||||
|
||||
.PHONY: clean-old-extensions
|
||||
@ -73,25 +79,23 @@ clean-old-extensions:
|
||||
find ./extensions -mindepth 1 -maxdepth 1 -type d '!' -exec test -e '{}/package.json' \; -exec rm -rf {} \;
|
||||
|
||||
.PHONY: build-extensions
|
||||
build-extensions: node_modules clean-old-extensions $(extension_node_modules) $(extension_dists)
|
||||
build-extensions: node_modules clean-old-extensions $(extension_dists)
|
||||
|
||||
.PHONY: test-extensions
|
||||
test-extensions: $(extension_node_modules)
|
||||
$(foreach dir, $(extensions), (cd $(dir) && npm run test || exit $?);)
|
||||
|
||||
.PHONY: copy-extension-themes
|
||||
copy-extension-themes:
|
||||
mkdir -p src/extensions/npm/extensions/dist/src/renderer/themes/
|
||||
cp $(wildcard src/renderer/themes/*.json) src/extensions/npm/extensions/dist/src/renderer/themes/
|
||||
|
||||
src/extensions/npm/extensions/__mocks__:
|
||||
cp -r __mocks__ src/extensions/npm/extensions/
|
||||
|
||||
src/extensions/npm/extensions/dist: node_modules
|
||||
src/extensions/npm/extensions/dist: src/extensions/npm/extensions/node_modules
|
||||
yarn compile:extension-types
|
||||
|
||||
src/extensions/npm/extensions/node_modules: src/extensions/npm/extensions/package.json
|
||||
cd src/extensions/npm/extensions/ && ../../../../node_modules/.bin/npm install --no-audit --no-fund
|
||||
|
||||
.PHONY: build-npm
|
||||
build-npm: build-extension-types copy-extension-themes src/extensions/npm/extensions/__mocks__
|
||||
build-npm: build-extension-types src/extensions/npm/extensions/__mocks__
|
||||
yarn npm:fix-package-version
|
||||
|
||||
.PHONY: build-extension-types
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Lens Open Source Project (OpenLens)
|
||||
|
||||
[](https://github.com/lensapp/lens/actions/workflows/test.yml)
|
||||
[](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
|
||||
[](https://join.slack.com/t/k8slens/shared_invite/zt-198iepl92-EPJsCckkJ~f887vWqJcgGA)
|
||||
|
||||
## The Repository
|
||||
|
||||
@ -19,7 +19,7 @@ Lens IDE a standalone application for MacOS, Windows and Linux operating systems
|
||||
|
||||
## Installation
|
||||
|
||||
See [Getting Started](https://docs.k8slens.dev/latest/getting-started/) page.
|
||||
See [Getting Started](https://docs.k8slens.dev/main/getting-started/install-lens/) page.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
# Release Process
|
||||
|
||||
Lens releases are built by CICD automatically on git tags. The typical release process flow is the following:
|
||||
|
||||
1. It is recommended to perform the release process from a folder used solely meant for creating releases (i.e. not your dev folder), with the Lens repo initialized (origin set to https://github.com/lensapp/lens).
|
||||
|
||||
1. If doing a patch release checkout the `release/vMAJOR.MINOR` branch for the appropriate `MAJOR`/`MINOR` version and manually `cherry-pick` the PRs required for the patch that were commited to master. If there are any conflicts they must be resolved manually. If necessary, get assistance from the PR authors.
|
||||
|
||||
This can be helped (if you have the `gh` CLI installed) by running:
|
||||
```
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map(.merge_commit_sha) | join(" ")'
|
||||
```
|
||||
|
||||
But you will probably need to verify that all the PRs have correct milestones.
|
||||
|
||||
1. From a clean and up to date `master` (or `release/vMAJOR.MINOR` if doing a patch release) run `npm version <version-type> --git-tag-version false` where `<version-type>` is one of the following:
|
||||
- `major`
|
||||
- `minor`
|
||||
- `patch`
|
||||
- `premajor [--preid=<prerelease-id>]`
|
||||
- `preminor [--preid=<prerelease-id>]`
|
||||
- `prepatch [--preid=<prerelease-id>]`
|
||||
- `prerelease [--preid=<prerelease-id>]`
|
||||
|
||||
where `<prerelease-id>` is generally one of:
|
||||
- `alpha`
|
||||
- `beta`
|
||||
- `rc`
|
||||
|
||||
This assumes origin is set to https://github.com/lensapp/lens.git. If not then set GIT_REMOTE to the remote that is set to https://github.com/lensapp/lens.git. For example run `GIT_REMOTE=upstream npm version ...`
|
||||
1. Open the PR (git should have printed a link to GitHub in the console) with the contents of all the accepted PRs since the last release. The PR description needs to be filled with the draft release description. From https://github.com/lensapp/lens click on Releases, the draft release should be first in the list, click `Edit` and copy/paste the markdown to the PR description. Add the `skip-changelog` label and click `Create Pull Request`. If this is a patch release be sure to set the PR base branch to `release/vMAJOR.MINOR` instead of `master`.
|
||||
|
||||
It might also help, if the release drafter isn't updating correctly. To grab the data for the PR description using `gh` and `jq`. You can run the following command to get all the titles:
|
||||
|
||||
```
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' | pbcopy
|
||||
```
|
||||
|
||||
And if you want to specify just bug fixes then you can add the following to the end of the `[ ... ]` section in the above command (before the `sort_by`) to just have bug fixes. Switch to `== "enhancement"` for enhancements and `all()` with `. != "bug && . != "enhancement"` for maintanence sections.
|
||||
|
||||
```
|
||||
| select(any(.labels | map(.name)[]; . == "bug"))
|
||||
```
|
||||
|
||||
1. After the PR is accepted and passes CI (and before merging), go to the same branch and run `make tag-release` (set GIT_REMOTE if necessary). This additionally triggers the azure jobs to build the binaries and put them on S3.
|
||||
1. If the CI fails at this stage the problem needs to be fixed. Sometimes an azure job fails due to outside service issues (e.g. Apple signing occasionally fails), in which case the specific azure job can be rerun from https://dev.azure.com/lensapp/lensapp/_build. Otherwise changes to the codebase may need to be done and committed to the release branch and pushed to https://github.com/lensapp/lens. CI will run again. As well the release tag needs to be manually set to this new commit. You can do something like:
|
||||
- `git push origin :refs/tags/vX.Y.Z-beta.N` (removes the tag from https://github.com/lensapp/lens)
|
||||
- `git tag -fa vX.Y.Z-beta.N` (move the tag locally to the current commit)
|
||||
- `git push origin --tags` (update the tags on https://github.com/lensapp/lens to reflect this local change)
|
||||
|
||||
Once the tag has been updated on origin (e.g. by `git push origin --tags`) the azure jobs are automatically triggered again.
|
||||
|
||||
1. Once CI passes again go to the releases tab on GitHub. You can use the existing draft release prepared by k8slens-bot (select the correct tag). Or you can create a new release from the tag that was created, and make sure that the change log is the same as that of the PR, and the title is the tag. Either way, click the prerelease checkbox if this is not a new major, minor, or patch version before clicking `Publish release`.
|
||||
1. Merge the release PR after the release is published. If it is a patch release then there is no need to squash the cherry-picked commits as part of the merge. GitHub should delete the branch once it is merged.
|
||||
1. If you have just released a new major or minor version then create a new `release/vMAJOR.MINOR` branch from that same tag and push it to https://github.com/lensapp/lens. Given the commit of the merged release PR from the master branch you can do this like
|
||||
|
||||
`git push origin <commit>:refs/heads/release/vX.Y`
|
||||
|
||||
This will be the target for future patch releases and shouldn't be deleted.
|
||||
|
||||
Other tasks
|
||||
post release:
|
||||
- generate a changelog from the prerelease descriptions (for major/minor releases)
|
||||
- announce the release on lens and lens-hq slack channels (release is announced automatically on the community slack lens channel through the above publishing process)
|
||||
- announce on lens-hq that master is open for PR merges for the next release (for major/minor releases)
|
||||
- update issues on github (bump those that did not make it into the release to a subsequent release) (for major/minor/patch releases)
|
||||
@ -2,5 +2,4 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export default {};
|
||||
6
__mocks__/assetMock.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export default ""; // mostly path to bundled file or data-url (webpack)
|
||||
@ -3,3 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export default {};
|
||||
|
||||
export const Uri = {
|
||||
file: (path: string) => path,
|
||||
};
|
||||
|
||||
export const editor = {
|
||||
getModel: () => ({}),
|
||||
create: () => ({}),
|
||||
};
|
||||
|
||||
15
__mocks__/react-beautiful-dnd.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
import type {
|
||||
DragDropContextProps,
|
||||
DraggableProps,
|
||||
DroppableProps,
|
||||
} from "react-beautiful-dnd";
|
||||
|
||||
export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>;
|
||||
export const Draggable = ({ children }: DraggableProps) => <>{ children }</>;
|
||||
export const Droppable = ({ children }: DroppableProps) => <>{ children }</>;
|
||||
17
__mocks__/react-virtualized-auto-sizer.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { Size } from "react-virtualized-auto-sizer";
|
||||
|
||||
export default ({ children } : { children: (size: Size) => React.ReactNode }) => {
|
||||
return (
|
||||
<div>
|
||||
{children({
|
||||
height: 420000,
|
||||
width: 100,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -5,11 +5,11 @@
|
||||
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark.json";
|
||||
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
|
||||
|
||||
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
|
||||
|
||||
const banner = `/*
|
||||
const banner = `/*
|
||||
Generated Lens theme CSS-variables, don't edit manually.
|
||||
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
|
||||
*/`;
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import jsdom from "jsdom";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function generateTrayIcon(
|
||||
{
|
||||
outputFilename = "trayIcon",
|
||||
svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"),
|
||||
outputFolder = path.resolve(__dirname, "./tray"),
|
||||
dpiSuffix = "2x",
|
||||
pixelSize = 32,
|
||||
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
|
||||
} = {}) {
|
||||
outputFilename += `${shouldUseDarkColors ? "Dark" : ""}Template`; // e.g. output trayIconDarkTemplate@2x.png
|
||||
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
||||
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
||||
|
||||
try {
|
||||
// Modify .SVG colors
|
||||
const trayIconColor = shouldUseDarkColors ? "black" : "white";
|
||||
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
||||
|
||||
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
|
||||
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
|
||||
// Resize and convert to .PNG
|
||||
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
|
||||
.resize({ width: pixelSize, height: pixelSize })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
// Save icon
|
||||
await fs.writeFile(pngIconDestPath, pngIconBuffer);
|
||||
console.info(`[DONE]: Tray icon saved at "${pngIconDestPath}"`);
|
||||
} catch (err) {
|
||||
console.error(`[ERROR]: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
const iconSizes: Record<string, number> = {
|
||||
"1x": 16,
|
||||
"2x": 32,
|
||||
"3x": 48,
|
||||
};
|
||||
|
||||
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
|
||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
|
||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
|
||||
});
|
||||
241
build/download_binaries.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import packageInfo from "../package.json";
|
||||
import type { FileHandle } from "fs/promises";
|
||||
import { open } from "fs/promises";
|
||||
import type { WriteStream } from "fs-extra";
|
||||
import { constants, ensureDir, unlink } from "fs-extra";
|
||||
import path from "path";
|
||||
import fetch from "node-fetch";
|
||||
import { promisify } from "util";
|
||||
import { pipeline as _pipeline, Transform, Writable } from "stream";
|
||||
import type { SingleBar } from "cli-progress";
|
||||
import { MultiBar } from "cli-progress";
|
||||
import { extract } from "tar-stream";
|
||||
import gunzip from "gunzip-maybe";
|
||||
import { getBinaryName, normalizedPlatform } from "../src/common/vars";
|
||||
import { isErrnoException } from "../src/common/utils";
|
||||
|
||||
const pipeline = promisify(_pipeline);
|
||||
|
||||
interface BinaryDownloaderArgs {
|
||||
readonly version: string;
|
||||
readonly platform: SupportedPlatform;
|
||||
readonly downloadArch: string;
|
||||
readonly fileArch: string;
|
||||
readonly binaryName: string;
|
||||
readonly baseDir: string;
|
||||
}
|
||||
|
||||
abstract class BinaryDownloader {
|
||||
protected abstract readonly url: string;
|
||||
protected readonly bar: SingleBar;
|
||||
protected readonly target: string;
|
||||
|
||||
protected getTransformStreams(file: Writable): (NodeJS.ReadWriteStream | NodeJS.WritableStream)[] {
|
||||
return [file];
|
||||
}
|
||||
|
||||
constructor(public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) {
|
||||
this.bar = multiBar.create(1, 0, args);
|
||||
this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName);
|
||||
}
|
||||
|
||||
async ensureBinary(): Promise<void> {
|
||||
if (process.env.LENS_SKIP_DOWNLOAD_BINARIES === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const stream = await fetch(this.url, {
|
||||
timeout: 15 * 60 * 1000, // 15min
|
||||
signal: controller.signal,
|
||||
});
|
||||
const total = Number(stream.headers.get("content-length"));
|
||||
const bar = this.bar;
|
||||
let fileHandle: FileHandle | undefined = undefined;
|
||||
|
||||
if (isNaN(total)) {
|
||||
throw new Error("no content-length header was present");
|
||||
}
|
||||
|
||||
bar.setTotal(total);
|
||||
|
||||
await ensureDir(path.dirname(this.target), 0o755);
|
||||
|
||||
try {
|
||||
/**
|
||||
* This is necessary because for some reason `createWriteStream({ flags: "wx" })`
|
||||
* was throwing someplace else and not here
|
||||
*/
|
||||
const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
|
||||
|
||||
await pipeline(
|
||||
stream.body,
|
||||
new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
bar.increment(chunk.length);
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
...this.getTransformStreams(new Writable({
|
||||
write(chunk, encoding, cb) {
|
||||
handle.write(chunk)
|
||||
.then(() => cb())
|
||||
.catch(cb);
|
||||
},
|
||||
})),
|
||||
);
|
||||
await fileHandle.chmod(0o755);
|
||||
await fileHandle.close();
|
||||
} catch (error) {
|
||||
await fileHandle?.close();
|
||||
|
||||
if (isErrnoException(error) && error.code === "EEXIST") {
|
||||
bar.increment(total); // mark as finished
|
||||
controller.abort(); // stop trying to download
|
||||
} else {
|
||||
await unlink(this.target);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LensK8sProxyDownloader extends BinaryDownloader {
|
||||
protected readonly url: string;
|
||||
|
||||
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
||||
const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform });
|
||||
|
||||
super({ ...args, binaryName }, bar);
|
||||
this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`;
|
||||
}
|
||||
}
|
||||
|
||||
class KubectlDownloader extends BinaryDownloader {
|
||||
protected readonly url: string;
|
||||
|
||||
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
||||
const binaryName = getBinaryName("kubectl", { forPlatform: args.platform });
|
||||
|
||||
super({ ...args, binaryName }, bar);
|
||||
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`;
|
||||
}
|
||||
}
|
||||
|
||||
class HelmDownloader extends BinaryDownloader {
|
||||
protected readonly url: string;
|
||||
|
||||
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
|
||||
const binaryName = getBinaryName("helm", { forPlatform: args.platform });
|
||||
|
||||
super({ ...args, binaryName }, bar);
|
||||
this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`;
|
||||
}
|
||||
|
||||
protected getTransformStreams(file: WriteStream) {
|
||||
const extracting = extract({
|
||||
allowUnknownFormat: false,
|
||||
});
|
||||
|
||||
extracting.on("entry", (headers, stream, next) => {
|
||||
if (headers.name.endsWith(this.args.binaryName)) {
|
||||
stream
|
||||
.pipe(file)
|
||||
.once("finish", () => next())
|
||||
.once("error", next);
|
||||
} else {
|
||||
stream.resume();
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
return [gunzip(3), extracting];
|
||||
}
|
||||
}
|
||||
|
||||
type SupportedPlatform = "darwin" | "linux" | "windows";
|
||||
|
||||
async function main() {
|
||||
const multiBar = new MultiBar({
|
||||
align: "left",
|
||||
clearOnComplete: false,
|
||||
hideCursor: true,
|
||||
autopadding: true,
|
||||
noTTYOutput: true,
|
||||
format: "[{bar}] {percentage}% | {downloadArch} {binaryName}",
|
||||
});
|
||||
const baseDir = path.join(__dirname, "..", "binaries", "client");
|
||||
const downloaders: BinaryDownloader[] = [
|
||||
new LensK8sProxyDownloader({
|
||||
version: packageInfo.config.k8sProxyVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "amd64",
|
||||
fileArch: "x64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
new KubectlDownloader({
|
||||
version: packageInfo.config.bundledKubectlVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "amd64",
|
||||
fileArch: "x64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
new HelmDownloader({
|
||||
version: packageInfo.config.bundledHelmVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "amd64",
|
||||
fileArch: "x64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
];
|
||||
|
||||
if (normalizedPlatform === "darwin") {
|
||||
downloaders.push(
|
||||
new LensK8sProxyDownloader({
|
||||
version: packageInfo.config.k8sProxyVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "arm64",
|
||||
fileArch: "arm64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
new KubectlDownloader({
|
||||
version: packageInfo.config.bundledKubectlVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "arm64",
|
||||
fileArch: "arm64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
new HelmDownloader({
|
||||
version: packageInfo.config.bundledHelmVersion,
|
||||
platform: normalizedPlatform,
|
||||
downloadArch: "arm64",
|
||||
fileArch: "arm64",
|
||||
baseDir,
|
||||
}, multiBar),
|
||||
);
|
||||
}
|
||||
|
||||
const settledResults = await Promise.allSettled(downloaders.map(downloader => (
|
||||
downloader.ensureBinary()
|
||||
.catch(error => {
|
||||
throw new Error(`Failed to download ${downloader.args.binaryName} for ${downloader.args.platform}/${downloader.args.downloadArch}: ${error}`);
|
||||
})
|
||||
)));
|
||||
|
||||
multiBar.stop();
|
||||
const errorResult = settledResults.find(res => res.status === "rejected") as PromiseRejectedResult | undefined;
|
||||
|
||||
if (errorResult) {
|
||||
console.error("234", String(errorResult.reason));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(error => console.error("from main", error));
|
||||
@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import packageInfo from "../package.json";
|
||||
import { isWindows } from "../src/common/vars";
|
||||
import { HelmCli } from "../src/main/helm/helm-cli";
|
||||
import * as path from "path";
|
||||
|
||||
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();
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import packageInfo from "../package.json";
|
||||
import fs from "fs";
|
||||
import request from "request";
|
||||
import md5File from "md5-file";
|
||||
import requestPromise from "request-promise-native";
|
||||
import { ensureDir, pathExists } from "fs-extra";
|
||||
import path from "path";
|
||||
import { noop } from "lodash";
|
||||
import { isLinux, isMac } from "../src/common/vars";
|
||||
|
||||
class KubectlDownloader {
|
||||
public kubectlVersion: string;
|
||||
protected url: string;
|
||||
protected path: string;
|
||||
protected dirname: string;
|
||||
|
||||
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
||||
this.kubectlVersion = clusterVersion;
|
||||
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
||||
|
||||
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
||||
this.dirname = path.dirname(target);
|
||||
this.path = target;
|
||||
}
|
||||
|
||||
protected async urlEtag() {
|
||||
const response = await requestPromise({
|
||||
method: "HEAD",
|
||||
uri: this.url,
|
||||
resolveWithFullResponse: true,
|
||||
}).catch(console.error);
|
||||
|
||||
if (response.headers["etag"]) {
|
||||
return response.headers["etag"].replace(/"/g, "");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public async checkBinary() {
|
||||
const exists = await pathExists(this.path);
|
||||
|
||||
if (exists) {
|
||||
const hash = md5File.sync(this.path);
|
||||
const etag = await this.urlEtag();
|
||||
|
||||
if (hash == etag) {
|
||||
console.log("Kubectl md5sum matches the remote etag");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Kubectl md5sum ${hash} does not match the remote etag ${etag}, unlinking and downloading again`);
|
||||
await fs.promises.unlink(this.path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async downloadKubectl() {
|
||||
if (await this.checkBinary()) {
|
||||
return console.log("Already exists and is valid");
|
||||
}
|
||||
|
||||
await ensureDir(path.dirname(this.path), 0o755);
|
||||
|
||||
const file = fs.createWriteStream(this.path);
|
||||
|
||||
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||
const requestOpts: request.UriOptions & request.CoreOptions = {
|
||||
uri: this.url,
|
||||
gzip: true,
|
||||
};
|
||||
const stream = request(requestOpts);
|
||||
|
||||
stream.on("complete", () => {
|
||||
console.log("kubectl binary download finished");
|
||||
file.end(noop);
|
||||
});
|
||||
|
||||
stream.on("error", (error) => {
|
||||
console.log(error);
|
||||
fs.unlink(this.path, noop);
|
||||
throw error;
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
file.on("close", () => {
|
||||
console.log("kubectl binary download closed");
|
||||
fs.chmod(this.path, 0o755, (err) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
stream.pipe(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
const downloadVersion = packageInfo.config.bundledKubectlVersion;
|
||||
const baseDir = path.join(__dirname, "..", "binaries", "client");
|
||||
|
||||
const downloads = [];
|
||||
|
||||
if (isMac) {
|
||||
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) => {
|
||||
console.log(dlOpts);
|
||||
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
||||
|
||||
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
||||
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
||||
});
|
||||
139
build/generate-tray-icons.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ensureDir, readFile } from "fs-extra";
|
||||
import { JSDOM } from "jsdom";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
|
||||
const size = Number(process.env.OUTPUT_SIZE || "16");
|
||||
const outputFolder = process.env.OUTPUT_DIR || "./build/tray";
|
||||
const inputFile = process.env.INPUT_SVG_PATH || "./src/renderer/components/icon/logo-lens.svg";
|
||||
const noticeFile = process.env.NOTICE_SVG_PATH || "./src/renderer/components/icon/notice.svg";
|
||||
const spinnerFile = process.env.SPINNER_SVG_PATH || "./src/renderer/components/icon/arrow-spinner.svg";
|
||||
|
||||
async function ensureOutputFoler() {
|
||||
await ensureDir(outputFolder);
|
||||
}
|
||||
|
||||
function getSvgStyling(colouring: "dark" | "light"): string {
|
||||
return `
|
||||
<style>
|
||||
ellipse {
|
||||
stroke: ${colouring === "dark" ? "white" : "black"} !important;
|
||||
}
|
||||
path, rect {
|
||||
fill: ${colouring === "dark" ? "white" : "black"} !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
type TargetSystems = "macos" | "windows-or-linux";
|
||||
|
||||
async function getBaseIconImage(system: TargetSystems) {
|
||||
const svgData = await readFile(inputFile, { encoding: "utf-8" });
|
||||
const dom = new JSDOM(`<body>${svgData}</body>`);
|
||||
const root = dom.window.document.body.getElementsByTagName("svg")[0];
|
||||
|
||||
root.innerHTML += getSvgStyling(system === "macos" ? "light" : "dark");
|
||||
|
||||
return Buffer.from(root.outerHTML);
|
||||
}
|
||||
|
||||
async function generateImage(image: Buffer, size: number, namePrefix: string) {
|
||||
sharp(image)
|
||||
.resize({ width: size, height: size })
|
||||
.png()
|
||||
.toFile(path.join(outputFolder, `${namePrefix}.png`));
|
||||
}
|
||||
|
||||
async function generateImages(image: Buffer, size: number, name: string) {
|
||||
await Promise.all([
|
||||
generateImage(image, size, name),
|
||||
generateImage(image, size*2, `${name}@2x`),
|
||||
generateImage(image, size*3, `${name}@3x`),
|
||||
generateImage(image, size*4, `${name}@4x`),
|
||||
]);
|
||||
}
|
||||
|
||||
async function generateImageWithSvg(baseImage: Buffer, system: TargetSystems, filePath: string) {
|
||||
const svgFile = await getIconImage(system, filePath);
|
||||
|
||||
const circleBuffer = await sharp(Buffer.from(`
|
||||
<svg viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="32" fill="black" />
|
||||
</svg>
|
||||
`))
|
||||
.toBuffer();
|
||||
|
||||
return sharp(baseImage)
|
||||
.resize({ width: 128, height: 128 })
|
||||
.composite([
|
||||
{
|
||||
input: circleBuffer,
|
||||
top: 64,
|
||||
left: 64,
|
||||
blend: "dest-out",
|
||||
},
|
||||
{
|
||||
input: (
|
||||
await sharp(svgFile)
|
||||
.resize({
|
||||
width: 60,
|
||||
height: 60,
|
||||
})
|
||||
.toBuffer()
|
||||
),
|
||||
top: 66,
|
||||
left: 66,
|
||||
},
|
||||
])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function getIconImage(system: TargetSystems, filePath: string) {
|
||||
const svgData = await readFile(filePath, { encoding: "utf-8" });
|
||||
const root = new JSDOM(svgData).window.document.getElementsByTagName("svg")[0];
|
||||
|
||||
root.innerHTML += getSvgStyling(system === "macos" ? "light" : "dark");
|
||||
|
||||
return Buffer.from(root.outerHTML);
|
||||
}
|
||||
|
||||
async function generateTrayIcons() {
|
||||
try {
|
||||
console.log("Generating tray icon pngs");
|
||||
await ensureOutputFoler();
|
||||
|
||||
const baseIconTemplateImage = await getBaseIconImage("macos");
|
||||
const baseIconImage = await getBaseIconImage("windows-or-linux");
|
||||
|
||||
const updateAvailableTemplateImage = await generateImageWithSvg(baseIconTemplateImage, "macos", noticeFile);
|
||||
const updateAvailableImage = await generateImageWithSvg(baseIconImage, "windows-or-linux", noticeFile);
|
||||
|
||||
const checkingForUpdatesTemplateImage = await generateImageWithSvg(baseIconTemplateImage, "macos", spinnerFile);
|
||||
const checkingForUpdatesImage = await generateImageWithSvg(baseIconImage, "windows-or-linux", spinnerFile);
|
||||
|
||||
await Promise.all([
|
||||
// Templates are for macOS only
|
||||
generateImages(baseIconTemplateImage, size, "trayIconTemplate"),
|
||||
generateImages(updateAvailableTemplateImage, size, "trayIconUpdateAvailableTemplate"),
|
||||
generateImages(updateAvailableTemplateImage, size, "trayIconUpdateAvailableTemplate"),
|
||||
generateImages(checkingForUpdatesTemplateImage, size, "trayIconCheckingForUpdatesTemplate"),
|
||||
|
||||
// Non-templates are for windows and linux
|
||||
generateImages(baseIconImage, size, "trayIcon"),
|
||||
generateImages(updateAvailableImage, size, "trayIconUpdateAvailable"),
|
||||
generateImages(checkingForUpdatesImage, size, "trayIconCheckingForUpdates"),
|
||||
]);
|
||||
|
||||
console.log("Generated all images");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
generateTrayIcons();
|
||||
@ -18,7 +18,7 @@ exports.default = async function notarizing(context) {
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
|
||||
return await notarize({
|
||||
appBundleId: "io.kontena.lens-app",
|
||||
appBundleId: process.env.APPBUNDLEID || "io.kontena.lens-app",
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: process.env.APPLEID,
|
||||
appleIdPassword: process.env.APPLEIDPASS,
|
||||
|
||||
BIN
build/tray/trayIcon.png
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
build/tray/trayIcon@2x.png
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
build/tray/trayIcon@3x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
build/tray/trayIcon@4x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
build/tray/trayIconCheckingForUpdates.png
Normal file
|
After Width: | Height: | Size: 504 B |
BIN
build/tray/trayIconCheckingForUpdates@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
build/tray/trayIconCheckingForUpdates@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
build/tray/trayIconCheckingForUpdates@4x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
build/tray/trayIconCheckingForUpdatesTemplate.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
build/tray/trayIconCheckingForUpdatesTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
build/tray/trayIconCheckingForUpdatesTemplate@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
build/tray/trayIconCheckingForUpdatesTemplate@4x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 397 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 717 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
build/tray/trayIconTemplate@4x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
build/tray/trayIconUpdateAvailable.png
Normal file
|
After Width: | Height: | Size: 518 B |
BIN
build/tray/trayIconUpdateAvailable@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
build/tray/trayIconUpdateAvailable@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
build/tray/trayIconUpdateAvailable@4x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
build/tray/trayIconUpdateAvailableTemplate.png
Normal file
|
After Width: | Height: | Size: 466 B |
BIN
build/tray/trayIconUpdateAvailableTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
build/tray/trayIconUpdateAvailableTemplate@3x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
build/tray/trayIconUpdateAvailableTemplate@4x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
6
build/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"./**/*",
|
||||
]
|
||||
}
|
||||
2
docs/architecture/decisions/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
@ -38,13 +38,15 @@ It contains a mix of Node.js fields, including scripts and dependencies, and Len
|
||||
Some of the most-important fields include:
|
||||
|
||||
- `name` and `publisher`: Lens uses `@<publisher>/<name>` as a unique ID for the extension.
|
||||
For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`.
|
||||
Lens uses this ID to uniquely identify your extension.
|
||||
For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`.
|
||||
Lens uses this ID to uniquely identify your extension.
|
||||
- `main`: the extension's entry point run in `main` process.
|
||||
- `renderer`: the extension's entry point run in `renderer` process.
|
||||
- `engines.lens`: the minimum version of Lens API that the extension depends upon.
|
||||
We only support the `^` range, which is also optional to specify, and only major and minor version numbers.
|
||||
Meaning that `^5.4` and `5.4` both mean the same thing, and the patch version in `5.4.2` is ignored.
|
||||
|
||||
``` javascript
|
||||
```javascript
|
||||
{
|
||||
"name": "helloworld-sample",
|
||||
"publisher": "lens-samples",
|
||||
@ -53,7 +55,8 @@ Lens uses this ID to uniquely identify your extension.
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/lensapp/lens-extension-samples",
|
||||
"engines": {
|
||||
"lens": "^4.0.0"
|
||||
"node": "^16.14.2",
|
||||
"lens": "5.4"
|
||||
},
|
||||
"main": "dist/main.js",
|
||||
"renderer": "dist/renderer.js",
|
||||
@ -65,17 +68,51 @@ Lens uses this ID to uniquely identify your extension.
|
||||
"react-open-doodles": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "^4.0.0-alpha.2",
|
||||
"@k8slens/extensions": "^5.4.6",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/node": "^12.0.0",
|
||||
"typescript": "^4.5.5",
|
||||
"@types/react": "^17.0.44",
|
||||
"@types/node": "^16.14.2",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Webpack configuration
|
||||
|
||||
The following webpack `externals` are provided by `Lens` and must be used (when available) to make sure that the versions used are in sync.
|
||||
|
||||
| Package | webpack external syntax | Lens versions | Available in Main | Available in Renderer |
|
||||
| ------------------ | --------------------------- | ------------- | ----------------- | --------------------- |
|
||||
| `mobx` | `var global.Mobx` | `>5.0.0` | ✅ | ✅ |
|
||||
| `mobx-react` | `var global.MobxReact` | `>5.0.0` | ❌ | ✅ |
|
||||
| `react` | `var global.React` | `>5.0.0` | ❌ | ✅ |
|
||||
| `react-router` | `var global.ReactRouter` | `>5.0.0` | ❌ | ✅ |
|
||||
| `react-router-dom` | `var global.ReactRouterDom` | `>5.0.0` | ❌ | ✅ |
|
||||
| `react-dom` | `var global.ReactDOM` | `>5.5.0` | ❌ | ✅ |
|
||||
|
||||
What is exported is the whole of the packages as a `*` import (within typescript).
|
||||
|
||||
For example, the following is how you would specify these within your webpack configuration files.
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"externals": [
|
||||
...
|
||||
{
|
||||
"mobx": "var global.Mobx"
|
||||
"mobx-react": "var global.MobxReact"
|
||||
"react": "var global.React"
|
||||
"react-router": "var global.ReactRouter"
|
||||
"react-router-dom": "var global.ReactRouterDom"
|
||||
"react-dom": "var global.ReactDOM"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Entry Files
|
||||
|
||||
Lens extensions can have two separate entry files.
|
||||
@ -95,20 +132,20 @@ The `Cluster Page` object registers the `/extension-example` path, and this path
|
||||
It also registers the `MenuItem` component that displays the `ExampleIcon` React component and the "Hello World" text in the left-side menu of the cluster dashboard.
|
||||
These React components are defined in the additional `./src/page.tsx` file.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
import { ExampleIcon, ExamplePage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleExtension extends Renderer.LensExtension {
|
||||
clusterPages = [
|
||||
{
|
||||
id: "extension-example",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
]
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -6,29 +6,29 @@ The Renderer Extension API allows you to access, configure, and customize Lens d
|
||||
|
||||
The custom Lens UI elements that you can add include:
|
||||
|
||||
* [Cluster pages](#clusterpages)
|
||||
* [Cluster page menus](#clusterpagemenus)
|
||||
* [Global pages](#globalpages)
|
||||
* [Welcome menus](#welcomemenus)
|
||||
* [App preferences](#apppreferences)
|
||||
* [Top bar items](#topbaritems)
|
||||
* [Status bar items](#statusbaritems)
|
||||
* [KubeObject menu items](#kubeobjectmenuitems)
|
||||
* [KubeObject detail items](#kubeobjectdetailitems)
|
||||
* [KubeObject status texts](#kubeobjectstatustexts)
|
||||
* [Kube workloads overview items](#kubeworkloadsoverviewitems)
|
||||
- [Cluster pages](#clusterpages)
|
||||
- [Cluster page menus](#clusterpagemenus)
|
||||
- [Global pages](#globalpages)
|
||||
- [Welcome menus](#welcomemenus)
|
||||
- [App preferences](#apppreferences)
|
||||
- [Top bar items](#topbaritems)
|
||||
- [Status bar items](#statusbaritems)
|
||||
- [KubeObject menu items](#kubeobjectmenuitems)
|
||||
- [KubeObject detail items](#kubeobjectdetailitems)
|
||||
- [KubeObject status texts](#kubeobjectstatustexts)
|
||||
- [Kube workloads overview items](#kubeworkloadsoverviewitems)
|
||||
|
||||
as well as catalog-related UI elements:
|
||||
|
||||
* [Entity settings](#entitysettings)
|
||||
* [Catalog entity detail items](#catalogentitydetailitems)
|
||||
- [Entity settings](#entitysettings)
|
||||
- [Catalog entity detail items](#catalogentitydetailitems)
|
||||
|
||||
All UI elements are based on React components.
|
||||
|
||||
Finally, you can also add commands and protocol handlers:
|
||||
|
||||
* [Command palette commands](#commandpalettecommands)
|
||||
* [protocol handlers](protocol-handlers.md)
|
||||
- [Command palette commands](#commandpalettecommands)
|
||||
- [protocol handlers](protocol-handlers.md)
|
||||
|
||||
## `Renderer.LensExtension` Class
|
||||
|
||||
@ -41,11 +41,11 @@ import { Renderer } from "@k8slens/extensions";
|
||||
|
||||
export default class ExampleExtensionMain extends Renderer.LensExtension {
|
||||
onActivate() {
|
||||
console.log('custom renderer process extension code started');
|
||||
console.log("custom renderer process extension code started");
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
console.log('custom renderer process extension de-activated');
|
||||
console.log("custom renderer process extension de-activated");
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -56,7 +56,7 @@ You can initiate custom code by implementing `onActivate()`.
|
||||
Implementing `onDeactivate()` gives you the opportunity to clean up after your extension.
|
||||
|
||||
!!! info
|
||||
Disable extensions from the Lens Extensions page:
|
||||
Disable extensions from the Lens Extensions page:
|
||||
|
||||
1. Navigate to **File** > **Extensions** in the top menu bar.
|
||||
(On Mac, it is **Lens** > **Extensions**.)
|
||||
@ -75,17 +75,17 @@ Add a cluster page definition to a `Renderer.LensExtension` subclass with the fo
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
import { ExampleIcon, ExamplePage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleExtension extends Renderer.LensExtension {
|
||||
clusterPages = [
|
||||
{
|
||||
id: "hello",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@ -93,24 +93,26 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
`clusterPages` is an array of objects that satisfy the `PageRegistration` interface.
|
||||
The properties of the `clusterPages` array objects are defined as follows:
|
||||
|
||||
* `id` is a string that identifies the page.
|
||||
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
* `Page` is of type ` React.ComponentType<any>`.
|
||||
It offers flexibility in defining the appearance and behavior of your page.
|
||||
- `id` is a string that identifies the page.
|
||||
- `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
- `Page` is of type ` React.ComponentType<any>`.
|
||||
It offers flexibility in defining the appearance and behavior of your page.
|
||||
|
||||
`ExamplePage` in the example above can be defined in `page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
|
||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||
export class ExamplePage extends React.Component<{
|
||||
extension: LensRendererExtension;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -130,17 +132,17 @@ By expanding on the above example, you can add a cluster page menu item to the `
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
import { ExampleIcon, ExamplePage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleExtension extends Renderer.LensExtension {
|
||||
clusterPages = [
|
||||
{
|
||||
id: "hello",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
clusterPageMenus = [
|
||||
@ -149,7 +151,7 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
title: "Hello World",
|
||||
components: {
|
||||
Icon: ExampleIcon,
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -159,10 +161,10 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
This element defines how the cluster page menu item will appear and what it will do when you click it.
|
||||
The properties of the `clusterPageMenus` array objects are defined as follows:
|
||||
|
||||
* `target` links to the relevant cluster page using `pageId`.
|
||||
* `pageId` takes the value of the relevant cluster page's `id` property.
|
||||
* `title` sets the name of the cluster page menu item that will appear in the left side menu.
|
||||
* `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
|
||||
- `target` links to the relevant cluster page using `pageId`.
|
||||
- `pageId` takes the value of the relevant cluster page's `id` property.
|
||||
- `title` sets the name of the cluster page menu item that will appear in the left side menu.
|
||||
- `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
|
||||
|
||||
The above example creates a menu item that reads **Hello World**.
|
||||
When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`.
|
||||
@ -171,7 +173,7 @@ This example requires the definition of another React-based component, `ExampleI
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
|
||||
type IconProps = Renderer.Component.IconProps;
|
||||
|
||||
@ -180,16 +182,18 @@ const {
|
||||
} = Renderer;
|
||||
|
||||
export function ExampleIcon(props: IconProps) {
|
||||
return <Icon {...props} material="pages" tooltip={"Hi!"}/>
|
||||
return <Icon {...props} material="pages" tooltip={"Hi!"} />;
|
||||
}
|
||||
|
||||
export class ExamplePage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||
export class ExamplePage extends React.Component<{
|
||||
extension: Renderer.LensExtension;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -198,32 +202,31 @@ Lens includes various built-in components available for extension developers to
|
||||
One of these is the `Renderer.Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
||||
The properties that `Renderer.Component.Icon` uses are defined as follows:
|
||||
|
||||
* `material` takes the name of the icon you want to use.
|
||||
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||
- `material` takes the name of the icon you want to use.
|
||||
- `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||
|
||||
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages.
|
||||
The following example groups two sub menu items under one parent menu item:
|
||||
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page"
|
||||
import React from "react"
|
||||
import { ExampleIcon, ExamplePage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleExtension extends Renderer.LensExtension {
|
||||
clusterPages = [
|
||||
{
|
||||
id: "hello",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this}/>,
|
||||
}
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bonjour",
|
||||
components: {
|
||||
Page: () => <ExemplePage extension={this}/>,
|
||||
}
|
||||
}
|
||||
Page: () => <ExamplePage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
clusterPageMenus = [
|
||||
@ -232,7 +235,7 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
title: "Greetings",
|
||||
components: {
|
||||
Icon: ExampleIcon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: "example",
|
||||
@ -240,16 +243,16 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
title: "Hello World",
|
||||
components: {
|
||||
Icon: ExampleIcon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
parentId: "example",
|
||||
target: { pageId: "bonjour" },
|
||||
title: "Bonjour le monde",
|
||||
components: {
|
||||
Icon: ExempleIcon,
|
||||
}
|
||||
}
|
||||
Icon: ExampleIcon,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@ -280,18 +283,18 @@ Unlike cluster pages, users can trigger global pages even when there is no activ
|
||||
The following example defines a `Renderer.LensExtension` subclass with a single global page definition:
|
||||
|
||||
```typescript
|
||||
import { Renderer } from '@k8slens/extensions';
|
||||
import { HelpPage } from './page';
|
||||
import React from 'react';
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { HelpPage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class HelpExtension extends Renderer.LensExtension {
|
||||
globalPages = [
|
||||
{
|
||||
id: "help",
|
||||
components: {
|
||||
Page: () => <HelpPage extension={this}/>,
|
||||
}
|
||||
}
|
||||
Page: () => <HelpPage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@ -299,24 +302,26 @@ export default class HelpExtension extends Renderer.LensExtension {
|
||||
`globalPages` is an array of objects that satisfy the `PageRegistration` interface.
|
||||
The properties of the `globalPages` array objects are defined as follows:
|
||||
|
||||
* `id` is a string that identifies the page.
|
||||
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
* `Page` is of type `React.ComponentType<any>`.
|
||||
It offers flexibility in defining the appearance and behavior of your page.
|
||||
- `id` is a string that identifies the page.
|
||||
- `components` matches the `PageComponents` interface for which there is one field, `Page`.
|
||||
- `Page` is of type `React.ComponentType<any>`.
|
||||
It offers flexibility in defining the appearance and behavior of your page.
|
||||
|
||||
`HelpPage` in the example above can be defined in `page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
|
||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
||||
export class HelpPage extends React.Component<{
|
||||
extension: LensRendererExtension;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Help yourself</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -328,11 +333,12 @@ This way, `HelpPage` can access all `HelpExtension` subclass data.
|
||||
This example code shows how to create a global page, but not how to make that page available to the Lens user.
|
||||
Global pages are typically made available in the following ways:
|
||||
|
||||
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
||||
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
||||
* To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
|
||||
- To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
||||
- To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
||||
- To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
|
||||
|
||||
### `welcomeMenus`
|
||||
|
||||
### `appPreferences`
|
||||
|
||||
The Lens **Preferences** page is a built-in global page.
|
||||
@ -342,22 +348,24 @@ The following example demonstrates adding a custom preference:
|
||||
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||
import {
|
||||
ExamplePreferenceHint,
|
||||
ExamplePreferenceInput,
|
||||
} from "./src/example-preference";
|
||||
import { observable } from "mobx";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||
|
||||
@observable preference = { enabled: false };
|
||||
|
||||
appPreferences = [
|
||||
{
|
||||
title: "Example Preferences",
|
||||
components: {
|
||||
Input: () => <ExamplePreferenceInput preference={this.preference}/>,
|
||||
Hint: () => <ExamplePreferenceHint/>
|
||||
}
|
||||
}
|
||||
Input: () => <ExamplePreferenceInput preference={this.preference} />,
|
||||
Hint: () => <ExamplePreferenceHint />,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@ -365,13 +373,13 @@ export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||
`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface.
|
||||
The properties of the `appPreferences` array objects are defined as follows:
|
||||
|
||||
* `title` sets the heading text displayed on the Preferences page.
|
||||
* `components` specifies two `React.Component` objects that define the interface for the preference.
|
||||
* `Input` specifies an interactive input element for the preference.
|
||||
* `Hint` provides descriptive information for the preference, shown below the `Input` element.
|
||||
- `title` sets the heading text displayed on the Preferences page.
|
||||
- `components` specifies two `React.Component` objects that define the interface for the preference.
|
||||
- `Input` specifies an interactive input element for the preference.
|
||||
- `Hint` provides descriptive information for the preference, shown below the `Input` element.
|
||||
|
||||
!!! note
|
||||
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
|
||||
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
|
||||
|
||||
`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance.
|
||||
This is how `ExampleRendererExtension` handles the state of the preference input.
|
||||
@ -386,22 +394,19 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
const {
|
||||
Component: {
|
||||
Checkbox,
|
||||
},
|
||||
Component: { Checkbox },
|
||||
} = Renderer;
|
||||
|
||||
export class ExamplePreferenceProps {
|
||||
preference: {
|
||||
enabled: boolean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
||||
|
||||
public constructor() {
|
||||
super({preference: { enabled: false}});
|
||||
super({ preference: { enabled: false } });
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@ -411,7 +416,9 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
|
||||
<Checkbox
|
||||
label="I understand appPreferences"
|
||||
value={preference.enabled}
|
||||
onChange={v => { preference.enabled = v; }}
|
||||
onChange={(v) => {
|
||||
preference.enabled = v;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -419,18 +426,16 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
|
||||
|
||||
export class ExamplePreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>This is an example of an appPreference for extensions.</span>
|
||||
);
|
||||
return <span>This is an example of an appPreference for extensions.</span>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Renderer.Component.Checkbox` using the following properties:
|
||||
|
||||
* `label` sets the text that displays next to the checkbox.
|
||||
* `value` is initially set to `preference.enabled`.
|
||||
* `onChange` is a function that responds when the state of the checkbox changes.
|
||||
- `label` sets the text that displays next to the checkbox.
|
||||
- `value` is initially set to `preference.enabled`.
|
||||
- `onChange` is a function that responds when the state of the checkbox changes.
|
||||
|
||||
`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props.
|
||||
This is an object with the single `enabled` property.
|
||||
@ -461,18 +466,18 @@ The following example adds a `statusBarItems` definition and a `globalPages` def
|
||||
It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||
|
||||
```typescript
|
||||
import { Renderer } from '@k8slens/extensions';
|
||||
import { HelpIcon, HelpPage } from "./page"
|
||||
import React from 'react';
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { HelpIcon, HelpPage } from "./page";
|
||||
import React from "react";
|
||||
|
||||
export default class HelpExtension extends Renderer.LensExtension {
|
||||
globalPages = [
|
||||
{
|
||||
id: "help",
|
||||
components: {
|
||||
Page: () => <HelpPage extension={this}/>,
|
||||
}
|
||||
}
|
||||
Page: () => <HelpPage extension={this} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
statusBarItems = [
|
||||
@ -486,7 +491,7 @@ export default class HelpExtension extends Renderer.LensExtension {
|
||||
<HelpIcon />
|
||||
My Status Bar Item
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -495,14 +500,14 @@ export default class HelpExtension extends Renderer.LensExtension {
|
||||
|
||||
The properties of the `statusBarItems` array objects are defined as follows:
|
||||
|
||||
* `Item` specifies the `React.Component` that will be shown on the status bar.
|
||||
By default, items are added starting from the right side of the status bar.
|
||||
Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text.
|
||||
The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
||||
* `onClick` determines what the `statusBarItem` does when it is clicked.
|
||||
In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method.
|
||||
`navigate` takes the `id` of the associated global page as a parameter.
|
||||
Thus, clicking the status bar item activates the associated global pages.
|
||||
- `Item` specifies the `React.Component` that will be shown on the status bar.
|
||||
By default, items are added starting from the right side of the status bar.
|
||||
Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text.
|
||||
The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
|
||||
- `onClick` determines what the `statusBarItem` does when it is clicked.
|
||||
In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method.
|
||||
`navigate` takes the `id` of the associated global page as a parameter.
|
||||
Thus, clicking the status bar item activates the associated global pages.
|
||||
|
||||
### `kubeObjectMenuItems`
|
||||
|
||||
@ -518,9 +523,9 @@ They also appear on the title bar of the details page for specific resources:
|
||||
The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action:
|
||||
|
||||
```typescript
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
||||
import { NamespaceMenuItem } from "./src/namespace-menu-item";
|
||||
|
||||
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||
type Namespace = Renderer.K8sApi.Namespace;
|
||||
@ -531,23 +536,24 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
kind: "Namespace",
|
||||
apiVersions: ["v1"],
|
||||
components: {
|
||||
MenuItem: (props: KubeObjectMenuProps<Namespace>) => <NamespaceMenuItem {...props} />
|
||||
}
|
||||
}
|
||||
MenuItem: (props: KubeObjectMenuProps<Namespace>) => (
|
||||
<NamespaceMenuItem {...props} />
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`kubeObjectMenuItems` is an array of objects matching the `KubeObjectMenuRegistration` interface.
|
||||
The example above adds a menu item for namespaces in the cluster dashboard.
|
||||
The properties of the `kubeObjectMenuItems` array objects are defined as follows:
|
||||
|
||||
* `kind` specifies the Kubernetes resource type the menu item will apply to.
|
||||
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||
* `components` defines the menu item's appearance and behavior.
|
||||
* `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties.
|
||||
In this example a `NamespaceMenuItem` object is returned.
|
||||
- `kind` specifies the Kubernetes resource type the menu item will apply to.
|
||||
- `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||
- `components` defines the menu item's appearance and behavior.
|
||||
- `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties.
|
||||
In this example a `NamespaceMenuItem` object is returned.
|
||||
|
||||
`NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`:
|
||||
|
||||
@ -556,11 +562,7 @@ import React from "react";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
|
||||
const {
|
||||
Component: {
|
||||
terminalStore,
|
||||
MenuItem,
|
||||
Icon,
|
||||
},
|
||||
Component: { terminalStore, MenuItem, Icon },
|
||||
Navigation,
|
||||
} = Renderer;
|
||||
|
||||
@ -587,12 +589,15 @@ export function NamespaceMenuItem(props: KubeObjectMenuProps<Namespace>) {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={getPods}>
|
||||
<Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
||||
<Icon
|
||||
material="speaker_group"
|
||||
interactive={toolbar}
|
||||
title="Get pods in terminal"
|
||||
/>
|
||||
<span className="title">Get Pods</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`NamespaceMenuItem` returns a `Renderer.Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property.
|
||||
@ -629,9 +634,11 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
apiVersions: ["v1"],
|
||||
priority: 10,
|
||||
components: {
|
||||
Details: (props: KubeObjectDetailsProps<Namespace>) => <NamespaceDetailsItem {...props} />
|
||||
}
|
||||
}
|
||||
Details: (props: KubeObjectDetailsProps<Namespace>) => (
|
||||
<NamespaceDetailsItem {...props} />
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
@ -640,15 +647,15 @@ export default class ExampleExtension extends Renderer.LensExtension {
|
||||
This example above adds a detail item for namespaces in the cluster dashboard.
|
||||
The properties of the `kubeObjectDetailItems` array objects are defined as follows:
|
||||
|
||||
* `kind` specifies the Kubernetes resource type the detail item will apply to.
|
||||
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||
* `components` defines the detail item's appearance and behavior.
|
||||
* `Details` provides a function that returns a `React.Component` given a set of detail item properties.
|
||||
In this example a `NamespaceDetailsItem` object is returned.
|
||||
- `kind` specifies the Kubernetes resource type the detail item will apply to.
|
||||
- `apiVersion` specifies the Kubernetes API version number to use with the resource type.
|
||||
- `components` defines the detail item's appearance and behavior.
|
||||
- `Details` provides a function that returns a `React.Component` given a set of detail item properties.
|
||||
In this example a `NamespaceDetailsItem` object is returned.
|
||||
|
||||
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { PodsDetailsList } from "./pods-details-list";
|
||||
import React from "react";
|
||||
@ -656,12 +663,8 @@ import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
const {
|
||||
K8sApi: {
|
||||
podsApi,
|
||||
},
|
||||
Component: {
|
||||
DrawerTitle,
|
||||
},
|
||||
K8sApi: { podsApi },
|
||||
Component: { DrawerTitle },
|
||||
} = Renderer;
|
||||
|
||||
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||
@ -669,7 +672,9 @@ type Namespace = Renderer.K8sApi.Namespace;
|
||||
type Pod = Renderer.K8sApi.Pod;
|
||||
|
||||
@observer
|
||||
export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps<Namespace>> {
|
||||
export class NamespaceDetailsItem extends React.Component<
|
||||
KubeObjectDetailsProps<Namespace>
|
||||
> {
|
||||
@observable private pods: Pod[];
|
||||
|
||||
async componentDidMount() {
|
||||
@ -681,10 +686,10 @@ export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<DrawerTitle title="Pods" />
|
||||
<PodsDetailsList pods={this.pods}/>
|
||||
<DrawerTitle>Pods</DrawerTitle>
|
||||
<PodsDetailsList pods={this.pods} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -709,26 +714,21 @@ Details are placed in drawers, and using `Renderer.Component.DrawerTitle` provid
|
||||
Multiple details in a drawer can be placed in `<Renderer.Component.DrawerItem>` elements for further separation, if desired.
|
||||
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
|
||||
const {
|
||||
Component: {
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Table,
|
||||
},
|
||||
Component: { TableHead, TableRow, TableCell, Table },
|
||||
} = Renderer;
|
||||
|
||||
type Pod = Renderer.K8sApi.Pod;
|
||||
|
||||
interface Props {
|
||||
interface PodsDetailsListProps {
|
||||
pods?: Pod[];
|
||||
}
|
||||
|
||||
export class PodsDetailsList extends React.Component<Props> {
|
||||
export class PodsDetailsList extends React.Component<PodsDetailsListProps> {
|
||||
getTableRow = (pod: Pod) => {
|
||||
return (
|
||||
<TableRow key={index} nowrap>
|
||||
@ -736,11 +736,11 @@ export class PodsDetailsList extends React.Component<Props> {
|
||||
<TableCell className="podAge">{pods[index].getAge()}</TableCell>
|
||||
<TableCell className="podStatus">{pods[index].getStatus()}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { pods } = this.props
|
||||
const { pods } = this.props;
|
||||
|
||||
if (!pods?.length) {
|
||||
return null;
|
||||
@ -754,7 +754,7 @@ export class PodsDetailsList extends React.Component<Props> {
|
||||
<TableCell className="podAge">Age</TableCell>
|
||||
<TableCell className="podStatus">Status</TableCell>
|
||||
</TableHead>
|
||||
{ pods.map(this.getTableRow) }
|
||||
{pods.map(this.getTableRow)}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -109,13 +109,13 @@ To allow the end-user to control the life cycle of this cluster feature the foll
|
||||
}
|
||||
} = Renderer;
|
||||
|
||||
interface Props {
|
||||
interface ExampleClusterFeatureSettingsProps {
|
||||
cluster: Common.Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExampleClusterFeatureSettings extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
export class ExampleClusterFeatureSettings extends React.Component<ExampleClusterFeatureSettingsProps> {
|
||||
constructor(props: ExampleClusterFeatureSettingsProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
4104
extensions/kube-object-event-status/package-lock.json
generated
@ -8,18 +8,15 @@
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack && npm pack",
|
||||
"dev": "webpack --watch",
|
||||
"build": "npx webpack && npm pack",
|
||||
"dev": "npx webpack -- --watch",
|
||||
"test": "echo NO TESTS"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"npm": "^8.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ module.exports = [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"react-dom": "var global.ReactDOM",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact",
|
||||
},
|
||||
|
||||
7570
extensions/metrics-cluster-feature/package-lock.json
generated
@ -8,9 +8,9 @@
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack && npm pack",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@",
|
||||
"build": "npx webpack && npm pack",
|
||||
"dev": "npx webpack -- --watch",
|
||||
"test": "npx jest --passWithNoTests --env=jsdom src $@",
|
||||
"clean": "rm -rf dist/ && rm *.tgz"
|
||||
},
|
||||
"files": [
|
||||
@ -19,10 +19,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"semver": "^7.3.2",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.44.2"
|
||||
"npm": "^8.5.3",
|
||||
"semver": "^7.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Common, Renderer } from "@k8slens/extensions";
|
||||
import type { Common } from "@k8slens/extensions";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { MetricsSettings } from "./src/metrics-settings";
|
||||
|
||||
export default class ClusterMetricsFeatureExtension extends Renderer.LensExtension {
|
||||
|
||||
@ -24,11 +24,6 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
# <%- if config.node_selector -%>
|
||||
# nodeSelector:
|
||||
# <%- node_selector.to_h.each do |key, value| -%>
|
||||
|
||||
@ -30,11 +30,6 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
|
||||
@ -119,3 +119,10 @@ rules:
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- scheduling.k8s.io
|
||||
resources:
|
||||
- priorityclasses
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
|
||||
@ -23,11 +23,6 @@ spec:
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
serviceAccountName: kube-state-metrics
|
||||
containers:
|
||||
- name: kube-state-metrics
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { Renderer, Common } from "@k8slens/extensions";
|
||||
import type { Common } from "@k8slens/extensions";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import semver from "semver";
|
||||
import * as path from "path";
|
||||
|
||||
|
||||
@ -3,10 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Common, Renderer } from "@k8slens/extensions";
|
||||
import type { Common } from "@k8slens/extensions";
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, observable, makeObservable } from "mobx";
|
||||
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
|
||||
import type { MetricsConfiguration } from "./metrics-feature";
|
||||
import { MetricsFeature } from "./metrics-feature";
|
||||
|
||||
const {
|
||||
K8sApi: {
|
||||
@ -17,13 +19,13 @@ const {
|
||||
},
|
||||
} = Renderer;
|
||||
|
||||
interface Props {
|
||||
export interface MetricsSettingsProps {
|
||||
cluster: Common.Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MetricsSettings extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
constructor(props: MetricsSettingsProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@ -206,14 +208,14 @@ export class MetricsSettings extends React.Component<Props> {
|
||||
<section>
|
||||
<SubTitle title="Prometheus" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.togglePrometheus(v.target.checked)}
|
||||
name="prometheus"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled Prometheus metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -224,14 +226,14 @@ export class MetricsSettings extends React.Component<Props> {
|
||||
<section>
|
||||
<SubTitle title="Kube State Metrics" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled kube-state-metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -243,14 +245,14 @@ export class MetricsSettings extends React.Component<Props> {
|
||||
<section>
|
||||
<SubTitle title="Node Exporter" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled node-exporter stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -269,9 +271,11 @@ export class MetricsSettings extends React.Component<Props> {
|
||||
className="w-60 h-14"
|
||||
/>
|
||||
|
||||
{this.canUpgrade && (<small className="hint">
|
||||
An update is available for enabled metrics components.
|
||||
</small>)}
|
||||
{this.canUpgrade && (
|
||||
<small className="hint">
|
||||
An update is available for enabled metrics components.
|
||||
</small>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -26,6 +26,7 @@ module.exports = [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"react-dom": "var global.ReactDOM",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact",
|
||||
},
|
||||
|
||||
7570
extensions/node-menu/package-lock.json
generated
@ -8,9 +8,9 @@
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack && npm pack",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
"build": "npx webpack && npm pack",
|
||||
"dev": "npx webpack -- --watch",
|
||||
"test": "npx jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
@ -18,9 +18,6 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"npm": "^8.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import React from "react";
|
||||
import { NodeMenu, NodeMenuProps } from "./src/node-menu";
|
||||
import type { NodeMenuProps } from "./src/node-menu";
|
||||
import { NodeMenu } from "./src/node-menu";
|
||||
|
||||
export default class NodeMenuRendererExtension extends Renderer.LensExtension {
|
||||
kubeObjectMenuItems = [
|
||||
|
||||
@ -68,7 +68,9 @@ export function NodeMenu(props: NodeMenuProps) {
|
||||
labelOk: `Drain Node`,
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to drain <b>{nodeName}</b>?
|
||||
{"Are you sure you want to drain "}
|
||||
<b>{nodeName}</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
});
|
||||
@ -77,26 +79,42 @@ export function NodeMenu(props: NodeMenuProps) {
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={shell}>
|
||||
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/>
|
||||
<Icon
|
||||
svg="ssh"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Node shell"}
|
||||
/>
|
||||
<span className="title">Shell</span>
|
||||
</MenuItem>
|
||||
{
|
||||
node.isUnschedulable()
|
||||
? (
|
||||
<MenuItem onClick={unCordon}>
|
||||
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} />
|
||||
<Icon
|
||||
material="play_circle_filled"
|
||||
tooltip={toolbar && "Uncordon"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Uncordon</span>
|
||||
</MenuItem>
|
||||
)
|
||||
: (
|
||||
<MenuItem onClick={cordon}>
|
||||
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} />
|
||||
<Icon
|
||||
material="pause_circle_filled"
|
||||
tooltip={toolbar && "Cordon"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Cordon</span>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
<MenuItem onClick={drain}>
|
||||
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/>
|
||||
<Icon
|
||||
material="delete_sweep"
|
||||
tooltip={toolbar && "Drain"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Drain</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@ -26,6 +26,7 @@ module.exports = [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"react-dom": "var global.ReactDOM",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact",
|
||||
},
|
||||
|
||||
7563
extensions/pod-menu/package-lock.json
generated
@ -8,9 +8,9 @@
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack && npm pack",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
"build": "npx webpack && npm pack",
|
||||
"dev": "npx webpack -- --watch",
|
||||
"test": "npx jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
@ -18,9 +18,6 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"npm": "^8.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
*/
|
||||
|
||||
import { Renderer } from "@k8slens/extensions";
|
||||
import { PodAttachMenu, PodAttachMenuProps } from "./src/attach-menu";
|
||||
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu";
|
||||
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu";
|
||||
import type { PodAttachMenuProps } from "./src/attach-menu";
|
||||
import { PodAttachMenu } from "./src/attach-menu";
|
||||
import type { PodShellMenuProps } from "./src/shell-menu";
|
||||
import { PodShellMenu } from "./src/shell-menu";
|
||||
import type { PodLogsMenuProps } from "./src/logs-menu";
|
||||
import { PodLogsMenu } from "./src/logs-menu";
|
||||
import React from "react";
|
||||
|
||||
export default class PodMenuRendererExtension extends Renderer.LensExtension {
|
||||
|
||||
@ -71,7 +71,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
||||
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/>
|
||||
<Icon
|
||||
material="pageview"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Attach to Pod"}
|
||||
/>
|
||||
<span className="title">Attach Pod</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -82,7 +86,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
||||
const { name } = container;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.attachToPod(name))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.attachToPod(name))}
|
||||
className="flex align-center"
|
||||
>
|
||||
<StatusBrick/>
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -46,7 +46,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/>
|
||||
<Icon
|
||||
material="subject"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Pod Logs"}
|
||||
/>
|
||||
<span className="title">Logs</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -63,7 +67,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.showLogs(container))}
|
||||
className="flex align-center"
|
||||
>
|
||||
{brick}
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -79,7 +79,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} />
|
||||
<Icon
|
||||
svg="ssh"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Pod Shell"}
|
||||
/>
|
||||
<span className="title">Shell</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -90,7 +94,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
||||
const { name } = container;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.execShell(name))}
|
||||
className="flex align-center"
|
||||
>
|
||||
<StatusBrick/>
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -26,6 +26,7 @@ module.exports = [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"react-dom": "var global.ReactDOM",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact",
|
||||
},
|
||||
|
||||
@ -52,7 +52,7 @@ describe("preferences page tests", () => {
|
||||
];
|
||||
|
||||
for (const { id, header } of pages) {
|
||||
await window.click(`[data-testid=${id}-tab]`);
|
||||
await window.click(`[data-testid=tab-link-for-${id}]`);
|
||||
await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`);
|
||||
}
|
||||
}, 10*60*1000);
|
||||
@ -60,9 +60,7 @@ describe("preferences page tests", () => {
|
||||
// Skipping, but will turn it on again in the follow up PR
|
||||
it.skip("ensures helm repos", async () => {
|
||||
await window.click("[data-testid=kubernetes-tab]");
|
||||
await window.waitForSelector("[data-testid=repository-name]", {
|
||||
timeout: 140_000,
|
||||
});
|
||||
await window.waitForSelector("[data-testid=repository-name]");
|
||||
await window.click("#HelmRepoSelect");
|
||||
await window.waitForSelector("div.Select__option");
|
||||
}, 10*60*1000);
|
||||
|
||||
@ -12,295 +12,11 @@
|
||||
import * as utils from "../helpers/utils";
|
||||
import { minikubeReady } from "../helpers/minikube";
|
||||
import type { Frame, Page } from "playwright";
|
||||
import { groupBy, toPairs } from "lodash/fp";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
|
||||
const TEST_NAMESPACE = "integration-tests";
|
||||
|
||||
function getSidebarSelectors(itemId: string) {
|
||||
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
||||
|
||||
return {
|
||||
expandSubMenu: `${root} .nav-item`,
|
||||
subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="${href}"]`,
|
||||
};
|
||||
}
|
||||
|
||||
function getLoadedSelector(page: CommonPage): string {
|
||||
if (page.expectedText) {
|
||||
return `${page.expectedSelector} >> text='${page.expectedText}'`;
|
||||
}
|
||||
|
||||
return page.expectedSelector;
|
||||
}
|
||||
|
||||
interface CommonPage {
|
||||
name: string;
|
||||
href: string;
|
||||
expectedSelector: string;
|
||||
expectedText?: string;
|
||||
}
|
||||
|
||||
interface TopPageTest {
|
||||
page: CommonPage;
|
||||
}
|
||||
|
||||
interface SubPageTest {
|
||||
drawerId: string;
|
||||
pages: CommonPage[];
|
||||
}
|
||||
|
||||
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: "/overview",
|
||||
expectedSelector: "div[data-testid='cluster-overview-page'] 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;
|
||||
|
||||
@ -309,162 +25,411 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
||||
await utils.clickWelcomeButton(window);
|
||||
|
||||
frame = await utils.lauchMinikubeClusterFromCatalog(window);
|
||||
}, 10*60*1000);
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanup();
|
||||
}, 10*60*1000);
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
it("shows cluster context menu in sidebar", async () => {
|
||||
await frame.click(`[data-testid="sidebar-cluster-dropdown"]`);
|
||||
await frame.waitForSelector(`.Menu >> text="Add to Hotbar"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Settings"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Disconnect"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Delete"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Remove"`);
|
||||
});
|
||||
|
||||
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}"]`);
|
||||
// FIXME: failed locally since metrics might already exist, cc @aleksfront
|
||||
it.skip("opens cluster settings by clicking link in no-metrics area", async () => {
|
||||
await frame.locator("text=Open cluster settings >> nth=0").click();
|
||||
await window.waitForSelector(`[data-testid="metrics-header"]`);
|
||||
});
|
||||
|
||||
await menuButton.click();
|
||||
await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`);
|
||||
it(
|
||||
"should navigate around common cluster pages",
|
||||
async () => {
|
||||
const scenariosByParent = pipeline(
|
||||
scenarios,
|
||||
groupBy("parentSidebarItemTestId"),
|
||||
toPairs,
|
||||
);
|
||||
|
||||
continue;
|
||||
for (const [parentSidebarItemTestId, scenarios] of scenariosByParent) {
|
||||
if (parentSidebarItemTestId !== "null") {
|
||||
await frame.click(`[data-testid="${parentSidebarItemTestId}"]`);
|
||||
}
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
await frame.click(`[data-testid="${scenario.sidebarItemTestId}"]`);
|
||||
|
||||
await frame.waitForSelector(
|
||||
scenario.expectedSelector,
|
||||
selectorTimeout,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
it(
|
||||
"show logs and highlight the log search entries",
|
||||
async () => {
|
||||
await navigateToPods(frame);
|
||||
|
||||
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 showPodLogsIcon = await frame.waitForSelector(
|
||||
".Drawer .drawer-title .Icon >> text=subject",
|
||||
);
|
||||
|
||||
showPodLogsIcon.click();
|
||||
|
||||
// Check if controls are available
|
||||
await frame.waitForSelector(".Dock.isOpen");
|
||||
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 navigateToNamespaces(frame);
|
||||
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 and then remove that pod via the context menu`,
|
||||
async () => {
|
||||
await navigateToNamespaces(frame);
|
||||
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}`);
|
||||
|
||||
await navigateToPods(frame);
|
||||
|
||||
const namespacesSelector = await frame.waitForSelector(
|
||||
".NamespaceSelect",
|
||||
);
|
||||
|
||||
await namespacesSelector.click();
|
||||
await namespacesSelector.type(TEST_NAMESPACE);
|
||||
await namespacesSelector.press("Enter");
|
||||
await namespacesSelector.click();
|
||||
|
||||
await frame.click(".Icon.new-dock-tab");
|
||||
|
||||
try {
|
||||
await frame.click("li.MenuItem.create-resource-tab", {
|
||||
// NOTE: the following shouldn't be required, but is because without it a TypeError is thrown
|
||||
// see: https://github.com/microsoft/playwright/issues/8229
|
||||
position: {
|
||||
y: 0,
|
||||
x: 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await frame.waitForTimeout(100_000);
|
||||
}
|
||||
|
||||
const { drawerId, pages } = test;
|
||||
const selectors = getSidebarSelectors(drawerId);
|
||||
const mainPageSelector = `${selectors.subMenuLink(pages[0].href)} >> text='${pages[0].name}'`;
|
||||
const testPodName = "nginx-create-pod-test";
|
||||
const monacoEditor = await frame.waitForSelector(`.Dock.isOpen [data-test-id="monaco-editor"]`);
|
||||
|
||||
await frame.click(selectors.expandSubMenu);
|
||||
await frame.waitForSelector(mainPageSelector);
|
||||
await monacoEditor.click();
|
||||
await monacoEditor.type("apiVersion: v1", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type("kind: Pod", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type("metadata:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(` name: ${testPodName}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.press("Backspace", { delay: 10 });
|
||||
await monacoEditor.type("spec:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(" containers:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(`- name: ${testPodName}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(" image: nginx:alpine", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
|
||||
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 showPodLogsIcon = await frame.waitForSelector(".Drawer .drawer-title .Icon >> text=subject");
|
||||
|
||||
showPodLogsIcon.click();
|
||||
|
||||
// Check if controls are available
|
||||
await frame.waitForSelector(".Dock.isOpen");
|
||||
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"]`);
|
||||
}
|
||||
|
||||
await frame.click(`a[href^="/pods"]`);
|
||||
|
||||
const namespacesSelector = await frame.waitForSelector(".NamespaceSelect");
|
||||
|
||||
await namespacesSelector.click();
|
||||
await namespacesSelector.type(TEST_NAMESPACE);
|
||||
await namespacesSelector.press("Enter");
|
||||
await namespacesSelector.click();
|
||||
|
||||
await frame.click(".Icon.new-dock-tab");
|
||||
|
||||
try {
|
||||
await frame.click("li.MenuItem.create-resource-tab", {
|
||||
// NOTE: the following shouldn't be required, but is because without it a TypeError is thrown
|
||||
// see: https://github.com/microsoft/playwright/issues/8229
|
||||
position: {
|
||||
y: 0,
|
||||
x: 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await frame.waitForTimeout(100_000);
|
||||
}
|
||||
|
||||
const testPodName = "nginx-create-pod-test";
|
||||
const monacoEditor = await frame.waitForSelector(`.Dock.isOpen [data-test-component="monaco-editor"]`);
|
||||
|
||||
await monacoEditor.click();
|
||||
await monacoEditor.type("apiVersion: v1", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type("kind: Pod", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type("metadata:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(` name: ${testPodName}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.press("Backspace", { delay: 10 });
|
||||
await monacoEditor.type("spec:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(" containers:", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(`- name: ${testPodName}`, { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
await monacoEditor.type(" image: nginx:alpine", { delay: 10 });
|
||||
await monacoEditor.press("Enter", { delay: 10 });
|
||||
|
||||
await frame.click(".Dock .Button >> text='Create'");
|
||||
await frame.waitForSelector(`.TableCell >> text=${testPodName}`);
|
||||
}, 10*60*1000);
|
||||
await frame.click(".Dock .Button >> text='Create'");
|
||||
await frame.waitForSelector(`.TableCell >> text=${testPodName}`);
|
||||
await frame.click(".TableRow .TableCell.menu");
|
||||
await frame.click(".MenuItem >> text=Delete");
|
||||
await frame.click("button >> text=Remove");
|
||||
await frame.waitForSelector(`.TableCell >> text=${testPodName}`, { state: "detached" });
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
});
|
||||
|
||||
const selectorTimeout = { timeout: 30000 };
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
expectedSelector: "div[data-testid='cluster-overview-page'] div.label",
|
||||
parentSidebarItemTestId: null,
|
||||
sidebarItemTestId: "sidebar-item-link-for-cluster-overview",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: null,
|
||||
sidebarItemTestId: "sidebar-item-link-for-nodes",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: 'h5 >> text="Overview"',
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-overview",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-pods",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-deployments",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-daemon-sets",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-stateful-sets",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-replica-sets",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-jobs",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-workloads",
|
||||
sidebarItemTestId: "sidebar-item-link-for-cron-jobs",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-config-maps",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-secrets",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-resource-quotas",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-limit-ranges",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-horizontal-pod-auto-scalers",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-config",
|
||||
sidebarItemTestId: "sidebar-item-link-for-pod-disruption-budgets",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-network",
|
||||
sidebarItemTestId: "sidebar-item-link-for-services",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-network",
|
||||
sidebarItemTestId: "sidebar-item-link-for-endpoints",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-network",
|
||||
sidebarItemTestId: "sidebar-item-link-for-ingresses",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-network",
|
||||
sidebarItemTestId: "sidebar-item-link-for-network-policies",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-storage",
|
||||
sidebarItemTestId: "sidebar-item-link-for-persistent-volume-claims",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-storage",
|
||||
sidebarItemTestId: "sidebar-item-link-for-persistent-volumes",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-storage",
|
||||
sidebarItemTestId: "sidebar-item-link-for-storage-classes",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: null,
|
||||
sidebarItemTestId: "sidebar-item-link-for-namespaces",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: null,
|
||||
sidebarItemTestId: "sidebar-item-link-for-events",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-helm",
|
||||
sidebarItemTestId: "sidebar-item-link-for-charts",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-helm",
|
||||
sidebarItemTestId: "sidebar-item-link-for-releases",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-service-accounts",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-roles",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-cluster-roles",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-role-bindings",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-cluster-role-bindings",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||
sidebarItemTestId: "sidebar-item-link-for-pod-security-policies",
|
||||
},
|
||||
|
||||
{
|
||||
expectedSelector: "h5.title",
|
||||
parentSidebarItemTestId: null,
|
||||
sidebarItemTestId: "sidebar-item-link-for-custom-resources",
|
||||
},
|
||||
];
|
||||
|
||||
const navigateToPods = async (frame: Frame) => {
|
||||
await frame.click(`[data-testid="sidebar-item-link-for-workloads"]`);
|
||||
await frame.click(`[data-testid="sidebar-item-link-for-pods"]`);
|
||||
};
|
||||
|
||||
const navigateToNamespaces = async (frame: Frame) => {
|
||||
await frame.click(`[data-testid="sidebar-item-link-for-namespaces"]`);
|
||||
};
|
||||
|
||||
@ -24,9 +24,9 @@ describe("Lens command palette", () => {
|
||||
utils.itIf(!isWindows)("opens command dialog from menu", async () => {
|
||||
await app.evaluate(async ({ app }) => {
|
||||
await app.applicationMenu
|
||||
.getMenuItemById("view")
|
||||
.submenu.getMenuItemById("command-palette")
|
||||
.click();
|
||||
?.getMenuItemById("view")
|
||||
?.submenu?.getMenuItemById("command-palette")
|
||||
?.click();
|
||||
});
|
||||
await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
|
||||
}, 10*60*1000);
|
||||
|
||||
@ -7,7 +7,8 @@ import { mkdirp, remove } from "fs-extra";
|
||||
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 type { ElectronApplication, Frame, Page } from "playwright";
|
||||
import { _electron as electron } from "playwright";
|
||||
import { noop } from "lodash";
|
||||
|
||||
export const appPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||
@ -107,6 +108,10 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
|
||||
|
||||
const frame = await minikubeFrame.contentFrame();
|
||||
|
||||
if (!frame) {
|
||||
throw new Error("No iframe for minikube found");
|
||||
}
|
||||
|
||||
await frame.waitForSelector("[data-testid=cluster-sidebar]");
|
||||
|
||||
return frame;
|
||||
|
||||
6
integration/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"./**/*",
|
||||
]
|
||||
}
|
||||
346
package.json
@ -3,38 +3,41 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.4.0-alpha.1",
|
||||
"version": "6.0.0",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"copyright": "© 2022 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "OpenLens Authors",
|
||||
"email": "info@k8slens.dev"
|
||||
},
|
||||
"scripts": {
|
||||
"adr:create": "echo \"What is the title?\"; read title; adr new \"$title\"",
|
||||
"adr:change-status": "echo \"Decision number?:\"; read decision; adr status $decision",
|
||||
"adr:update-readme": "adr update",
|
||||
"adr:list": "adr list",
|
||||
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||
"dev-build": "concurrently yarn:compile:*",
|
||||
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||
"dev:main": "yarn run compile:main --watch",
|
||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||
"dev-run": "nodemon --watch ./static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||
"dev:main": "yarn run compile:main --watch --progress",
|
||||
"dev:renderer": "yarn run ts-node webpack/dev-server.ts",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
|
||||
"compile:main": "yarn run webpack --config webpack/main.ts",
|
||||
"compile:renderer": "yarn run webpack --config webpack/renderer.ts",
|
||||
"compile:extension-types": "yarn run webpack --config webpack/extensions.ts",
|
||||
"npm:fix-build-version": "yarn run ts-node build/set_build_version.ts",
|
||||
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
|
||||
"build:linux": "yarn run compile && electron-builder --linux --dir",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --dir",
|
||||
"build:win": "yarn run compile && electron-builder --win --dir",
|
||||
"integration": "jest --runInBand --detectOpenHandles --forceExit integration",
|
||||
"test:unit": "func() { jest ${1} --watch --testPathIgnorePatterns integration; }; func",
|
||||
"test:integration": "func() { jest ${1:-xyz} --watch --runInBand --detectOpenHandles --forceExit --modulePaths=[\"<rootDir>/integration/\"]; }; func",
|
||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
|
||||
"download-bins": "concurrently yarn:download:*",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"download:binaries": "yarn run ts-node build/download_binaries.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/generate-tray-icons.ts",
|
||||
"build:theme-vars": "yarn run ts-node build/build_theme_vars.ts",
|
||||
"lint": "PROD=true yarn run eslint --ext js,ts,tsx --max-warnings=0 .",
|
||||
"lint:fix": "yarn run lint --fix",
|
||||
@ -47,22 +50,28 @@
|
||||
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.21.2",
|
||||
"bundledHelmVersion": "3.6.3",
|
||||
"sentryDsn": ""
|
||||
"k8sProxyVersion": "0.2.1",
|
||||
"bundledKubectlVersion": "1.23.3",
|
||||
"bundledHelmVersion": "3.7.2",
|
||||
"sentryDsn": "",
|
||||
"contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14 <15"
|
||||
"node": ">=16 <17"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverage": false,
|
||||
"verbose": true,
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
"^.+\\.(t|j)sx?$": [
|
||||
"@swc/jest"
|
||||
]
|
||||
},
|
||||
"testEnvironment": "jsdom",
|
||||
"resolver": "<rootDir>/src/jest-28-resolver.js",
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||
"\\.(css|scss)$": "identity-obj-proxy",
|
||||
"\\.(svg|png|jpg|eot|woff2?|ttf)$": "<rootDir>/__mocks__/assetMock.ts"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/dist",
|
||||
@ -72,11 +81,11 @@
|
||||
"<rootDir>/src/jest.setup.ts",
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
"globalSetup": "<rootDir>/src/jest.timezone.ts",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/jest-after-env.setup.ts"
|
||||
],
|
||||
"runtime": "@side/jest-runtime"
|
||||
},
|
||||
"build": {
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
@ -130,8 +139,12 @@
|
||||
"to": "./${arch}/kubectl"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/${arch}/helm3/helm3",
|
||||
"to": "./helm3/helm3"
|
||||
"from": "binaries/client/linux/${arch}/lens-k8s-proxy",
|
||||
"to": "./${arch}/lens-k8s-proxy"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/linux/${arch}/helm",
|
||||
"to": "./${arch}/helm"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -151,8 +164,12 @@
|
||||
"to": "./${arch}/kubectl"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/${arch}/helm3/helm3",
|
||||
"to": "./helm3/helm3"
|
||||
"from": "binaries/client/darwin/${arch}/lens-k8s-proxy",
|
||||
"to": "./${arch}/lens-k8s-proxy"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/darwin/${arch}/helm",
|
||||
"to": "./${arch}/helm"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -162,16 +179,16 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "binaries/client/windows/x64/kubectl.exe",
|
||||
"to": "./x64/kubectl.exe"
|
||||
"from": "binaries/client/windows/${arch}/kubectl.exe",
|
||||
"to": "./${arch}/kubectl.exe"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/windows/ia32/kubectl.exe",
|
||||
"to": "./ia32/kubectl.exe"
|
||||
"from": "binaries/client/windows/${arch}/lens-k8s-proxy.exe",
|
||||
"to": "./${arch}/lens-k8s-proxy.exe"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/x64/helm3/helm3.exe",
|
||||
"to": "./helm3/helm3.exe"
|
||||
"from": "binaries/client/windows/${arch}/helm.exe",
|
||||
"to": "./${arch}/helm.exe"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -190,210 +207,231 @@
|
||||
"role": "Viewer"
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"@astronautlabs/jsonpath/underscore": "^1.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.16.1",
|
||||
"@ogre-tools/injectable": "3.2.0",
|
||||
"@ogre-tools/injectable-react": "3.2.0",
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"@sentry/integrations": "^6.15.0",
|
||||
"@types/circular-dependency-plugin": "5.0.4",
|
||||
"@astronautlabs/jsonpath": "^1.1.0",
|
||||
"@hapi/call": "^9.0.0",
|
||||
"@hapi/subtext": "^7.0.4",
|
||||
"@kubernetes/client-node": "^0.17.0",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"@ogre-tools/fp": "9.0.1",
|
||||
"@ogre-tools/injectable": "9.0.2",
|
||||
"@ogre-tools/injectable-extension-for-auto-registration": "9.0.2",
|
||||
"@ogre-tools/injectable-extension-for-mobx": "9.0.2",
|
||||
"@ogre-tools/injectable-react": "9.0.2",
|
||||
"@sentry/electron": "^3.0.7",
|
||||
"@sentry/integrations": "^6.19.3",
|
||||
"@side/jest-runtime": "^1.0.1",
|
||||
"@types/circular-dependency-plugin": "5.0.5",
|
||||
"abort-controller": "^3.0.0",
|
||||
"auto-bind": "^4.0.0",
|
||||
"autobind-decorator": "^2.4.0",
|
||||
"await-lock": "^2.1.0",
|
||||
"await-lock": "^2.2.2",
|
||||
"byline": "^5.0.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"conf": "^7.1.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-updater": "^4.6.1",
|
||||
"electron-updater": "^4.6.5",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"filehound": "^1.17.5",
|
||||
"filehound": "^1.17.6",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"got": "^11.8.3",
|
||||
"got": "^11.8.5",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"handlebars": "^4.7.7",
|
||||
"history": "^4.10.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"immer": "^9.0.6",
|
||||
"joi": "^17.5.0",
|
||||
"immer": "^9.0.15",
|
||||
"joi": "^17.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^16.7.0",
|
||||
"jsonpath": "^1.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"mac-ca": "^1.0.6",
|
||||
"marked": "^4.0.10",
|
||||
"marked": "^4.0.18",
|
||||
"md5-file": "^5.0.0",
|
||||
"mobx": "^6.3.7",
|
||||
"mobx": "^6.6.1",
|
||||
"mobx-observable-history": "^2.0.3",
|
||||
"mobx-react": "^7.2.1",
|
||||
"mock-fs": "^5.1.2",
|
||||
"moment": "^2.29.1",
|
||||
"mobx-react": "^7.5.2",
|
||||
"mobx-utils": "^6.0.4",
|
||||
"mock-fs": "^5.1.4",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"monaco-editor": "^0.29.1",
|
||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||
"node-fetch": "lensapp/node-fetch#2.x",
|
||||
"node-pty": "^0.10.1",
|
||||
"npm": "^6.14.15",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-pty": "0.10.1",
|
||||
"npm": "^6.14.17",
|
||||
"p-limit": "^3.1.0",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-material-ui-carousel": "^2.3.8",
|
||||
"react-material-ui-carousel": "^2.3.11",
|
||||
"react-router": "^5.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
"readable-stream": "^3.6.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"rfc6902": "^4.0.2",
|
||||
"semver": "^7.3.2",
|
||||
"selfsigned": "^2.0.1",
|
||||
"semver": "^7.3.7",
|
||||
"shell-env": "^3.0.1",
|
||||
"spdy": "^4.0.2",
|
||||
"tar": "^6.1.11",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"tempy": "1.0.1",
|
||||
"url-parse": "^1.5.3",
|
||||
"typed-regex": "^0.0.8",
|
||||
"url-parse": "^1.5.10",
|
||||
"uuid": "^8.3.2",
|
||||
"win-ca": "^3.4.5",
|
||||
"winston": "^3.3.3",
|
||||
"win-ca": "^3.5.0",
|
||||
"winston": "^3.8.1",
|
||||
"winston-console-format": "^1.0.8",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.5.5"
|
||||
"ws": "^8.8.1",
|
||||
"xterm-link-provider": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@async-fn/jest": "1.5.3",
|
||||
"@async-fn/jest": "1.6.4",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@sentry/types": "^6.14.1",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@sentry/types": "^6.19.7",
|
||||
"@swc/core": "^1.2.241",
|
||||
"@swc/jest": "^0.2.22",
|
||||
"@testing-library/dom": "^7.31.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/byline": "^4.2.33",
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/color": "^3.0.2",
|
||||
"@types/chart.js": "^2.9.36",
|
||||
"@types/circular-dependency-plugin": "5.0.5",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/command-line-args": "^5.2.0",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/dompurify": "^2.3.1",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/electron-devtools-installer": "^2.2.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/glob-to-regexp": "^0.4.1",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/gunzip-maybe": "^1.4.0",
|
||||
"@types/hapi__call": "^9.0.0",
|
||||
"@types/hapi__subtext": "^7.0.0",
|
||||
"@types/html-webpack-plugin": "^3.2.6",
|
||||
"@types/http-proxy": "^1.17.7",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/http-proxy": "^1.17.9",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/marked": "^4.0.1",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/md5-file": "^4.0.2",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mini-css-extract-plugin": "^2.4.0",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "14.17.33",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/node": "^16.11.47",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/npm": "^2.0.32",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"@types/randomcolor": "^0.5.6",
|
||||
"@types/react": "^17.0.34",
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-router-dom": "^5.3.2",
|
||||
"@types/react-select": "3.1.2",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/react-dom": "^17.0.16",
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/readable-stream": "^2.3.12",
|
||||
"@types/readable-stream": "^2.3.13",
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/request-promise-native": "^1.0.18",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/sharp": "^0.29.4",
|
||||
"@types/semver": "^7.3.10",
|
||||
"@types/sharp": "^0.30.5",
|
||||
"@types/spdy": "^3.4.5",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/tcp-port-used": "^1.0.1",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/url-parse": "^1.4.5",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@types/webpack": "^4.41.32",
|
||||
"@types/webpack-dev-server": "^3.11.6",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||
"@typescript-eslint/parser": "^5.10.1",
|
||||
"@types/url-parse": "^1.4.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-server": "^4.7.2",
|
||||
"@types/webpack-env": "^1.17.0",
|
||||
"@types/webpack-node-externals": "^2.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||
"@typescript-eslint/parser": "^5.31.0",
|
||||
"adr": "^1.4.1",
|
||||
"ansi_up": "^5.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"cli-progress": "^3.11.2",
|
||||
"color": "^3.2.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"command-line-args": "^5.2.1",
|
||||
"concurrently": "^7.3.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.4",
|
||||
"electron": "^14.2.4",
|
||||
"electron-builder": "^22.14.5",
|
||||
"dompurify": "^2.3.10",
|
||||
"electron": "^19.0.13",
|
||||
"electron-builder": "^23.3.3",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"eslint": "^8.7.0",
|
||||
"esbuild": "^0.15.5",
|
||||
"esbuild-loader": "^2.19.0",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"flex.box": "^3.4.4",
|
||||
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"fork-ts-checker-webpack-plugin": "^6.5.2",
|
||||
"gunzip-maybe": "^1.4.2",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "26.6.3",
|
||||
"jest": "^28.1.3",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.18",
|
||||
"jest-mock-extended": "^2.0.7",
|
||||
"make-plural": "^6.2.2",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"node-gyp": "7.1.2",
|
||||
"node-loader": "^1.0.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"playwright": "^1.17.1",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"mock-http": "^1.1.0",
|
||||
"node-gyp": "^8.3.0",
|
||||
"node-loader": "^2.0.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"playwright": "^1.24.2",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-loader": "^6.2.1",
|
||||
"randomcolor": "^0.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-refresh": "^0.9.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-select": "3.2.0",
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-window": "^1.8.6",
|
||||
"sass": "^1.45.1",
|
||||
"sass-loader": "^10.2.0",
|
||||
"sharp": "^0.29.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"tailwindcss": "^3.0.7",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^10.4.0",
|
||||
"type-fest": "^1.0.2",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.7",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-select": "^5.4.0",
|
||||
"react-select-event": "^5.5.1",
|
||||
"react-table": "^7.8.0",
|
||||
"react-window": "^1.8.7",
|
||||
"sass": "^1.54.2",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sharp": "^0.30.7",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"tar-stream": "^2.2.0",
|
||||
"ts-loader": "^9.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"type-fest": "^2.14.0",
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typedoc": "0.22.10",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.5.2",
|
||||
"typedoc": "0.23.8",
|
||||
"typedoc-plugin-markdown": "^3.13.1",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.3",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"xterm": "^4.15.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.10.0",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"xterm": "^4.19.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
217
scripts/create-release-pr.mjs
Executable file
@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// This script creates a release PR
|
||||
import { execSync, exec, spawn } from "child_process";
|
||||
import commandLineArgs from "command-line-args";
|
||||
import fse from "fs-extra";
|
||||
import { basename } from "path";
|
||||
import semver from "semver";
|
||||
import { promisify } from "util";
|
||||
|
||||
const {
|
||||
SemVer,
|
||||
valid: semverValid,
|
||||
rcompare: semverRcompare,
|
||||
lte: semverLte,
|
||||
} = semver;
|
||||
const { readJsonSync } = fse;
|
||||
const execP = promisify(exec);
|
||||
|
||||
const options = commandLineArgs([
|
||||
{
|
||||
name: "type",
|
||||
defaultOption: true,
|
||||
},
|
||||
{
|
||||
name: "preid",
|
||||
},
|
||||
]);
|
||||
|
||||
const validReleaseValues = [
|
||||
"major",
|
||||
"minor",
|
||||
"patch",
|
||||
];
|
||||
const validPrereleaseValues = [
|
||||
"premajor",
|
||||
"preminor",
|
||||
"prepatch",
|
||||
"prerelease",
|
||||
];
|
||||
const validPreidValues = [
|
||||
"alpha",
|
||||
"beta",
|
||||
];
|
||||
|
||||
const errorMessages = {
|
||||
noReleaseType: `No release type provided. Valid options are: ${[...validReleaseValues, ...validPrereleaseValues].join(", ")}`,
|
||||
invalidRelease: (invalid) => `Invalid release type was provided (value was "${invalid}"). Valid options are: ${[...validReleaseValues, ...validPrereleaseValues].join(", ")}`,
|
||||
noPreid: `No preid was provided. Use '--preid' to specify. Valid options are: ${validPreidValues.join(", ")}`,
|
||||
invalidPreid: (invalid) => `Invalid preid was provided (value was "${invalid}"). Valid options are: ${validPreidValues.join(", ")}`,
|
||||
wrongCwd: "It looks like you are running this script from the 'scripts' directory. This script assumes it is run from the root of the git repo",
|
||||
};
|
||||
|
||||
if (!options.type) {
|
||||
console.error(errorMessages.noReleaseType);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (validReleaseValues.includes(options.type)) {
|
||||
// do nothing, is valid
|
||||
} else if (validPrereleaseValues.includes(options.type)) {
|
||||
if (!options.preid) {
|
||||
console.error(errorMessages.noPreid);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!validPreidValues.includes(options.preid)) {
|
||||
console.error(errorMessages.invalidPreid(options.preid));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(errorMessages.invalidRelease(options.type));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (basename(process.cwd()) === "scripts") {
|
||||
console.error(errorMessages.wrongCwd);
|
||||
}
|
||||
|
||||
|
||||
const currentVersion = new SemVer(readJsonSync("./package.json").version);
|
||||
|
||||
console.log(`current version: ${currentVersion.format()}`);
|
||||
console.log("fetching tags...");
|
||||
execSync("git fetch --tags --force");
|
||||
|
||||
const actualTags = execSync("git tag --list", { encoding: "utf-8" }).split(/\r?\n/).map(line => line.trim());
|
||||
const [previousReleasedVersion] = actualTags
|
||||
.map(semverValid)
|
||||
.filter(Boolean)
|
||||
.sort(semverRcompare)
|
||||
.filter(version => semverLte(version, currentVersion));
|
||||
|
||||
const npmVersionArgs = [
|
||||
"npm",
|
||||
"version",
|
||||
options.type,
|
||||
];
|
||||
|
||||
if (options.preid) {
|
||||
npmVersionArgs.push(`--preid=${options.preid}`);
|
||||
}
|
||||
|
||||
npmVersionArgs.push("--git-tag-version false");
|
||||
|
||||
execSync(npmVersionArgs.join(" "), { stdio: "ignore" });
|
||||
|
||||
const newVersion = new SemVer(readJsonSync("./package.json").version);
|
||||
const newVersionMilestone = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
|
||||
|
||||
console.log(`new version: ${newVersion.format()}`);
|
||||
|
||||
const getMergedPrsArgs = [
|
||||
"gh",
|
||||
"pr",
|
||||
"list",
|
||||
"--limit=500", // Should be big enough, if not we need to release more often ;)
|
||||
"--state=merged",
|
||||
"--base=master",
|
||||
"--json mergeCommit,title,author,labels,number,milestone",
|
||||
];
|
||||
|
||||
console.log("retreiving last 500 PRs to create release PR body...");
|
||||
const mergedPrs = JSON.parse(execSync(getMergedPrsArgs.join(" "), { encoding: "utf-8" }));
|
||||
const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === newVersionMilestone);
|
||||
const relaventPrsQuery = await Promise.all(
|
||||
milestoneRelevantPrs.map(async pr => ({
|
||||
pr,
|
||||
stdout: (await execP(`git tag v${previousReleasedVersion} --no-contains ${pr.mergeCommit.oid}`)).stdout,
|
||||
})),
|
||||
);
|
||||
const relaventPrs = relaventPrsQuery
|
||||
.filter(query => query.stdout)
|
||||
.map(query => query.pr)
|
||||
.filter(pr => pr.labels.every(label => label.name !== "skip-changelog"));
|
||||
|
||||
const enhancementPrLabelName = "enhancement";
|
||||
const bugfixPrLabelName = "bug";
|
||||
|
||||
const enhancementPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === enhancementPrLabelName));
|
||||
const bugfixPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === bugfixPrLabelName));
|
||||
const maintenencePrs = relaventPrs.filter(pr => pr.labels.every(label => label.name !== bugfixPrLabelName && label.name !== enhancementPrLabelName));
|
||||
|
||||
console.log("Found:");
|
||||
console.log(`${enhancementPrs.length} enhancement PRs`);
|
||||
console.log(`${bugfixPrs.length} bug fix PRs`);
|
||||
console.log(`${maintenencePrs.length} maintenence PRs`);
|
||||
|
||||
const prBodyLines = [
|
||||
`## Changes since ${previousReleasedVersion}`,
|
||||
"",
|
||||
];
|
||||
|
||||
function getPrEntry(pr) {
|
||||
return `- ${pr.title} (**[#${pr.number}](https://github.com/lensapp/lens/pull/${pr.number})**) https://github.com/${pr.author.login}`;
|
||||
}
|
||||
|
||||
if (enhancementPrs.length > 0) {
|
||||
prBodyLines.push(
|
||||
"## 🚀 Features",
|
||||
"",
|
||||
...enhancementPrs.map(getPrEntry),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
if (bugfixPrs.length > 0) {
|
||||
prBodyLines.push(
|
||||
"## 🐛 Bug Fixes",
|
||||
"",
|
||||
...bugfixPrs.map(getPrEntry),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
if (maintenencePrs.length > 0) {
|
||||
prBodyLines.push(
|
||||
"## 🧰 Maintenance",
|
||||
"",
|
||||
...maintenencePrs.map(getPrEntry),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
const prBody = prBodyLines.join("\n");
|
||||
const prBase = newVersion.patch === 0
|
||||
? "master"
|
||||
: `release/v${newVersion.major}.${newVersion.minor}`;
|
||||
const createPrArgs = [
|
||||
"pr",
|
||||
"create",
|
||||
"--base", prBase,
|
||||
"--title", `release ${newVersion.format()}`,
|
||||
"--label", "skip-changelog",
|
||||
"--body-file", "-",
|
||||
];
|
||||
|
||||
const createPrProcess = spawn("gh", createPrArgs, { stdio: "pipe" });
|
||||
let result = "";
|
||||
|
||||
createPrProcess.stdout.on("data", (chunk) => result += chunk);
|
||||
|
||||
createPrProcess.stdin.write(prBody);
|
||||
createPrProcess.stdin.end();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
createPrProcess.on("close", () => {
|
||||
createPrProcess.stdout.removeAllListeners();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
@ -7,10 +7,9 @@ import mockFs from "mock-fs";
|
||||
import { BaseStore } from "../base-store";
|
||||
import { action, comparer, makeObservable, observable, toJS } from "mobx";
|
||||
import { readFileSync } from "fs";
|
||||
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
|
||||
|
||||
import directoryForUserDataInjectable
|
||||
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
ipcMain: {
|
||||
@ -26,9 +25,9 @@ interface TestStoreModel {
|
||||
}
|
||||
|
||||
class TestStore extends BaseStore<TestStoreModel> {
|
||||
@observable a: string;
|
||||
@observable b: string;
|
||||
@observable c: string;
|
||||
@observable a = "";
|
||||
@observable b = "";
|
||||
@observable c = "";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@ -77,14 +76,12 @@ class TestStore extends BaseStore<TestStoreModel> {
|
||||
describe("BaseStore", () => {
|
||||
let store: TestStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
beforeEach(() => {
|
||||
const mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");
|
||||
mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
await dis.runSetups();
|
||||
|
||||
store = undefined;
|
||||
TestStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
@ -99,9 +96,9 @@ describe("BaseStore", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
store.disableSync();
|
||||
TestStore.resetInstance();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe("persistence", () => {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { CatalogCategory, CatalogCategoryRegistry, CatalogCategorySpec } from "../catalog";
|
||||
import type { CatalogCategorySpec } from "../catalog";
|
||||
import { CatalogCategory, CatalogCategoryRegistry } from "../catalog";
|
||||
|
||||
class TestCatalogCategoryRegistry extends CatalogCategoryRegistry { }
|
||||
|
||||
|
||||
52
src/common/__tests__/catalog-entity.test.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { CatalogCategory } from "../catalog";
|
||||
import type { CatalogCategorySpec } from "../catalog";
|
||||
|
||||
class TestCatalogCategoryWithoutBadge extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
|
||||
public metadata = {
|
||||
name: "Test Category",
|
||||
icon: "",
|
||||
};
|
||||
|
||||
public spec: CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [],
|
||||
names: {
|
||||
kind: "Test",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class TestCatalogCategoryWithBadge extends TestCatalogCategoryWithoutBadge {
|
||||
getBadge() {
|
||||
return (<div>Test Badge</div>);
|
||||
}
|
||||
}
|
||||
|
||||
describe("CatalogCategory", () => {
|
||||
it("returns name", () => {
|
||||
const category = new TestCatalogCategoryWithoutBadge();
|
||||
|
||||
expect(category.getName()).toEqual("Test Category");
|
||||
});
|
||||
|
||||
it("doesn't return badge by default", () => {
|
||||
const category = new TestCatalogCategoryWithoutBadge();
|
||||
|
||||
expect(category.getBadge()).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns a badge", () => {
|
||||
const category = new TestCatalogCategoryWithBadge();
|
||||
|
||||
expect(category.getBadge()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -7,34 +7,36 @@ import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import type { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type {
|
||||
DependencyInjectionContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
|
||||
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
|
||||
import directoryForUserDataInjectable
|
||||
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../vars/app-version.injectable";
|
||||
import assert from "assert";
|
||||
import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
|
||||
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
|
||||
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
|
||||
import fsInjectable from "../fs/fs.injectable";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const testDataIcon = fs.readFileSync(
|
||||
"test-data/cluster-store-migration-icon.png",
|
||||
);
|
||||
const clusterServerUrl = "https://localhost";
|
||||
const kubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost
|
||||
server: ${clusterServerUrl}
|
||||
name: test
|
||||
contexts:
|
||||
- context:
|
||||
@ -75,22 +77,26 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
describe("cluster-store", () => {
|
||||
let mainDi: DependencyInjectionContainer;
|
||||
let mainDi: DiContainer;
|
||||
let clusterStore: ClusterStore;
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
let createCluster: CreateCluster;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
|
||||
mainDi = dis.mainDi;
|
||||
|
||||
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
mainDi.override(directoryForTempInjectable, () => "some-temp-directory");
|
||||
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
|
||||
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
mainDi.override(normalizedPlatformInjectable, () => "darwin");
|
||||
|
||||
await dis.runSetups();
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
mainDi.unoverride(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -105,10 +111,6 @@ describe("cluster-store", () => {
|
||||
getCustomKubeConfigDirectoryInjectable,
|
||||
);
|
||||
|
||||
// TODO: Remove these by removing Singleton base-class from BaseStore
|
||||
ClusterStore.getInstance(false)?.unregisterIpcListener();
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({}),
|
||||
@ -117,7 +119,11 @@ describe("cluster-store", () => {
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
|
||||
clusterStore.unregisterIpcListener();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -138,6 +144,8 @@ describe("cluster-store", () => {
|
||||
getCustomKubeConfigDirectory("foo"),
|
||||
kubeconfig,
|
||||
),
|
||||
}, {
|
||||
clusterServerUrl,
|
||||
});
|
||||
|
||||
clusterStore.addCluster(cluster);
|
||||
@ -146,6 +154,8 @@ describe("cluster-store", () => {
|
||||
it("adds new cluster to store", async () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("foo");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
|
||||
expect(storedCluster.preferences.icon).toBe(
|
||||
@ -197,8 +207,6 @@ describe("cluster-store", () => {
|
||||
|
||||
describe("config with existing clusters", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
"temp-kube-config": kubeconfig,
|
||||
"some-directory-for-user-data": {
|
||||
@ -237,6 +245,8 @@ describe("cluster-store", () => {
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
@ -247,6 +257,8 @@ describe("cluster-store", () => {
|
||||
it("allows to retrieve a cluster", () => {
|
||||
const storedCluster = clusterStore.getById("cluster1");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("cluster1");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||
});
|
||||
@ -285,8 +297,6 @@ users:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
"invalid-kube-config": invalidKubeconfig,
|
||||
"valid-kube-config": kubeconfig,
|
||||
@ -319,6 +329,8 @@ users:
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
@ -335,7 +347,6 @@ users:
|
||||
|
||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
@ -361,6 +372,10 @@ users:
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
mainDi.override(appVersionInjectable, () => "3.6.0");
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
@ -377,6 +392,7 @@ users:
|
||||
it("migrates to modern format with icon not in file", async () => {
|
||||
const { icon } = clusterStore.clustersList[0].preferences;
|
||||
|
||||
assert(icon);
|
||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { appEventBus, AppEvent } from "../app-event-bus/event-bus";
|
||||
import { Console } from "console";
|
||||
import type { AppEvent } from "../app-event-bus/event-bus";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { assert, Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
@ -12,14 +13,15 @@ console = new Console(stdout, stderr);
|
||||
describe("event bus tests", () => {
|
||||
describe("emit", () => {
|
||||
it("emits an event", () => {
|
||||
let event: AppEvent = null;
|
||||
let event: AppEvent | undefined;
|
||||
|
||||
appEventBus.addListener((data) => {
|
||||
event = data;
|
||||
});
|
||||
|
||||
appEventBus.emit({ name: "foo", action: "bar" });
|
||||
expect(event.name).toBe("foo");
|
||||
assert(event);
|
||||
expect(event?.name).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||