1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into fix/consistent-inputs

This commit is contained in:
Alex Andreev 2022-08-23 11:21:04 +03:00
commit c21340a734
2478 changed files with 218392 additions and 64516 deletions

6
.adr.json Normal file
View File

@ -0,0 +1,6 @@
{
"language": "en",
"path": "docs/architecture/decisions/",
"prefix": "",
"digits": 4
}

View File

@ -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/'))"

View File

@ -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",
},
],
}],
},
},
],

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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: |

View File

@ -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

View File

@ -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 }}

View File

@ -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
View 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"
}
}

View File

@ -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"

View File

@ -1,4 +1,4 @@
Copyright (c) 2021 OpenLens Authors.
Copyright (c) 2022 OpenLens Authors.
Portions of this software are licensed as follows:

View File

@ -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

View File

@ -1,7 +1,7 @@
# Lens Open Source Project (OpenLens)
[![Build Status](https://github.com/lensapp/lens/actions/workflows/test.yml/badge.svg)](https://github.com/lensapp/lens/actions/workflows/test.yml)
[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](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

View File

@ -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)

View File

@ -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
View 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)

View File

@ -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: () => ({}),
};

View 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 }</>;

View 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>
);
};

View File

@ -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)}
*/`;

View File

@ -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
View 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));

View File

@ -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();
}

View File

@ -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")));
});

View 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();

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

BIN
build/tray/trayIcon@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

BIN
build/tray/trayIcon@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
build/tray/trayIcon@4x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

6
build/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"./**/*",
]
}

View File

@ -0,0 +1,2 @@
# Architecture Decision Records

View File

@ -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} />,
},
},
];
}
```

View File

@ -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>
);

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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",
},

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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| -%>

View File

@ -30,11 +30,6 @@ spec:
operator: In
values:
- linux
- matchExpressions:
- key: beta.kubernetes.io/os
operator: In
values:
- linux
securityContext:
runAsNonRoot: true
runAsUser: 65534

View File

@ -119,3 +119,10 @@ rules:
verbs:
- list
- watch
- apiGroups:
- scheduling.k8s.io
resources:
- priorityclasses
verbs:
- list
- watch

View File

@ -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

View File

@ -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";

View File

@ -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>
</>
);

View File

@ -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",
},

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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 = [

View File

@ -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>
</>

View File

@ -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",
},

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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",
},

View File

@ -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);

View File

@ -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"]`);
};

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"./**/*",
]
}

View File

@ -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
View 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);

View File

@ -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", () => {

View File

@ -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 { }

View 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();
});
});

View File

@ -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);
});
});

View File

@ -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");
});
});
});

Some files were not shown because too many files have changed in this diff Show More