Merge branch 'master' into dependabot/npm_and_yarn/mock-fs-4.14.0
@ -24,12 +24,12 @@ jobs:
|
|||||||
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
|
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Set the tag name as an environment variable
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
displayName: Install Node.js
|
displayName: Install Node.js
|
||||||
|
|
||||||
- task: Cache@2
|
- task: Cache@2
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
|
||||||
@ -37,7 +37,7 @@ jobs:
|
|||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- bash: |
|
- bash: |
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
@ -47,13 +47,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
- script: make node_modules
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
|
||||||
- script: make build-npm
|
- script: make build-npm
|
||||||
displayName: Generate npm package
|
displayName: Generate npm package
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
@ -75,12 +75,12 @@ jobs:
|
|||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Set the tag name as an environment variable
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
displayName: Install Node.js
|
displayName: Install Node.js
|
||||||
|
|
||||||
- task: Cache@2
|
- task: Cache@2
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||||
@ -88,7 +88,7 @@ jobs:
|
|||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- bash: |
|
- bash: |
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
@ -98,13 +98,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
- script: make node_modules
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
|
||||||
- script: make build-npm
|
- script: make build-npm
|
||||||
displayName: Generate npm package
|
displayName: Generate npm package
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
@ -128,12 +128,12 @@ jobs:
|
|||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Set the tag name as an environment variable
|
displayName: Set the tag name as an environment variable
|
||||||
|
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(node_version)
|
versionSpec: $(node_version)
|
||||||
displayName: Install Node.js
|
displayName: Install Node.js
|
||||||
|
|
||||||
- task: Cache@2
|
- task: Cache@2
|
||||||
inputs:
|
inputs:
|
||||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||||
@ -141,7 +141,7 @@ jobs:
|
|||||||
yarn | "$(Agent.OS)"
|
yarn | "$(Agent.OS)"
|
||||||
path: $(YARN_CACHE_FOLDER)
|
path: $(YARN_CACHE_FOLDER)
|
||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
|
|
||||||
- bash: |
|
- bash: |
|
||||||
set -e
|
set -e
|
||||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||||
@ -151,13 +151,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||||
displayName: Customize config
|
displayName: Customize config
|
||||||
|
|
||||||
- script: make node_modules
|
- script: make node_modules
|
||||||
displayName: Install dependencies
|
displayName: Install dependencies
|
||||||
|
|
||||||
- script: make build-npm
|
- script: make build-npm
|
||||||
displayName: Generate npm package
|
displayName: Generate npm package
|
||||||
|
|
||||||
- bash: |
|
- bash: |
|
||||||
sudo chown root:root /
|
sudo chown root:root /
|
||||||
sudo apt-get update && sudo apt-get install -y snapd
|
sudo apt-get update && sudo apt-get install -y snapd
|
||||||
@ -168,7 +168,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SNAP_LOGIN: $(SNAP_LOGIN)
|
SNAP_LOGIN: $(SNAP_LOGIN)
|
||||||
displayName: Setup snapcraft
|
displayName: Setup snapcraft
|
||||||
|
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -129,6 +129,7 @@ module.exports = {
|
|||||||
"avoidEscape": true,
|
"avoidEscape": true,
|
||||||
"allowTemplateLiterals": true,
|
"allowTemplateLiterals": true,
|
||||||
}],
|
}],
|
||||||
|
"react/prop-types": "off",
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
@ -196,6 +197,7 @@ module.exports = {
|
|||||||
"avoidEscape": true,
|
"avoidEscape": true,
|
||||||
"allowTemplateLiterals": true,
|
"allowTemplateLiterals": true,
|
||||||
}],
|
}],
|
||||||
|
"react/prop-types": "off",
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
|
|||||||
11
.github/actions/add-card-to-project/Dockerfile
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Container image that runs your code
|
||||||
|
FROM alpine:3.10
|
||||||
|
|
||||||
|
RUN apk add --no-cache --no-progress curl jq
|
||||||
|
|
||||||
|
# Copies your code file from your action repository to the filesystem path `/` of the container
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Code file to execute when the docker container starts up (`entrypoint.sh`)
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
23
.github/actions/add-card-to-project/action.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: 'add_card_to_project'
|
||||||
|
description: 'A GitHub Action to add a card to a project and set the card position'
|
||||||
|
author: 'Steve Richards'
|
||||||
|
branding:
|
||||||
|
icon: 'command'
|
||||||
|
color: 'blue'
|
||||||
|
inputs:
|
||||||
|
project:
|
||||||
|
description: 'The url of the project to be assigned to.'
|
||||||
|
required: true
|
||||||
|
column_name:
|
||||||
|
description: 'The column name of the project, defaults to "To do" for issues and "In progress" for pull requests.'
|
||||||
|
required: false
|
||||||
|
card_position:
|
||||||
|
description: 'The card position of the card in the column, defaults to "bottom". Valid values are "top", "bottom" and "after:<card id>".'
|
||||||
|
required: false
|
||||||
|
runs:
|
||||||
|
using: 'docker'
|
||||||
|
image: 'Dockerfile'
|
||||||
|
args:
|
||||||
|
- ${{ inputs.project }}
|
||||||
|
- ${{ inputs.column_name }}
|
||||||
|
- ${{ inputs.card_position }}
|
||||||
157
.github/actions/add-card-to-project/entrypoint.sh
vendored
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
#!/bin/sh -l
|
||||||
|
|
||||||
|
PROJECT_URL="$INPUT_PROJECT"
|
||||||
|
if [ -z "$PROJECT_URL" ]; then
|
||||||
|
echo "PROJECT_URL is not defined." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
get_project_type() {
|
||||||
|
_PROJECT_URL="$1"
|
||||||
|
|
||||||
|
case "$_PROJECT_URL" in
|
||||||
|
https://github.com/orgs/*)
|
||||||
|
echo "org"
|
||||||
|
;;
|
||||||
|
https://github.com/users/*)
|
||||||
|
echo "user"
|
||||||
|
;;
|
||||||
|
https://github.com/*/projects/*)
|
||||||
|
echo "repo"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid PROJECT_URL: $_PROJECT_URL" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
unset _PROJECT_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
find_project_id() {
|
||||||
|
_PROJECT_TYPE="$1"
|
||||||
|
_PROJECT_URL="$2"
|
||||||
|
|
||||||
|
case "$_PROJECT_TYPE" in
|
||||||
|
org)
|
||||||
|
_ORG_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/orgs/\([^/]\+\)/projects/[0-9]\+@\1@')
|
||||||
|
_ENDPOINT="https://api.github.com/orgs/$_ORG_NAME/projects"
|
||||||
|
;;
|
||||||
|
user)
|
||||||
|
_USER_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/users/\([^/]\+\)/projects/[0-9]\+@\1@')
|
||||||
|
_ENDPOINT="https://api.github.com/users/$_USER_NAME/projects"
|
||||||
|
;;
|
||||||
|
repo)
|
||||||
|
_ENDPOINT="https://api.github.com/repos/$GITHUB_REPOSITORY/projects"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
_PROJECTS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
"$_ENDPOINT")
|
||||||
|
|
||||||
|
_PROJECTID=$(echo "$_PROJECTS" | jq -r ".[] | select(.html_url == \"$_PROJECT_URL\").id")
|
||||||
|
|
||||||
|
if [ "$_PROJECTID" != "" ]; then
|
||||||
|
echo "$_PROJECTID"
|
||||||
|
else
|
||||||
|
echo "No project was found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset _PROJECT_TYPE _PROJECT_URL _ORG_NAME _USER_NAME _ENDPOINT _PROJECTS _PROJECTID
|
||||||
|
}
|
||||||
|
|
||||||
|
find_column_id() {
|
||||||
|
_PROJECT_ID="$1"
|
||||||
|
_INITIAL_COLUMN_NAME="$2"
|
||||||
|
|
||||||
|
_COLUMNS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
"https://api.github.com/projects/$_PROJECT_ID/columns")
|
||||||
|
|
||||||
|
|
||||||
|
echo "$_COLUMNS" | jq -r ".[] | select(.name == \"$_INITIAL_COLUMN_NAME\").id"
|
||||||
|
unset _PROJECT_ID _INITIAL_COLUMN_NAME _COLUMNS
|
||||||
|
}
|
||||||
|
|
||||||
|
PROJECT_TYPE=$(get_project_type "${PROJECT_URL:?<Error> required this environment variable}")
|
||||||
|
|
||||||
|
if [ "$PROJECT_TYPE" = org ] || [ "$PROJECT_TYPE" = user ]; then
|
||||||
|
if [ -z "$MY_GITHUB_TOKEN" ]; then
|
||||||
|
echo "MY_GITHUB_TOKEN not defined" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN="$MY_GITHUB_TOKEN" # It's User's personal access token. It should be secret.
|
||||||
|
else
|
||||||
|
if [ -z "$GITHUB_TOKEN" ]; then
|
||||||
|
echo "GITHUB_TOKEN not defined" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN="$GITHUB_TOKEN" # GitHub sets. The scope in only the repository containing the workflow file.
|
||||||
|
fi
|
||||||
|
|
||||||
|
INITIAL_COLUMN_NAME="$INPUT_COLUMN_NAME"
|
||||||
|
if [ -z "$INITIAL_COLUMN_NAME" ]; then
|
||||||
|
# assing the column name by default
|
||||||
|
INITIAL_COLUMN_NAME='To do'
|
||||||
|
if [ "$GITHUB_EVENT_NAME" == "pull_request" ] || [ "$GITHUB_EVENT_NAME" == "pull_request_target" ]; then
|
||||||
|
echo "changing col name for PR event"
|
||||||
|
INITIAL_COLUMN_NAME='In progress'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ID=$(find_project_id "$PROJECT_TYPE" "$PROJECT_URL")
|
||||||
|
INITIAL_COLUMN_ID=$(find_column_id "$PROJECT_ID" "${INITIAL_COLUMN_NAME:?<Error> required this environment variable}")
|
||||||
|
|
||||||
|
if [ -z "$INITIAL_COLUMN_ID" ]; then
|
||||||
|
echo "INITIAL_COLUMN_ID is not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$GITHUB_EVENT_NAME" in
|
||||||
|
issues)
|
||||||
|
ISSUE_ID=$(jq -r '.issue.id' < "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
# Add this issue to the project column
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"content_type\": \"Issue\", \"content_id\": $ISSUE_ID}" \
|
||||||
|
"https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards")
|
||||||
|
;;
|
||||||
|
pull_request|pull_request_target)
|
||||||
|
PULL_REQUEST_ID=$(jq -r '.pull_request.id' < "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
# Add this pull_request to the project column
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"content_type\": \"PullRequest\", \"content_id\": $PULL_REQUEST_ID}" \
|
||||||
|
"https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Nothing to be done on this action: $GITHUB_EVENT_NAME" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CARDS_ID=$(echo "$_CARDS" | jq -r .id)
|
||||||
|
unset _CARDS
|
||||||
|
|
||||||
|
if [ -z "$CARDS_ID" ]; then
|
||||||
|
echo "CARDS_ID is not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INITIAL_CARD_POSITION="$INPUT_CARD_POSITION"
|
||||||
|
if [ -z "$INITIAL_CARD_POSITION" ]; then
|
||||||
|
# assign the card position by default
|
||||||
|
INITIAL_CARD_POSITION='bottom'
|
||||||
|
fi
|
||||||
|
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"position\": \"$INITIAL_CARD_POSITION\"}" \
|
||||||
|
"https://api.github.com/projects/columns/cards/$CARDS_ID/moves")
|
||||||
1
.github/release-drafter.yml
vendored
@ -10,6 +10,7 @@ categories:
|
|||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
labels:
|
labels:
|
||||||
- 'chore'
|
- 'chore'
|
||||||
|
- 'area/documentation'
|
||||||
- 'area/ci'
|
- 'area/ci'
|
||||||
- 'area/tests'
|
- 'area/tests'
|
||||||
- 'dependencies'
|
- 'dependencies'
|
||||||
|
|||||||
33
.github/workflows/add-to-project-board.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Add Card to Project(s)
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
env:
|
||||||
|
MY_GITHUB_TOKEN: ${{ secrets.PROJECT_GITHUB_TOKEN }}
|
||||||
|
EVENT_TYPE: $GITHUB_EVENT_NAME
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_card_to_project:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Add Card to Project(s)
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Get Event Type
|
||||||
|
run: echo $GITHUB_EVENT_NAME
|
||||||
|
- name: Assign NEW issues to project 1
|
||||||
|
uses: ./.github/actions/add-card-to-project
|
||||||
|
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||||
|
with:
|
||||||
|
project: 'https://github.com/orgs/lensapp/projects/1'
|
||||||
|
column_name: 'Backlog'
|
||||||
|
card_position: 'bottom'
|
||||||
|
- name: Assign NEW pull requests to project 1
|
||||||
|
uses: ./.github/actions/add-card-to-project
|
||||||
|
if: github.event_name == 'pull_request_target' && github.event.action == 'opened'
|
||||||
|
with:
|
||||||
|
project: 'https://github.com/orgs/lensapp/projects/1'
|
||||||
|
column_name: 'PRs'
|
||||||
|
card_position: 'bottom'
|
||||||
13
.github/workflows/test.yml
vendored
@ -77,15 +77,12 @@ jobs:
|
|||||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||||
name: Install integration test dependencies
|
name: Install integration test dependencies
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
|
||||||
|
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
|
||||||
name: Run Linux integration tests
|
name: Run Linux integration tests
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|
||||||
- run: make integration-win
|
- run: make integration
|
||||||
name: Run Windows integration tests
|
name: Run integration tests
|
||||||
shell: bash
|
shell: bash
|
||||||
if: runner.os == 'Windows'
|
if: runner.os != 'Linux'
|
||||||
|
|
||||||
- run: make integration-mac
|
|
||||||
name: Run macOS integration tests
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
|
|||||||
23
Makefile
@ -44,35 +44,24 @@ release-version:
|
|||||||
|
|
||||||
.PHONY: tag-release
|
.PHONY: tag-release
|
||||||
tag-release:
|
tag-release:
|
||||||
scripts/tag-release.sh
|
scripts/tag-release.sh $(CMD_ARGS)
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: binaries/client
|
test: binaries/client
|
||||||
yarn run jest $(or $(CMD_ARGS), "src")
|
yarn run jest $(or $(CMD_ARGS), "src")
|
||||||
|
|
||||||
.PHONY: integration-linux
|
.PHONY: integration
|
||||||
integration-linux: binaries/client build-extension-types build-extensions
|
integration: build
|
||||||
yarn build:linux
|
|
||||||
yarn integration
|
|
||||||
|
|
||||||
.PHONY: integration-mac
|
|
||||||
integration-mac: binaries/client build-extension-types build-extensions
|
|
||||||
# rm ${HOME}/Library/Application\ Support/Lens
|
|
||||||
yarn build:mac
|
|
||||||
yarn integration
|
|
||||||
|
|
||||||
.PHONY: integration-win
|
|
||||||
integration-win: binaries/client build-extension-types build-extensions
|
|
||||||
# rm %APPDATA%/Lens
|
|
||||||
yarn build:win
|
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: node_modules binaries/client
|
build: node_modules binaries/client
|
||||||
yarn run npm:fix-build-version
|
yarn run npm:fix-build-version
|
||||||
$(MAKE) build-extensions
|
$(MAKE) build-extensions -B
|
||||||
yarn run compile
|
yarn run compile
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
|
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||||
|
rm -rf node_modules/win-ca/pem
|
||||||
yarn run electron-builder --publish onTag --x64 --ia32
|
yarn run electron-builder --publish onTag --x64 --ia32
|
||||||
else
|
else
|
||||||
yarn run electron-builder --publish onTag
|
yarn run electron-builder --publish onTag
|
||||||
|
|||||||
@ -25,20 +25,20 @@ import fs from "fs-extra";
|
|||||||
|
|
||||||
export async function generateTrayIcon(
|
export async function generateTrayIcon(
|
||||||
{
|
{
|
||||||
outputFilename = "tray_icon", // e.g. output tray_icon_dark@2x.png
|
outputFilename = "trayIcon",
|
||||||
svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"),
|
svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"),
|
||||||
outputFolder = path.resolve(__dirname, "./tray"),
|
outputFolder = path.resolve(__dirname, "./tray"),
|
||||||
dpiSuffix = "2x",
|
dpiSuffix = "2x",
|
||||||
pixelSize = 32,
|
pixelSize = 32,
|
||||||
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
|
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
|
||||||
} = {}) {
|
} = {}) {
|
||||||
outputFilename += shouldUseDarkColors ? "_dark" : "";
|
outputFilename += `${shouldUseDarkColors ? "Dark" : ""}Template`; // e.g. output trayIconDarkTemplate@2x.png
|
||||||
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
||||||
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Modify .SVG colors
|
// Modify .SVG colors
|
||||||
const trayIconColor = shouldUseDarkColors ? "white" : "black";
|
const trayIconColor = shouldUseDarkColors ? "black" : "white";
|
||||||
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
||||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
||||||
|
|
||||||
|
|||||||
@ -29,9 +29,6 @@ const versionInfo = semver.parse(appInfo.version);
|
|||||||
const buildNumber = process.env.BUILD_NUMBER || Date.now().toString();
|
const buildNumber = process.env.BUILD_NUMBER || Date.now().toString();
|
||||||
|
|
||||||
function getBuildChannel(): string {
|
function getBuildChannel(): string {
|
||||||
/**
|
|
||||||
* Note: it is by design that we don't use `rc` as a build channel for these versions
|
|
||||||
*/
|
|
||||||
switch (versionInfo.prerelease?.[0]) {
|
switch (versionInfo.prerelease?.[0]) {
|
||||||
case "beta":
|
case "beta":
|
||||||
return "beta";
|
return "beta";
|
||||||
@ -64,7 +61,7 @@ async function writeOutNewVersions() {
|
|||||||
function main() {
|
function main() {
|
||||||
const prereleaseParts: string[] = [getBuildChannel()];
|
const prereleaseParts: string[] = [getBuildChannel()];
|
||||||
|
|
||||||
if (versionInfo.prerelease) {
|
if (versionInfo.prerelease && versionInfo.prerelease.length > 1) {
|
||||||
prereleaseParts.push(versionInfo.prerelease[1].toString());
|
prereleaseParts.push(versionInfo.prerelease[1].toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
build/tray/trayIconDarkTemplate.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
build/tray/trayIconDarkTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 925 B |
BIN
build/tray/trayIconDarkTemplate@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 478 B After Width: | Height: | Size: 460 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
@ -51,4 +51,4 @@ To provide feedback on the documentation or issues with the Lens Extension API,
|
|||||||
|
|
||||||
## Downloading Lens
|
## Downloading Lens
|
||||||
|
|
||||||
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.
|
[Download Lens](https://k8slens.dev/) for macOS, Windows, or Linux.
|
||||||
|
|||||||
@ -78,7 +78,7 @@ npm run dev
|
|||||||
You must restart Lens for the extension to load.
|
You must restart Lens for the extension to load.
|
||||||
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
|
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
|
||||||
|
|
||||||
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/latest/clusters/adding-clusters) for details on how to add a cluster in Lens IDE.
|
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE.
|
||||||
You will see the "Hello World" page in the left-side cluster menu.
|
You will see the "Hello World" page in the left-side cluster menu.
|
||||||
|
|
||||||
## Develop the Extension
|
## Develop the Extension
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 712 KiB |
@ -60,7 +60,7 @@ Implementing `onDeactivate()` gives you the opportunity to clean up after your e
|
|||||||
|
|
||||||
1. Navigate to **File** > **Extensions** in the top menu bar.
|
1. Navigate to **File** > **Extensions** in the top menu bar.
|
||||||
(On Mac, it is **Lens** > **Extensions**.)
|
(On Mac, it is **Lens** > **Extensions**.)
|
||||||
2. Click **Disable** on the extension you want to disable.
|
2. For the extension you want to disable, open the context menu (click on the three vertical dots) and choose **Disable**.
|
||||||
|
|
||||||
The example above logs messages when the extension is enabled and disabled.
|
The example above logs messages when the extension is enabled and disabled.
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ Use cluster pages to display information about or add functionality to the activ
|
|||||||
It is also possible to include custom details from other clusters.
|
It is also possible to include custom details from other clusters.
|
||||||
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
|
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
|
||||||
|
|
||||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
Add a cluster page definition to a `Renderer.LensExtension` subclass with the following example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
@ -124,7 +124,7 @@ Use `clusterPageMenus`, covered in the next section, to add cluster pages to the
|
|||||||
|
|
||||||
### `clusterPageMenus`
|
### `clusterPageMenus`
|
||||||
|
|
||||||
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav.
|
`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav, below Lens's standard cluster menus like **Workloads**, **Custom Resources**, etc.
|
||||||
|
|
||||||
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
||||||
|
|
||||||
@ -266,18 +266,18 @@ Activating this menu item toggles on and off the appearance of the submenu below
|
|||||||
The remaining two cluster page menu objects define the contents of the submenu.
|
The remaining two cluster page menu objects define the contents of the submenu.
|
||||||
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case.
|
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case.
|
||||||
|
|
||||||
This is what the example will look like, including how the menu item will appear in the secondary left nav:
|
This is what the example could look like, including how the menu item will appear in the secondary left nav:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### `globalPages`
|
### `globalPages`
|
||||||
|
|
||||||
Global pages are independent of the cluster dashboard and can fill the entire Lens UI.
|
Global pages are independent of the cluster dashboard and can fill the entire Lens UI.
|
||||||
Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
|
Their primary use is to display information and provide functionality across clusters (or catalog entities), including customized data and functionality unique to your extension.
|
||||||
|
|
||||||
Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page.
|
Unlike cluster pages, users can trigger global pages even when there is no active cluster (or catalog entity).
|
||||||
You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems).
|
|
||||||
Unlike cluster pages, users can trigger global pages even when there is no active cluster.
|
|
||||||
|
|
||||||
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
The following example defines a `Renderer.LensExtension` subclass with a single global page definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
@ -326,11 +326,11 @@ This allows the `HelpExtension` object to be passed in the global page definitio
|
|||||||
This way, `HelpPage` can access all `HelpExtension` subclass data.
|
This way, `HelpPage` can access all `HelpExtension` subclass data.
|
||||||
|
|
||||||
This example code shows how to create a global page, but not how to make that page available to the Lens user.
|
This example code shows how to create a global page, but not how to make that page available to the Lens user.
|
||||||
Global pages can be made available in the following ways:
|
Global pages are typically made available in the following ways:
|
||||||
|
|
||||||
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
|
||||||
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
|
||||||
* To add global pages to the left side menu, see [`globalPageMenus`](#globalpagemenus).
|
* To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
|
||||||
|
|
||||||
### `welcomeMenus`
|
### `welcomeMenus`
|
||||||
### `appPreferences`
|
### `appPreferences`
|
||||||
@ -381,6 +381,7 @@ In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExampleP
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Renderer } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
import { makeObservable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@ -399,6 +400,11 @@ export class ExamplePreferenceProps {
|
|||||||
@observer
|
@observer
|
||||||
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super({preference: { enabled: false}});
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { preference } = this.props;
|
const { preference } = this.props;
|
||||||
return (
|
return (
|
||||||
@ -432,7 +438,7 @@ It is used to indicate the state of the preference, and it is bound to the check
|
|||||||
|
|
||||||
`ExamplePreferenceHint` is a simple text span.
|
`ExamplePreferenceHint` is a simple text span.
|
||||||
|
|
||||||
The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
The above example introduces the decorators `makeObservable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
|
||||||
`mobx` simplifies state management.
|
`mobx` simplifies state management.
|
||||||
Without it, this example would not visually update the checkbox properly when the user activates it.
|
Without it, this example would not visually update the checkbox properly when the user activates it.
|
||||||
[Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements.
|
[Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements.
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "kube-object-event-status",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Adds kube object status from events",
|
|
||||||
"renderer": "dist/renderer.js",
|
|
||||||
"lens": {
|
|
||||||
"metadata": {},
|
|
||||||
"styles": []
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack && npm pack",
|
|
||||||
"dev": "webpack --watch",
|
|
||||||
"test": "echo NO TESTS"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**/*"
|
|
||||||
],
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
|
||||||
"ts-loader": "^8.0.4",
|
|
||||||
"typescript": "^4.0.3",
|
|
||||||
"webpack": "^4.44.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -281,7 +281,9 @@ export class MetricsSettings extends React.Component<Props> {
|
|||||||
waiting={this.inProgress}
|
waiting={this.inProgress}
|
||||||
onClick={() => this.save()}
|
onClick={() => this.save()}
|
||||||
primary
|
primary
|
||||||
disabled={!this.changed} />
|
disabled={!this.changed}
|
||||||
|
className="w-60 h-14"
|
||||||
|
/>
|
||||||
|
|
||||||
{this.canUpgrade && (<small className="hint">
|
{this.canUpgrade && (<small className="hint">
|
||||||
An update is available for enabled metrics components.
|
An update is available for enabled metrics components.
|
||||||
|
|||||||
@ -86,23 +86,26 @@ export function NodeMenu(props: NodeMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={shell}>
|
<MenuItem onClick={shell}>
|
||||||
<Icon svg="ssh" interactive={toolbar} title="Node shell"/>
|
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/>
|
||||||
<span className="title">Shell</span>
|
<span className="title">Shell</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!node.isUnschedulable() && (
|
{
|
||||||
<MenuItem onClick={cordon}>
|
node.isUnschedulable()
|
||||||
<Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/>
|
? (
|
||||||
<span className="title">Cordon</span>
|
<MenuItem onClick={unCordon}>
|
||||||
</MenuItem>
|
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} />
|
||||||
)}
|
<span className="title">Uncordon</span>
|
||||||
{node.isUnschedulable() && (
|
</MenuItem>
|
||||||
<MenuItem onClick={unCordon}>
|
)
|
||||||
<Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/>
|
: (
|
||||||
<span className="title">Uncordon</span>
|
<MenuItem onClick={cordon}>
|
||||||
</MenuItem>
|
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} />
|
||||||
)}
|
<span className="title">Cordon</span>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
<MenuItem onClick={drain}>
|
<MenuItem onClick={drain}>
|
||||||
<Icon material="delete_sweep" title="Drain" interactive={toolbar}/>
|
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/>
|
||||||
<span className="title">Drain</span>
|
<span className="title">Drain</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
||||||
<Icon material="pageview" interactive={toolbar} title="Attach to Pod"/>
|
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/>
|
||||||
<span className="title">Attach Pod</span>
|
<span className="title">Attach Pod</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
<Icon material="subject" title="Logs" interactive={toolbar}/>
|
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/>
|
||||||
<span className="title">Logs</span>
|
<span className="title">Logs</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
<Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} />
|
||||||
<span className="title">Shell</span>
|
<span className="title">Shell</span>
|
||||||
{containers.length > 1 && (
|
{containers.length > 1 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -41,40 +41,38 @@ describe("Lens cluster pages", () => {
|
|||||||
const BACKSPACE = "\uE003";
|
const BACKSPACE = "\uE003";
|
||||||
let app: Application;
|
let app: Application;
|
||||||
const ready = minikubeReady(TEST_NAMESPACE);
|
const ready = minikubeReady(TEST_NAMESPACE);
|
||||||
|
let clusterAdded = false;
|
||||||
|
|
||||||
utils.describeIf(ready)("test common pages", () => {
|
utils.describeIf(ready)("test common pages", () => {
|
||||||
let clusterAdded = false;
|
|
||||||
const addCluster = async () => {
|
const addCluster = async () => {
|
||||||
await app.client.waitUntilTextExists("div", "Catalog");
|
|
||||||
await waitForMinikubeDashboard(app);
|
await waitForMinikubeDashboard(app);
|
||||||
await app.client.click('a[href="/nodes"]');
|
await app.client.click('a[href="/nodes"]');
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
await app.client.waitUntilTextExists("div.TableCell", "Ready");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appStartAddCluster = async () => {
|
||||||
|
app = await utils.appStart();
|
||||||
|
await addCluster();
|
||||||
|
clusterAdded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tearDown = async () => {
|
||||||
|
await utils.tearDown(app);
|
||||||
|
clusterAdded = false;
|
||||||
|
};
|
||||||
|
|
||||||
describe("cluster add", () => {
|
describe("cluster add", () => {
|
||||||
utils.beforeAllWrapped(async () => {
|
utils.beforeAllWrapped(async () => {
|
||||||
app = await utils.appStart();
|
app = await utils.appStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.afterAllWrapped(async () => {
|
utils.afterAllWrapped(tearDown);
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows to add a cluster", async () => {
|
it("allows to add a cluster", async () => {
|
||||||
await addCluster();
|
await addCluster();
|
||||||
clusterAdded = true;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStartAddCluster = async () => {
|
|
||||||
if (clusterAdded) {
|
|
||||||
app = await utils.appStart();
|
|
||||||
await addCluster();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSidebarSelectors(itemId: string) {
|
function getSidebarSelectors(itemId: string) {
|
||||||
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
const root = `.SidebarItem[data-test-id="${itemId}"]`;
|
||||||
|
|
||||||
@ -86,12 +84,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("cluster pages", () => {
|
describe("cluster pages", () => {
|
||||||
utils.beforeAllWrapped(appStartAddCluster);
|
utils.beforeAllWrapped(appStartAddCluster);
|
||||||
|
utils.afterAllWrapped(tearDown);
|
||||||
utils.afterAllWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tests: {
|
const tests: {
|
||||||
drawer?: string
|
drawer?: string
|
||||||
@ -376,12 +369,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("viewing pod logs", () => {
|
describe("viewing pod logs", () => {
|
||||||
utils.beforeEachWrapped(appStartAddCluster);
|
utils.beforeEachWrapped(appStartAddCluster);
|
||||||
|
utils.afterEachWrapped(tearDown);
|
||||||
utils.afterEachWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`shows a log for a pod`, async () => {
|
it(`shows a log for a pod`, async () => {
|
||||||
expect(clusterAdded).toBe(true);
|
expect(clusterAdded).toBe(true);
|
||||||
@ -409,8 +397,12 @@ describe("Lens cluster pages", () => {
|
|||||||
// Open logs tab in dock
|
// Open logs tab in dock
|
||||||
await app.client.click(".list .TableRow:first-child");
|
await app.client.click(".list .TableRow:first-child");
|
||||||
await app.client.waitForVisible(".Drawer");
|
await app.client.waitForVisible(".Drawer");
|
||||||
await app.client.waitForVisible(`ul.KubeObjectMenu li.MenuItem i[title="Logs"]`);
|
|
||||||
await app.client.click("ul.KubeObjectMenu li.MenuItem i[title='Logs']");
|
const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']";
|
||||||
|
|
||||||
|
await app.client.waitForVisible(logsButton);
|
||||||
|
await app.client.click(logsButton);
|
||||||
|
|
||||||
// Check if controls are available
|
// Check if controls are available
|
||||||
await app.client.waitForVisible(".LogList .VirtualList");
|
await app.client.waitForVisible(".LogList .VirtualList");
|
||||||
await app.client.waitForVisible(".LogResourceSelector");
|
await app.client.waitForVisible(".LogResourceSelector");
|
||||||
@ -427,12 +419,7 @@ describe("Lens cluster pages", () => {
|
|||||||
|
|
||||||
describe("cluster operations", () => {
|
describe("cluster operations", () => {
|
||||||
utils.beforeEachWrapped(appStartAddCluster);
|
utils.beforeEachWrapped(appStartAddCluster);
|
||||||
|
utils.afterEachWrapped(tearDown);
|
||||||
utils.afterEachWrapped(async () => {
|
|
||||||
if (app?.isRunning()) {
|
|
||||||
return utils.tearDown(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows default namespace", async () => {
|
it("shows default namespace", async () => {
|
||||||
expect(clusterAdded).toBe(true);
|
expect(clusterAdded).toBe(true);
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export async function waitForMinikubeDashboard(app: Application) {
|
|||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
await app.client.click("div.TableRow");
|
await app.client.click("div.TableRow");
|
||||||
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
||||||
|
await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
|
|||||||
@ -37,27 +37,27 @@ interface DoneCallback {
|
|||||||
* This is necessary because Jest doesn't do this correctly.
|
* This is necessary because Jest doesn't do this correctly.
|
||||||
* @param fn The function to call
|
* @param fn The function to call
|
||||||
*/
|
*/
|
||||||
export function wrapJestLifecycle(fn: () => Promise<void>): (done: DoneCallback) => void {
|
export function wrapJestLifecycle(fn: () => Promise<void> | void): (done: DoneCallback) => void {
|
||||||
return function (done: DoneCallback) {
|
return function (done: DoneCallback) {
|
||||||
fn()
|
(async () => fn())()
|
||||||
.then(() => done())
|
.then(() => done())
|
||||||
.catch(error => done.fail(error));
|
.catch(error => done.fail(error));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeAllWrapped(fn: () => Promise<void>): void {
|
export function beforeAllWrapped(fn: () => Promise<void> | void): void {
|
||||||
beforeAll(wrapJestLifecycle(fn));
|
beforeAll(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeEachWrapped(fn: () => Promise<void>): void {
|
export function beforeEachWrapped(fn: () => Promise<void> | void): void {
|
||||||
beforeEach(wrapJestLifecycle(fn));
|
beforeEach(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function afterAllWrapped(fn: () => Promise<void>): void {
|
export function afterAllWrapped(fn: () => Promise<void> | void): void {
|
||||||
afterAll(wrapJestLifecycle(fn));
|
afterAll(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function afterEachWrapped(fn: () => Promise<void>): void {
|
export function afterEachWrapped(fn: () => Promise<void> | void): void {
|
||||||
afterEach(wrapJestLifecycle(fn));
|
afterEach(wrapJestLifecycle(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,13 +99,17 @@ export async function appStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showCatalog(app: Application) {
|
export async function showCatalog(app: Application) {
|
||||||
await app.client.waitUntilTextExists("[data-test-id=catalog-link]", "Catalog");
|
await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon");
|
||||||
await app.client.click("[data-test-id=catalog-link]");
|
await app.client.click("#hotbarIcon-catalog-entity .Icon");
|
||||||
}
|
}
|
||||||
|
|
||||||
type AsyncPidGetter = () => Promise<number>;
|
type AsyncPidGetter = () => Promise<number>;
|
||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app?: Application) {
|
||||||
|
if (!app?.isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
||||||
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
|
|||||||
36
package.json
@ -3,12 +3,13 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.0-beta.11",
|
"version": "5.1.0",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "OpenLens Authors"
|
"name": "OpenLens Authors",
|
||||||
|
"email": "info@k8slens.dev"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
|
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||||
@ -48,7 +49,8 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.18.15",
|
"bundledKubectlVersion": "1.18.15",
|
||||||
"bundledHelmVersion": "3.5.4"
|
"bundledHelmVersion": "3.5.4",
|
||||||
|
"sentryDsn": ""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12 <13"
|
"node": ">=12 <13"
|
||||||
@ -61,8 +63,7 @@
|
|||||||
},
|
},
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||||
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
|
||||||
},
|
},
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/dist",
|
"<rootDir>/dist",
|
||||||
@ -182,6 +183,8 @@
|
|||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.14.3",
|
"@kubernetes/client-node": "^0.14.3",
|
||||||
|
"@sentry/electron": "^2.5.0",
|
||||||
|
"@sentry/integrations": "^6.8.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"array-move": "^3.0.1",
|
"array-move": "^3.0.1",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
@ -197,7 +200,6 @@
|
|||||||
"electron-updater": "^4.3.1",
|
"electron-updater": "^4.3.1",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"filehound": "^1.17.4",
|
"filehound": "^1.17.4",
|
||||||
"filenamify": "^4.1.0",
|
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@ -250,6 +252,8 @@
|
|||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
|
"@sentry/react": "^6.8.0",
|
||||||
|
"@sentry/types": "^6.8.0",
|
||||||
"@testing-library/jest-dom": "^5.13.0",
|
"@testing-library/jest-dom": "^5.13.0",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@types/byline": "^4.2.32",
|
"@types/byline": "^4.2.32",
|
||||||
@ -273,7 +277,7 @@
|
|||||||
"@types/marked": "^2.0.3",
|
"@types/marked": "^2.0.3",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.10.0",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/module-alias": "^2.0.0",
|
"@types/module-alias": "^2.0.0",
|
||||||
"@types/node": "12.20",
|
"@types/node": "12.20",
|
||||||
"@types/npm": "^2.0.31",
|
"@types/npm": "^2.0.31",
|
||||||
@ -315,26 +319,28 @@
|
|||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"css-loader": "^5.2.6",
|
"css-loader": "^5.2.6",
|
||||||
"deepdash": "^5.3.5",
|
"deepdash": "^5.3.5",
|
||||||
"dompurify": "^2.0.11",
|
"dompurify": "^2.0.17",
|
||||||
"electron": "^9.4.4",
|
"electron": "^9.4.4",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
|
"esbuild": "^0.12.12",
|
||||||
|
"esbuild-loader": "^2.13.1",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-plugin-unused-imports": "^1.0.1",
|
"eslint-plugin-unused-imports": "^1.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"flex.box": "^3.4.4",
|
"flex.box": "^3.4.4",
|
||||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^4.5.2",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"include-media": "^1.4.9",
|
"include-media": "^1.4.9",
|
||||||
"jest": "^26.0.1",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.16",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^1.6.0",
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^1.0.3",
|
||||||
@ -347,7 +353,7 @@
|
|||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"raw-loader": "^4.0.1",
|
"raw-loader": "^4.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
@ -377,6 +383,6 @@
|
|||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"xterm": "^4.12.0",
|
"xterm": "^4.12.0",
|
||||||
"xterm-addon-fit": "^0.4.0"
|
"xterm-addon-fit": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
key="$1"
|
||||||
|
|
||||||
|
case $key in
|
||||||
|
-f|--force)
|
||||||
|
FORCE="--force"
|
||||||
|
shift # past argument
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
if [[ `git branch --show-current` =~ ^release/v ]]
|
if [[ `git branch --show-current` =~ ^release/v ]]
|
||||||
then
|
then
|
||||||
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
|
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
|
||||||
git tag ${VERSION_STRING}
|
git tag ${VERSION_STRING} ${FORCE}
|
||||||
git push ${GIT_REMOTE:-origin} ${VERSION_STRING}
|
git push ${GIT_REMOTE:-origin} ${VERSION_STRING} ${FORCE}
|
||||||
else
|
else
|
||||||
echo "You must be in a release branch"
|
echo "You must be in a release branch"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -95,7 +95,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
await ClusterStore.createInstance().load();
|
ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -203,7 +203,7 @@ describe("config with existing clusters", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -285,7 +285,7 @@ users:
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -344,7 +344,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -414,7 +414,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -456,7 +456,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -495,7 +495,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -531,7 +531,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return ClusterStore.createInstance().load();
|
return ClusterStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import mockFs from "mock-fs";
|
|||||||
import { ClusterStore } from "../cluster-store";
|
import { ClusterStore } from "../cluster-store";
|
||||||
import { HotbarStore } from "../hotbar-store";
|
import { HotbarStore } from "../hotbar-store";
|
||||||
|
|
||||||
jest.mock("../../renderer/api/catalog-entity-registry", () => ({
|
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
||||||
catalogEntityRegistry: {
|
catalogEntityRegistry: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -39,7 +39,14 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({
|
|||||||
name: "my_shiny_cluster",
|
name: "my_shiny_cluster",
|
||||||
source: "remote"
|
source: "remote"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
uid: "catalog-entity",
|
||||||
|
name: "Catalog",
|
||||||
|
source: "app"
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -120,29 +127,31 @@ jest.mock("electron", () => {
|
|||||||
|
|
||||||
describe("HotbarStore", () => {
|
describe("HotbarStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ClusterStore.resetInstance();
|
mockFs({
|
||||||
|
"tmp": {
|
||||||
|
"lens-hotbar-store.json": JSON.stringify({})
|
||||||
|
}
|
||||||
|
});
|
||||||
ClusterStore.createInstance();
|
ClusterStore.createInstance();
|
||||||
|
HotbarStore.createInstance();
|
||||||
HotbarStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
ClusterStore.resetInstance();
|
||||||
|
HotbarStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("load", () => {
|
describe("load", () => {
|
||||||
it("loads one hotbar by default", () => {
|
it("loads one hotbar by default", () => {
|
||||||
HotbarStore.createInstance().load();
|
|
||||||
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("add", () => {
|
describe("add", () => {
|
||||||
it("adds a hotbar", () => {
|
it("adds a hotbar", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.add({ name: "hottest" });
|
hotbarStore.add({ name: "hottest" });
|
||||||
expect(hotbarStore.hotbars.length).toEqual(2);
|
expect(hotbarStore.hotbars.length).toEqual(2);
|
||||||
});
|
});
|
||||||
@ -150,106 +159,104 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
describe("hotbar items", () => {
|
describe("hotbar items", () => {
|
||||||
it("initially creates 12 empty cells", () => {
|
it("initially creates 12 empty cells", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
expect(hotbarStore.getActive().items.length).toEqual(12);
|
expect(hotbarStore.getActive().items.length).toEqual(12);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds items", () => {
|
it("initially adds catalog entity as first item", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
|
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds items", () => {
|
||||||
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes items", () => {
|
it("removes items", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.removeFromHotbar("test");
|
hotbarStore.removeFromHotbar("test");
|
||||||
|
hotbarStore.removeFromHotbar("catalog-entity");
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(0);
|
expect(items.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if removing with invalid uid", () => {
|
it("does nothing if removing with invalid uid", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.removeFromHotbar("invalid uid");
|
hotbarStore.removeFromHotbar("invalid uid");
|
||||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||||
|
|
||||||
expect(items.length).toEqual(1);
|
expect(items.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves item to empty cell", () => {
|
it("moves item to empty cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[5]).toBeNull();
|
expect(hotbarStore.getActive().items[6]).toBeNull();
|
||||||
|
|
||||||
hotbarStore.restackItems(1, 5);
|
hotbarStore.restackItems(1, 5);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
||||||
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("minikube");
|
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves items down", () => {
|
it("moves items down", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
// aws -> test
|
// aws -> catalog
|
||||||
hotbarStore.restackItems(2, 0);
|
hotbarStore.restackItems(3, 0);
|
||||||
|
|
||||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||||
|
|
||||||
expect(items.slice(0, 4)).toEqual(["aws", "test", "minikube", null]);
|
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves items up", () => {
|
it("moves items up", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(minikubeCluster);
|
hotbarStore.addToHotbar(minikubeCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
|
|
||||||
// test -> aws
|
// test -> aws
|
||||||
hotbarStore.restackItems(0, 2);
|
hotbarStore.restackItems(1, 3);
|
||||||
|
|
||||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||||
|
|
||||||
expect(items.slice(0, 4)).toEqual(["minikube", "aws", "test", null]);
|
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing when item moved to same cell", () => {
|
it("does nothing when item moved to same cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.restackItems(0, 0);
|
hotbarStore.restackItems(1, 1);
|
||||||
|
|
||||||
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
|
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("new items takes first empty cell", () => {
|
it("new items takes first empty cell", () => {
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
hotbarStore.addToHotbar(awsCluster);
|
hotbarStore.addToHotbar(awsCluster);
|
||||||
hotbarStore.restackItems(0, 3);
|
hotbarStore.restackItems(0, 3);
|
||||||
@ -260,13 +267,13 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
it("throws if invalid arguments provided", () => {
|
it("throws if invalid arguments provided", () => {
|
||||||
// Prevent writing to stderr during this render.
|
// Prevent writing to stderr during this render.
|
||||||
const err = console.error;
|
const { error, warn } = console;
|
||||||
|
|
||||||
console.error = jest.fn();
|
console.error = jest.fn();
|
||||||
|
console.warn = jest.fn();
|
||||||
|
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.load();
|
|
||||||
hotbarStore.addToHotbar(testCluster);
|
hotbarStore.addToHotbar(testCluster);
|
||||||
|
|
||||||
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
||||||
@ -275,7 +282,8 @@ describe("HotbarStore", () => {
|
|||||||
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
|
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
|
||||||
|
|
||||||
// Restore writing to stderr.
|
// Restore writing to stderr.
|
||||||
console.error = err;
|
console.error = error;
|
||||||
|
console.warn = warn;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -346,7 +354,7 @@ describe("HotbarStore", () => {
|
|||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
return HotbarStore.createInstance().load();
|
HotbarStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -19,10 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Console } from "console";
|
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr);
|
|
||||||
|
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("electron", () => {
|
jest.mock("electron", () => {
|
||||||
@ -37,26 +33,27 @@ jest.mock("electron", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { UserStore } from "../user-store";
|
import { UserStore } from "../user-store";
|
||||||
|
import { Console } from "console";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||||
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
|
import type { ClusterStoreModel } from "../cluster-store";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
describe("user store tests", () => {
|
describe("user store tests", () => {
|
||||||
describe("for an empty config", () => {
|
describe("for an empty config", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||||
|
|
||||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
return UserStore.getInstance().load();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
|
UserStore.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||||
@ -72,7 +69,7 @@ describe("user store tests", () => {
|
|||||||
us.httpsProxy = "abcd://defg";
|
us.httpsProxy = "abcd://defg";
|
||||||
|
|
||||||
expect(us.httpsProxy).toBe("abcd://defg");
|
expect(us.httpsProxy).toBe("abcd://defg");
|
||||||
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||||
|
|
||||||
us.colorTheme = "light";
|
us.colorTheme = "light";
|
||||||
expect(us.colorTheme).toBe("light");
|
expect(us.colorTheme).toBe("light");
|
||||||
@ -81,11 +78,9 @@ describe("user store tests", () => {
|
|||||||
it("correctly resets theme to default value", async () => {
|
it("correctly resets theme to default value", async () => {
|
||||||
const us = UserStore.getInstance();
|
const us = UserStore.getInstance();
|
||||||
|
|
||||||
us.isLoaded = true;
|
|
||||||
|
|
||||||
us.colorTheme = "some other theme";
|
us.colorTheme = "some other theme";
|
||||||
await us.resetTheme();
|
us.resetTheme();
|
||||||
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly calculates if the last seen version is an old release", () => {
|
it("correctly calculates if the last seen version is an old release", () => {
|
||||||
@ -100,21 +95,39 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
describe("migrations", () => {
|
describe("migrations", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"config.json": JSON.stringify({
|
"config.json": JSON.stringify({
|
||||||
user: { username: "foobar" },
|
user: { username: "foobar" },
|
||||||
preferences: { colorTheme: "light" },
|
preferences: { colorTheme: "light" },
|
||||||
lastSeenAppVersion: "1.2.3"
|
lastSeenAppVersion: "1.2.3"
|
||||||
})
|
}),
|
||||||
|
"lens-cluster-store.json": JSON.stringify({
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
id: "foobar",
|
||||||
|
kubeConfigPath: "tmp/extension_data/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "barfoo",
|
||||||
|
kubeConfigPath: "some/other/path",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} as ClusterStoreModel),
|
||||||
|
"extension_data": {},
|
||||||
|
},
|
||||||
|
"some": {
|
||||||
|
"other": {
|
||||||
|
"path": "is file",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return UserStore.createInstance().load();
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,5 +136,12 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
|
||||||
|
const us = UserStore.getInstance();
|
||||||
|
|
||||||
|
expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false);
|
||||||
|
expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,41 +23,48 @@ import path from "path";
|
|||||||
import Config from "conf";
|
import Config from "conf";
|
||||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||||
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
||||||
import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
|
||||||
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
export interface BaseStoreParams<T> extends ConfOptions<T> {
|
||||||
autoLoad?: boolean;
|
|
||||||
syncEnabled?: boolean;
|
|
||||||
syncOptions?: IReactionOptions;
|
syncOptions?: IReactionOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: T should only contain base JSON serializable types.
|
* Note: T should only contain base JSON serializable types.
|
||||||
*/
|
*/
|
||||||
export abstract class BaseStore<T = any> extends Singleton {
|
export abstract class BaseStore<T> extends Singleton {
|
||||||
protected storeConfig?: Config<T>;
|
protected storeConfig?: Config<T>;
|
||||||
protected syncDisposers: Disposer[] = [];
|
protected syncDisposers: Disposer[] = [];
|
||||||
|
|
||||||
@observable isLoaded = false;
|
protected constructor(protected params: BaseStoreParams<T>) {
|
||||||
|
|
||||||
get whenLoaded() {
|
|
||||||
return when(() => this.isLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(protected params: BaseStoreParams) {
|
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.params = {
|
/**
|
||||||
autoLoad: false,
|
* This must be called after the last child's constructor is finished (or just before it finishes)
|
||||||
syncEnabled: true,
|
*/
|
||||||
...params,
|
load() {
|
||||||
};
|
this.storeConfig = new Config({
|
||||||
this.init();
|
...this.params,
|
||||||
|
projectName: "lens",
|
||||||
|
projectVersion: getAppVersion(),
|
||||||
|
cwd: this.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res: any = this.fromStore(this.storeConfig.store);
|
||||||
|
|
||||||
|
if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) {
|
||||||
|
console.error(`${this.name} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enableSync();
|
||||||
|
|
||||||
|
logger.info(`[STORE]: LOADED from ${this.path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -76,31 +83,6 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
return this.storeConfig?.path || "";
|
return this.storeConfig?.path || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
|
||||||
if (this.params.autoLoad) {
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
|
||||||
await this.whenLoaded;
|
|
||||||
this.enableSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
|
||||||
|
|
||||||
this.storeConfig = new Config({
|
|
||||||
...confOptions,
|
|
||||||
projectName: "lens",
|
|
||||||
projectVersion: getAppVersion(),
|
|
||||||
cwd: this.cwd(),
|
|
||||||
});
|
|
||||||
logger.info(`[STORE]: LOADED from ${this.path}`);
|
|
||||||
this.fromStore(this.storeConfig.store);
|
|
||||||
this.isLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected cwd() {
|
protected cwd() {
|
||||||
return (app || remote.app).getPath("userData");
|
return (app || remote.app).getPath("userData");
|
||||||
}
|
}
|
||||||
@ -159,10 +141,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
protected applyWithoutSync(callback: () => void) {
|
protected applyWithoutSync(callback: () => void) {
|
||||||
this.disableSync();
|
this.disableSync();
|
||||||
runInAction(callback);
|
runInAction(callback);
|
||||||
|
this.enableSync();
|
||||||
if (this.params.syncEnabled) {
|
|
||||||
this.enableSync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onSync(model: T) {
|
protected onSync(model: T) {
|
||||||
@ -184,6 +163,9 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
/**
|
/**
|
||||||
* fromStore is called internally when a child class syncs with the file
|
* fromStore is called internally when a child class syncs with the file
|
||||||
* system.
|
* system.
|
||||||
|
*
|
||||||
|
* Note: This function **must** be synchronous.
|
||||||
|
*
|
||||||
* @param data the parsed information read from the stored JSON file
|
* @param data the parsed information read from the stored JSON file
|
||||||
*/
|
*/
|
||||||
protected abstract fromStore(data: T): void;
|
protected abstract fromStore(data: T): void;
|
||||||
|
|||||||
76
src/common/catalog-entities/general.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { navigate } from "../../renderer/navigation";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
|
||||||
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
|
|
||||||
|
interface GeneralEntitySpec extends CatalogEntitySpec {
|
||||||
|
path: string;
|
||||||
|
icon?: {
|
||||||
|
material?: string;
|
||||||
|
background?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogEntityStatus, GeneralEntitySpec> {
|
||||||
|
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "General";
|
||||||
|
|
||||||
|
async onRun() {
|
||||||
|
navigate(this.spec.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSettingsOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDetailsOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onContextMenuOpen(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeneralCategory extends CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "General",
|
||||||
|
icon: "settings"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: GeneralEntity
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "General"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry.add(new GeneralCategory());
|
||||||
@ -19,5 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./general";
|
||||||
export * from "./kubernetes-cluster";
|
export * from "./kubernetes-cluster";
|
||||||
export * from "./web-link";
|
export * from "./web-link";
|
||||||
|
|||||||
@ -27,9 +27,10 @@ import { requestMain } from "../ipc";
|
|||||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||||
import { addClusterURL } from "../routes";
|
import { addClusterURL } from "../routes";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||||
import { HotbarStore } from "../hotbar-store";
|
import { HotbarStore } from "../hotbar-store";
|
||||||
|
|
||||||
export type KubernetesClusterPrometheusMetrics = {
|
export interface KubernetesClusterPrometheusMetrics {
|
||||||
address?: {
|
address?: {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
service: string;
|
service: string;
|
||||||
@ -37,56 +38,55 @@ export type KubernetesClusterPrometheusMetrics = {
|
|||||||
prefix: string;
|
prefix: string;
|
||||||
};
|
};
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type KubernetesClusterSpec = {
|
export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||||
kubeconfigPath: string;
|
kubeconfigPath: string;
|
||||||
kubeconfigContext: string;
|
kubeconfigContext: string;
|
||||||
iconData?: string;
|
|
||||||
metrics?: {
|
metrics?: {
|
||||||
source: string;
|
source: string;
|
||||||
prometheus?: KubernetesClusterPrometheusMetrics;
|
prometheus?: KubernetesClusterPrometheusMetrics;
|
||||||
}
|
};
|
||||||
};
|
icon?: {
|
||||||
|
// TODO: move to CatalogEntitySpec once any-entity icons are supported
|
||||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
src?: string;
|
||||||
phase: "connected" | "disconnected" | "deleting";
|
material?: string;
|
||||||
|
background?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
distro?: string;
|
||||||
public readonly kind = "KubernetesCluster";
|
kubeVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
||||||
|
|
||||||
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
|
phase: KubernetesClusterStatusPhase;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
|
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public static readonly kind = "KubernetesCluster";
|
||||||
|
|
||||||
|
public readonly apiVersion = KubernetesCluster.apiVersion;
|
||||||
|
public readonly kind = KubernetesCluster.kind;
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (app) {
|
if (app) {
|
||||||
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
|
await ClusterStore.getInstance().getById(this.metadata.uid)?.activate();
|
||||||
|
} else {
|
||||||
if (!cluster) return;
|
await requestMain(clusterActivateHandler, this.metadata.uid, false);
|
||||||
|
|
||||||
await cluster.activate();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await requestMain(clusterActivateHandler, this.metadata.uid, false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
if (app) {
|
if (app) {
|
||||||
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
|
ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect();
|
||||||
|
} else {
|
||||||
if (!cluster) return;
|
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
|
||||||
|
|
||||||
cluster.disconnect();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRun(context: CatalogEntityActionContext) {
|
async onRun(context: CatalogEntityActionContext) {
|
||||||
@ -118,29 +118,33 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
},
|
},
|
||||||
confirm: {
|
confirm: {
|
||||||
// TODO: change this to be a <p> tag with better formatting once this code can accept it.
|
// TODO: change this to be a <p> tag with better formatting once this code can accept it.
|
||||||
message: `Delete the "${this.metadata.name}" context from "${this.metadata.labels.file}"?`
|
message: `Delete the "${this.metadata.name}" context from "${this.spec.kubeconfigPath}"?`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.status.phase == "connected") {
|
switch (this.status.phase) {
|
||||||
context.menuItems.push({
|
case "connected":
|
||||||
title: "Disconnect",
|
case "connecting":
|
||||||
icon: "link_off",
|
context.menuItems.push({
|
||||||
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
title: "Disconnect",
|
||||||
});
|
icon: "link_off",
|
||||||
} else {
|
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
||||||
context.menuItems.push({
|
});
|
||||||
title: "Connect",
|
break;
|
||||||
icon: "link",
|
case "disconnected":
|
||||||
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`)
|
context.menuItems.push({
|
||||||
});
|
title: "Connect",
|
||||||
|
icon: "link",
|
||||||
|
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`)
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
catalogCategoryRegistry
|
||||||
|
.getCategoryForEntity<KubernetesClusterCategory>(this)
|
||||||
if (category) category.emit("contextMenuOpen", this, context);
|
?.emit("contextMenuOpen", this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -149,6 +149,7 @@ export interface CatalogEntityAddMenuContext {
|
|||||||
|
|
||||||
export type CatalogEntitySpec = Record<string, any>;
|
export type CatalogEntitySpec = Record<string, any>;
|
||||||
|
|
||||||
|
|
||||||
export interface CatalogEntityData<
|
export interface CatalogEntityData<
|
||||||
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
|
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
|
||||||
Status extends CatalogEntityStatus = CatalogEntityStatus,
|
Status extends CatalogEntityStatus = CatalogEntityStatus,
|
||||||
|
|||||||
@ -71,8 +71,13 @@ export interface ClusterModel {
|
|||||||
*/
|
*/
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated this is used only for hotbar migrations from 4.2.X
|
||||||
|
*/
|
||||||
|
workspaces?: string[];
|
||||||
|
|
||||||
/** User context in kubeconfig */
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName: string;
|
||||||
|
|
||||||
/** Preferences */
|
/** Preferences */
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
@ -96,6 +101,8 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
hiddenMetrics?: string[];
|
hiddenMetrics?: string[];
|
||||||
|
nodeShellImage?: string;
|
||||||
|
imagePullSecret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterPrometheusPreferences {
|
export interface ClusterPrometheusPreferences {
|
||||||
@ -110,6 +117,10 @@ export interface ClusterPrometheusPreferences {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialStates = "cluster:states";
|
||||||
|
|
||||||
|
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
private static StateChannel = "cluster:state";
|
private static StateChannel = "cluster:state";
|
||||||
|
|
||||||
@ -121,8 +132,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
clusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
protected disposer = disposer();
|
protected disposer = disposer();
|
||||||
|
|
||||||
@ -137,31 +148,27 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
this.pushStateToViewsAutomatically();
|
this.pushStateToViewsAutomatically();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async loadInitialOnRenderer() {
|
||||||
const initialStates = "cluster:states";
|
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||||
|
|
||||||
await super.load();
|
for (const { id, state } of await requestMain(initialStates)) {
|
||||||
|
this.getById(id)?.setState(state);
|
||||||
if (ipcRenderer) {
|
|
||||||
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
|
||||||
|
|
||||||
for (const { id, state } of await requestMain(initialStates)) {
|
|
||||||
this.getById(id)?.setState(state);
|
|
||||||
}
|
|
||||||
} else if (ipcMain) {
|
|
||||||
ipcMainHandle(initialStates, () => {
|
|
||||||
return this.clustersList.map(cluster => ({
|
|
||||||
id: cluster.id,
|
|
||||||
state: cluster.getState(),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provideInitialFromMain() {
|
||||||
|
ipcMainHandle(initialStates, () => {
|
||||||
|
return this.clustersList.map(cluster => ({
|
||||||
|
id: cluster.id,
|
||||||
|
state: cluster.getState(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected pushStateToViewsAutomatically() {
|
protected pushStateToViewsAutomatically() {
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
this.disposer.push(
|
this.disposer.push(
|
||||||
@ -243,8 +250,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
cluster = new Cluster(clusterModel);
|
cluster = new Cluster(clusterModel);
|
||||||
}
|
}
|
||||||
newClusters.set(clusterModel.id, cluster);
|
newClusters.set(clusterModel.id, cluster);
|
||||||
} catch {
|
} catch (error) {
|
||||||
// ignore
|
logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
76
src/renderer/components/layout/login-layout.scss → src/common/getTSLoader.ts
Executable file → Normal file
@ -19,53 +19,45 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
.LoginLayout {
|
/**
|
||||||
$logo-size: 6 * $unit;
|
* A function returning webpack ts/tsx loader
|
||||||
height: 100vh;
|
*
|
||||||
padding: $padding * 2;
|
* depends on env LENS_DEV_USE_ESBUILD_LOADER to use esbuild-loader (faster) or good-old ts-loader
|
||||||
|
*
|
||||||
|
* @param testRegExp - the regex for webpack to conditional find the files
|
||||||
|
* @returns ts/tsx webpack loader configuration object
|
||||||
|
*/
|
||||||
|
const getTSLoader = (
|
||||||
|
testRegExp: RegExp, transpileOnly = true
|
||||||
|
) => {
|
||||||
|
const useEsbuildLoader = process.env.LENS_DEV_USE_ESBUILD_LOADER === "true";
|
||||||
|
|
||||||
.logo {
|
useEsbuildLoader && console.info(`\n🚀 using esbuild-loader for ts(x)`);
|
||||||
width: $logo-size;
|
|
||||||
height: $logo-size;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
svg * {
|
if (useEsbuildLoader) {
|
||||||
fill: $lensBlue;
|
return {
|
||||||
}
|
test: testRegExp,
|
||||||
|
loader: "esbuild-loader",
|
||||||
|
options: {
|
||||||
|
loader: "tsx",
|
||||||
|
target: "es2015",
|
||||||
|
implementation: esbuild
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
return {
|
||||||
font-size: $font-size-small;
|
test: testRegExp,
|
||||||
}
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
.main {
|
loader: "ts-loader",
|
||||||
$spacing: $padding * 3;
|
options: {
|
||||||
width: 100%;
|
transpileOnly,
|
||||||
min-width: 34 * $unit;
|
|
||||||
max-width: 42 * $unit;
|
|
||||||
margin: $padding * 2 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
padding: $spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
background: $layoutBackground;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
color: $textColorAccent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
.content {
|
export default getTSLoader;
|
||||||
background: $contentColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
font-size: $font-size-small;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -26,6 +26,7 @@ import * as uuid from "uuid";
|
|||||||
import isNull from "lodash/isNull";
|
import isNull from "lodash/isNull";
|
||||||
import { toJS } from "./utils";
|
import { toJS } from "./utils";
|
||||||
import { CatalogEntity } from "./catalog";
|
import { CatalogEntity } from "./catalog";
|
||||||
|
import { catalogEntity } from "../main/catalog-sources/general";
|
||||||
|
|
||||||
export interface HotbarItem {
|
export interface HotbarItem {
|
||||||
entity: {
|
entity: {
|
||||||
@ -38,16 +39,12 @@ export interface HotbarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hotbar {
|
export type Hotbar = Required<HotbarCreateOptions>;
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
items: HotbarItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HotbarCreateOptions {
|
export interface HotbarCreateOptions {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
items?: HotbarItem[];
|
items?: (HotbarItem | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotbarStoreModel {
|
export interface HotbarStoreModel {
|
||||||
@ -71,6 +68,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarId() {
|
get activeHotbarId() {
|
||||||
@ -91,16 +89,17 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbarIndex(this.activeHotbarId);
|
return this.hotbarIndex(this.activeHotbarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get initialItems() {
|
static getInitialItems() {
|
||||||
return [...Array.from(Array(defaultHotbarCells).fill(null))];
|
return [...Array.from(Array(defaultHotbarCells).fill(null))];
|
||||||
}
|
}
|
||||||
|
|
||||||
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
@action
|
||||||
if (data.hotbars?.length === 0) {
|
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||||
|
if (!data.hotbars || !data.hotbars.length) {
|
||||||
this.hotbars = [{
|
this.hotbars = [{
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
name: "Default",
|
name: "Default",
|
||||||
items: this.initialItems,
|
items: this.defaultHotbarInitialItems,
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
this.hotbars = data.hotbars;
|
this.hotbars = data.hotbars;
|
||||||
@ -117,6 +116,16 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get defaultHotbarInitialItems() {
|
||||||
|
const { metadata: { uid, name, source } } = catalogEntity;
|
||||||
|
const initialItem = { entity: { uid, name, source }};
|
||||||
|
|
||||||
|
return [
|
||||||
|
initialItem,
|
||||||
|
...Array.from(Array(defaultHotbarCells - 1).fill(null))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
getActive() {
|
getActive() {
|
||||||
return this.getById(this.activeHotbarId);
|
return this.getById(this.activeHotbarId);
|
||||||
}
|
}
|
||||||
@ -129,18 +138,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(data: HotbarCreateOptions) {
|
@action
|
||||||
|
add(data: HotbarCreateOptions, { setActive = false } = {}) {
|
||||||
const {
|
const {
|
||||||
id = uuid.v4(),
|
id = uuid.v4(),
|
||||||
items = this.initialItems,
|
items = HotbarStore.getInitialItems(),
|
||||||
name,
|
name,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const hotbar = { id, name, items };
|
this.hotbars.push({ id, name, items });
|
||||||
|
|
||||||
this.hotbars.push(hotbar as Hotbar);
|
if (setActive) {
|
||||||
|
this._activeHotbarId = id;
|
||||||
return hotbar as Hotbar;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import { ExtensionsStore } from "../../extensions/extensions-store";
|
|||||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||||
import type { LensExtension } from "../../extensions/lens-extension";
|
import type { LensExtension } from "../../extensions/lens-extension";
|
||||||
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
||||||
|
import { when } from "mobx";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
@ -178,16 +180,28 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
|
|
||||||
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||||
const name = [publisher, partialName].filter(Boolean).join("/");
|
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||||
|
const extensionLoader = ExtensionLoader.getInstance();
|
||||||
|
|
||||||
const extension = ExtensionLoader.getInstance().getInstanceByName(name);
|
try {
|
||||||
|
/**
|
||||||
if (!extension) {
|
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
|
*/
|
||||||
|
await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
|
||||||
|
} catch(error) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
|
const extension = extensionLoader.getInstanceByName(name);
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExtensionsStore.getInstance().isEnabled(extension)) {
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
77
src/common/sentry.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
||||||
|
import * as Sentry from "@sentry/electron";
|
||||||
|
import { sentryDsn, isProduction } from "./vars";
|
||||||
|
import { UserStore } from "./user-store";
|
||||||
|
import logger from "../main/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Translate" 'browser' to 'main' as Lens developer more familiar with the term 'main'
|
||||||
|
*/
|
||||||
|
function mapProcessName(processType: string) {
|
||||||
|
if (processType === "browser") {
|
||||||
|
return "main";
|
||||||
|
}
|
||||||
|
|
||||||
|
return processType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Sentry for the current process so to send errors for debugging.
|
||||||
|
*/
|
||||||
|
export function SentryInit() {
|
||||||
|
const processName = mapProcessName(process.type);
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
beforeSend: (event) => {
|
||||||
|
// default to false, in case instance of UserStore is not created (yet)
|
||||||
|
const allowErrorReporting = UserStore.getInstance(false)?.allowErrorReporting ?? false;
|
||||||
|
|
||||||
|
if (allowErrorReporting) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: ${allowErrorReporting}. Sentry event is caught but not sent to server.`);
|
||||||
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
|
||||||
|
logger.info(event);
|
||||||
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
|
||||||
|
|
||||||
|
// if return null, the event won't be sent
|
||||||
|
// ref https://github.com/getsentry/sentry-javascript/issues/2039
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
dsn: sentryDsn,
|
||||||
|
integrations: [
|
||||||
|
new CaptureConsole({ levels: ["error"] }),
|
||||||
|
new Dedupe(),
|
||||||
|
new Offline()
|
||||||
|
],
|
||||||
|
initialScope: {
|
||||||
|
tags: {
|
||||||
|
|
||||||
|
"process": processName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: isProduction ? "production" : "development",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -33,5 +33,9 @@ if (isMac) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
try {
|
||||||
|
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[CA]: failed to force load: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,263 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ThemeId } from "../renderer/theme.store";
|
|
||||||
import { app, remote } from "electron";
|
|
||||||
import semver from "semver";
|
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
|
||||||
import moment from "moment-timezone";
|
|
||||||
import { BaseStore } from "./base-store";
|
|
||||||
import migrations from "../migrations/user-store";
|
|
||||||
import { getAppVersion } from "./utils/app-version";
|
|
||||||
import { appEventBus } from "./event-bus";
|
|
||||||
import path from "path";
|
|
||||||
import os from "os";
|
|
||||||
import { fileNameMigration } from "../migrations/user-store";
|
|
||||||
import { ObservableToggleSet, toJS } from "../renderer/utils";
|
|
||||||
|
|
||||||
export interface UserStoreModel {
|
|
||||||
lastSeenAppVersion: string;
|
|
||||||
preferences: UserPreferencesModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
|
||||||
filePath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KubeconfigSyncValue { }
|
|
||||||
|
|
||||||
export interface UserPreferencesModel {
|
|
||||||
httpsProxy?: string;
|
|
||||||
shell?: string;
|
|
||||||
colorTheme?: string;
|
|
||||||
localeTimezone?: string;
|
|
||||||
allowUntrustedCAs?: boolean;
|
|
||||||
allowTelemetry?: boolean;
|
|
||||||
downloadMirror?: string | "default";
|
|
||||||
downloadKubectlBinaries?: boolean;
|
|
||||||
downloadBinariesPath?: string;
|
|
||||||
kubectlBinariesPath?: string;
|
|
||||||
openAtLogin?: boolean;
|
|
||||||
hiddenTableColumns?: [string, string[]][];
|
|
||||||
syncKubeconfigEntries?: KubeconfigSyncEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserStore extends BaseStore<UserStoreModel> {
|
|
||||||
static readonly defaultTheme: ThemeId = "lens-dark";
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
configName: "lens-user-store",
|
|
||||||
migrations,
|
|
||||||
});
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable lastSeenAppVersion = "0.0.0";
|
|
||||||
@observable allowTelemetry = true;
|
|
||||||
@observable allowUntrustedCAs = false;
|
|
||||||
@observable colorTheme = UserStore.defaultTheme;
|
|
||||||
@observable localeTimezone = moment.tz.guess(true) || "UTC";
|
|
||||||
@observable downloadMirror = "default";
|
|
||||||
@observable httpsProxy?: string;
|
|
||||||
@observable shell?: string;
|
|
||||||
@observable downloadBinariesPath?: string;
|
|
||||||
@observable kubectlBinariesPath?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download kubectl binaries matching cluster version
|
|
||||||
*/
|
|
||||||
@observable downloadKubectlBinaries = true;
|
|
||||||
@observable openAtLogin = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The column IDs under each configurable table ID that have been configured
|
|
||||||
* to not be shown
|
|
||||||
*/
|
|
||||||
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set of file/folder paths to be synced
|
|
||||||
*/
|
|
||||||
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>([
|
|
||||||
[path.join(os.homedir(), ".kube"), {}]
|
|
||||||
]);
|
|
||||||
|
|
||||||
async load(): Promise<void> {
|
|
||||||
/**
|
|
||||||
* This has to be here before the call to `new Config` in `super.load()`
|
|
||||||
* as we have to make sure that file is in the expected place for that call
|
|
||||||
*/
|
|
||||||
await fileNameMigration();
|
|
||||||
await super.load();
|
|
||||||
|
|
||||||
if (app) {
|
|
||||||
// track telemetry availability
|
|
||||||
reaction(() => this.allowTelemetry, allowed => {
|
|
||||||
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
|
|
||||||
});
|
|
||||||
|
|
||||||
// open at system start-up
|
|
||||||
reaction(() => this.openAtLogin, openAtLogin => {
|
|
||||||
app.setLoginItemSettings({
|
|
||||||
openAtLogin,
|
|
||||||
openAsHidden: true,
|
|
||||||
args: ["--hidden"]
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get isNewVersion() {
|
|
||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get resolvedShell(): string | undefined {
|
|
||||||
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
|
|
||||||
* @param tableId The ID of the table to be checked against
|
|
||||||
* @param columnIds The list of IDs the check if one is hidden
|
|
||||||
* @returns true if at least one column under the table is set to hidden
|
|
||||||
*/
|
|
||||||
isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean {
|
|
||||||
if (columnIds.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = this.hiddenTableColumns.get(tableId);
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return columnIds.some(columnId => config.has(columnId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
/**
|
|
||||||
* Toggles the hidden configuration of a table's column
|
|
||||||
*/
|
|
||||||
toggleTableColumnVisibility(tableId: string, columnId: string) {
|
|
||||||
this.hiddenTableColumns.get(tableId)?.toggle(columnId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async resetTheme() {
|
|
||||||
await this.whenLoaded;
|
|
||||||
this.colorTheme = UserStore.defaultTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
saveLastSeenAppVersion() {
|
|
||||||
appEventBus.emit({ name: "app", action: "whats-new-seen" });
|
|
||||||
this.lastSeenAppVersion = getAppVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setLocaleTimezone(tz: string) {
|
|
||||||
this.localeTimezone = tz;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
|
||||||
const { lastSeenAppVersion, preferences } = data;
|
|
||||||
|
|
||||||
if (lastSeenAppVersion) {
|
|
||||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preferences) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.httpsProxy = preferences.httpsProxy;
|
|
||||||
this.shell = preferences.shell;
|
|
||||||
this.colorTheme = preferences.colorTheme;
|
|
||||||
this.localeTimezone = preferences.localeTimezone;
|
|
||||||
this.allowUntrustedCAs = preferences.allowUntrustedCAs;
|
|
||||||
this.allowTelemetry = preferences.allowTelemetry;
|
|
||||||
this.downloadMirror = preferences.downloadMirror;
|
|
||||||
this.downloadKubectlBinaries = preferences.downloadKubectlBinaries;
|
|
||||||
this.downloadBinariesPath = preferences.downloadBinariesPath;
|
|
||||||
this.kubectlBinariesPath = preferences.kubectlBinariesPath;
|
|
||||||
this.openAtLogin = preferences.openAtLogin;
|
|
||||||
|
|
||||||
if (preferences.hiddenTableColumns) {
|
|
||||||
this.hiddenTableColumns.replace(
|
|
||||||
preferences.hiddenTableColumns
|
|
||||||
.map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.syncKubeconfigEntries) {
|
|
||||||
this.syncKubeconfigEntries.replace(
|
|
||||||
preferences.syncKubeconfigEntries.map(({ filePath, ...rest }) => [filePath, rest])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): UserStoreModel {
|
|
||||||
const hiddenTableColumns: [string, string[]][] = [];
|
|
||||||
const syncKubeconfigEntries: KubeconfigSyncEntry[] = [];
|
|
||||||
|
|
||||||
for (const [key, values] of this.hiddenTableColumns.entries()) {
|
|
||||||
hiddenTableColumns.push([key, Array.from(values)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [filePath, rest] of this.syncKubeconfigEntries) {
|
|
||||||
syncKubeconfigEntries.push({ filePath, ...rest });
|
|
||||||
}
|
|
||||||
|
|
||||||
const model: UserStoreModel = {
|
|
||||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
|
||||||
preferences: {
|
|
||||||
httpsProxy: toJS(this.httpsProxy),
|
|
||||||
shell: toJS(this.shell),
|
|
||||||
colorTheme: toJS(this.colorTheme),
|
|
||||||
localeTimezone: toJS(this.localeTimezone),
|
|
||||||
allowUntrustedCAs: toJS(this.allowUntrustedCAs),
|
|
||||||
allowTelemetry: toJS(this.allowTelemetry),
|
|
||||||
downloadMirror: toJS(this.downloadMirror),
|
|
||||||
downloadKubectlBinaries: toJS(this.downloadKubectlBinaries),
|
|
||||||
downloadBinariesPath: toJS(this.downloadBinariesPath),
|
|
||||||
kubectlBinariesPath: toJS(this.kubectlBinariesPath),
|
|
||||||
openAtLogin: toJS(this.openAtLogin),
|
|
||||||
hiddenTableColumns,
|
|
||||||
syncKubeconfigEntries,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return toJS(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getting default directory to download kubectl binaries
|
|
||||||
* @returns string
|
|
||||||
*/
|
|
||||||
export function getDefaultKubectlPath(): string {
|
|
||||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
|
||||||
}
|
|
||||||
@ -19,11 +19,5 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.AppInit {
|
export * from "./user-store";
|
||||||
height: 100%;
|
export type { KubeconfigSyncEntry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||||
|
|
||||||
.waiting-services {
|
|
||||||
font-size: small;
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
251
src/common/user-store/preferences-helpers.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
|
import { ObservableToggleSet } from "../utils";
|
||||||
|
|
||||||
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeconfigSyncValue { }
|
||||||
|
|
||||||
|
interface PreferenceDescription<T, R = T> {
|
||||||
|
fromStore(val: T | undefined): R;
|
||||||
|
toStore(val: R): T | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpsProxy: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
return val || undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const shell: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
return val || undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorTheme: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val || ThemeStore.defaultTheme;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === ThemeStore.defaultTheme) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const localeTimezone: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val || moment.tz.guess(true) || "UTC";
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === moment.tz.guess(true) || val === "UTC") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowUntrustedCAs: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? false;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowTelemetry: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowErrorReporting: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadMirror: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? "default";
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === "default") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadKubectlBinaries: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadBinariesPath: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const kubectlBinariesPath: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAtLogin: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? false;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return new Map(
|
||||||
|
(val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
const res: [string, string[]][] = [];
|
||||||
|
|
||||||
|
for (const [table, columnes] of val) {
|
||||||
|
if (columnes.size) {
|
||||||
|
res.push([table, Array.from(columnes)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.length ? res : undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainKubeFolder = path.join(os.homedir(), ".kube");
|
||||||
|
|
||||||
|
const syncKubeconfigEntries: PreferenceDescription<KubeconfigSyncEntry[], Map<string, KubeconfigSyncValue>> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return new Map(
|
||||||
|
val
|
||||||
|
?.map(({ filePath, ...rest }) => [filePath, rest])
|
||||||
|
?? [[mainKubeFolder, {}]]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val.size === 1 && val.has(mainKubeFolder)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
|
||||||
|
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||||
|
|
||||||
|
export type UserStoreFlatModel = {
|
||||||
|
[field in keyof typeof DESCRIPTORS]: UserStoreModelType<field>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserPreferencesModel = {
|
||||||
|
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DESCRIPTORS = {
|
||||||
|
httpsProxy,
|
||||||
|
shell,
|
||||||
|
colorTheme,
|
||||||
|
localeTimezone,
|
||||||
|
allowUntrustedCAs,
|
||||||
|
allowTelemetry,
|
||||||
|
allowErrorReporting,
|
||||||
|
downloadMirror,
|
||||||
|
downloadKubectlBinaries,
|
||||||
|
downloadBinariesPath,
|
||||||
|
kubectlBinariesPath,
|
||||||
|
openAtLogin,
|
||||||
|
hiddenTableColumns,
|
||||||
|
syncKubeconfigEntries,
|
||||||
|
};
|
||||||
214
src/common/user-store/user-store.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app, remote } from "electron";
|
||||||
|
import semver from "semver";
|
||||||
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
|
import { BaseStore } from "../base-store";
|
||||||
|
import migrations from "../../migrations/user-store";
|
||||||
|
import { getAppVersion } from "../utils/app-version";
|
||||||
|
import { kubeConfigDefaultPath } from "../kube-helpers";
|
||||||
|
import { appEventBus } from "../event-bus";
|
||||||
|
import path from "path";
|
||||||
|
import { fileNameMigration } from "../../migrations/user-store";
|
||||||
|
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||||
|
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
|
export interface UserStoreModel {
|
||||||
|
lastSeenAppVersion: string;
|
||||||
|
preferences: UserPreferencesModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
configName: "lens-user-store",
|
||||||
|
migrations,
|
||||||
|
});
|
||||||
|
|
||||||
|
makeObservable(this);
|
||||||
|
fileNameMigration();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable lastSeenAppVersion = "0.0.0";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used in add-cluster page for providing context
|
||||||
|
*/
|
||||||
|
@observable kubeConfigPath = kubeConfigDefaultPath;
|
||||||
|
@observable seenContexts = observable.set<string>();
|
||||||
|
@observable newContexts = observable.set<string>();
|
||||||
|
@observable allowTelemetry: boolean;
|
||||||
|
@observable allowErrorReporting: boolean;
|
||||||
|
@observable allowUntrustedCAs: boolean;
|
||||||
|
@observable colorTheme: string;
|
||||||
|
@observable localeTimezone: string;
|
||||||
|
@observable downloadMirror: string;
|
||||||
|
@observable httpsProxy?: string;
|
||||||
|
@observable shell?: string;
|
||||||
|
@observable downloadBinariesPath?: string;
|
||||||
|
@observable kubectlBinariesPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download kubectl binaries matching cluster version
|
||||||
|
*/
|
||||||
|
@observable downloadKubectlBinaries: boolean;
|
||||||
|
@observable openAtLogin: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The column IDs under each configurable table ID that have been configured
|
||||||
|
* to not be shown
|
||||||
|
*/
|
||||||
|
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of file/folder paths to be synced
|
||||||
|
*/
|
||||||
|
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>();
|
||||||
|
|
||||||
|
@computed get isNewVersion() {
|
||||||
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get resolvedShell(): string | undefined {
|
||||||
|
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
||||||
|
}
|
||||||
|
|
||||||
|
startMainReactions() {
|
||||||
|
// track telemetry availability
|
||||||
|
reaction(() => this.allowTelemetry, allowed => {
|
||||||
|
appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// open at system start-up
|
||||||
|
reaction(() => this.openAtLogin, openAtLogin => {
|
||||||
|
app.setLoginItemSettings({
|
||||||
|
openAtLogin,
|
||||||
|
openAsHidden: true,
|
||||||
|
args: ["--hidden"]
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
|
||||||
|
* @param tableId The ID of the table to be checked against
|
||||||
|
* @param columnIds The list of IDs the check if one is hidden
|
||||||
|
* @returns true if at least one column under the table is set to hidden
|
||||||
|
*/
|
||||||
|
isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean {
|
||||||
|
if (columnIds.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.hiddenTableColumns.get(tableId);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnIds.some(columnId => config.has(columnId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
/**
|
||||||
|
* Toggles the hidden configuration of a table's column
|
||||||
|
*/
|
||||||
|
toggleTableColumnVisibility(tableId: string, columnId: string) {
|
||||||
|
this.hiddenTableColumns.get(tableId)?.toggle(columnId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
resetTheme() {
|
||||||
|
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
saveLastSeenAppVersion() {
|
||||||
|
appEventBus.emit({ name: "app", action: "whats-new-seen" });
|
||||||
|
this.lastSeenAppVersion = getAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setLocaleTimezone(tz: string) {
|
||||||
|
this.localeTimezone = tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) {
|
||||||
|
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences });
|
||||||
|
|
||||||
|
if (lastSeenAppVersion) {
|
||||||
|
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy);
|
||||||
|
this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell);
|
||||||
|
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme);
|
||||||
|
this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone);
|
||||||
|
this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs);
|
||||||
|
this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry);
|
||||||
|
this.allowErrorReporting = DESCRIPTORS.allowErrorReporting.fromStore(preferences?.allowErrorReporting);
|
||||||
|
this.downloadMirror = DESCRIPTORS.downloadMirror.fromStore(preferences?.downloadMirror);
|
||||||
|
this.downloadKubectlBinaries = DESCRIPTORS.downloadKubectlBinaries.fromStore(preferences?.downloadKubectlBinaries);
|
||||||
|
this.downloadBinariesPath = DESCRIPTORS.downloadBinariesPath.fromStore(preferences?.downloadBinariesPath);
|
||||||
|
this.kubectlBinariesPath = DESCRIPTORS.kubectlBinariesPath.fromStore(preferences?.kubectlBinariesPath);
|
||||||
|
this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin);
|
||||||
|
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
|
||||||
|
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): UserStoreModel {
|
||||||
|
const model: UserStoreModel = {
|
||||||
|
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||||
|
preferences: {
|
||||||
|
httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy),
|
||||||
|
shell: DESCRIPTORS.shell.toStore(this.shell),
|
||||||
|
colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme),
|
||||||
|
localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone),
|
||||||
|
allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs),
|
||||||
|
allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry),
|
||||||
|
allowErrorReporting: DESCRIPTORS.allowErrorReporting.toStore(this.allowErrorReporting),
|
||||||
|
downloadMirror: DESCRIPTORS.downloadMirror.toStore(this.downloadMirror),
|
||||||
|
downloadKubectlBinaries: DESCRIPTORS.downloadKubectlBinaries.toStore(this.downloadKubectlBinaries),
|
||||||
|
downloadBinariesPath: DESCRIPTORS.downloadBinariesPath.toStore(this.downloadBinariesPath),
|
||||||
|
kubectlBinariesPath: DESCRIPTORS.kubectlBinariesPath.toStore(this.kubectlBinariesPath),
|
||||||
|
openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin),
|
||||||
|
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
|
||||||
|
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return toJS(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting default directory to download kubectl binaries
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export function getDefaultKubectlPath(): string {
|
||||||
|
return path.join((app || remote.app).getPath("userData"), "binaries");
|
||||||
|
}
|
||||||
130
src/common/utils/__tests__/paths.test.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describeIf } from "../../../../integration/helpers/utils";
|
||||||
|
import { isWindows } from "../../vars";
|
||||||
|
import { isLogicalChildPath } from "../paths";
|
||||||
|
|
||||||
|
describe("isLogicalChildPath", () => {
|
||||||
|
describeIf(isWindows)("windows tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Foo\\Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Foo/Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "D:\\Foo\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeIf(!isWindows)("posix tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foobar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/../bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/./bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/...bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..\\.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/bar/../foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/\\bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "./bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -45,6 +45,7 @@ export * from "./openExternal";
|
|||||||
export * from "./paths";
|
export * from "./paths";
|
||||||
export * from "./reject-promise";
|
export * from "./reject-promise";
|
||||||
export * from "./singleton";
|
export * from "./singleton";
|
||||||
|
export * from "./sort-compare";
|
||||||
export * from "./splitArray";
|
export * from "./splitArray";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
export * from "./toggle-set";
|
export * from "./toggle-set";
|
||||||
|
|||||||
@ -33,3 +33,35 @@ function resolveTilde(filePath: string) {
|
|||||||
export function resolvePath(filePath: string): string {
|
export function resolvePath(filePath: string): string {
|
||||||
return path.resolve(resolveTilde(filePath));
|
return path.resolve(resolveTilde(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if `testPath` represents a potential filesystem entry that would be
|
||||||
|
* logically "within" the `parentPath` directory.
|
||||||
|
*
|
||||||
|
* This function will return `true` in the above case, and `false` otherwise.
|
||||||
|
* It will return `false` if the two paths are the same (after resolving them).
|
||||||
|
*
|
||||||
|
* The function makes no FS calls and is platform dependant. Meaning that the
|
||||||
|
* results are only guaranteed to be correct for the platform you are running
|
||||||
|
* on.
|
||||||
|
* @param parentPath The known path of a directory
|
||||||
|
* @param testPath The path that is to be tested
|
||||||
|
*/
|
||||||
|
export function isLogicalChildPath(parentPath: string, testPath: string): boolean {
|
||||||
|
parentPath = path.resolve(parentPath);
|
||||||
|
testPath = path.resolve(testPath);
|
||||||
|
|
||||||
|
if (parentPath === testPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (testPath.length >= parentPath.length) {
|
||||||
|
if (testPath === parentPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
testPath = path.dirname(testPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -46,12 +46,15 @@ export class Singleton {
|
|||||||
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||||
if (!Singleton.instances.has(this)) {
|
if (!Singleton.instances.has(this)) {
|
||||||
if (Singleton.creating.length > 0) {
|
if (Singleton.creating.length > 0) {
|
||||||
throw new TypeError("Cannot create a second singleton while creating a first");
|
throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
Singleton.creating = this.name;
|
try {
|
||||||
Singleton.instances.set(this, new this(...args));
|
Singleton.creating = this.name;
|
||||||
Singleton.creating = "";
|
Singleton.instances.set(this, new this(...args));
|
||||||
|
} finally {
|
||||||
|
Singleton.creating = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Singleton.instances.get(this) as T;
|
return Singleton.instances.get(this) as T;
|
||||||
|
|||||||
55
src/common/utils/sort-compare.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import semver, { SemVer } from "semver";
|
||||||
|
|
||||||
|
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
|
||||||
|
if (left < right) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left === right) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartVersion {
|
||||||
|
version: string;
|
||||||
|
__version?: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): -1 | 0 | 1 {
|
||||||
|
if (left.__version && right.__version) {
|
||||||
|
return semver.compare(right.__version, left.__version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!left.__version && right.__version) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.__version && !right.__version) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortCompare(left.version, right.version);
|
||||||
|
}
|
||||||
@ -69,4 +69,6 @@ export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5
|
|||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
export const appSemVer = new SemVer(packageInfo.version);
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
export const docsUrl = "https://docs.k8slens.dev/main/" as string;
|
||||||
|
|
||||||
|
export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
|
||||||
|
|||||||
@ -55,9 +55,11 @@ export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
|
|||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action protected async fromStore(data: Partial<WeblinkStoreModel> = {}) {
|
@action
|
||||||
|
protected fromStore(data: Partial<WeblinkStoreModel> = {}) {
|
||||||
this.weblinks = data.weblinks || [];
|
this.weblinks = data.weblinks || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isCompatibleExtension } from "../extension-compatibility";
|
||||||
|
import { Console } from "console";
|
||||||
|
import { stdout, stderr } from "process";
|
||||||
|
import type { LensExtensionManifest } from "../lens-extension";
|
||||||
|
import { appSemVer } from "../../common/vars";
|
||||||
|
|
||||||
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
|
describe("extension compatibility", () => {
|
||||||
|
describe("appSemVer with no prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("appSemVer with prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = ["beta", 3];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -39,6 +39,12 @@ jest.mock("../extension-installer", () => ({
|
|||||||
installPackage: jest.fn()
|
installPackage: jest.fn()
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||||
|
|||||||
33
src/extensions/extension-compatibility.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import semver from "semver";
|
||||||
|
import { appSemVer } from "../common/vars";
|
||||||
|
import type { LensExtensionManifest } from "./lens-extension";
|
||||||
|
|
||||||
|
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
||||||
|
if (manifest.engines?.lens) {
|
||||||
|
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
||||||
|
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -34,9 +34,8 @@ import { extensionInstaller } from "./extension-installer";
|
|||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
import semver from "semver";
|
|
||||||
import { appSemVer } from "../common/vars";
|
|
||||||
import { isProduction } from "../common/vars";
|
import { isProduction } from "../common/vars";
|
||||||
|
import { isCompatibleExtension } from "./extension-compatibility";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
id: LensExtensionId;
|
id: LensExtensionId;
|
||||||
@ -357,21 +356,17 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||||
try {
|
try {
|
||||||
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
||||||
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
|
const id = this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled });
|
||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
let isCompatible = isBundled;
|
const isCompatible = isBundled || isCompatibleExtension(manifest);
|
||||||
|
|
||||||
if (manifest.engines?.lens) {
|
|
||||||
isCompatible = semver.satisfies(appSemVer, manifest.engines.lens);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: installedManifestPath,
|
id,
|
||||||
absolutePath,
|
absolutePath,
|
||||||
manifestPath: installedManifestPath,
|
manifestPath: id,
|
||||||
manifest,
|
manifest,
|
||||||
isBundled,
|
isBundled,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
|
|||||||
@ -48,6 +48,13 @@ export class ExtensionLoader extends Singleton {
|
|||||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the set of extensions that don't come with either
|
||||||
|
* - Main.LensExtension when running in the main process
|
||||||
|
* - Renderer.LensExtension when running in the renderer process
|
||||||
|
*/
|
||||||
|
protected nonInstancesByName = observable.set<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
||||||
*/
|
*/
|
||||||
@ -77,7 +84,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
if (this.instancesByName.has(change.newValue.name)) {
|
if (this.instancesByName.has(change.newValue.name)) {
|
||||||
throw new TypeError("Extension names must be unique");
|
throw new TypeError("Extension names must be unique");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instancesByName.set(change.newValue.name, change.newValue);
|
this.instancesByName.set(change.newValue.name, change.newValue);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
@ -101,7 +108,20 @@ export class ExtensionLoader extends Singleton {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstanceByName(name: string): LensExtension | undefined {
|
/**
|
||||||
|
* Get the extension instance by its manifest name
|
||||||
|
* @param name The name of the extension
|
||||||
|
* @returns one of the following:
|
||||||
|
* - the instance of `Main.LensExtension` on the main process if created
|
||||||
|
* - the instance of `Renderer.LensExtension` on the renderer process if created
|
||||||
|
* - `null` if no class definition is provided for the current process
|
||||||
|
* - `undefined` if the name is not known about
|
||||||
|
*/
|
||||||
|
getInstanceByName(name: string): LensExtension | null | undefined {
|
||||||
|
if (this.nonInstancesByName.has(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.instancesByName.get(name);
|
return this.instancesByName.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +144,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
await this.initMain();
|
await this.initMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
|
await Promise.all([this.whenLoaded]);
|
||||||
|
|
||||||
// broadcasting extensions between main/renderer processes
|
// broadcasting extensions between main/renderer processes
|
||||||
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
||||||
@ -145,6 +165,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
this.extensions.set(extension.id, extension);
|
this.extensions.set(extension.id, extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
removeInstance(lensExtensionId: LensExtensionId) {
|
removeInstance(lensExtensionId: LensExtensionId) {
|
||||||
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||||
const instance = this.instances.get(lensExtensionId);
|
const instance = this.instances.get(lensExtensionId);
|
||||||
@ -157,6 +178,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
instance.disable();
|
instance.disable();
|
||||||
this.events.emit("remove", instance);
|
this.events.emit("remove", instance);
|
||||||
this.instances.delete(lensExtensionId);
|
this.instances.delete(lensExtensionId);
|
||||||
|
this.nonInstancesByName.delete(instance.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||||
}
|
}
|
||||||
@ -170,6 +192,10 @@ export class ExtensionLoader extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
||||||
|
this.extensions.get(lensExtensionId).isEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
protected async initMain() {
|
protected async initMain() {
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
this.loadOnMain();
|
this.loadOnMain();
|
||||||
@ -302,13 +328,14 @@ export class ExtensionLoader extends Singleton {
|
|||||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||||
return reaction(() => this.toJSON(), installedExtensions => {
|
return reaction(() => this.toJSON(), installedExtensions => {
|
||||||
for (const [extId, extension] of installedExtensions) {
|
for (const [extId, extension] of installedExtensions) {
|
||||||
const alreadyInit = this.instances.has(extId);
|
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||||
|
|
||||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = this.requireExtension(extension);
|
const LensExtensionClass = this.requireExtension(extension);
|
||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
|
this.nonInstancesByName.add(extension.manifest.name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,13 +26,13 @@ import type { LensExtension } from "./lens-extension";
|
|||||||
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
||||||
protected extension: LensExtension;
|
protected extension: LensExtension;
|
||||||
|
|
||||||
async loadExtension(extension: LensExtension) {
|
loadExtension(extension: LensExtension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
load() {
|
||||||
if (!this.extension) { return; }
|
if (!this.extension) { return; }
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
configName: "lens-extensions",
|
configName: "lens-extensions",
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
@ -50,12 +51,10 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||||
|
|
||||||
isEnabled(extId: LensExtensionId): boolean {
|
isEnabled({ id, isBundled }: { id: string, isBundled: boolean }): boolean {
|
||||||
const state = this.state.get(extId);
|
|
||||||
|
|
||||||
// By default false, so that copied extensions are disabled by default.
|
// By default false, so that copied extensions are disabled by default.
|
||||||
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
||||||
return Boolean(state?.enabled);
|
return isBundled || Boolean(this.state.get(id)?.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import type * as registries from "./registries";
|
|||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -37,7 +38,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: registries.TopBarRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "*",
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "*",
|
||||||
"conf": "^7.0.1"
|
"conf": "^7.0.1",
|
||||||
|
"typed-emitter": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,14 +22,24 @@
|
|||||||
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
||||||
import { LensExtension } from "../../lens-extension";
|
import { LensExtension } from "../../lens-extension";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import fse from "fs-extra";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
|
import { ThemeStore } from "../../../renderer/theme.store";
|
||||||
|
import { TerminalStore } from "../../renderer-api/components";
|
||||||
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
let ext: LensExtension = null;
|
let ext: LensExtension = null;
|
||||||
|
|
||||||
describe("getPageUrl", () => {
|
describe("page registry tests", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
ext = new LensExtension({
|
ext = new LensExtension({
|
||||||
manifest: {
|
manifest: {
|
||||||
@ -43,6 +53,9 @@ describe("getPageUrl", () => {
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isCompatible: true
|
isCompatible: true
|
||||||
});
|
});
|
||||||
|
UserStore.createInstance();
|
||||||
|
ThemeStore.createInstance();
|
||||||
|
TerminalStore.createInstance();
|
||||||
ClusterPageRegistry.createInstance();
|
ClusterPageRegistry.createInstance();
|
||||||
GlobalPageRegistry.createInstance().add({
|
GlobalPageRegistry.createInstance().add({
|
||||||
id: "page-with-params",
|
id: "page-with-params",
|
||||||
@ -54,70 +67,6 @@ describe("getPageUrl", () => {
|
|||||||
test2: "" // no default value, just declaration
|
test2: "" // no default value, just declaration
|
||||||
},
|
},
|
||||||
}, ext);
|
}, ext);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
GlobalPageRegistry.resetInstance();
|
|
||||||
ClusterPageRegistry.resetInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a page url for extension", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows to pass base url as parameter", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes @ and replace `/` to `--`", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds / prefix", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("normalize possible multi-slashes in page.id", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets page url with custom params", () => {
|
|
||||||
const params: PageParams = { test1: "one", test2: "2" };
|
|
||||||
const searchParams = new URLSearchParams(params);
|
|
||||||
const pageUrl = getExtensionPageUrl({
|
|
||||||
extensionId: ext.name,
|
|
||||||
pageId: "page-with-params",
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets page url with default custom params", () => {
|
|
||||||
const defaultPageUrl = getExtensionPageUrl({
|
|
||||||
extensionId: ext.name,
|
|
||||||
pageId: "page-with-params",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("globalPageRegistry", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
ext = new LensExtension({
|
|
||||||
manifest: {
|
|
||||||
name: "@acme/foo-bar",
|
|
||||||
version: "0.1.1"
|
|
||||||
},
|
|
||||||
id: "/this/is/fake/package.json",
|
|
||||||
absolutePath: "/absolute/fake/",
|
|
||||||
manifestPath: "/this/is/fake/package.json",
|
|
||||||
isBundled: false,
|
|
||||||
isEnabled: true,
|
|
||||||
isCompatible: true
|
|
||||||
});
|
|
||||||
ClusterPageRegistry.createInstance();
|
|
||||||
GlobalPageRegistry.createInstance().add([
|
GlobalPageRegistry.createInstance().add([
|
||||||
{
|
{
|
||||||
id: "test-page",
|
id: "test-page",
|
||||||
@ -142,33 +91,82 @@ describe("globalPageRegistry", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
GlobalPageRegistry.resetInstance();
|
GlobalPageRegistry.resetInstance();
|
||||||
ClusterPageRegistry.resetInstance();
|
ClusterPageRegistry.resetInstance();
|
||||||
|
TerminalStore.resetInstance();
|
||||||
|
ThemeStore.resetInstance();
|
||||||
|
UserStore.resetInstance();
|
||||||
|
fse.remove("tmp");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getByPageTarget", () => {
|
describe("getPageUrl", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("returns a page url for extension", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||||
|
|
||||||
expect(page.id).toEqual(undefined);
|
|
||||||
expect(page.extensionId).toEqual(ext.name);
|
|
||||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns matching page", () => {
|
it("allows to pass base url as parameter", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||||
pageId: "test-page",
|
|
||||||
extensionId: ext.name
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(page.id).toEqual("test-page");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null if target not found", () => {
|
it("removes @ and replace `/` to `--`", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||||
pageId: "wrong-page",
|
});
|
||||||
extensionId: ext.name
|
|
||||||
|
it("adds / prefix", () => {
|
||||||
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalize possible multi-slashes in page.id", () => {
|
||||||
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets page url with custom params", () => {
|
||||||
|
const params: PageParams = { test1: "one", test2: "2" };
|
||||||
|
const searchParams = new URLSearchParams(params);
|
||||||
|
const pageUrl = getExtensionPageUrl({
|
||||||
|
extensionId: ext.name,
|
||||||
|
pageId: "page-with-params",
|
||||||
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page).toBeNull();
|
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets page url with default custom params", () => {
|
||||||
|
const defaultPageUrl = getExtensionPageUrl({
|
||||||
|
extensionId: ext.name,
|
||||||
|
pageId: "page-with-params",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("globalPageRegistry", () => {
|
||||||
|
describe("getByPageTarget", () => {
|
||||||
|
it("matching to first registered page without id", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
||||||
|
|
||||||
|
expect(page.id).toEqual(undefined);
|
||||||
|
expect(page.extensionId).toEqual(ext.name);
|
||||||
|
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns matching page", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||||
|
pageId: "test-page",
|
||||||
|
extensionId: ext.name
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page.id).toEqual("test-page");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if target not found", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||||
|
pageId: "wrong-page",
|
||||||
|
extensionId: ext.name
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,20 +20,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import type { CatalogEntity } from "../common-api/catalog";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface CatalogEntityDetailComponents {
|
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
|
||||||
Details: React.ComponentType<any>;
|
entity: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogEntityDetailRegistration {
|
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||||
|
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: CatalogEntityDetailComponents;
|
components: CatalogEntityDetailComponents<T>;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
|
||||||
getItemsForKind(kind: string, apiVersion: string) {
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
const items = this.getItems().filter((item) => {
|
const items = this.getItems().filter((item) => {
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// layouts
|
// layouts
|
||||||
|
export * from "../../renderer/components/layout/main-layout";
|
||||||
|
export * from "../../renderer/components/layout/setting-layout";
|
||||||
export * from "../../renderer/components/layout/page-layout";
|
export * from "../../renderer/components/layout/page-layout";
|
||||||
export * from "../../renderer/components/layout/wizard-layout";
|
export * from "../../renderer/components/layout/wizard-layout";
|
||||||
export * from "../../renderer/components/layout/tab-layout";
|
export * from "../../renderer/components/layout/tab-layout";
|
||||||
@ -65,5 +67,5 @@ export * from "../../renderer/components/+events/kube-event-details";
|
|||||||
|
|
||||||
// specific exports
|
// specific exports
|
||||||
export * from "../../renderer/components/status-brick";
|
export * from "../../renderer/components/status-brick";
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
|
||||||
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||||
|
|||||||
@ -22,6 +22,14 @@
|
|||||||
import { UserStore } from "../../common/user-store";
|
import { UserStore } from "../../common/user-store";
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
|
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
enum ServiceResult {
|
enum ServiceResult {
|
||||||
Success,
|
Success,
|
||||||
@ -70,6 +78,10 @@ function getHandler() {
|
|||||||
|
|
||||||
describe("ContextHandler", () => {
|
describe("ContextHandler", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockFs({
|
||||||
|
"tmp": {}
|
||||||
|
});
|
||||||
|
|
||||||
PrometheusProviderRegistry.createInstance();
|
PrometheusProviderRegistry.createInstance();
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
@ -77,6 +89,7 @@ describe("ContextHandler", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
PrometheusProviderRegistry.resetInstance();
|
PrometheusProviderRegistry.resetInstance();
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPrometheusService", () => {
|
describe("getPrometheusService", () => {
|
||||||
|
|||||||
@ -46,7 +46,8 @@ jest.mock("winston", () => ({
|
|||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
getPath: () => "/foo",
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -77,8 +78,6 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
|
|||||||
describe("kube auth proxy tests", () => {
|
describe("kube auth proxy tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
UserStore.resetInstance();
|
|
||||||
UserStore.createInstance();
|
|
||||||
|
|
||||||
const mockMinikubeConfig = {
|
const mockMinikubeConfig = {
|
||||||
"minikube-config.yml": JSON.stringify({
|
"minikube-config.yml": JSON.stringify({
|
||||||
@ -102,13 +101,16 @@ describe("kube auth proxy tests", () => {
|
|||||||
}],
|
}],
|
||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
}),
|
||||||
|
"tmp": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockMinikubeConfig);
|
mockFs(mockMinikubeConfig);
|
||||||
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
72
src/main/catalog-sources/general.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { GeneralEntity } from "../../common/catalog-entities/general";
|
||||||
|
import { catalogURL, preferencesURL } from "../../common/routes";
|
||||||
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
|
|
||||||
|
export const catalogEntity = new GeneralEntity({
|
||||||
|
metadata: {
|
||||||
|
uid: "catalog-entity",
|
||||||
|
name: "Catalog",
|
||||||
|
source: "app",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
path: catalogURL(),
|
||||||
|
icon: {
|
||||||
|
material: "view_list",
|
||||||
|
background: "#3d90ce"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "active",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const preferencesEntity = new GeneralEntity({
|
||||||
|
metadata: {
|
||||||
|
uid: "preferences-entity",
|
||||||
|
name: "Preferences",
|
||||||
|
source: "app",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
path: preferencesURL(),
|
||||||
|
icon: {
|
||||||
|
material: "settings",
|
||||||
|
background: "#3d90ce"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "active",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const generalEntities = observable([
|
||||||
|
catalogEntity,
|
||||||
|
preferencesEntity
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function syncGeneralEntities() {
|
||||||
|
catalogEntityRegistry.addObservableSource("lens:general", generalEntities);
|
||||||
|
}
|
||||||
@ -19,5 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { initializeWeblinks } from "./weblinks";
|
export { syncWeblinks } from "./weblinks";
|
||||||
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
export { KubeconfigSyncManager } from "./kubeconfig-sync";
|
||||||
|
export { syncGeneralEntities } from "./general";
|
||||||
|
|||||||
@ -69,7 +69,7 @@ async function validateLink(link: WebLink) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function initializeWeblinks() {
|
export function syncWeblinks() {
|
||||||
const weblinkStore = WeblinkStore.getInstance();
|
const weblinkStore = WeblinkStore.getInstance();
|
||||||
const weblinks = observable.array(defaultLinks);
|
const weblinks = observable.array(defaultLinks);
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
||||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog";
|
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import { iter } from "../../common/utils";
|
import { iter } from "../../common/utils";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@ -62,6 +62,10 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
return items as T[];
|
return items as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
||||||
|
return this.getItemsForApiKind(apiVersion, kind);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
|||||||
@ -23,12 +23,12 @@ import "../common/cluster-ipc";
|
|||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
import { catalogEntityRegistry } from "./catalog";
|
import { catalogEntityRegistry } from "./catalog";
|
||||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesClusterStatusPhase } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
|
|
||||||
@ -102,13 +102,14 @@ export class ClusterManager extends Singleton {
|
|||||||
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
this.updateEntityStatus(entity, cluster);
|
||||||
|
|
||||||
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
|
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
entity.metadata.labels = {
|
||||||
|
...entity.metadata.labels,
|
||||||
|
...cluster.labels,
|
||||||
|
};
|
||||||
|
entity.metadata.distro = cluster.distribution;
|
||||||
|
entity.metadata.kubeVersion = cluster.version;
|
||||||
|
entity.metadata.name = cluster.name;
|
||||||
entity.spec.metrics ||= { source: "local" };
|
entity.spec.metrics ||= { source: "local" };
|
||||||
|
|
||||||
if (entity.spec.metrics.source === "local") {
|
if (entity.spec.metrics.source === "local") {
|
||||||
@ -119,7 +120,12 @@ export class ClusterManager extends Singleton {
|
|||||||
entity.spec.metrics.prometheus = prometheus;
|
entity.spec.metrics.prometheus = prometheus;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.spec.iconData = cluster.preferences.icon;
|
if (cluster.preferences.icon) {
|
||||||
|
entity.spec.icon ??= {};
|
||||||
|
entity.spec.icon.src = cluster.preferences.icon;
|
||||||
|
} else {
|
||||||
|
entity.spec.icon = null;
|
||||||
|
}
|
||||||
|
|
||||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||||
}
|
}
|
||||||
@ -130,7 +136,22 @@ export class ClusterManager extends Singleton {
|
|||||||
entity.status.phase = "deleting";
|
entity.status.phase = "deleting";
|
||||||
entity.status.enabled = false;
|
entity.status.enabled = false;
|
||||||
} else {
|
} else {
|
||||||
entity.status.phase = cluster?.accessible ? "connected" : "disconnected";
|
entity.status.phase = ((): KubernetesClusterStatusPhase => {
|
||||||
|
if (!cluster) {
|
||||||
|
return "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cluster.accessible) {
|
||||||
|
return "connected";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.disconnected) {
|
||||||
|
return "connecting";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "disconnected";
|
||||||
|
})();
|
||||||
|
|
||||||
entity.status.enabled = true;
|
entity.status.enabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,12 +236,15 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
|||||||
name: cluster.name,
|
name: cluster.name,
|
||||||
source: "local",
|
source: "local",
|
||||||
labels: {
|
labels: {
|
||||||
distro: cluster.distribution,
|
...cluster.labels,
|
||||||
}
|
},
|
||||||
|
distro: cluster.distribution,
|
||||||
|
kubeVersion: cluster.version,
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
kubeconfigPath: cluster.kubeConfigPath,
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
kubeconfigContext: cluster.contextName
|
kubeconfigContext: cluster.contextName,
|
||||||
|
icon: {}
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
|
|||||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
|
import { initialNodeShellImage } from "../common/cluster-store";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -120,6 +121,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
@observable workspace: string;
|
@observable workspace: string;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@observable workspaces: string[];
|
||||||
/**
|
/**
|
||||||
* Kubernetes API server URL
|
* Kubernetes API server URL
|
||||||
*
|
*
|
||||||
@ -230,8 +235,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes distribution
|
||||||
|
*/
|
||||||
@computed get distribution(): string {
|
@computed get distribution(): string {
|
||||||
return this.metadata.distribution?.toString() || "unknown";
|
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes version
|
||||||
|
*/
|
||||||
|
@computed get version(): string {
|
||||||
|
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,13 +261,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return toJS({ prometheus, prometheusProvider });
|
return toJS({ prometheus, prometheusProvider });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kubernetes version
|
|
||||||
*/
|
|
||||||
get version(): string {
|
|
||||||
return String(this.metadata?.version ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.id = model.id;
|
this.id = model.id;
|
||||||
@ -294,6 +302,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.workspace = model.workspace;
|
this.workspace = model.workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.workspaces) {
|
||||||
|
this.workspaces = model.workspaces;
|
||||||
|
}
|
||||||
|
|
||||||
if (model.contextName) {
|
if (model.contextName) {
|
||||||
this.contextName = model.contextName;
|
this.contextName = model.contextName;
|
||||||
}
|
}
|
||||||
@ -589,6 +601,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
contextName: this.contextName,
|
contextName: this.contextName,
|
||||||
kubeConfigPath: this.kubeConfigPath,
|
kubeConfigPath: this.kubeConfigPath,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
|
workspaces: this.workspaces,
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
metadata: this.metadata,
|
metadata: this.metadata,
|
||||||
accessibleNamespaces: this.accessibleNamespaces,
|
accessibleNamespaces: this.accessibleNamespaces,
|
||||||
@ -736,4 +749,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
||||||
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get nodeShellImage(): string {
|
||||||
|
return this.preferences.nodeShellImage || initialNodeShellImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
get imagePullSecret(): string {
|
||||||
|
return this.preferences.imagePullSecret || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||||
});
|
});
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,6 +34,36 @@ export class HelmChartManager {
|
|||||||
switch (this.repo.name) {
|
switch (this.repo.name) {
|
||||||
case "stable":
|
case "stable":
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
"invalid-semver": [
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.3.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver but more",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.4.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
],
|
||||||
"apm-server": [
|
"apm-server": [
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
|
|||||||
@ -62,6 +62,36 @@ describe("Helm Service tests", () => {
|
|||||||
digest: "test"
|
digest: "test"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"invalid-semver": [
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.4.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.3.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver but more",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
],
|
||||||
"redis": [
|
"redis": [
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
|
|||||||
@ -19,13 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import semver from "semver";
|
import semver, { SemVer } from "semver";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { HelmRepoManager } from "./helm-repo-manager";
|
import { HelmRepoManager } from "./helm-repo-manager";
|
||||||
import { HelmChartManager } from "./helm-chart-manager";
|
import { HelmChartManager } from "./helm-chart-manager";
|
||||||
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||||
|
import { iter, sortCompareChartVersions } from "../../common/utils";
|
||||||
|
|
||||||
interface GetReleaseValuesArgs {
|
interface GetReleaseValuesArgs {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -132,28 +133,55 @@ class HelmService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
||||||
const groups = new Map(Object.entries(chartGroups));
|
return Object.fromEntries(
|
||||||
|
iter.filterMap(
|
||||||
|
Object.entries(chartGroups),
|
||||||
|
([name, charts]) => {
|
||||||
|
for (const chart of charts) {
|
||||||
|
if (chart.deprecated) {
|
||||||
|
// ignore chart group if any chart is deprecated
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [chartName, charts] of groups) {
|
return [name, charts];
|
||||||
if (charts[0].deprecated) {
|
}
|
||||||
groups.delete(chartName);
|
)
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortCharts(charts: HelmChart[]) {
|
||||||
|
interface ExtendedHelmChart extends HelmChart {
|
||||||
|
__version: SemVer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(groups);
|
const chartsWithVersion = Array.from(
|
||||||
|
iter.map(
|
||||||
|
charts,
|
||||||
|
(chart => {
|
||||||
|
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
|
||||||
|
|
||||||
|
if (!__version) {
|
||||||
|
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
||||||
|
}
|
||||||
|
|
||||||
|
(chart as ExtendedHelmChart).__version = __version;
|
||||||
|
|
||||||
|
return chart as ExtendedHelmChart;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return chartsWithVersion
|
||||||
|
.sort(sortCompareChartVersions)
|
||||||
|
.map(chart => (delete chart.__version, chart as HelmChart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
||||||
for (const key in chartGroups) {
|
return Object.fromEntries(
|
||||||
chartGroups[key] = chartGroups[key].sort((first, second) => {
|
Object.entries(chartGroups)
|
||||||
const firstVersion = semver.coerce(first.version || 0);
|
.map(([name, charts]) => [name, this.sortCharts(charts)])
|
||||||
const secondVersion = semver.coerce(second.version || 0);
|
);
|
||||||
|
|
||||||
return semver.compare(secondVersion, firstVersion);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return chartGroups;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,11 +48,22 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
|||||||
import { pushCatalogToRenderer } from "./catalog-pusher";
|
import { pushCatalogToRenderer } from "./catalog-pusher";
|
||||||
import { catalogEntityRegistry } from "./catalog";
|
import { catalogEntityRegistry } from "./catalog";
|
||||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||||
import { KubeconfigSyncManager } from "./catalog-sources";
|
import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources";
|
||||||
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
||||||
import configurePackages from "../common/configure-packages";
|
import configurePackages from "../common/configure-packages";
|
||||||
import { PrometheusProviderRegistry } from "./prometheus";
|
import { PrometheusProviderRegistry } from "./prometheus";
|
||||||
import * as initializers from "./initializers";
|
import * as initializers from "./initializers";
|
||||||
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
|
import { HotbarStore } from "../common/hotbar-store";
|
||||||
|
import { UserStore } from "../common/user-store";
|
||||||
|
import { WeblinkStore } from "../common/weblink-store";
|
||||||
|
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||||
|
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
import { SentryInit } from "../common/sentry";
|
||||||
|
|
||||||
|
// This has to be called before start using winton-based logger
|
||||||
|
// For example, before any logger.log
|
||||||
|
SentryInit();
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
const cleanup = disposer();
|
const cleanup = disposer();
|
||||||
@ -123,8 +134,28 @@ app.on("ready", async () => {
|
|||||||
PrometheusProviderRegistry.createInstance();
|
PrometheusProviderRegistry.createInstance();
|
||||||
initializers.initPrometheusProviderRegistry();
|
initializers.initPrometheusProviderRegistry();
|
||||||
|
|
||||||
await initializers.initializeStores();
|
/**
|
||||||
initializers.initializeWeblinks();
|
* The following sync MUST be done before HotbarStore creation, because that
|
||||||
|
* store has migrations that will remove items that previous migrations add
|
||||||
|
* if this is not presant
|
||||||
|
*/
|
||||||
|
syncGeneralEntities();
|
||||||
|
|
||||||
|
logger.info("💾 Loading stores");
|
||||||
|
|
||||||
|
UserStore.createInstance().startMainReactions();
|
||||||
|
|
||||||
|
// ClusterStore depends on: UserStore
|
||||||
|
ClusterStore.createInstance().provideInitialFromMain();
|
||||||
|
|
||||||
|
// HotbarStore depends on: ClusterStore
|
||||||
|
HotbarStore.createInstance();
|
||||||
|
|
||||||
|
ExtensionsStore.createInstance();
|
||||||
|
FilesystemProvisionerStore.createInstance();
|
||||||
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
|
syncWeblinks();
|
||||||
|
|
||||||
HelmRepoManager.createInstance(); // create the instance
|
HelmRepoManager.createInstance(); // create the instance
|
||||||
|
|
||||||
@ -132,7 +163,7 @@ app.on("ready", async () => {
|
|||||||
handleWsUpgrade,
|
handleWsUpgrade,
|
||||||
req => ClusterManager.getInstance().getClusterForRequest(req),
|
req => ClusterManager.getInstance().getClusterForRequest(req),
|
||||||
);
|
);
|
||||||
|
|
||||||
ClusterManager.createInstance().init();
|
ClusterManager.createInstance().init();
|
||||||
KubeconfigSyncManager.createInstance();
|
KubeconfigSyncManager.createInstance();
|
||||||
|
|
||||||
@ -186,10 +217,6 @@ app.on("ready", async () => {
|
|||||||
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ExtensionLoader.getInstance().whenLoaded.then(() => {
|
|
||||||
LensProtocolRouterMain.getInstance().extensionsLoaded = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("🧩 Initializing extensions");
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
// call after windowManager to see splash earlier
|
||||||
|
|||||||
@ -22,5 +22,3 @@
|
|||||||
export * from "./registries";
|
export * from "./registries";
|
||||||
export * from "./metrics-providers";
|
export * from "./metrics-providers";
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./weblinks";
|
|
||||||
export * from "./stores";
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IpcMainInvokeEvent } from "electron";
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
||||||
@ -50,9 +50,9 @@ export function initIpcMainHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
||||||
const entity = catalogEntityRegistry.getById<KubernetesCluster>(clusterId);
|
const entity = catalogEntityRegistry.getById(clusterId);
|
||||||
|
|
||||||
for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) {
|
for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) {
|
||||||
kubeEntity.status.active = false;
|
kubeEntity.status.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { HotbarStore } from "../../common/hotbar-store";
|
|
||||||
import { ClusterStore } from "../../common/cluster-store";
|
|
||||||
import { UserStore } from "../../common/user-store";
|
|
||||||
import { ExtensionsStore } from "../../extensions/extensions-store";
|
|
||||||
import { FilesystemProvisionerStore } from "../extension-filesystem";
|
|
||||||
import { WeblinkStore } from "../../common/weblink-store";
|
|
||||||
import logger from "../logger";
|
|
||||||
|
|
||||||
export async function initializeStores() {
|
|
||||||
const userStore = UserStore.createInstance();
|
|
||||||
const clusterStore = ClusterStore.createInstance();
|
|
||||||
const hotbarStore = HotbarStore.createInstance();
|
|
||||||
const extensionsStore = ExtensionsStore.createInstance();
|
|
||||||
const filesystemStore = FilesystemProvisionerStore.createInstance();
|
|
||||||
const weblinkStore = WeblinkStore.createInstance();
|
|
||||||
|
|
||||||
logger.info("💾 Loading stores");
|
|
||||||
// preload
|
|
||||||
await Promise.all([
|
|
||||||
userStore.load(),
|
|
||||||
clusterStore.load(),
|
|
||||||
hotbarStore.load(),
|
|
||||||
extensionsStore.load(),
|
|
||||||
filesystemStore.load(),
|
|
||||||
weblinkStore.load()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@ -27,7 +27,7 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
readonly id: string = "helm";
|
readonly id: string = "helm";
|
||||||
readonly name: string = "Helm";
|
readonly name: string = "Helm";
|
||||||
readonly rateAccuracy: string = "5m";
|
readonly rateAccuracy: string = "5m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
readonly id: string = "lens";
|
readonly id: string = "lens";
|
||||||
readonly name: string = "Lens";
|
readonly name: string = "Lens";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = false;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly id: string = "operator";
|
readonly id: string = "operator";
|
||||||
readonly name: string = "Prometheus Operator";
|
readonly name: string = "Prometheus Operator";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
readonly id: string = "stacklight";
|
readonly id: string = "stacklight";
|
||||||
readonly name: string = "Stacklight";
|
readonly name: string = "Stacklight";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -28,9 +28,17 @@ import { LensExtension } from "../../../extensions/main-api";
|
|||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
||||||
import { LensProtocolRouterMain } from "../router";
|
import { LensProtocolRouterMain } from "../router";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("../../../common/ipc");
|
jest.mock("../../../common/ipc");
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function throwIfDefined(val: any): void {
|
function throwIfDefined(val: any): void {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
throw val;
|
throw val;
|
||||||
@ -39,12 +47,14 @@ function throwIfDefined(val: any): void {
|
|||||||
|
|
||||||
describe("protocol router tests", () => {
|
describe("protocol router tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockFs({
|
||||||
|
"tmp": {}
|
||||||
|
});
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
ExtensionLoader.createInstance();
|
ExtensionLoader.createInstance();
|
||||||
|
|
||||||
const lpr = LensProtocolRouterMain.createInstance();
|
const lpr = LensProtocolRouterMain.createInstance();
|
||||||
|
|
||||||
lpr.extensionsLoaded = true;
|
|
||||||
lpr.rendererLoaded = true;
|
lpr.rendererLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,23 +64,24 @@ describe("protocol router tests", () => {
|
|||||||
ExtensionsStore.resetInstance();
|
ExtensionsStore.resetInstance();
|
||||||
ExtensionLoader.resetInstance();
|
ExtensionLoader.resetInstance();
|
||||||
LensProtocolRouterMain.resetInstance();
|
LensProtocolRouterMain.resetInstance();
|
||||||
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on non-lens URLS", () => {
|
it("should throw on non-lens URLS", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(lpr.route("https://google.ca")).toBeUndefined();
|
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when host not internal or extension", () => {
|
it("should throw when host not internal or extension", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(lpr.route("lens://foobar")).toBeUndefined();
|
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
@ -103,13 +114,13 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app")).toBeUndefined();
|
expect(await lpr.route("lens://app")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -119,14 +130,14 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call handler if matches", () => {
|
it("should call handler if matches", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = true; });
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page")).toBeUndefined();
|
expect(await lpr.route("lens://app/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -135,7 +146,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler", () => {
|
it("should call most exact handler", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -143,7 +154,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -183,7 +194,7 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -251,7 +262,7 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -268,7 +279,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 3 found handlers", () => {
|
it("should call most exact handler with 3 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -278,7 +289,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
@ -287,7 +298,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 2 found handlers", () => {
|
it("should call most exact handler with 2 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -296,7 +307,7 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,6 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||||
|
|
||||||
@observable rendererLoaded = false;
|
@observable rendererLoaded = false;
|
||||||
@observable extensionsLoaded = false;
|
|
||||||
|
|
||||||
protected disposers = disposer();
|
protected disposers = disposer();
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
* This will send an IPC message to the renderer router to do the same
|
* This will send an IPC message to the renderer router to do the same
|
||||||
* in the renderer.
|
* in the renderer.
|
||||||
*/
|
*/
|
||||||
public route(rawUrl: string) {
|
public async route(rawUrl: string) {
|
||||||
try {
|
try {
|
||||||
const url = new URLParse(rawUrl, true);
|
const url = new URLParse(rawUrl, true);
|
||||||
|
|
||||||
@ -82,15 +81,15 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
|
||||||
const routeInternally = checkHost(url);
|
const routeInternally = checkHost(url);
|
||||||
|
|
||||||
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
||||||
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
|
|
||||||
|
|
||||||
if (routeInternally) {
|
if (routeInternally) {
|
||||||
this._routeToInternal(url);
|
this._routeToInternal(url);
|
||||||
} else {
|
} else {
|
||||||
this.disposers.push(when(() => this.extensionsLoaded, () => this._routeToExtension(url)));
|
await this._routeToExtension(url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||||
|
|||||||
@ -168,7 +168,10 @@ export class LensProxy extends Singleton {
|
|||||||
if (!res.headersSent && req.url) {
|
if (!res.headersSent && req.url) {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
if (url.searchParams.has("watch")) res.flushHeaders();
|
if (url.searchParams.has("watch")) {
|
||||||
|
res.statusCode = proxyRes.statusCode;
|
||||||
|
res.flushHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,6 +180,8 @@ export class LensProxy extends Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url });
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||||
|
|
||||||
@ -196,7 +201,7 @@ export class LensProxy extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res.writeHead(500).end("Oops, something went wrong.");
|
res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export class LocalShellSession extends ShellSession {
|
|||||||
case "bash":
|
case "bash":
|
||||||
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
||||||
case "fish":
|
case "fish":
|
||||||
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPathP}"`];
|
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return ["--login"];
|
return ["--login"];
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -77,13 +77,16 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}],
|
}],
|
||||||
containers: [{
|
containers: [{
|
||||||
name: "shell",
|
name: "shell",
|
||||||
image: "docker.io/alpine:3.13",
|
image: this.cluster.nodeShellImage,
|
||||||
securityContext: {
|
securityContext: {
|
||||||
privileged: true,
|
privileged: true,
|
||||||
},
|
},
|
||||||
command: ["nsenter"],
|
command: ["nsenter"],
|
||||||
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
||||||
}],
|
}],
|
||||||
|
imagePullSecrets: [{
|
||||||
|
name: this.cluster.imagePullSecret,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
interface GetPortArgs {
|
interface GetPortArgs {
|
||||||
/**
|
/**
|
||||||
@ -47,11 +48,15 @@ interface GetPortArgs {
|
|||||||
* @returns A Promise for port number
|
* @returns A Promise for port number
|
||||||
*/
|
*/
|
||||||
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
||||||
|
const logLines: string[] = [];
|
||||||
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
return new Promise<number>((resolve, reject) => {
|
||||||
const handler = (data: any) => {
|
const handler = (data: any) => {
|
||||||
const logItem: string = data.toString();
|
const logItem: string = data.toString();
|
||||||
const match = logItem.match(args.lineRegex);
|
const match = logItem.match(args.lineRegex);
|
||||||
|
|
||||||
|
logLines.push(logItem);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// use unknown protocol so that there is no default port
|
// use unknown protocol so that there is no default port
|
||||||
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
||||||
@ -64,6 +69,7 @@ export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number
|
|||||||
};
|
};
|
||||||
const timeoutID = setTimeout(() => {
|
const timeoutID = setTimeout(() => {
|
||||||
stream.off("data", handler);
|
stream.off("data", handler);
|
||||||
|
logger.warn(`[getPortFrom]: failed to retrieve port via ${args.lineRegex.toString()}: ${logLines}`);
|
||||||
reject(new Error("failed to retrieve port from stream"));
|
reject(new Error("failed to retrieve port from stream"));
|
||||||
}, args.timeout ?? 5000);
|
}, args.timeout ?? 5000);
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export interface EnvironmentVariables {
|
|||||||
let shellSyncFailed = false;
|
let shellSyncFailed = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to get the shell environment per the user's existing startup scripts.
|
* Attempts to get the shell environment per the user's existing startup scripts.
|
||||||
* If the environment can't be retrieved after 5 seconds an error message is logged.
|
* If the environment can't be retrieved after 5 seconds an error message is logged.
|
||||||
* Subsequent calls after such a timeout simply log an error message without trying
|
* Subsequent calls after such a timeout simply log an error message without trying
|
||||||
* to get the environment, unless forceRetry is true.
|
* to get the environment, unless forceRetry is true.
|
||||||
@ -52,7 +52,7 @@ export async function shellEnv(shell?: string, forceRetry = false) : Promise<Env
|
|||||||
shellEnvironment(shell),
|
shellEnvironment(shell),
|
||||||
new Promise((_resolve, reject) => setTimeout(() => {
|
new Promise((_resolve, reject) => setTimeout(() => {
|
||||||
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
||||||
}, 5_000))
|
}, 30_000))
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`shellEnv: ${error}`);
|
logger.error(`shellEnv: ${error}`);
|
||||||
@ -61,6 +61,6 @@ export async function shellEnv(shell?: string, forceRetry = false) : Promise<Env
|
|||||||
} else {
|
} else {
|
||||||
logger.error("shellSync(): Resolving shell environment took too long. Please review your shell configuration.");
|
logger.error("shellSync(): Resolving shell environment took too long. Please review your shell configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return envVars;
|
return envVars;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import path from "path";
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import type { ClusterModel } from "../../common/cluster-store";
|
import type { ClusterModel } from "../../common/cluster-store";
|
||||||
import { MigrationDeclaration, migrationLog } from "../helpers";
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
interface Pre500WorkspaceStoreModel {
|
interface Pre500WorkspaceStoreModel {
|
||||||
workspaces: {
|
workspaces: {
|
||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
version: "5.0.0-beta.10",
|
version: "5.0.0-beta.10",
|
||||||
run(store) {
|
run(store) {
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
|
const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
|
||||||
const workspaces = new Map<string, string>(); // mapping from WorkspaceId to name
|
const workspaces = new Map<string, string>(); // mapping from WorkspaceId to name
|
||||||
@ -45,9 +45,7 @@ export default {
|
|||||||
workspaces.set(id, name);
|
workspaces.set(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationLog("workspaces", JSON.stringify([...workspaces.entries()]));
|
const clusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||||
|
|
||||||
const clusters: ClusterModel[] = store.get("clusters");
|
|
||||||
|
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
||||||
@ -58,8 +56,6 @@ export default {
|
|||||||
|
|
||||||
store.set("clusters", clusters);
|
store.set("clusters", clusters);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
migrationLog("error", error.path);
|
|
||||||
|
|
||||||
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
|
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
|
||||||
// ignore lens-workspace-store.json being missing
|
// ignore lens-workspace-store.json being missing
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
133
src/migrations/cluster-store/5.0.0-beta.13.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-store";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
import { generateNewIdFor } from "../utils";
|
||||||
|
import path from "path";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { moveSync, removeSync } from "fs-extra";
|
||||||
|
|
||||||
|
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
|
||||||
|
if (left.prometheus && left.prometheusProvider) {
|
||||||
|
return {
|
||||||
|
prometheus: left.prometheus,
|
||||||
|
prometheusProvider: left.prometheusProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right.prometheus && right.prometheusProvider) {
|
||||||
|
return {
|
||||||
|
prometheus: right.prometheus,
|
||||||
|
prometheusProvider: right.prometheusProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
|
||||||
|
return {
|
||||||
|
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||||
|
clusterName: left.clusterName || right.clusterName || undefined,
|
||||||
|
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||||
|
icon: left.icon || right.icon || undefined,
|
||||||
|
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||||
|
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||||
|
...mergePrometheusPreferences(left, right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
|
||||||
|
return {
|
||||||
|
...right,
|
||||||
|
...left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSet(...iterables: Iterable<string>[]): string[] {
|
||||||
|
const res = new Set<string>();
|
||||||
|
|
||||||
|
for (const iterable of iterables) {
|
||||||
|
for (const val of iterable) {
|
||||||
|
res.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...res];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel {
|
||||||
|
return {
|
||||||
|
id: prev.id,
|
||||||
|
kubeConfigPath: prev.kubeConfigPath,
|
||||||
|
contextName: prev.contextName,
|
||||||
|
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||||
|
metadata: prev.metadata,
|
||||||
|
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
|
||||||
|
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||||
|
workspace: prev.workspace || right.workspace,
|
||||||
|
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: string, oldId: string }): void {
|
||||||
|
const oldPath = path.resolve(folder, `${oldId}.json`);
|
||||||
|
const newPath = path.resolve(folder, `${newId}.json`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
moveSync(oldPath, newPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (String(error).includes("dest already exists")) {
|
||||||
|
migrationLog(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`);
|
||||||
|
removeSync(oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: "5.0.0-beta.13",
|
||||||
|
run(store) {
|
||||||
|
const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
|
||||||
|
|
||||||
|
const oldClusters: ClusterModel[] = store.get("clusters");
|
||||||
|
const clusters = new Map<string, ClusterModel>();
|
||||||
|
|
||||||
|
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||||
|
const newId = generateNewIdFor(cluster);
|
||||||
|
|
||||||
|
if (clusters.has(newId)) {
|
||||||
|
migrationLog(`Duplicate entries for ${newId}`, { oldId });
|
||||||
|
clusters.set(newId, mergeClusterModel(clusters.get(newId), cluster));
|
||||||
|
} else {
|
||||||
|
migrationLog(`First entry for ${newId}`, { oldId });
|
||||||
|
clusters.set(newId, {
|
||||||
|
...cluster,
|
||||||
|
id: newId,
|
||||||
|
workspaces: [cluster.workspace].filter(Boolean),
|
||||||
|
});
|
||||||
|
moveStorageFolder({ folder, newId, oldId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.set("clusters", [...clusters.values()]);
|
||||||
|
}
|
||||||
|
} as MigrationDeclaration;
|
||||||
@ -31,6 +31,7 @@ import version270Beta0 from "./2.7.0-beta.0";
|
|||||||
import version270Beta1 from "./2.7.0-beta.1";
|
import version270Beta1 from "./2.7.0-beta.1";
|
||||||
import version360Beta1 from "./3.6.0-beta.1";
|
import version360Beta1 from "./3.6.0-beta.1";
|
||||||
import version500Beta10 from "./5.0.0-beta.10";
|
import version500Beta10 from "./5.0.0-beta.10";
|
||||||
|
import version500Beta13 from "./5.0.0-beta.13";
|
||||||
import snap from "./snap";
|
import snap from "./snap";
|
||||||
|
|
||||||
export default joinMigrations(
|
export default joinMigrations(
|
||||||
@ -42,5 +43,6 @@ export default joinMigrations(
|
|||||||
version270Beta1,
|
version270Beta1,
|
||||||
version360Beta1,
|
version360Beta1,
|
||||||
version500Beta10,
|
version500Beta10,
|
||||||
|
version500Beta13,
|
||||||
snap,
|
snap,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -44,8 +44,10 @@ export function joinMigrations(...declarations: MigrationDeclaration[]): Migrati
|
|||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
iter.map(
|
iter.map(
|
||||||
migrations,
|
migrations,
|
||||||
([v, fns]) => [v, (store: Conf<any>) => {
|
([v, fns]) => [v, (store: Conf<any>) => {
|
||||||
|
migrationLog(`Running ${v} migration for ${store.path}`);
|
||||||
|
|
||||||
for (const fn of fns) {
|
for (const fn of fns) {
|
||||||
fn(store);
|
fn(store);
|
||||||
}
|
}
|
||||||
|
|||||||