diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 6bb07489ec..a4108e6762 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -1,19 +1,7 @@ variables: YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn -pr: - branches: - include: - - master - - releases/* - paths: - exclude: - - .github/* - - docs/* - - mkdocs/* +pr: none trigger: - branches: - include: - - '*' tags: include: - "*" @@ -57,11 +45,6 @@ jobs: displayName: Run tests - script: make test-extensions displayName: Run In-tree Extension tests - - bash: | - rm -rf extensions/telemetry - make integration-win - git checkout extensions/telemetry - displayName: Run integration tests - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build @@ -69,6 +52,8 @@ jobs: WIN_CSC_LINK: $(WIN_CSC_LINK) WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD) GH_TOKEN: $(GH_TOKEN) + AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) + AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) - job: macOS pool: vmImage: macOS-10.14 @@ -101,13 +86,6 @@ jobs: displayName: Run tests - script: make test-extensions displayName: Run In-tree Extension tests - - bash: | - rm -rf extensions/telemetry - make integration-mac - git checkout extensions/telemetry - displayName: Run integration tests - - script: make test-extensions - displayName: Run In-tree Extension tests - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build @@ -117,6 +95,8 @@ jobs: CSC_LINK: $(CSC_LINK) CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD) GH_TOKEN: $(GH_TOKEN) + AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) + AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) - job: Linux pool: vmImage: ubuntu-16.04 @@ -149,20 +129,6 @@ jobs: displayName: Run tests - script: make test-extensions displayName: Run In-tree Extension tests - - bash: | - sudo apt-get update - sudo apt-get install libgconf-2-4 conntrack -y - curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 - sudo install minikube-linux-amd64 /usr/local/bin/minikube - sudo minikube start --driver=none - # Although the kube and minikube config files are in placed $HOME they are owned by root - sudo chown -R $USER $HOME/.kube $HOME/.minikube - displayName: Install integration test dependencies - - bash: | - rm -rf extensions/telemetry - xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux - git checkout extensions/telemetry - displayName: Run integration tests - bash: | sudo chown root:root / sudo apt-get update && sudo apt-get install -y snapd @@ -178,6 +144,8 @@ jobs: condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: GH_TOKEN: $(GH_TOKEN) + AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) + AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) - script: make publish-npm displayName: Publish npm package condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000000..a77a36c653 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,17 @@ +# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates +# for config options + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 4 + reviewers: + - "lensapp/lens-maintainers" + labels: + - "dependencies" + versioning-strategy: + lockfile-only: false + increase: true diff --git a/.eslintrc.js b/.eslintrc.js index 3fd52c2465..57ee07348f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,8 @@ module.exports = { "avoidEscape": true, "allowTemplateLiterals": true, }], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "semi": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", @@ -101,6 +103,8 @@ module.exports = { }], "semi": "off", "@typescript-eslint/semi": ["error"], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", "template-curly-spacing": "error", @@ -162,6 +166,8 @@ module.exports = { }], "semi": "off", "@typescript-eslint/semi": ["error"], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", "template-curly-spacing": "error", diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..fccf0482de --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# This is the default code owners for the whole Lens repo +* @lensapp/lens-ide diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000000..02ae36727e --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,30 @@ +exclude-labels: + - 'skip-changelog' +categories: + - title: '🚀 Features' + labels: + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'bug' + - title: '🧰 Maintenance' + labels: + - 'chore' + - 'area/ci' + - 'area/tests' + - 'dependencies' + +template: | + ## Changes since $PREVIOUS_TAG + + $CHANGES + + ### Download + + - Lens v$RESOLVED_VERSION - Linux + - [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage) + - [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb) + - [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm) + - [Snapcraft](https://snapcraft.io/kontena-lens) + - [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg) + - [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0c6340d6a2..afbcc74367 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: '3.x' - + - name: Checkout Release from lens uses: actions/checkout@v2 with: @@ -83,12 +83,12 @@ jobs: mike deploy --push master - name: Get the release version - if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408) + if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - name: mkdocs deploy new release - if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408) + if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease run: | mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest mike set-default --push ${{ steps.get_version.outputs.VERSION }} diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml new file mode 100644 index 0000000000..dfa16b99c7 --- /dev/null +++ b/.github/workflows/maintenance.yml @@ -0,0 +1,22 @@ +name: "Maintenance" +on: + # So that PRs touching the same files as the push are updated + push: + # So that the `dirtyLabel` is removed if conflicts are resolve + # We recommend `pull_request_target` so that github secrets are available. + # In `pull_request` we wouldn't be able to change labels of fork PRs + pull_request_target: + types: [synchronize] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: check if prs are dirty + uses: eps1lon/actions-label-merge-conflict@releases/2.x + with: + dirtyLabel: "PR: needs rebase" + removeOnDirtyLabel: "PR: ready to ship" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." + commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly." diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000000..ec49fec6e5 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,16 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..d7f3959eb8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,83 @@ +name: Test +on: + - pull_request +jobs: + build: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-16.04, macos-10.15, windows-2019] + node-version: [12.x] + steps: + - name: Checkout Release from lens + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Using Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - run: make node_modules + name: Install dependencies + + - run: make build-npm + name: Generate npm package + + - run: make -j2 build-extensions + name: Build bundled extensions + + - run: make test + name: Run tests + + - run: make test-extensions + name: Run In-tree Extension tests + + - run: | + sudo apt-get update + sudo apt-get install libgconf-2-4 conntrack -y + curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + sudo install minikube-linux-amd64 /usr/local/bin/minikube + sudo minikube start --driver=none + # Although the kube and minikube config files are in placed $HOME they are owned by root + sudo chown -R $USER $HOME/.kube $HOME/.minikube + name: Install integration test dependencies + if: runner.os == 'Linux' + - run: | + set -e + rm -rf extensions/telemetry + xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux + git checkout extensions/telemetry + name: Run Linux integration tests + if: runner.os == 'Linux' + + - run: | + set -e + rm -rf extensions/telemetry + make integration-win + git checkout extensions/telemetry + name: Run Windows integration tests + shell: bash + if: runner.os == 'Windows' + + - run: | + set -e + rm -rf extensions/telemetry + make integration-mac + git checkout extensions/telemetry + name: Run macOS integration tests + if: runner.os == 'macOS' diff --git a/.gitignore b/.gitignore index 88d48ab5ee..d018f3b251 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,3 @@ types/extension-renderer-api.d.ts extensions/*/dist docs/extensions/api site/ -.vscode/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..7d7cef13d8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,57 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "protocol": "inspector", + "preLaunchTask": "compile-dev", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "runtimeArgs": [ + "--remote-debugging-port=9223", + "--inspect", + "." + ], + "outputCapture": "std" + }, + { + "name": "Renderer Process", + "type": "pwa-chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}", + "timeout": 30000 + }, + { + "name": "Integration Tests", + "type": "node", + "request": "launch", + "console": "integratedTerminal", + "runtimeArgs": [ + "${workspaceFolder}/node_modules/.bin/jest", + "--runInBand", + "integration" + ], + }, + { + "name": "Unit Tests", + "type": "node", + "request": "launch", + "internalConsoleOptions": "openOnSessionStart", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", + "args": [ + "--env=jsdom", + "-i", + "src" + ] + } + ], +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..03a7b306f7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "group": "build", + "command": "yarn", + "args": [ + "debug-build" + ], + "problemMatcher": [], + "label": "compile-dev", + "detail": "Compiles main and extension types" + } + ] +} \ No newline at end of file diff --git a/.yarnrc b/.yarnrc index 5119ded102..f11de8a2a9 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "9.1.0" +target "9.4.4" runtime "electron" diff --git a/LICENSE b/LICENSE index b7a6d74205..37da2d2e07 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,12 @@ -Copyright (c) 2020 Mirantis, Inc. +Copyright (c) 2021 Mirantis, Inc. Portions of this software are licensed as follows: -* All content residing under the "docs/" directory of this repository, if that +* All content residing under the "docs/" directory of this repository, if that directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license". -* All third party components incorporated into the Lens Software are licensed +* All third party components incorporated into the Lens Software are licensed under the original license provided by the owner of the applicable component. -* Content outside of the above mentioned directories or restrictions above is +* Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/Makefile b/Makefile index 362ef3b830..658be09690 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +CMD_ARGS = $(filter-out $@,$(MAKECMDGOALS)) + +%: + @: + EXTENSIONS_DIR = ./extensions extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}) extension_node_modules = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/node_modules) @@ -9,23 +14,23 @@ else DETECTED_OS := $(shell uname) endif -binaries/client: +binaries/client: node_modules yarn download-bins -node_modules: +node_modules: yarn.lock yarn install --frozen-lockfile yarn check --verify-tree --integrity -static/build/LensDev.html: +static/build/LensDev.html: node_modules yarn compile:renderer .PHONY: compile-dev -compile-dev: +compile-dev: node_modules yarn compile:main --cache yarn compile:renderer --cache .PHONY: dev -dev: node_modules binaries/client build-extensions static/build/LensDev.html +dev: binaries/client build-extensions static/build/LensDev.html yarn dev .PHONY: lint @@ -34,27 +39,30 @@ lint: .PHONY: test test: binaries/client - yarn test + yarn run jest $(or $(CMD_ARGS), "src") .PHONY: integration-linux -integration-linux: build-extension-types build-extensions +integration-linux: binaries/client build-extension-types build-extensions +# ifdef XDF_CONFIG_HOME +# rm -rf ${XDG_CONFIG_HOME}/.config/Lens +# else +# rm -rf ${HOME}/.config/Lens +# endif yarn build:linux yarn integration .PHONY: integration-mac -integration-mac: build-extension-types build-extensions +integration-mac: binaries/client build-extension-types build-extensions + # rm ${HOME}/Library/Application\ Support/Lens yarn build:mac yarn integration .PHONY: integration-win -integration-win: build-extension-types build-extensions +integration-win: binaries/client build-extension-types build-extensions + # rm %APPDATA%/Lens yarn build:win yarn integration -.PHONY: test-app -test-app: - yarn test - .PHONY: build build: node_modules binaries/client build-extensions ifeq "$(DETECTED_OS)" "Windows" @@ -70,7 +78,7 @@ $(extension_dists): src/extensions/npm/extensions/dist cd $(@:/dist=) && npm run build .PHONY: build-extensions -build-extensions: $(extension_node_modules) $(extension_dists) +build-extensions: node_modules $(extension_node_modules) $(extension_dists) .PHONY: test-extensions test-extensions: $(extension_node_modules) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..b1a54ec9d0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +Team Lens encourages users who become aware of a security vulnerability in Lens to contact Team Lens with details of the vulnerability. Team Lens has established an email address that should be used for reporting a vulnerability. Please send descriptions of any vulnerabilities found to security@k8slens.dev. Please include details on the software and hardware configuration of your system so that we can duplicate the issue being reported. + +Team Lens hopes that users encountering a new vulnerability will contact us privately as it is in the best interests of our users that Team Lens has an opportunity to investigate and confirm a suspected vulnerability before it becomes public knowledge. + +In the case of vulnerabilities found in third-party software components used in Lens, please also notify Team Lens as described above. diff --git a/__mocks__/imageMock.ts b/__mocks__/imageMock.ts index a099545376..f053ebf797 100644 --- a/__mocks__/imageMock.ts +++ b/__mocks__/imageMock.ts @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/__mocks__/styleMock.ts b/__mocks__/styleMock.ts index a099545376..f053ebf797 100644 --- a/__mocks__/styleMock.ts +++ b/__mocks__/styleMock.ts @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/build/icons/512x512.png b/build/icons/512x512.png index 2c953f6efd..e08b9f5b15 100644 Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ diff --git a/build/icons/512x512@2x.png b/build/icons/512x512@2x.png new file mode 100644 index 0000000000..da0aa8ae80 Binary files /dev/null and b/build/icons/512x512@2x.png differ diff --git a/docs/clusters/adding-clusters.md b/docs/clusters/adding-clusters.md index d153d8c9bf..c9a533a700 100644 --- a/docs/clusters/adding-clusters.md +++ b/docs/clusters/adding-clusters.md @@ -2,7 +2,7 @@ Add clusters by clicking the **Add Cluster** button in the left-side menu. -1. Click the **Add Cluster** button (indicated with a '+' icon). +1. Click the **Add Cluster** button (indicated with a '+' icon). Or [click here](lens://app/cluster). 2. Enter the path to your kubeconfig file. You'll need to have a kubeconfig file for the cluster you want to add. You can either browse for the path from the file system or or enter it directly. Selected [cluster contexts](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) are added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts. diff --git a/docs/extensions/README.md b/docs/extensions/README.md index 7866b65497..f8906d75ee 100644 --- a/docs/extensions/README.md +++ b/docs/extensions/README.md @@ -1,6 +1,9 @@ # Lens Extension API -Customize and enhance the Lens experience with the Lens Extension API. Use the extension API to create menus or page content. The same extension API was used to create many of Lens's core features. +Customize and enhance the Lens experience with the Lens Extension API. +Use the extension API to create menus or page content. +The same extension API was used to create many of Lens's core features. +To install your first extension you should goto the [extension page](lens://app/extensions) in lens. This documentation describes: diff --git a/docs/extensions/capabilities/color-reference.md b/docs/extensions/capabilities/color-reference.md index 660e0fe067..6a38ba861c 100644 --- a/docs/extensions/capabilities/color-reference.md +++ b/docs/extensions/capabilities/color-reference.md @@ -43,6 +43,7 @@ You can use theme-based CSS Variables to style an extension according to the act ## Button Colors - `--buttonPrimaryBackground`: button background color for primary actions. - `--buttonDefaultBackground`: default button background color. +- `--buttonLightBackground`: light button background color. - `--buttonAccentBackground`: accent button background color. - `--buttonDisabledBackground`: disabled button background color. diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md index 6fdb87db82..1410dfc148 100644 --- a/docs/extensions/capabilities/common-capabilities.md +++ b/docs/extensions/capabilities/common-capabilities.md @@ -40,7 +40,7 @@ This extension can register custom app menus that will be displayed on OS native Example: -``` typescript +```typescript import { LensMainExtension, windowManager } from "@k8slens/extensions" export default class ExampleMainExtension extends LensMainExtension { @@ -92,7 +92,7 @@ export default class ExampleMainExtension extends LensRendererExtension { This extension can register custom global pages (views) to Lens's main window. The global page is a full-screen page that hides all other content from a window. -``` typescript +```typescript import React from "react" import { Component, LensRendererExtension } from "@k8slens/extensions" import { ExamplePage } from "./src/example-page" @@ -123,7 +123,7 @@ export default class ExampleRendererExtension extends LensRendererExtension { This extension can register custom app preferences. It is responsible for storing a state for custom preferences. -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions" import { myCustomPreferencesStore } from "./src/my-custom-preferences-store" @@ -147,7 +147,7 @@ export default class ExampleRendererExtension extends LensRendererExtension { This extension can register custom cluster pages. These pages are visible in a cluster menu when a cluster is opened. -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions"; import { ExampleIcon, ExamplePage } from "./src/page" @@ -180,7 +180,7 @@ export default class ExampleExtension extends LensRendererExtension { This extension can register installable features for a cluster. These features are visible in the "Cluster Settings" page. -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions" import { MyCustomFeature } from "./src/my-custom-feature" @@ -209,18 +209,20 @@ export default class ExampleExtension extends LensRendererExtension { This extension can register custom icons and text to a status bar area. -``` typescript +```typescript import React from "react"; import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions"; export default class ExampleExtension extends LensRendererExtension { statusBarItems = [ { - item: ( -
this.navigate("/example-page")} > - -
- ) + components: { + Item: ( +
this.navigate("/example-page")} > + +
+ ) + } } ] } @@ -231,7 +233,7 @@ export default class ExampleExtension extends LensRendererExtension { This extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions. -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions"; import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item" @@ -254,7 +256,7 @@ export default class ExampleExtension extends LensRendererExtension { This extension can register custom details (content) for specified Kubernetes kinds/apiVersions. -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions"; import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details" diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index 8db209dc82..d288f0cd64 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -21,12 +21,13 @@ Each guide or code sample includes the following: | [Components](components.md) | | | [KubeObjectListLayout](kube-object-list-layout.md) | | | [Working with mobx](working-with-mobx.md) | | +| [Protocol Handlers](protocol-handlers.md) | | ## Samples | Sample | APIs | | ----- | ----- | -[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | [minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension
Store.clusterStore
Store.workspaceStore | [styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | [styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md index 46524e97d5..3433f98e6f 100644 --- a/docs/extensions/guides/generator.md +++ b/docs/extensions/guides/generator.md @@ -42,7 +42,7 @@ Next, you'll try changing the way the new menu item appears in the UI. You'll ch Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`: -```tsx +```typescript clusterPageMenus = [ { target: { pageId: "hello" }, diff --git a/docs/extensions/guides/images/clusterpagemenus.png b/docs/extensions/guides/images/clusterpagemenus.png new file mode 100644 index 0000000000..3ed1c79e5b Binary files /dev/null and b/docs/extensions/guides/images/clusterpagemenus.png differ diff --git a/docs/extensions/guides/images/globalpagemenus.png b/docs/extensions/guides/images/globalpagemenus.png new file mode 100644 index 0000000000..e986cc32e9 Binary files /dev/null and b/docs/extensions/guides/images/globalpagemenus.png differ diff --git a/docs/extensions/guides/images/routing-diag.png b/docs/extensions/guides/images/routing-diag.png new file mode 100644 index 0000000000..9185ce94d8 Binary files /dev/null and b/docs/extensions/guides/images/routing-diag.png differ diff --git a/docs/extensions/guides/protocol-handlers.md b/docs/extensions/guides/protocol-handlers.md new file mode 100644 index 0000000000..8e13c8436a --- /dev/null +++ b/docs/extensions/guides/protocol-handlers.md @@ -0,0 +1,83 @@ +# Lens Protocol Handlers + +Lens has a file association with the `lens://` protocol. +This means that Lens can be opened by external programs by providing a link that has `lens` as its protocol. +Lens provides a routing mechanism that extensions can use to register custom handlers. + +## Registering A Protocol Handler + +The field `protocolHandlers` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#protocolhandlers) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#protocolhandlers). +This field will be iterated through every time a `lens://` request gets sent to the application. +The `pathSchema` argument must comply with the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package's `compileToRegex` function. + +Once you have registered a handler it will be called when a user opens a link on their computer. +Handlers will be run in both `main` and `renderer` in parallel with no synchronization between the two processes. +Furthermore, both `main` and `renderer` are routed separately. +In other words, which handler is selected in either process is independent from the list of possible handlers in the other. + +Example of registering a handler: + +```typescript +import { LensMainExtension, Interface } from "@k8slens/extensions"; + +function rootHandler(params: Iterface.ProtocolRouteParams) { + console.log("routed to ExampleExtension", params); +} + +export default class ExampleExtensionMain extends LensMainExtension { + protocolHandlers = [ + pathSchema: "/", + handler: rootHandler, + ] +} +``` + +For testing the routing of URIs the `open` (on macOS) or `xdg-open` (on most linux) CLI utilities can be used. +For the above handler, the following URI would be always routed to it: + +``` +open lens://extension/example-extension/ +``` + +## Deregistering A Protocol Handler + +All that is needed to deregister a handler is to remove it from the array of handlers. + +## Routing Algorithm + +The routing mechanism for extensions is quite straight forward. +For example consider an extension `example-extension` which is published by the `@mirantis` org. +If it were to register a handler with `"/display/:type"` as its corresponding link then we would match the following URI like this: + +![Lens Protocol Link Resolution](images/routing-diag.png) + +Once matched, the handler would be called with the following argument (note both `"search"` and `"pathname"` will always be defined): + +```json +{ + "search": { + "text": "Hello" + }, + "pathname": { + "type": "notification" + } +} +``` + +As the diagram above shows, the search (or query) params are not considered as part of the handler resolution. +If the URI had instead been `lens://extension/@mirantis/example-extension/display/notification/green` then a third (and optional) field will have the rest of the path. +The `tail` field would be filled with `"/green"`. +If multiple `pathSchema`'s match a given URI then the most specific handler will be called. + +For example consider the following `pathSchema`'s: + +1. `"/"` +1. `"/display"` +1. `"/display/:type"` +1. `"/show/:id"` + +The URI sub-path `"/display"` would be routed to #2 since it is an exact match. +On the other hand, the subpath `"/display/notification"` would be routed to #3. + +The URI is routed to the most specific matching `pathSchema`. +This way the `"/"` (root) `pathSchema` acts as a sort of catch all or default route if no other route matches. diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md index f64cb5fd49..ffe7ca12e9 100644 --- a/docs/extensions/guides/renderer-extension.md +++ b/docs/extensions/guides/renderer-extension.md @@ -1,15 +1,28 @@ # Renderer Extension -The renderer extension api is the interface to Lens's renderer process (Lens runs in main and renderer processes). -It allows you to access, configure, and customize Lens data, add custom Lens UI elements, and generally run custom code in Lens's renderer process. -The custom Lens UI elements that can be added include global pages, cluster pages, cluster page menus, cluster features, app preferences, status bar items, KubeObject menu items, and KubeObject details items. -These UI elements are based on React components. +The Renderer Extension API is the interface to Lens's renderer process. Lens runs in both the main and renderer processes. The Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, and run custom code in Lens's renderer process. + +The custom Lens UI elements that you can add include: + +* [Cluster pages](#clusterpages) +* [Cluster page menus](#clusterpagemenus) +* [Global pages](#globalpages) +* [Global page menus](#globalpagemenus) +* [Cluster features](#clusterfeatures) +* [App preferences](#apppreferences) +* [Status bar items](#statusbaritems) +* [KubeObject menu items](#kubeobjectmenuitems) +* [KubeObject detail items](#kubeobjectdetailitems) + +All UI elements are based on React components. ## `LensRendererExtension` Class -To create a renderer extension simply extend the `LensRendererExtension` class: +### `onActivate()` and `onDeactivate()` Methods -``` typescript +To create a renderer extension, extend the `LensRendererExtension` class: + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; export default class ExampleExtensionMain extends LensRendererExtension { @@ -23,23 +36,23 @@ export default class ExampleExtensionMain extends LensRendererExtension { } ``` -There are two methods that you can implement to facilitate running your custom code. -`onActivate()` is called when your extension has been successfully enabled. -By implementing `onActivate()` you can initiate your custom code. -`onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. -The example above simply logs messages when the extension is enabled and disabled. +Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. You can initiate custom code by implementing `onActivate()`. Implementing `onDeactivate()` gives you the opportunity to clean up after your extension. + +!!! info + Disable extensions from the Lens Extensions page: + + 1. Navigate to **File** > **Extensions** in the top menu bar. (On Mac, it is **Lens** > **Extensions**.) + 2. Click **Disable** on the extension you want to disable. + +The example above logs messages when the extension is enabled and disabled. ### `clusterPages` -Cluster pages appear as part of the cluster dashboard. -They are accessible from the side bar, and are shown in the menu list after *Custom Resources*. -It is conventional to use a cluster page to show information or provide functionality pertaining to the active cluster, along with custom data and functionality your extension may have. -However, it is not limited to the active cluster. -Also, your extension can gain access to the Kubernetes resources in the active cluster in a straightforward manner using the [`clusterStore`](../stores#clusterstore). +Cluster pages appear in the cluster dashboard. Use cluster pages to display information about or add functionality to the active cluster. It is also possible to include custom details from other clusters. Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore). -The following example adds a cluster page definition to a `LensRendererExtension` subclass: +Add a cluster page definition to a `LensRendererExtension` subclass with the following example: -``` typescript +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import { ExampleIcon, ExamplePage } from "./page" import React from "react" @@ -56,14 +69,15 @@ export default class ExampleExtension extends LensRendererExtension { } ``` -Cluster pages are objects matching the `PageRegistration` interface. -The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above. -The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()). -The `components` field matches the `PageComponents` interface for wich there is one field, `Page`. -`Page` is of type ` React.ComponentType`, which gives you great flexibility in defining the appearance and behaviour of your page. -For the example above `ExamplePage` can be defined in `page.tsx`: +`clusterPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `clusterPages` array objects are defined as follows: -``` typescript +* `id` is a string that identifies the page. +* `components` matches the `PageComponents` interface for which there is one field, `Page`. +* `Page` is of type ` React.ComponentType`. It offers flexibility in defining the appearance and behavior of your page. + +`ExamplePage` in the example above can be defined in `page.tsx`: + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import React from "react" @@ -78,16 +92,17 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens } ``` -Note that the `ExamplePage` class defines a property named `extension`. -This allows the `ExampleExtension` object to be passed in React-style in the cluster page definition, so that `ExamplePage` can access any `ExampleExtension` subclass data. +Note that the `ExamplePage` class defines the `extension` property. This allows the `ExampleExtension` object to be passed in the cluster page definition in the React style. This way, `ExamplePage` can access all `ExampleExtension` subclass data. + +The above example shows how to create a cluster page, but not how to make that page available to the Lens user. Use `clusterPageMenus`, covered in the next section, to add cluster pages to the Lens UI. ### `clusterPageMenus` -The above example code shows how to create a cluster page but not how to make it available to the Lens user. -Cluster pages are typically made available through a menu item in the cluster dashboard sidebar. -Expanding on the above example a cluster page menu is added to the `ExampleExtension` definition: +`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav. -``` typescript +By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition: + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import { ExampleIcon, ExamplePage } from "./page" import React from "react" @@ -114,16 +129,18 @@ export default class ExampleExtension extends LensRendererExtension { } ``` -Cluster page menus are objects matching the `ClusterPageMenuRegistration` interface. -They define the appearance of the cluster page menu item in the cluster dashboard sidebar and the behaviour when the cluster page menu item is activated (typically by a mouse click). -The example above uses the `target` field to set the behaviour as a link to the cluster page with `id` of `"hello"`. -This is done by setting `target`'s `pageId` field to `"hello"`. -The cluster page menu item's appearance is defined by setting the `title` field to the text that is to be displayed in the cluster dashboard sidebar. -The `components` field is used to set an icon that appears to the left of the `title` text in the sidebar. -Thus when the `"Hello World"` menu item is activated the cluster dashboard will show the contents of `ExamplePage`. -This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`: +`clusterPageMenus` is an array of objects that satisfy the `ClusterPageMenuRegistration` interface. This element defines how the cluster page menu item will appear and what it will do when you click it. The properties of the `clusterPageMenus` array objects are defined as follows: -``` typescript +* `target` links to the relevant cluster page using `pageId`. +* `pageId` takes the value of the relevant cluster page's `id` property. +* `title` sets the name of the cluster page menu item that will appear in the left side menu. +* `components` is used to set an icon that appears to the left of the `title` text in the left side menu. + +The above example creates a menu item that reads **Hello World**. When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`. + +This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows: + +```typescript import { LensRendererExtension, Component } from "@k8slens/extensions"; import React from "react" @@ -142,16 +159,15 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens } ``` -`ExampleIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`. -Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/). -One can be selected by name via the `material` field. -`ExampleIcon` also sets a tooltip, shown when the Lens user hovers over the icon with a mouse, by setting the `tooltip` field. +Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The properties that `Component.Icon` uses are defined as follows: -A cluster page menu can also be used to define a foldout submenu in the cluster dashboard sidebar. -This enables the grouping of cluster pages. -The following example shows how to specify a submenu having two menu items: +* `material` takes the name of the icon you want to use. +* `tooltip` sets the text you want to appear when a user hovers over the icon. -``` typescript +`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages. The following example groups two sub menu items under one parent menu item: + + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import { ExampleIcon, ExamplePage } from "./page" import React from "react" @@ -201,7 +217,8 @@ export default class ExampleExtension extends LensRendererExtension { ``` The above defines two cluster pages and three cluster page menu objects. -The cluster page definitons are straightforward. +The cluster page definitions are straightforward. +The three cluster page menu objects include one parent menu item and two sub menu items. The first cluster page menu object defines the parent of a foldout submenu. Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu. Also note that the `target` field is not specified (it is ignored if the `id` field is specified). @@ -209,19 +226,19 @@ This cluster page menu object specifies the `title` and `components` fields, whi Initially the submenu is hidden. Activating this menu item toggles on and off the appearance of the submenu below it. The remaining two cluster page menu objects define the contents of the submenu. -A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case +A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case. + +This is what the example will look like, including how the menu item will appear in the secondary left nav: ### `globalPages` -Global pages appear independently of the cluster dashboard and they fill the Lens UI space. -A global page is typically triggered from the cluster menu using a [global page menu](#globalpagemenus). -They can also be triggered by a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). -Global pages can appear even when there is no active cluster, unlike cluster pages. -It is conventional to use a global page to show information and provide functionality relevant across clusters, along with custom data and functionality that your extension may have. +Global pages are independent of the cluster dashboard and can fill the entire Lens UI. Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension. + +Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page. You can also trigger a global page with a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). Unlike cluster pages, users can trigger global pages even when there is no active cluster. The following example defines a `LensRendererExtension` subclass with a single global page definition: -``` typescript +```typescript import { LensRendererExtension } from '@k8slens/extensions'; import { HelpPage } from './page'; import React from 'react'; @@ -238,14 +255,15 @@ export default class HelpExtension extends LensRendererExtension { } ``` -Global pages are objects matching the `PageRegistration` interface. -The `id` field identifies the page, and at its simplest is just a string identifier, as shown in the example above. -The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()). -The `components` field matches the `PageComponents` interface for which there is one field, `Page`. -`Page` is of type ` React.ComponentType`, which gives you great flexibility in defining the appearance and behaviour of your page. -For the example above `HelpPage` can be defined in `page.tsx`: +`globalPages` is an array of objects that satisfy the `PageRegistration` interface. The properties of the `globalPages` array objects are defined as follows: -``` typescript +* `id` is a string that identifies the page. +* `components` matches the `PageComponents` interface for which there is one field, `Page`. +* `Page` is of type `React.ComponentType`. It offers flexibility in defining the appearance and behavior of your page. + +`HelpPage` in the example above can be defined in `page.tsx`: + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import React from "react" @@ -260,22 +278,21 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension } ``` -Note that the `HelpPage` class defines a property named `extension`. -This allows the `HelpExtension` object to be passed in React-style in the global page definition, so that `HelpPage` can access any `HelpExtension` subclass data. +Note that the `HelpPage` class defines the `extension` property. This allows the `HelpExtension` object to be passed in the global page definition in the React-style. This way, `HelpPage` can access all `HelpExtension` subclass data. -This example code shows how to create a global page but not how to make it available to the Lens user. -Global pages are typically made available through a number of ways. -Menu items can be added to the Lens app menu system and set to open a global page when activated (See [`appMenus` in the Main Extension guide](../main-extension#appmenus)). -Interactive elements can be placed on the status bar (the blue strip along the bottom of the Lens UI) and can be configured to link to a global page when activated (See [`statusBarItems`](#statusbaritems)). -As well, global pages can be made accessible from the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon. -Global page menu icons that are defined using [`globalPageMenus`](#globalpagemenus) appear below the Add Cluster icon. +This example code shows how to create a global page, but not how to make that page available to the Lens user. Global pages can be made available in the following ways: + +* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide. +* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems). +* To add global pages to the left side menu, see [`globalPageMenus`](#globalpagemenus). ### `globalPageMenus` -Global page menus connect a global page to the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon. -Expanding on the example from [`globalPages`](#globalPages) a global page menu is added to the `HelpExtension` definition: +`globalPageMenus` allows you to add global page menu items to the left nav. -``` typescript +By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition: + +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import { HelpIcon, HelpPage } from "./page" import React from "react" @@ -302,16 +319,18 @@ export default class HelpExtension extends LensRendererExtension { } ``` -Global page menus are objects matching the `PageMenuRegistration` interface. -They define the appearance of the global page menu item in the cluster menu and the behaviour when the global page menu item is activated (typically by a mouse click). -The example above uses the `target` field to set the behaviour as a link to the global page with `id` of `"help"`. -This is done by setting `target`'s `pageId` field to `"help"`. -The global page menu item's appearance is defined by setting the `title` field to the text that is to be displayed as a tooltip in the cluster menu. -The `components` field is used to set an icon that appears in the cluster menu. -Thus when the `"Help"` icon is activated the contents of `ExamplePage` will be shown. -This example requires the definition of another React-based component, `HelpIcon`, which has been added to `page.tsx`: +`globalPageMenus` is an array of objects that satisfy the `PageMenuRegistration` interface. This element defines how the global page menu item will appear and what it will do when you click it. The properties of the `globalPageMenus` array objects are defined as follows: -``` typescript +* `target` links to the relevant global page using `pageId`. +* `pageId` takes the value of the relevant global page's `id` property. +* `title` sets the name of the global page menu item that will display as a tooltip in the left nav. +* `components` is used to set an icon that appears in the left nav. + +The above example creates a "Help" icon menu item. When users click the icon, the Lens UI will display the contents of `ExamplePage`. + +This example requires the definition of another React-based component, `HelpIcon`. Update `page.tsx` from the example above with the `HelpIcon` definition, as follows: + +```typescript import { LensRendererExtension, Component } from "@k8slens/extensions"; import React from "react" @@ -330,18 +349,25 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension } ``` -`HelpIcon` introduces one of Lens's built-in components available to extension developers, the `Component.Icon`. -Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/). -One can be selected by name via the `material` field. +Lens includes various built-in components available for extension developers to use. One of these is the `Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io). The property that `Component.Icon` uses is defined as follows: + +* `material` takes the name of the icon you want to use. + +This is what the example will look like, including how the menu item will appear in the left nav: + +![globalPageMenus](images/globalpagemenus.png) ### `clusterFeatures` Cluster features are Kubernetes resources that can be applied to and managed within the active cluster. -They can be installed/uninstalled by the Lens user from the [cluster settings page](). +They can be installed and uninstalled by the Lens user from the cluster **Settings** page. + +!!! info + To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**. The following example shows how to add a cluster feature as part of a `LensRendererExtension`: -``` typescript +```typescript import { LensRendererExtension } from "@k8slens/extensions" import { ExampleFeature } from "./src/example-feature" import React from "react" @@ -364,34 +390,44 @@ export default class ExampleFeatureExtension extends LensRendererExtension { ]; } ``` -The `title` and `components.Description` fields provide content that appears on the cluster settings page, in the **Features** section. -The `feature` field must specify an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implement the following methods: -``` typescript +The properties of the `clusterFeatures` array objects are defined as follows: + +* `title` and `components.Description` provide content that appears on the cluster settings page, in the **Features** section. +* `feature` specifies an instance which extends the abstract class `ClusterFeature.Feature`, and specifically implements the following methods: + +```typescript abstract install(cluster: Cluster): Promise; abstract upgrade(cluster: Cluster): Promise; abstract uninstall(cluster: Cluster): Promise; abstract updateStatus(cluster: Cluster): Promise; ``` -The `install()` method is typically called by Lens when a user has indicated that this feature is to be installed (i.e. clicked **Install** for the feature on the cluster settings page). -The implementation of this method should install kubernetes resources using the `applyResources()` method, or by directly accessing the kubernetes api ([`K8sApi`](tbd)). +The four methods listed above are defined as follows: -The `upgrade()` method is typically called by Lens when a user has indicated that this feature is to be upgraded (i.e. clicked **Upgrade** for the feature on the cluster settings page). -The implementation of this method should upgrade the kubernetes resources already installed, if relevant to the feature. +* The `install()` method installs Kubernetes resources using the `applyResources()` method, or by directly accessing the [Kubernetes API](../api/README.md). +This method is typically called when a user indicates that they want to install the feature (i.e., by clicking **Install** for the feature in the cluster settings page). -The `uninstall()` method is typically called by Lens when a user has indicated that this feature is to be uninstalled (i.e. clicked **Uninstall** for the feature on the cluster settings page). -The implementation of this method should uninstall kubernetes resources using the kubernetes api (`K8sApi`) +* The `upgrade()` method upgrades the Kubernetes resources already installed, if they are relevant to the feature. +This method is typically called when a user indicates that they want to upgrade the feature (i.e., by clicking **Upgrade** for the feature in the cluster settings page). -The `updateStatus()` method is called periodically by Lens to determine details about the feature's current status. -The implementation of this method should provide the current status information in the `status` field of the `ClusterFeature.Feature` parent class. -The `status.currentVersion` and `status.latestVersion` fields may be displayed by Lens in describing the feature. -The `status.installed` field should be set to true if the feature is currently installed, otherwise false. -Also, Lens relies on the `status.canUpgrade` field to determine if the feature can be upgraded (i.e a new version could be available) so the implementation should set the `status.canUpgrade` field according to specific rules for the feature, if relevant. +* The `uninstall()` method uninstalls Kubernetes resources using the [Kubernetes API](../api/README.md). +This method is typically called when a user indicates that they want to uninstall the feature (i.e., by clicking **Uninstall** for the feature in the cluster settings page). + +* The `updateStatus()` method provides the current status information in the `status` field of the `ClusterFeature.Feature` parent class. +Lens periodically calls this method to determine details about the feature's current status. +The implementation of this method should uninstall Kubernetes resources using the Kubernetes api (`K8sApi`) +Consider using the following properties with `updateStatus()`: + + * `status.currentVersion` and `status.latestVersion` may be displayed by Lens in the feature's description. + + * `status.installed` should be set to `true` if the feature is installed, and `false` otherwise. + + * `status.canUpgrade` is set according to a rule meant to determine whether the feature can be upgraded. This rule can involve `status.currentVersion` and `status.latestVersion`, if desired. The following shows a very simple implementation of a `ClusterFeature`: -``` typescript +```typescript import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions"; import * as path from "path"; @@ -435,9 +471,9 @@ export class ExampleFeature extends ClusterFeature.Feature { } ``` -This example implements the `install()` method by simply invoking the helper `applyResources()` method. +This example implements the `install()` method by invoking the helper `applyResources()` method. `applyResources()` tries to apply all resources read from all files found in the folder path provided. -In this case this folder path is the `../resources` subfolder relative to current source code's folder. +In this case the folder path is the `../resources` subfolder relative to the current source code's folder. The file `../resources/example-pod.yml` could contain: ``` yaml @@ -451,21 +487,21 @@ spec: image: nginx ``` -The `upgrade()` method in the example above is implemented by simply invoking the `install()` method. -Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps. +The example above implements the four methods as follows: -The `uninstall()` method is implemented in the example above by utilizing the [`K8sApi`](tbd) provided by Lens to simply delete the `example-pod` pod applied by the `install()` method. +* It implements `upgrade()` by invoking the `install()` method. Depending on the feature to be supported by an extension, upgrading may require additional and/or different steps. -The `updateStatus()` method is implemented above by using the [`K8sApi`](tbd) as well, this time to get information from the `example-pod` pod, in particular to determine if it is installed, what version is associated with it, and if it can be upgraded. -How the status is updated for a specific cluster feature is up to the implementation. +* It implements `uninstall()` by utilizing the [Kubernetes API](../api/README.md) which Lens provides to delete the `example-pod` applied by the `install()` method. + +* It implements `updateStatus()` by using the [Kubernetes API](../api/README.md) which Lens provides to determine whether the `example-pod` is installed, what version is associated with it, and whether it can be upgraded. The implementation determines what the status is for a specific cluster feature. ### `appPreferences` -The Preferences page is a built-in global page. -Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global options, for Lens and extensions alike. +The Lens **Preferences** page is a built-in global page. You can use Lens extensions to add custom preferences to the Preferences page, providing a single location for users to configure global options. + The following example demonstrates adding a custom preference: -``` typescript +```typescript import { LensRendererExtension } from "@k8slens/extensions"; import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference"; import { observable } from "mobx"; @@ -487,15 +523,22 @@ export default class ExampleRendererExtension extends LensRendererExtension { } ``` -App preferences are objects matching the `AppPreferenceRegistration` interface. -The `title` field specifies the text to show as the heading on the Preferences page. -The `components` field specifies two `React.Component` objects defining the interface for the preference. -`Input` should specify an interactive input element for your preference and `Hint` should provide descriptive information for the preference, which is shown below the `Input` element. -`ExamplePreferenceInput` expects its React props set to an `ExamplePreferenceProps` instance, which is how `ExampleRendererExtension` handles the state of the preference input. -`ExampleRendererExtension` has the field `preference`, which is provided to `ExamplePreferenceInput` when it is created. -In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx`: +`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface. The properties of the `appPreferences` array objects are defined as follows: -``` typescript +* `title` sets the heading text displayed on the Preferences page. +* `components` specifies two `React.Component` objects that define the interface for the preference. + * `Input` specifies an interactive input element for the preference. + * `Hint` provides descriptive information for the preference, shown below the `Input` element. + +!!! note + Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension. + +`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance. This is how `ExampleRendererExtension` handles the state of the preference input. +`ExampleRendererExtension` has a `preference` field, which you will add to `ExamplePreferenceInput`. + +In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows: + +```typescript import { Component } from "@k8slens/extensions"; import { observer } from "mobx-react"; import React from "react"; @@ -530,30 +573,28 @@ export class ExamplePreferenceHint extends React.Component { } ``` -`ExamplePreferenceInput` implements a simple checkbox (using Lens's `Component.Checkbox`). -It provides `label` as the text to display next to the checkbox and an `onChange` function, which reacts to the checkbox state change. -The checkbox's `value` is initially set to `preference.enabled`. -`ExamplePreferenceInput` is defined with React props of `ExamplePreferenceProps` type, which is an object with a single field, `enabled`. -This is used to indicate the state of the preference, and is bound to the checkbox state in `onChange`. +`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties: + +* `label` sets the text that displays next to the checkbox. +* `value` is initially set to `preference.enabled`. +* `onChange` is a function that responds when the state of the checkbox changes. + +`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props. This is an object with the single `enabled` property. It is used to indicate the state of the preference, and it is bound to the checkbox state in `onChange`. + `ExamplePreferenceHint` is a simple text span. -Note that the input and the hint could comprise of more sophisticated elements, according to the needs of the extension. -Note that the above example introduces decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. -`mobx` simplifies state management and without it this example would not visually update the checkbox properly when the user activates it. -[Lens uses `mobx` extensively for state management](../working-with-mobx) of its own UI elements and it is recommended that extensions rely on it too. -Alternatively, React's state management can be used, though `mobx` is typically simpler to use. +The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. `mobx` simplifies state management. Without it, this example would not visually update the checkbox properly when the user activates it. [Lens uses `mobx`](../working-with-mobx) extensively for state management of its own UI elements. We recommend that extensions rely on it, as well. +Alternatively, you can use React's state management, though `mobx` is typically simpler to use. -Also note that an extension's state data can be managed using an `ExtensionStore` object, which conveniently handles persistence and synchronization. -The example above defined a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state primarily to simplify the code for this guide, but it is recommended to manage your extension's state data using [`ExtensionStore`](../stores#extensionstore) +Note that you can manage an extension's state data using an `ExtensionStore` object, which conveniently handles persistence and synchronization. To simplify this guide, the example above defines a `preference` field in the `ExampleRendererExtension` class definition to hold the extension's state. However, we recommend that you manage your extension's state data using [`ExtensionStore`](../stores#extensionstore). ### `statusBarItems` -The Status bar is the blue strip along the bottom of the Lens UI. -Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page, or even an external page. +The status bar is the blue strip along the bottom of the Lens UI. `statusBarItems` are `React.ReactNode` types. They can be used to display status information, or act as links to global pages as well as external pages. -The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and configures the status bar item to navigate to the global page upon activation (normally a mouse click): +The following example adds a `statusBarItems` definition and a `globalPages` definition to a `LensRendererExtension` subclass. It configures the status bar item to navigate to the global page upon activation (normally a mouse click): -``` typescript +```typescript import { LensRendererExtension } from '@k8slens/extensions'; import { HelpIcon, HelpPage } from "./page" import React from 'react'; @@ -570,39 +611,41 @@ export default class HelpExtension extends LensRendererExtension { statusBarItems = [ { - item: ( -
this.navigate("help")} - > - - My Status Bar Item -
- ), + components: { + Item: ( +
this.navigate("help")} + > + + My Status Bar Item +
+ ) + }, }, ]; } ``` -The `item` field of a status bar item specifies the `React.Component` to be shown on the status bar. -By default items are added starting from the right side of the status bar. -Typically, `item` would specify an icon and/or a short string of text, considering the limited space on the status bar. -In the example above the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus) is reused. -Also, the `item` provides a link to the global page by setting the `onClick` property to a function that calls the `LensRendererExtension` `navigate()` method. -`navigate()` takes as a parameter the id of the global page, which is shown when `navigate()` is called. +The properties of the `statusBarItems` array objects are defined as follows: + +* `Item` specifies the `React.Component` that will be shown on the status bar. By default, items are added starting from the right side of the status bar. Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text. The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus). +* `onClick` determines what the `statusBarItem` does when it is clicked. In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method. `navigate` takes the `id` of the associated global page as a parameter. Thus, clicking the status bar item activates the associated global pages. ### `kubeObjectMenuItems` -An extension can add custom menu items (including actions) for specific Kubernetes resource kinds/apiVersions. -These menu items appear under the `...` for each listed resource in the cluster dashboard, and on the title bar of the details page for a specific resource: +An extension can add custom menu items (`kubeObjectMenuItems`) for specific Kubernetes resource kinds and apiVersions. +`kubeObjectMenuItems` appear under the vertical ellipsis for each listed resource in the cluster dashboard: ![List](images/kubeobjectmenuitem.png) +They also appear on the title bar of the details page for specific resources: + ![Details](images/kubeobjectmenuitemdetail.png) -The following example shows how to add a menu for Namespace resources, and associate an action with it: +The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action: -``` typescript +```typescript import React from "react" import { LensRendererExtension } from "@k8slens/extensions"; import { NamespaceMenuItem } from "./src/namespace-menu-item" @@ -621,12 +664,13 @@ export default class ExampleExtension extends LensRendererExtension { ``` -Kube object menu items are objects matching the `KubeObjectMenuRegistration` interface. -The `kind` field specifies the kubernetes resource type to apply this menu item to, and the `apiVersion` field specifies the kubernetes api to use in relation to this resource type. -This example adds a menu item for namespaces in the cluster dashboard. -The `components` field defines the menu item's appearance and behaviour. -The `MenuItem` field provides a function that returns a `React.Component` given a set of menu item properties. -In this example a `NamespaceMenuItem` object is returned. +`kubeObjectMenuItems` is an array of objects matching the `KubeObjectMenuRegistration` interface. The example above adds a menu item for namespaces in the cluster dashboard. The properties of the `kubeObjectMenuItems` array objects are defined as follows: + +* `kind` specifies the Kubernetes resource type the menu item will apply to. +* `apiVersion` specifies the Kubernetes API version number to use with the resource type. +* `components` defines the menu item's appearance and behavior. +* `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties. In this example a `NamespaceMenuItem` object is returned. + `NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`: ```typescript @@ -661,24 +705,20 @@ export function NamespaceMenuItem(props: Component.KubeObjectMenuProps>` it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`. -This object can be queried for many details about the current namespace. -In this example the namespace's name is obtained in `componentDidMount()` using the `K8sApi.Namespace` `getName()` method. -The namespace's name is needed to limit the list of pods to only those in this namespace. -To get the list of pods this example uses the kubernetes pods api, specifically the `K8sApi.podsApi.list()` method. -The `K8sApi.podsApi` is automatically configured for the currently active cluster. +Since `NamespaceDetailsItem` extends `React.Component>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`. +You can query this object for many details about the current namespace. +In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method. +Use the namespace's name to limit the list of pods only to those in the relevant namespace. +To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method. +The `K8sApi.podsApi` is automatically configured for the active cluster. -Note that `K8sApi.podsApi.list()` is an asynchronous method, and ideally getting the pods list should be done before rendering the `NamespaceDetailsItem`. +Note that `K8sApi.podsApi.list()` is an asynchronous method. +Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`. It is a common technique in React development to await async calls in `componentDidMount()`. However, `componentDidMount()` is called right after the first call to `render()`. -In order to effect a subsequent `render()` call React must be made aware of a state change. -Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list is updated. +In order to effect a subsequent `render()` call, React must be made aware of a state change. +Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) are used to ensure `NamespaceDetailsItem` renders when the pods list updates. This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`. -Finally, the `NamespaceDetailsItem` is rendered using the `render()` method. +Finally, the `NamespaceDetailsItem` renders using the `render()` method. Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one. Multiple details in a drawer can be placed in `` elements for further separation, if desired. The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`: @@ -800,6 +842,9 @@ export class PodsDetailsList extends React.Component { ![DetailsWithPods](images/kubeobjectdetailitemwithpods.png) - For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods. +Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods. Construct the table using the `Component.Table` and related elements. + + +For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods. The table is constructed using the `Component.Table` and related elements. -See [`Component` documentation](url?) for further details. \ No newline at end of file +See [`Component` documentation](https://docs.k8slens.dev/master/extensions/api/modules/_renderer_api_components_/) for further details. diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md index 981a7cda3e..0319ee0aab 100644 --- a/docs/extensions/guides/stores.md +++ b/docs/extensions/guides/stores.md @@ -1,18 +1,16 @@ # Stores -Stores are components that persist and synchronize state data. Lens utilizes a number of stores for maintaining a variety of state information. -A few of these are exposed by the extensions api for use by the extension developer. +Stores are components that persist and synchronize state data. Lens uses a number of stores to maintain various kinds of state information, including: -- The `ClusterStore` manages cluster state data such as cluster details, and which cluster is active. -- The `WorkspaceStore` similarly manages workspace state data, such as workspace name, and which clusters belong to a given workspace. -- The `ExtensionStore` is a store for managing custom extension state data. +* The `ClusterStore` manages cluster state data (such as cluster details), and it tracks which cluster is active. +* The `WorkspaceStore` manages workspace state data (such as the workspace name), and and it tracks which clusters belong to a given workspace. +* The `ExtensionStore` manages custom extension state data. + +This guide focuses on the `ExtensionStore`. ## ExtensionStore -Extension developers can create their own store for managing state data by extending the `ExtensionStore` class. -This guide shows how to create a store for the [`appPreferences` guide example](../renderer-extension#apppreferences), which demonstrates how to add a custom preference to the Preferences page. -The preference is a simple boolean that indicates whether something is enabled or not. -The problem with that example is that the enabled state is not stored anywhere, and reverts to the default the next time Lens is started. +Extension developers can create their own store for managing state data by extending the `ExtensionStore` class. This guide shows how to create a store for the [`appPreferences`](../renderer-extension#apppreferences) guide example, which demonstrates how to add a custom preference to the **Preferences** page. The preference is a simple boolean that indicates whether or not something is enabled. However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted. The following example code creates a store for the `appPreferences` guide example: @@ -53,25 +51,14 @@ export class ExamplePreferencesStore extends Store.ExtensionStore(); ``` -First the extension's data model is defined using a simple type, `ExamplePreferencesModel`, which has a single field, `enabled`, representing the preference's state. -`ExamplePreferencesStore` extends `Store.ExtensionStore`, based on the `ExamplePreferencesModel`. -The field `enabled` is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference. -Note the use of the `observer` decorator on the `enabled` field. -As for the [`appPreferences` guide example](../renderer-extension#apppreferences), [`mobx`](https://mobx.js.org/README.html) is used for the UI state management, ensuring the checkbox updates when activated by the user. +First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. This has a single field, `enabled`, which represents the preference's state. `ExamplePreferencesStore` extends `Store.ExtensionStore`, which is based on the `ExamplePreferencesModel`. The `enabled` field is added to the `ExamplePreferencesStore` class to hold the "live" or current state of the preference. Note the use of the `observable` decorator on the `enabled` field. The [`appPreferences`](../renderer-extension#apppreferences) guide example uses [MobX](https://mobx.js.org/README.html) for the UI state management, ensuring the checkbox updates when it's activated by the user. -Then the constructor and two abstract methods are implemented. -In the constructor, the name of the store (`"example-preferences-store"`), and the default (initial) value for the preference state (`enabled: false`) are specified. -The `fromStore()` method is called by Lens internals when the store is loaded, and gives the extension the opportunity to retrieve the stored state data values based on the defined data model. -Here, the `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked. -The `toJSON()` method is complementary to `fromStore()`, and is called when the store is being saved. -`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. -The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here. +Next, our example implements the constructor and two abstract methods. The constructor specifies the name of the store (`"example-preferences-store"`) and the default (initial) value for the preference state (`enabled: false`). Lens internals call the `fromStore()` method when the store loads. It gives the extension the opportunity to retrieve the stored state data values based on the defined data model. The `enabled` field of the `ExamplePreferencesStore` is set to the value from the store whenever `fromStore()` is invoked. The `toJSON()` method is complementary to `fromStore()`. It is called when the store is being saved. +`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here. -Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance()`, and exported for use by other parts of the extension. -Note that `examplePreferencesStore` is a singleton, calling this function again will not create a new store. +Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance()`, and exported for use by other parts of the extension. Note that `examplePreferencesStore` is a singleton. Calling this function again will not create a new store. -The following example code, modified from the [`appPreferences` guide example](../renderer-extension#apppreferences) demonstrates how to use the extension store. -`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`: +The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store. `examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`: ``` typescript import { LensMainExtension } from "@k8slens/extensions"; @@ -84,8 +71,8 @@ export default class ExampleMainExtension extends LensMainExtension { } ``` -Here, `examplePreferencesStore` is loaded with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`. -Similarly, `examplePreferencesStore` must be loaded in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`: +Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`. +Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`: ``` typescript import { LensRendererExtension } from "@k8slens/extensions"; @@ -111,8 +98,7 @@ export default class ExampleRendererExtension extends LensRendererExtension { } ``` -Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. -Also, there is no longer the need for the `preference` field in the `ExampleRendererExtension` class, as the props for `ExamplePreferenceInput` is now `examplePreferencesStore`. +Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`. `ExamplePreferenceInput` is defined in `./src/example-preference.tsx`: ``` typescript @@ -151,5 +137,5 @@ export class ExamplePreferenceHint extends React.Component { ``` The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type. -Everything else works as before except now the `enabled` state persists across Lens restarts because it is managed by the +Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the `examplePreferencesStore`. \ No newline at end of file diff --git a/docs/extensions/guides/working-with-mobx.md b/docs/extensions/guides/working-with-mobx.md index 5577ff6bdc..41ddc487a6 100644 --- a/docs/extensions/guides/working-with-mobx.md +++ b/docs/extensions/guides/working-with-mobx.md @@ -1,23 +1,26 @@ -# Working with mobx +# Working with MobX ## Introduction -Lens uses `mobx` as its state manager on top of React's state management system. -This helps with having a more declarative style of managing state, as opposed to `React`'s native `setState` mechanism. -You should already have a basic understanding of how `React` handles state ([read here](https://reactjs.org/docs/faq-state.html) for more information). -However, if you do not, here is a quick overview. +Lens uses MobX on top of React's state management system. +The result is a more declarative state management style, rather than React's native `setState` mechanism. -- A `React.Component` is generic over both `Props` and `State` (with default empty object types). -- `Props` should be considered read-only from the point of view of the component and is the mechanism for passing in "arguments" to a component. -- `State` is a component's internal state and can be read by accessing the parent field `state`. -- `State` **must** be updated using the `setState` parent method which merges the new data with the old state. -- `React` does do some optimizations around re-rendering components after quick successions of `setState` calls. +You can review how React handles state management [here](https://reactjs.org/docs/faq-state.html). -## How mobx works: +The following is a quick overview: -`mobx` is a package that provides an abstraction over `React`'s state management. The three main concepts are: -- `observable`: data stored in the component's `state` -- `action`: a function that modifies any `observable` data -- `computed`: data that is derived from `observable` data but is not actually stored. Think of this as computing `isEmpty` vs an `observable` field called `count`. +* `React.Component` is generic with respect to both `props` and `state` (which default to the empty object type). +* `props` should be considered read-only from the point of view of the component, and it is the mechanism for passing in arguments to a component. +* `state` is a component's internal state, and can be read by accessing the super-class field `state`. +* `state` **must** be updated using the `setState` parent method which merges the new data with the old state. +* React does some optimizations around re-rendering components after quick successions of `setState` calls. -Further reading is available from `mobx`'s [website](https://mobx.js.org/the-gist-of-mobx.html). +## How MobX Works: + +MobX is a package that provides an abstraction over React's state management system. The three main concepts are: + +* `observable` is a marker for data stored in the component's `state`. +* `action` is a function that modifies any `observable` data. +* `computed` is a marker for data that is derived from `observable` data, but that is not actually stored. Think of this as computing `isEmpty` rather than an observable field called `count`. + +Further reading is available on the [MobX website](https://mobx.js.org/the-gist-of-mobx.html). diff --git a/docs/extensions/testing-and-publishing/testing.md b/docs/extensions/testing-and-publishing/testing.md index ab10c5eab3..7b9d9723ba 100644 --- a/docs/extensions/testing-and-publishing/testing.md +++ b/docs/extensions/testing-and-publishing/testing.md @@ -10,7 +10,7 @@ For example, I have a component `GlobalPageMenuIcon` and want to test if `props. My component `GlobalPageMenuIcon` -```tsx +```typescript import React from "react" import { Component: { Icon } } from "@k8slens/extensions"; @@ -61,7 +61,7 @@ In the Renderer process, `console.log()` is printed in the Console in Developer ### Main Process Logs -Viewing the logs from the Main process is a little trickier, since they cannot be printed using Developer Tools. +Viewing the logs from the Main process is a little trickier, since they cannot be printed using Developer Tools. #### macOS diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 704e59d97b..8f5811c1be 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -5,7 +5,7 @@ Lens is lightweight and simple to install. You'll be up and running in just a fe ## System Requirements -Review the [System Requirements](/supporting/requirements/) to check if your computer configuration is supported. +Review the [System Requirements](../supporting/requirements.md) to check if your computer configuration is supported. ## macOS @@ -28,6 +28,28 @@ Review the [System Requirements](/supporting/requirements/) to check if your com See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options. +After installing Lens manually (not using a package manager file such as `.deb` or `.rpm`) the following will need to be done to allow protocol handling. +This assumes that your linux distribution uses `xdg-open` and the `xdg-*` suite of programs for determining which application can handle custom URIs. + +1. Create a file called `lens.desktop` in either `~/.local/share/applications/` or `/usr/share/applications` (if you have permissions and are installing Lens for all users). +1. That file should have the following contents, with `` being the absolute path to where you have installed the unpacked `Lens` executable: + ``` + [Desktop Entry] + Name=Lens + Exec= %U + Terminal=false + Type=Application + Icon=lens + StartupWMClass=Lens + Comment=Lens - The Kubernetes IDE + MimeType=x-scheme-handler/lens; + Categories=Network; + ``` +1. Then run the following command: + ``` + xdg-settings set default-url-scheme-handler lens lens.desktop + ``` +1. If that succeeds (exits with code `0`) then your Lens install should be set up to handle `lens://` URIs. ### Snap @@ -50,6 +72,6 @@ To stay current with the Lens features, you can review the [release notes](https ## Next Steps +- [Launch Lens](lens://app/landing) - [Add clusters](../clusters/adding-clusters.md) - [Watch introductory videos](./introductory-videos.md) - diff --git a/docs/getting-started/preferences.md b/docs/getting-started/preferences.md index 527f5e6df5..8146965cc5 100644 --- a/docs/getting-started/preferences.md +++ b/docs/getting-started/preferences.md @@ -1,12 +1,11 @@ # Preferences - ## Color Themes The Color Themes option in Lens preferences lets you set the colors in the Lens user interface to suit your liking. -1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). -2. Select your preferred theme from the **Color Theme** dropdown. +1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=appearance). +2. Select your preferred theme from the **Color Theme** dropdown. ![Color Theme](images/color-theme.png) @@ -19,10 +18,9 @@ Lens collects telemetry data, which is used to help us understand how to improve If you don't wish to send usage data to Mirantis, you can disable the "Telemetry & Usage Tracking" in the Lens preferences. -1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). +1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=telemetry-tracking). 2. Scroll down to **Telemetry & Usage Tracking** -3. Uncheck **Allow Telemetry & Usage Tracking**. +3. Uncheck **Allow Telemetry & Usage Tracking**. This will silence all telemetry events from Lens going forward. Telemetry information may have been collected and sent up until the point when you disable this setting. ![Disable Telemetry & Usage Tracking](images/disabled-telemetry-usage-tracking.png) - diff --git a/docs/helm/README.md b/docs/helm/README.md index b46bead65c..f07bced7f0 100644 --- a/docs/helm/README.md +++ b/docs/helm/README.md @@ -4,7 +4,7 @@ Lens has integration to Helm making it easy to install and manage Helm charts an ![Helm Charts](images/helm-charts.png) -## Managing Helm Reporistories +## Managing Helm Repositories Used Helm repositories are possible to configure in the [Preferences](/getting-started/preferences). Lens app will fetch available Helm repositories from the [Artifact HUB](https://artifacthub.io/) and automatically add `bitnami` repository by default if no other repositories are already configured. If any other repositories are needed to add, those can be added manually via command line. **Note!** Configured Helm repositories are added globally to user's computer, so other processes can see those as well. @@ -18,4 +18,4 @@ Lens will list all charts from configured Helm repositries on Apps section. To i To update a Helm release, you can open the release details and modify the release values and click "Save" button. To upgrade or downgrade the release, click "Upgrade" button in the release details. In the release editor you can select a new chart version and edit the release values if needed and then click "Upgrade" or "Upgrade and Close" button. ## Deleting a Helm Release -To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually! \ No newline at end of file +To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually! diff --git a/extensions/example-extension/package-lock.json b/extensions/example-extension/package-lock.json index 954ba2e41f..56830a49d0 100644 --- a/extensions/example-extension/package-lock.json +++ b/extensions/example-extension/package-lock.json @@ -2159,24 +2159,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2796,7 +2796,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3227,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4367,6 +4369,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4381,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4390,6 +4394,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4399,6 +4404,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4407,7 +4413,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5398,7 +5405,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6275,7 +6283,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", @@ -6870,9 +6879,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { diff --git a/extensions/kube-object-event-status/package-lock.json b/extensions/kube-object-event-status/package-lock.json index 6b30c1d2e1..75d33773bb 100644 --- a/extensions/kube-object-event-status/package-lock.json +++ b/extensions/kube-object-event-status/package-lock.json @@ -986,24 +986,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -3497,9 +3497,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/extensions/kube-object-event-status/src/resolver.tsx b/extensions/kube-object-event-status/src/resolver.tsx index 69691c2e79..5e9151288f 100644 --- a/extensions/kube-object-event-status/src/resolver.tsx +++ b/extensions/kube-object-event-status/src/resolver.tsx @@ -56,4 +56,4 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb text: `${event.message}`, timestamp: event.metadata.creationTimestamp }; -} \ No newline at end of file +} diff --git a/extensions/license-menu-item/package-lock.json b/extensions/license-menu-item/package-lock.json index 071d2f62a6..6d478d652b 100644 --- a/extensions/license-menu-item/package-lock.json +++ b/extensions/license-menu-item/package-lock.json @@ -2231,24 +2231,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2868,7 +2868,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3298,6 +3299,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4460,6 +4462,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4474,6 +4477,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4483,6 +4487,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4492,6 +4497,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4500,7 +4506,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5516,7 +5523,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6406,7 +6414,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", @@ -7001,9 +7010,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json index 334135b4eb..139de9092b 100644 --- a/extensions/metrics-cluster-feature/package-lock.json +++ b/extensions/metrics-cluster-feature/package-lock.json @@ -2173,24 +2173,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2816,7 +2816,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3246,6 +3247,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4394,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4408,6 +4411,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -5434,7 +5438,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6311,7 +6316,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", @@ -6906,9 +6912,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { diff --git a/extensions/metrics-cluster-feature/package.json b/extensions/metrics-cluster-feature/package.json index 1b1b5850ec..80a8ddd60a 100644 --- a/extensions/metrics-cluster-feature/package.json +++ b/extensions/metrics-cluster-feature/package.json @@ -12,7 +12,6 @@ "dev": "npm run build --watch", "test": "jest --passWithNoTests --env=jsdom src $@" }, - "dependencies": {}, "devDependencies": { "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "jest": "^26.6.3", diff --git a/extensions/metrics-cluster-feature/renderer.tsx b/extensions/metrics-cluster-feature/renderer.tsx index a32296a408..ea8a666103 100644 --- a/extensions/metrics-cluster-feature/renderer.tsx +++ b/extensions/metrics-cluster-feature/renderer.tsx @@ -1,21 +1,55 @@ -import { LensRendererExtension } from "@k8slens/extensions"; +import { LensRendererExtension, Store, Interface, Component } from "@k8slens/extensions"; import { MetricsFeature } from "./src/metrics-feature"; -import React from "react"; export default class ClusterMetricsFeatureExtension extends LensRendererExtension { - clusterFeatures = [ - { - title: "Metrics Stack", - components: { - Description: () => ( - - Enable timeseries data visualization (Prometheus stack) for your cluster. - Install this only if you don't have existing Prometheus stack installed. - You can see preview of manifests here. - - ) - }, - feature: new MetricsFeature() + onActivate() { + const category = Store.catalogCategories.getForGroupKind("entity.k8slens.dev", "KubernetesCluster"); + + if (!category) { + return; } - ]; + + category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this)); + } + + async clusterContextMenuOpen(cluster: Store.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) { + if (!cluster.status.active) { + return; + } + + const metricsFeature = new MetricsFeature(); + + await metricsFeature.updateStatus(cluster); + + if (metricsFeature.status.installed) { + if (metricsFeature.status.canUpgrade) { + ctx.menuItems.unshift({ + icon: "refresh", + title: "Upgrade Lens Metrics stack", + onClick: async () => { + metricsFeature.upgrade(cluster); + } + }); + } + ctx.menuItems.unshift({ + icon: "toggle_off", + title: "Uninstall Lens Metrics stack", + onClick: async () => { + await metricsFeature.uninstall(cluster); + + Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 }); + } + }); + } else { + ctx.menuItems.unshift({ + icon: "toggle_on", + title: "Install Lens Metrics stack", + onClick: async () => { + metricsFeature.install(cluster); + + Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 }); + } + }); + } + } } diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts index 43dfd7743c..2f830ae92e 100644 --- a/extensions/metrics-cluster-feature/src/metrics-feature.ts +++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts @@ -49,7 +49,7 @@ export class MetricsFeature extends ClusterFeature.Feature { storageClass: null, }; - async install(cluster: Store.Cluster): Promise { + async install(cluster: Store.KubernetesCluster): Promise { // Check if there are storageclasses const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass); const scs = await storageClassApi.list(); @@ -62,11 +62,11 @@ export class MetricsFeature extends ClusterFeature.Feature { super.applyResources(cluster, path.join(__dirname, "../resources/")); } - async upgrade(cluster: Store.Cluster): Promise { + async upgrade(cluster: Store.KubernetesCluster): Promise { return this.install(cluster); } - async updateStatus(cluster: Store.Cluster): Promise { + async updateStatus(cluster: Store.KubernetesCluster): Promise { try { const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet); const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"}); @@ -87,12 +87,13 @@ export class MetricsFeature extends ClusterFeature.Feature { return this.status; } - async uninstall(cluster: Store.Cluster): Promise { + async uninstall(cluster: Store.KubernetesCluster): Promise { const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace); const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding); const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole); await namespaceApi.delete({name: "lens-metrics"}); await clusterRoleBindingApi.delete({name: "lens-prometheus"}); - await clusterRoleApi.delete({name: "lens-prometheus"}); } + await clusterRoleApi.delete({name: "lens-prometheus"}); + } } diff --git a/extensions/node-menu/package-lock.json b/extensions/node-menu/package-lock.json index 5e1eec009a..262c8705da 100644 --- a/extensions/node-menu/package-lock.json +++ b/extensions/node-menu/package-lock.json @@ -2159,24 +2159,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2796,7 +2796,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3227,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4382,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4396,6 +4399,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4405,6 +4409,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4414,6 +4419,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4422,7 +4428,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5438,7 +5445,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6315,7 +6323,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", @@ -6910,9 +6919,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { diff --git a/extensions/pod-menu/package-lock.json b/extensions/pod-menu/package-lock.json index ea98213fb3..5d2c15069a 100644 --- a/extensions/pod-menu/package-lock.json +++ b/extensions/pod-menu/package-lock.json @@ -626,7 +626,644 @@ }, "@k8slens/extensions": { "version": "file:../../src/extensions/npm/extensions", - "dev": true + "dev": true, + "requires": { + "@material-ui/core": "*", + "@types/node": "*", + "@types/react-select": "*", + "conf": "^7.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "@material-ui/core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz", + "integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.2", + "@material-ui/system": "^4.11.2", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/styles": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", + "integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + } + }, + "@material-ui/system": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz", + "integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "dev": true + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@types/node": { + "version": "14.14.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz", + "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "@types/react-dom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-select": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz", + "integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/react-dom": "*", + "@types/react-transition-group": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", + "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true + }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "dev": true + }, + "conf": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz", + "integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==", + "dev": true, + "requires": { + "ajv": "^6.12.2", + "atomically": "^1.3.1", + "debounce-fn": "^4.0.0", + "dot-prop": "^5.2.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.3", + "make-dir": "^3.1.0", + "onetime": "^5.1.0", + "pkg-up": "^3.1.0", + "semver": "^7.3.2" + } + }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==", + "dev": true + }, + "debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "requires": { + "mimic-fn": "^3.0.0" + } + }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==", + "dev": true + }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "dev": true, + "requires": { + "symbol-observable": "1.2.0" + } + }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true + }, + "jss": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz", + "integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "jss-plugin-camel-case": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz", + "integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.0" + } + }, + "jss-plugin-default-unit": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz", + "integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-global": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz", + "integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-nested": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz", + "integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz", + "integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz", + "integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz", + "integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } }, "@sinonjs/commons": { "version": "1.8.1", @@ -2159,24 +2796,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2796,7 +3433,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3864,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4382,6 +5021,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4396,6 +5036,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4405,6 +5046,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4414,6 +5056,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4422,7 +5065,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5438,7 +6082,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6315,7 +6960,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", @@ -6910,9 +7556,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx index 706efcf128..1063207d0c 100644 --- a/extensions/pod-menu/src/logs-menu.tsx +++ b/extensions/pod-menu/src/logs-menu.tsx @@ -9,13 +9,9 @@ export class PodLogsMenu extends React.Component { Navigation.hideDetails(); const pod = this.props.object; - Component.createPodLogsTab({ - pod, - containers: pod.getContainers(), - initContainers: pod.getInitContainers(), + Component.logTabStore.createPodTab({ + selectedPod: pod, selectedContainer: container, - showTimestamps: false, - previous: false, }); } diff --git a/extensions/survey/main.ts b/extensions/survey/main.ts new file mode 100644 index 0000000000..6fc78f3554 --- /dev/null +++ b/extensions/survey/main.ts @@ -0,0 +1,9 @@ +import { LensMainExtension } from "@k8slens/extensions"; +import { surveyPreferencesStore } from "./src/survey-preferences-store"; + +export default class SurveyMainExtension extends LensMainExtension { + + async onActivate() { + await surveyPreferencesStore.loadExtension(this); + } +} diff --git a/extensions/survey/package-lock.json b/extensions/survey/package-lock.json new file mode 100644 index 0000000000..9bb44b1de4 --- /dev/null +++ b/extensions/survey/package-lock.json @@ -0,0 +1,7928 @@ +{ + "name": "lens-survey", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "@jest/environment": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + } + }, + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@k8slens/extensions": { + "version": "file:../../src/extensions/npm/extensions", + "dev": true, + "requires": { + "@material-ui/core": "*", + "@types/node": "*", + "@types/react-select": "*", + "conf": "^7.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "@material-ui/core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz", + "integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.2", + "@material-ui/system": "^4.11.2", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/styles": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", + "integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + } + }, + "@material-ui/system": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz", + "integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "dev": true + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@types/node": { + "version": "14.14.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", + "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true + } + } + }, + "@types/react-dom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-select": { + "version": "3.0.28", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.28.tgz", + "integrity": "sha512-Gfg3a/EPLyUQkfezcCQkmLW1Vz6+ziclJhn8dpBUEYJF3IUoxS81ToAi3ky2xtnAyk2wJFMXLvE73KiUd56yTA==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/react-dom": "*", + "@types/react-transition-group": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", + "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true + }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "dev": true + }, + "conf": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz", + "integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==", + "dev": true, + "requires": { + "ajv": "^6.12.2", + "atomically": "^1.3.1", + "debounce-fn": "^4.0.0", + "dot-prop": "^5.2.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.3", + "make-dir": "^3.1.0", + "onetime": "^5.1.0", + "pkg-up": "^3.1.0", + "semver": "^7.3.2" + } + }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==", + "dev": true + }, + "debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "requires": { + "mimic-fn": "^3.0.0" + } + }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==", + "dev": true + }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "dev": true, + "requires": { + "symbol-observable": "1.2.0" + } + }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true + }, + "jss": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz", + "integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true + } + } + }, + "jss-plugin-camel-case": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz", + "integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.0" + } + }, + "jss-plugin-default-unit": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz", + "integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-global": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz", + "integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-nested": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz", + "integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz", + "integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz", + "integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz", + "integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/babel__core": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", + "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", + "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/prettier": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz", + "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", + "dev": true + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "dev": true, + "requires": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chokidar": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz", + "integrity": "sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cjs-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "emittery": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", + "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "got": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.1.tgz", + "integrity": "sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true, + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", + "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "import-local": "^3.0.2", + "jest-cli": "^26.6.3" + }, + "dependencies": { + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" + } + } + } + }, + "jest-changed-files": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "execa": "^4.0.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-config": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2" + } + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" + } + }, + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" + } + }, + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + } + }, + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "dev": true, + "requires": { + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + } + }, + "jest-mock": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" + } + }, + "jest-runner": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.7.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + } + }, + "jest-runtime": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^0.6.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.4.1" + } + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "natural-compare": "^1.4.0", + "pretty-format": "^26.6.2", + "semver": "^7.3.2" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-validate": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "leven": "^3.1.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.6.2", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "dev": true + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "dev": true, + "requires": { + "mime-db": "1.45.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", + "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==", + "dev": true + }, + "p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true, + "optional": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prompts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "refiner-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/refiner-js/-/refiner-js-1.0.1.tgz", + "integrity": "sha512-k8AWRzOOa+FfV1stuJZfCA7qxdI7fqqpu9171X3EN29KoDPir8IedttHQWANDBvrW4lmCUgg5T5hWUVXwUtiAw==", + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==", + "dev": true + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "string-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", + "integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "ts-loader": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.14.tgz", + "integrity": "sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "optional": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + }, + "v8-to-istanbul": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz", + "integrity": "sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, + "requires": { + "chokidar": "^3.4.1", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "webpack": { + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", + "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.5.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/extensions/survey/package.json b/extensions/survey/package.json new file mode 100644 index 0000000000..fb8dfcce11 --- /dev/null +++ b/extensions/survey/package.json @@ -0,0 +1,28 @@ +{ + "name": "lens-survey", + "version": "0.1.0", + "description": "Lens survey", + "main": "dist/main.js", + "renderer": "dist/renderer.js", + "lens": { + "metadata": {}, + "styles": [] + }, + "scripts": { + "build": "webpack -p", + "dev": "webpack --watch", + "test": "jest --passWithNoTests --env=jsdom src $@" + }, + "dependencies": {}, + "devDependencies": { + "@k8slens/extensions": "file:../../src/extensions/npm/extensions", + "got": "^11.8.1", + "jest": "^26.6.3", + "node-machine-id": "^1.1.12", + "react": "^16.13.1", + "refiner-js": "^1.0.1", + "ts-loader": "^8.0.4", + "typescript": "^4.0.3", + "webpack": "^4.44.2" + } +} diff --git a/extensions/survey/renderer.tsx b/extensions/survey/renderer.tsx new file mode 100644 index 0000000000..0a8d8a678a --- /dev/null +++ b/extensions/survey/renderer.tsx @@ -0,0 +1,25 @@ +import { LensRendererExtension } from "@k8slens/extensions"; +import { survey } from "./src/survey"; +import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference"; +import { surveyPreferencesStore } from "./src/survey-preferences-store"; +import React from "react"; + +export default class SurveyRendererExtension extends LensRendererExtension { + appPreferences = [ + { + title: "In-App Surveys", + showInPreferencesTab: "telemetry", + components: { + Hint: () => , + Input: () => + } + } + ]; + async onActivate() { + // Activate extension only on main renderer + if (window.location.hostname === "localhost") { + await surveyPreferencesStore.loadExtension(this); + survey.start(); + } + } +} diff --git a/extensions/survey/src/refiner-js.d.ts b/extensions/survey/src/refiner-js.d.ts new file mode 100644 index 0000000000..5d63c98e2d --- /dev/null +++ b/extensions/survey/src/refiner-js.d.ts @@ -0,0 +1,3 @@ +declare module "refiner-js" { + export default function Refiner(key: string, value: string|object|number|Boolean|Array): void; +} diff --git a/extensions/survey/src/survey-preference.tsx b/extensions/survey/src/survey-preference.tsx new file mode 100644 index 0000000000..edd9d3cc03 --- /dev/null +++ b/extensions/survey/src/survey-preference.tsx @@ -0,0 +1,27 @@ +import { Component } from "@k8slens/extensions"; +import React from "react"; +import { observer } from "mobx-react"; +import { SurveyPreferencesStore } from "./survey-preferences-store"; + +@observer +export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> { + render() { + const { survey } = this.props; + + return ( + survey.enabled = v } + /> + ); + } +} + +export class SurveyPreferenceHint extends React.Component { + render() { + return ( + This will allow you to participate in surveys to improve the Lens experience. + ); + } +} diff --git a/extensions/survey/src/survey-preferences-store.ts b/extensions/survey/src/survey-preferences-store.ts new file mode 100644 index 0000000000..f15e582fe3 --- /dev/null +++ b/extensions/survey/src/survey-preferences-store.ts @@ -0,0 +1,36 @@ +import { Store } from "@k8slens/extensions"; +import { observable, toJS, when } from "mobx"; + +export type SurveyPreferencesModel = { + enabled: boolean; +}; + +export class SurveyPreferencesStore extends Store.ExtensionStore { + + @observable enabled = true; + + whenEnabled = when(() => this.enabled); + + private constructor() { + super({ + configName: "preferences-store", + defaults: { + enabled: true + } + }); + } + + protected fromStore({ enabled }: SurveyPreferencesModel): void { + this.enabled = enabled; + } + + toJSON(): SurveyPreferencesModel { + return toJS({ + enabled: this.enabled + }, { + recurseEverything: true + }); + } +} + +export const surveyPreferencesStore = SurveyPreferencesStore.getInstance(); diff --git a/extensions/survey/src/survey.ts b/extensions/survey/src/survey.ts new file mode 100644 index 0000000000..8bd6d2579b --- /dev/null +++ b/extensions/survey/src/survey.ts @@ -0,0 +1,46 @@ +import { Util } from "@k8slens/extensions"; +import { machineId } from "node-machine-id"; +import Refiner from "refiner-js"; +import got from "got"; +import { surveyPreferencesStore } from "./survey-preferences-store"; + +type SurveyIdResponse = { + surveyId: string; +}; +export class Survey extends Util.Singleton { + static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43"; + protected anonymousId: string; + + private constructor() { + super(); + } + + async start() { + await surveyPreferencesStore.whenEnabled; + + const surveyId = await this.fetchSurveyId(); + + if (surveyId) { + Refiner("setProject", Survey.PROJECT_ID); + Refiner("identifyUser", { + id: surveyId, + }); + + } + } + + async fetchSurveyId() { + try { + const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev"; + const anonymousId = await machineId(); + const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"}); + + return (body as SurveyIdResponse).surveyId; + } catch(error) { + return null; + } + + } +} + +export const survey = Survey.getInstance(); diff --git a/extensions/survey/tsconfig.json b/extensions/survey/tsconfig.json new file mode 100644 index 0000000000..05c3fa6671 --- /dev/null +++ b/extensions/survey/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "outDir": "dist", + "baseUrl": ".", + "module": "CommonJS", + "target": "ES2017", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "sourceMap": false, + "declaration": false, + "strict": false, + "noImplicitAny": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "jsx": "react", + "paths": { + "*": [ + "node_modules/*", + "../../types/*" + ] + } + }, + "include": [ + "renderer.ts", + "src/**/*" + ] +} diff --git a/extensions/survey/webpack.config.js b/extensions/survey/webpack.config.js new file mode 100644 index 0000000000..c7862dc2c9 --- /dev/null +++ b/extensions/survey/webpack.config.js @@ -0,0 +1,67 @@ +const path = require("path"); + +module.exports = [ + { + entry: "./main.ts", + context: __dirname, + target: "electron-main", + mode: "production", + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + externals: [ + { + "@k8slens/extensions": "var global.LensExtensions", + "react": "var global.React", + "mobx": "var global.Mobx" + } + ], + resolve: { + extensions: [ ".tsx", ".ts", ".js" ], + }, + output: { + libraryTarget: "commonjs2", + globalObject: "this", + filename: "main.js", + path: path.resolve(__dirname, "dist"), + }, + }, + { + entry: "./renderer.tsx", + context: __dirname, + target: "electron-renderer", + mode: "production", + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + externals: [ + { + "@k8slens/extensions": "var global.LensExtensions", + "react": "var global.React", + "mobx": "var global.Mobx", + "mobx-react": "var global.MobxReact" + } + ], + resolve: { + extensions: [ ".tsx", ".ts", ".js" ], + }, + output: { + libraryTarget: "commonjs2", + globalObject: "this", + filename: "renderer.js", + path: path.resolve(__dirname, "dist"), + }, + }, +]; diff --git a/extensions/telemetry/package-lock.json b/extensions/telemetry/package-lock.json index 89e30dc2f4..6731a67d11 100644 --- a/extensions/telemetry/package-lock.json +++ b/extensions/telemetry/package-lock.json @@ -2255,24 +2255,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2901,7 +2901,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3337,6 +3338,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4533,6 +4535,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4547,6 +4550,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4556,6 +4560,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4564,13 +4569,15 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "dev": true, + "optional": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4579,7 +4586,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5595,7 +5603,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -7089,9 +7098,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/extensions/telemetry/package.json b/extensions/telemetry/package.json index 5e32f25b42..a5df1626f7 100644 --- a/extensions/telemetry/package.json +++ b/extensions/telemetry/package.json @@ -13,7 +13,6 @@ "dev": "webpack --watch", "test": "jest --passWithNoTests --env=jsdom src $@" }, - "dependencies": {}, "devDependencies": { "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "@types/analytics-node": "^3.1.3", diff --git a/extensions/telemetry/renderer.tsx b/extensions/telemetry/renderer.tsx index 1078d18f4c..15fda68ede 100644 --- a/extensions/telemetry/renderer.tsx +++ b/extensions/telemetry/renderer.tsx @@ -8,6 +8,8 @@ export default class TelemetryRendererExtension extends LensRendererExtension { appPreferences = [ { title: "Telemetry & Usage Tracking", + showInPreferencesTab: "telemetry", + id: "telemetry-tracking", components: { Hint: () => , Input: () => diff --git a/extensions/telemetry/src/tracker.ts b/extensions/telemetry/src/tracker.ts index 28595a6a93..76b046f63b 100644 --- a/extensions/telemetry/src/tracker.ts +++ b/extensions/telemetry/src/tracker.ts @@ -102,13 +102,12 @@ export class Tracker extends Util.Singleton { } protected reportData() { - const clustersList = Store.clusterStore.enabledClustersList; + const clustersList = Store.catalogEntities.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster"); this.event("generic-data", "report", { appVersion: App.version, os: this.os, clustersCount: clustersList.length, - workspacesCount: Store.workspaceStore.enabledWorkspacesList.length, extensions: App.getEnabledExtensions() }); @@ -118,10 +117,10 @@ export class Tracker extends Util.Singleton { }); } - protected reportClusterData(cluster: Store.ClusterModel) { + protected reportClusterData(cluster: Store.KubernetesCluster) { this.event("cluster-data", "report", { id: cluster.metadata.id, - managed: !!cluster.ownerRef, + managed: cluster.metadata.source !== "local", kubernetesVersion: cluster.metadata.version, distribution: cluster.metadata.distribution, nodesCount: cluster.metadata.nodes, diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 410712e4f0..518ede0376 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -1,3 +1,7 @@ +/** + * @jest-environment node + */ + /* Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the @@ -5,83 +9,34 @@ cluster and vice versa. */ import { Application } from "spectron"; -import * as util from "../helpers/utils"; -import { spawnSync } from "child_process"; +import * as utils from "../helpers/utils"; +import { listHelmRepositories } from "../helpers/utils"; +import { fail } from "assert"; jest.setTimeout(60000); // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) describe("Lens integration tests", () => { - const TEST_NAMESPACE = "integration-tests"; - const BACKSPACE = "\uE003"; let app: Application; - const appStart = async () => { - app = util.setup(); - await app.start(); - // Wait for splash screen to be closed - while (await app.client.getWindowCount() > 1); - await app.client.windowByIndex(0); - await app.client.waitUntilWindowLoaded(); - }; - const clickWhatsNew = async (app: Application) => { - await app.client.waitUntilTextExists("h1", "What's new?"); - await app.client.click("button.primary"); - await app.client.waitUntilTextExists("h1", "Welcome"); - }; - const minikubeReady = (): boolean => { - // determine if minikube is running - { - const { status } = spawnSync("minikube status", { shell: true }); - - if (status !== 0) { - console.warn("minikube not running"); - - return false; - } - } - - // Remove TEST_NAMESPACE if it already exists - { - const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); - - if (status === 0) { - console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); - - const { status, stdout, stderr } = spawnSync( - `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, - { shell: true }, - ); - - if (status !== 0) { - console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`); - - return false; - } - - console.log(stdout.toString()); - } - } - - return true; - }; - const ready = minikubeReady(); describe("app start", () => { - beforeAll(appStart, 20000); + utils.beforeAllWrapped(async () => { + app = await utils.appStart(); + }); - afterAll(async () => { + utils.afterAllWrapped(async () => { if (app?.isRunning()) { - await util.tearDown(app); + await utils.tearDown(app); } }); it('shows "whats new"', async () => { - await clickWhatsNew(app); + await utils.clickWhatsNew(app); }); it('shows "add cluster"', async () => { await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster"); - await app.client.waitUntilTextExists("h2", "Add Cluster"); + await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig"); }); describe("preferences page", () => { @@ -89,11 +44,28 @@ describe("Lens integration tests", () => { const appName: string = process.platform === "darwin" ? "Lens" : "File"; await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); - await app.client.waitUntilTextExists("h2", "Preferences"); + await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION"); + }); + + it("shows all tabs and their contents", async () => { + await app.client.click("[data-testid=application-tab]"); + await app.client.click("[data-testid=proxy-tab]"); + await app.client.waitUntilTextExists("[data-testid=proxy-header]", "PROXY"); + await app.client.click("[data-testid=kube-tab]"); + await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "KUBERNETES"); + await app.client.click("[data-testid=telemetry-tab]"); + await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "TELEMETRY"); }); it("ensures helm repos", async () => { - await app.client.waitUntilTextExists("div.repos #message-bitnami", "bitnami"); // wait for the helm-cli to fetch the bitnami repo + const repos = await listHelmRepositories(); + + if (!repos[0]) { + fail("Lens failed to add Bitnami repository"); + } + + await app.client.click("[data-testid=kube-tab]"); + await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s) await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) }); @@ -104,470 +76,4 @@ describe("Lens integration tests", () => { await app.client.keys("Meta"); }); }); - - util.describeIf(ready)("workspaces", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); - } - }); - - it("creates new workspace", async () => { - await clickWhatsNew(app); - await app.client.click("#current-workspace .Icon"); - await app.client.click('a[href="/workspaces"]'); - await app.client.click(".Workspaces button.Button"); - await app.client.keys("test-workspace"); - await app.client.click(".Workspaces .Input.description input"); - await app.client.keys("test description"); - await app.client.click(".Workspaces .workspace.editing .Icon"); - await app.client.waitUntilTextExists(".workspace .name a", "test-workspace"); - }); - - it("adds cluster in default workspace", async () => { - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - - it("adds cluster in test-workspace", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]'); - await app.client.click('.WorkspaceMenu li[title="test description"]'); - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - }); - - it("checks if default workspace has active cluster", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible(".WorkspaceMenu > li:first-of-type"); - await app.client.click(".WorkspaceMenu > li:first-of-type"); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - }); - - const addMinikubeCluster = async (app: Application) => { - await app.client.click("div.add-cluster"); - await app.client.waitUntilTextExists("div", "Select kubeconfig file"); - await app.client.click("div.Select__control"); // show the context drop-down list - await app.client.waitUntilTextExists("div", "minikube"); - - if (!await app.client.$("button.primary").isEnabled()) { - await app.client.click("div.minikube"); // select minikube context - } // else the only context, which must be 'minikube', is automatically selected - await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) - await app.client.click("button.primary"); // add minikube cluster - }; - const waitForMinikubeDashboard = async (app: Application) => { - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.frame("minikube"); - await app.client.waitUntilTextExists("span.link-text", "Cluster"); - }; - - util.describeIf(ready)("cluster tests", () => { - let clusterAdded = false; - const addCluster = async () => { - await clickWhatsNew(app); - await addMinikubeCluster(app); - await waitForMinikubeDashboard(app); - await app.client.click('a[href="/nodes"]'); - await app.client.waitUntilTextExists("div.TableCell", "Ready"); - }; - - describe("cluster add", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); - } - }); - - it("allows to add a cluster", async () => { - await addCluster(); - clusterAdded = true; - }); - }); - - const appStartAddCluster = async () => { - if (clusterAdded) { - await appStart(); - await addCluster(); - } - }; - - describe("cluster pages", () => { - - beforeAll(appStartAddCluster, 40000); - - afterAll(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); - } - }); - - const tests: { - drawer?: string - drawerId?: string - pages: { - name: string, - href: string, - expectedSelector: string, - expectedText: string - }[] - }[] = [{ - drawer: "", - drawerId: "", - pages: [{ - name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", - expectedText: "Master" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Nodes", - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - }] - }, - { - drawer: "Workloads", - drawerId: "workloads", - pages: [{ - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "ReplicaSets", - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - }] - }, - { - drawer: "Configuration", - drawerId: "config", - pages: [{ - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - }] - }, - { - drawer: "Network", - drawerId: "networks", - pages: [{ - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - }] - }, - { - drawer: "Storage", - drawerId: "storage", - pages: [{ - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Namespaces", - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Events", - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - }] - }, - { - drawer: "Apps", - drawerId: "apps", - pages: [{ - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - }] - }, - { - drawer: "Access Control", - drawerId: "users", - pages: [{ - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - }] - }, - { - drawer: "Custom Resources", - drawerId: "custom-resources", - pages: [{ - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - }] - }]; - - tests.forEach(({ drawer = "", drawerId = "", pages }) => { - if (drawer !== "") { - it(`shows ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name); - }); - } - pages.forEach(({ name, href, expectedSelector, expectedText }) => { - it(`shows ${drawer}->${name} page`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - }); - - if (drawer !== "") { - // hide the drawer - it(`hides ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow(); - }); - } - }); - }); - - describe("viewing pod logs", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); - } - }); - - it(`shows a logs for a pod`, async () => { - expect(clusterAdded).toBe(true); - // Go to Pods page - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - // Open logs tab in dock - await app.client.click(".list .TableRow:first-child"); - await app.client.waitForVisible(".Drawer"); - await app.client.click(".drawer-title .Menu li:nth-child(2)"); - // Check if controls are available - await app.client.waitForVisible(".PodLogs .VirtualList"); - await app.client.waitForVisible(".PodLogControls"); - await app.client.waitForVisible(".PodLogControls .SearchInput"); - await app.client.waitForVisible(".PodLogControls .SearchInput input"); - // Search for semicolon - await app.client.keys(":"); - await app.client.waitForVisible(".PodLogs .list span.active"); - // Click through controls - await app.client.click(".PodLogControls .timestamps-icon"); - await app.client.click(".PodLogControls .undo-icon"); - }); - }); - - describe("cluster operations", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return util.tearDown(app); - } - }); - - it("shows default namespace", async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - }); - - it(`creates ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - await app.client.click("button.add-button"); - await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); - await app.client.keys(`${TEST_NAMESPACE}\n`); - await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); - }); - - it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - await app.client.click(".Icon.new-dock-tab"); - await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); - await app.client.click("li.MenuItem.create-resource-tab"); - await app.client.waitForVisible(".CreateResource div.ace_content"); - // Write pod manifest to editor - await app.client.keys("apiVersion: v1\n"); - await app.client.keys("kind: Pod\n"); - await app.client.keys("metadata:\n"); - await app.client.keys(" name: nginx-create-pod-test\n"); - await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); - await app.client.keys(`${BACKSPACE}spec:\n`); - await app.client.keys(" containers:\n"); - await app.client.keys("- name: nginx-create-pod-test\n"); - await app.client.keys(" image: nginx:alpine\n"); - // Create deployment - await app.client.waitForEnabled("button.Button=Create & Close"); - await app.client.click("button.Button=Create & Close"); - // Wait until first bits of pod appears on dashboard - await app.client.waitForExist(".name=nginx-create-pod-test"); - // Open pod details - await app.client.click(".name=nginx-create-pod-test"); - await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); - }); - }); - }); }); diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts new file mode 100644 index 0000000000..2d7807e22b --- /dev/null +++ b/integration/__tests__/cluster-pages.tests.ts @@ -0,0 +1,470 @@ +/* + Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE + namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the + TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube + cluster and vice versa. +*/ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; +import { exec } from "child_process"; +import * as util from "util"; + +export const promiseExec = util.promisify(exec); + +jest.setTimeout(60000); + +// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) +describe("Lens cluster pages", () => { + const TEST_NAMESPACE = "integration-tests"; + const BACKSPACE = "\uE003"; + let app: Application; + const ready = minikubeReady(TEST_NAMESPACE); + + utils.describeIf(ready)("test common pages", () => { + let clusterAdded = false; + const addCluster = async () => { + await utils.clickWhatsNew(app); + await utils.clickWelcomeNotification(app); + await app.client.waitUntilTextExists("div", "Catalog"); + await addMinikubeCluster(app); + await waitForMinikubeDashboard(app); + await app.client.click('a[href="/nodes"]'); + await app.client.waitUntilTextExists("div.TableCell", "Ready"); + }; + + describe("cluster add", () => { + utils.beforeAllWrapped(async () => { + app = await utils.appStart(); + }); + + utils.afterAllWrapped(async () => { + if (app?.isRunning()) { + return utils.tearDown(app); + } + }); + + it("allows to add a cluster", async () => { + await addCluster(); + clusterAdded = true; + }); + }); + + const appStartAddCluster = async () => { + if (clusterAdded) { + app = await utils.appStart(); + await addCluster(); + } + }; + + function getSidebarSelectors(itemId: string) { + const root = `.SidebarItem[data-test-id="${itemId}"]`; + + return { + expandSubMenu: `${root} .nav-item`, + subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`, + }; + } + + describe("cluster pages", () => { + utils.beforeAllWrapped(appStartAddCluster); + + utils.afterAllWrapped(async () => { + if (app?.isRunning()) { + return utils.tearDown(app); + } + }); + + const tests: { + drawer?: string + drawerId?: string + pages: { + name: string, + href: string, + expectedSelector: string, + expectedText: string + }[] + }[] = [{ + drawer: "", + drawerId: "", + pages: [{ + name: "Cluster", + href: "cluster", + expectedSelector: "div.ClusterOverview div.label", + expectedText: "Master" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Nodes", + href: "nodes", + expectedSelector: "h5.title", + expectedText: "Nodes" + }] + }, + { + drawer: "Workloads", + drawerId: "workloads", + pages: [{ + name: "Overview", + href: "workloads", + expectedSelector: "h5.box", + expectedText: "Overview" + }, + { + name: "Pods", + href: "pods", + expectedSelector: "h5.title", + expectedText: "Pods" + }, + { + name: "Deployments", + href: "deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" + }, + { + name: "DaemonSets", + href: "daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" + }, + { + name: "StatefulSets", + href: "statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" + }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, + { + name: "Jobs", + href: "jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" + }, + { + name: "CronJobs", + href: "cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }] + }, + { + drawer: "Configuration", + drawerId: "config", + pages: [{ + name: "ConfigMaps", + href: "configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" + }, + { + name: "Secrets", + href: "secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" + }, + { + name: "Resource Quotas", + href: "resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" + }, + { + name: "Limit Ranges", + href: "limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, + { + name: "HPA", + href: "hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" + }, + { + name: "Pod Disruption Budgets", + href: "poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }] + }, + { + drawer: "Network", + drawerId: "networks", + pages: [{ + name: "Services", + href: "services", + expectedSelector: "h5.title", + expectedText: "Services" + }, + { + name: "Endpoints", + href: "endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }] + }, + { + drawer: "Storage", + drawerId: "storage", + pages: [{ + name: "Persistent Volume Claims", + href: "persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Namespaces", + href: "namespaces", + expectedSelector: "h5.title", + expectedText: "Namespaces" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Events", + href: "events", + expectedSelector: "h5.title", + expectedText: "Events" + }] + }, + { + drawer: "Apps", + drawerId: "apps", + pages: [{ + name: "Charts", + href: "apps/charts", + expectedSelector: "div.HelmCharts input", + expectedText: "" + }, + { + name: "Releases", + href: "apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }] + }, + { + drawer: "Access Control", + drawerId: "users", + pages: [{ + name: "Service Accounts", + href: "service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Role Bindings", + href: "role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Roles", + href: "roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Pod Security Policies", + href: "pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }] + }, + { + drawer: "Custom Resources", + drawerId: "custom-resources", + pages: [{ + name: "Definitions", + href: "crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }] + }]; + + tests.forEach(({ drawer = "", drawerId = "", pages }) => { + const selectors = getSidebarSelectors(drawerId); + + if (drawer !== "") { + it(`shows ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(selectors.expandSubMenu); + await app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name); + }); + + pages.forEach(({ name, href, expectedSelector, expectedText }) => { + it(`shows ${drawer}->${name} page`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(selectors.subMenuLink(href)); + await app.client.waitUntilTextExists(expectedSelector, expectedText); + }); + }); + + it(`hides ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(selectors.expandSubMenu); + await expect(app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name, 100)).rejects.toThrow(); + }); + } else { + const { href, name, expectedText, expectedSelector } = pages[0]; + + it(`shows page ${name}`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`a[href^="/${href}"]`); + await app.client.waitUntilTextExists(expectedSelector, expectedText); + }); + } + }); + }); + + describe("viewing pod logs", () => { + utils.beforeEachWrapped(appStartAddCluster); + + utils.afterEachWrapped(async () => { + if (app?.isRunning()) { + return utils.tearDown(app); + } + }); + + it(`shows a log for a pod`, async () => { + expect(clusterAdded).toBe(true); + // Go to Pods page + await app.client.click(getSidebarSelectors("workloads").expandSubMenu); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + await app.client.click(".NamespaceSelect"); + await app.client.keys("kube-system"); + await app.client.keys("Enter");// "\uE007" + await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + let podMenuItemEnabled = false; + + // Wait until extensions are enabled on renderer + while (!podMenuItemEnabled) { + const logs = await app.client.getRenderProcessLogs(); + + podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); + + if (!podMenuItemEnabled) { + await new Promise(r => setTimeout(r, 1000)); + } + } + await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions + // Open logs tab in dock + await app.client.click(".list .TableRow:first-child"); + await app.client.waitForVisible(".Drawer"); + await app.client.click(".drawer-title .Menu li:nth-child(2)"); + // Check if controls are available + await app.client.waitForVisible(".LogList .VirtualList"); + await app.client.waitForVisible(".LogResourceSelector"); + //await app.client.waitForVisible(".LogSearch .SearchInput"); + await app.client.waitForVisible(".LogSearch .SearchInput input"); + // Search for semicolon + await app.client.keys(":"); + await app.client.waitForVisible(".LogList .list span.active"); + // Click through controls + await app.client.click(".LogControls .show-timestamps"); + await app.client.click(".LogControls .show-previous"); + }); + }); + + describe("cluster operations", () => { + utils.beforeEachWrapped(appStartAddCluster); + + utils.afterEachWrapped(async () => { + if (app?.isRunning()) { + return utils.tearDown(app); + } + }); + + it("shows default namespace", async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + }); + + it(`creates ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + await app.client.click("button.add-button"); + await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); + await app.client.keys(`${TEST_NAMESPACE}\n`); + await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); + }); + + it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(getSidebarSelectors("workloads").expandSubMenu); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + + await app.client.click(".NamespaceSelect"); + await app.client.keys(TEST_NAMESPACE); + await app.client.keys("Enter");// "\uE007" + await app.client.click(".Icon.new-dock-tab"); + await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); + await app.client.click("li.MenuItem.create-resource-tab"); + await app.client.waitForVisible(".CreateResource div.ace_content"); + // Write pod manifest to editor + await app.client.keys("apiVersion: v1\n"); + await app.client.keys("kind: Pod\n"); + await app.client.keys("metadata:\n"); + await app.client.keys(" name: nginx-create-pod-test\n"); + await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); + await app.client.keys(`${BACKSPACE}spec:\n`); + await app.client.keys(" containers:\n"); + await app.client.keys("- name: nginx-create-pod-test\n"); + await app.client.keys(" image: nginx:alpine\n"); + // Create deployment + await app.client.waitForEnabled("button.Button=Create & Close"); + await app.client.click("button.Button=Create & Close"); + // Wait until first bits of pod appears on dashboard + await app.client.waitForExist(".name=nginx-create-pod-test"); + // Open pod details + await app.client.click(".name=nginx-create-pod-test"); + await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); + }); + }); + }); +}); diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts new file mode 100644 index 0000000000..08da064bd2 --- /dev/null +++ b/integration/__tests__/command-palette.tests.ts @@ -0,0 +1,27 @@ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; + +jest.setTimeout(60000); + +describe("Lens command palette", () => { + let app: Application; + + describe("menu", () => { + utils.beforeAllWrapped(async () => { + app = await utils.appStart(); + }); + + utils.afterAllWrapped(async () => { + if (app?.isRunning()) { + await utils.tearDown(app); + } + }); + + it("opens command dialog from menu", async () => { + await utils.clickWhatsNew(app); + await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette..."); + await app.client.waitUntilTextExists(".Select__option", "Preferences: Open"); + await app.client.keys("Escape"); + }); + }); +}); diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts new file mode 100644 index 0000000000..2c62279e4b --- /dev/null +++ b/integration/helpers/minikube.ts @@ -0,0 +1,61 @@ +import { spawnSync } from "child_process"; +import { Application } from "spectron"; + +export function minikubeReady(testNamespace: string): boolean { + // determine if minikube is running + { + const { status } = spawnSync("minikube status", { shell: true }); + + if (status !== 0) { + console.warn("minikube not running"); + + return false; + } + } + + // Remove TEST_NAMESPACE if it already exists + { + const { status } = spawnSync(`minikube kubectl -- get namespace ${testNamespace}`, { shell: true }); + + if (status === 0) { + console.warn(`Removing existing ${testNamespace} namespace`); + + const { status, stdout, stderr } = spawnSync( + `minikube kubectl -- delete namespace ${testNamespace}`, + { shell: true }, + ); + + if (status !== 0) { + console.warn(`Error removing ${testNamespace} namespace: ${stderr.toString()}`); + + return false; + } + + console.log(stdout.toString()); + } + } + + return true; +} + +export async function addMinikubeCluster(app: Application) { + await app.client.click("button.add-button"); + await app.client.waitUntilTextExists("div", "Select kubeconfig file"); + await app.client.click("div.Select__control"); // show the context drop-down list + await app.client.waitUntilTextExists("div", "minikube"); + + if (!await app.client.$("button.primary").isEnabled()) { + await app.client.click("div.minikube"); // select minikube context + } // else the only context, which must be 'minikube', is automatically selected + await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) + await app.client.click("button.primary"); // add minikube cluster + await app.client.waitUntilTextExists("div.TableCell", "minikube"); + await app.client.click("div.TableRow"); +} + +export async function waitForMinikubeDashboard(app: Application) { + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.frame("minikube"); + await app.client.waitUntilTextExists("span.link-text", "Cluster"); +} diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index f445a9ae48..1417f3f04e 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -1,4 +1,6 @@ import { Application } from "spectron"; +import * as util from "util"; +import { exec } from "child_process"; const AppPaths: Partial> = { "win32": "./dist/win-unpacked/Lens.exe", @@ -6,6 +8,39 @@ const AppPaths: Partial> = { "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", }; +interface DoneCallback { + (...args: any[]): any; + fail(error?: string | { message: string }): any; +} + +/** + * This is necessary because Jest doesn't do this correctly. + * @param fn The function to call + */ +export function wrapJestLifecycle(fn: () => Promise): (done: DoneCallback) => void { + return function (done: DoneCallback) { + fn() + .then(() => done()) + .catch(error => done.fail(error)); + }; +} + +export function beforeAllWrapped(fn: () => Promise): void { + beforeAll(wrapJestLifecycle(fn)); +} + +export function beforeEachWrapped(fn: () => Promise): void { + beforeEach(wrapJestLifecycle(fn)); +} + +export function afterAllWrapped(fn: () => Promise): void { + afterAll(wrapJestLifecycle(fn)); +} + +export function afterEachWrapped(fn: () => Promise): void { + afterEach(wrapJestLifecycle(fn)); +} + export function itIf(condition: boolean) { return condition ? it : it.skip; } @@ -26,6 +61,38 @@ export function setup(): Application { }); } +export const keys = { + backspace: "\uE003" +}; + +export async function appStart() { + const app = setup(); + + await app.start(); + // Wait for splash screen to be closed + while (await app.client.getWindowCount() > 1); + await app.client.windowByIndex(0); + await app.client.waitUntilWindowLoaded(); + + return app; +} + +export async function clickWhatsNew(app: Application) { + await app.client.waitUntilTextExists("h1", "What's new?"); + await app.client.click("button.primary"); + await app.client.waitUntilTextExists("div", "Catalog"); +} + +export async function clickWelcomeNotification(app: Application) { + const itemsText = await app.client.$("div.info-panel").getText(); + + if (itemsText === "0 item") { + // welcome notification should be present, dismiss it + await app.client.waitUntilTextExists("div.message", "Welcome!"); + await app.client.click(".notification i.Icon.close"); + } +} + type AsyncPidGetter = () => Promise; export async function tearDown(app: Application) { @@ -39,3 +106,26 @@ export async function tearDown(app: Application) { console.error(e); } } + +export const promiseExec = util.promisify(exec); + +type HelmRepository = { + name: string; + url: string; +}; + +export async function listHelmRepositories(retries = 0): Promise{ + if (retries < 5) { + try { + const { stdout: reposJson } = await promiseExec("helm repo list -o json"); + + return JSON.parse(reposJson); + } catch { + await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository + + return await listHelmRepositories((retries + 1)); + } + } + + return []; +} diff --git a/jsonnet/custom-prometheus.jsonnet b/jsonnet/lens/custom-prometheus.jsonnet similarity index 100% rename from jsonnet/custom-prometheus.jsonnet rename to jsonnet/lens/custom-prometheus.jsonnet diff --git a/mkdocs.yml b/mkdocs.yml index ad3e19572e..337e0c4b11 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,7 +5,7 @@ site_url: https://docs.k8slens.dev docs_dir: docs/ repo_name: GitHub repo_url: https://github.com/lensapp/lens -copyright: Copyright © 2020 Mirantis Inc. - All rights reserved. +copyright: Copyright © 2021 Mirantis Inc. - All rights reserved. edit_uri: "" nav: - Overview: README.md @@ -34,7 +34,7 @@ nav: - Main Extension: extensions/guides/main-extension.md - Renderer Extension: extensions/guides/renderer-extension.md - Stores: extensions/guides/stores.md - - Working with mobx: extensions/guides/working-with-mobx.md + - Working with MobX: extensions/guides/working-with-mobx.md - Testing and Publishing: - Testing Extensions: extensions/testing-and-publishing/testing.md - Publishing Extensions: extensions/testing-and-publishing/publishing.md @@ -81,6 +81,8 @@ markdown_extensions: - toc: permalink: "#" toc_depth: 3 + - admonition: {} + - pymdownx.details: {} extra: generator: false diff --git a/package.json b/package.json index 7d00909318..bb82612ea2 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.0-alpha.0", + "version": "5.0.0-alpha.1", "main": "static/build/main.js", - "copyright": "© 2020, Mirantis, Inc.", + "copyright": "© 2021, Mirantis, Inc.", "license": "MIT", "author": { "name": "Mirantis, Inc.", @@ -13,7 +13,8 @@ "scripts": { "dev": "concurrently -k \"yarn run dev-run -C\" yarn:dev:*", "dev-build": "concurrently yarn:compile:*", - "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", + "debug-build": "concurrently yarn:compile:main yarn:compile:extension-types", + "dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"", "dev:main": "yarn run compile:main --watch", "dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts", "dev:extension-types": "yarn run compile:extension-types --watch", @@ -25,8 +26,7 @@ "build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens", "build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens", "build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens", - "test": "jest --env=jsdom src $@", - "integration": "jest --coverage integration $@", + "integration": "jest --runInBand integration", "dist": "yarn run compile && electron-builder --publish onTag", "dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32", "dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null", @@ -35,14 +35,14 @@ "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts", "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts", - "lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 .", + "lint": "yarn run eslint --ext js,ts,tsx --max-warnings=0 .", "lint:fix": "yarn run lint --fix", "mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest", "verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict", "typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts" }, "config": { - "bundledKubectlVersion": "1.17.15", + "bundledKubectlVersion": "1.18.15", "bundledHelmVersion": "3.4.2" }, "engines": { @@ -51,7 +51,6 @@ "jest": { "collectCoverage": false, "verbose": true, - "testEnvironment": "node", "transform": { "^.+\\.tsx?$": "ts-jest" }, @@ -70,6 +69,7 @@ ] }, "build": { + "generateUpdatesFilesForAllChannels": true, "files": [ "static/build/main.js" ], @@ -103,7 +103,10 @@ ], "linux": { "category": "Network", + "artifactName": "${productName}-${version}.${arch}.${ext}", "target": [ + "deb", + "rpm", "snap", "AppImage" ], @@ -154,17 +157,26 @@ ] }, "nsis": { - "include": "build/installer.nsh" + "include": "build/installer.nsh", + "oneClick": false, + "allowToChangeInstallationDirectory": true + }, + "snap": { + "confinement": "classic" }, "publish": [ { - "provider": "github", - "repo": "lens", - "owner": "lensapp" + "provider": "s3", + "bucket": "lens-binaries", + "path": "/ide" } ], - "snap": { - "confinement": "classic" + "protocols": { + "name": "Lens Protocol Handler", + "schemes": [ + "lens" + ], + "role": "Viewer" } }, "lens": { @@ -174,15 +186,18 @@ "node-menu", "metrics-cluster-feature", "license-menu-item", - "kube-object-event-status" + "kube-object-event-status", + "survey" ] }, "dependencies": { "@hapi/call": "^8.0.0", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.12.0", + "abort-controller": "^3.0.0", "array-move": "^3.0.0", "await-lock": "^2.1.0", + "byline": "^5.0.0", "chalk": "^4.1.0", "chokidar": "^3.4.3", "command-exists": "1.2.9", @@ -195,17 +210,19 @@ "fs-extra": "^9.0.1", "handlebars": "^4.7.6", "http-proxy": "^1.18.1", + "immer": "^8.0.1", "js-yaml": "^3.14.0", "jsdom": "^16.4.0", "jsonpath": "^1.0.2", "lodash": "^4.17.15", "mac-ca": "^1.0.4", - "marked": "^1.1.0", + "marked": "^1.2.7", "md5-file": "^5.0.0", "mobx": "^5.15.7", "mobx-observable-history": "^1.0.3", "mobx-react": "^6.2.2", "mock-fs": "^4.12.0", + "moment": "^2.26.0", "node-pty": "^0.9.0", "npm": "^6.14.8", "openid-client": "^3.15.2", @@ -215,15 +232,17 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-router": "^5.2.0", + "readable-stream": "^3.6.0", "request": "^2.88.2", "request-promise-native": "^1.0.8", "semver": "^7.3.2", "serializr": "^2.0.3", - "shell-env": "^3.0.0", + "shell-env": "^3.0.1", "spdy": "^4.0.2", "tar": "^6.0.5", "tcp-port-used": "^1.0.1", "tempy": "^0.5.0", + "url-parse": "^1.4.7", "uuid": "^8.3.2", "win-ca": "^3.2.0", "winston": "^3.2.1", @@ -233,9 +252,11 @@ "devDependencies": { "@emeraldpay/hashicon-react": "^0.4.0", "@material-ui/core": "^4.10.1", + "@material-ui/lab": "^4.0.0-alpha.57", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@testing-library/jest-dom": "^5.11.5", - "@testing-library/react": "^11.1.0", + "@testing-library/react": "^11.2.6", + "@types/byline": "^4.2.32", "@types/chart.js": "^2.9.21", "@types/circular-dependency-plugin": "^5.0.1", "@types/color": "^3.0.1", @@ -244,7 +265,7 @@ "@types/electron-devtools-installer": "^2.2.0", "@types/electron-window-state": "^2.0.34", "@types/fs-extra": "^9.0.1", - "@types/hapi": "^18.0.3", + "@types/hapi": "^18.0.5", "@types/hoist-non-react-statics": "^3.3.1", "@types/html-webpack-plugin": "^3.2.3", "@types/http-proxy": "^1.17.4", @@ -268,6 +289,7 @@ "@types/react-router-dom": "^5.1.6", "@types/react-select": "^3.0.13", "@types/react-window": "^1.8.2", + "@types/readable-stream": "^2.3.9", "@types/request": "^2.48.5", "@types/request-promise-native": "^1.0.17", "@types/semver": "^7.2.0", @@ -279,25 +301,27 @@ "@types/tempy": "^0.3.0", "@types/terser-webpack-plugin": "^3.0.0", "@types/universal-analytics": "^0.4.4", + "@types/url-parse": "^1.4.3", "@types/uuid": "^8.3.0", "@types/webdriverio": "^4.13.0", "@types/webpack": "^4.41.17", "@types/webpack-dev-server": "^3.11.1", "@types/webpack-env": "^1.15.2", "@types/webpack-node-externals": "^1.7.1", - "@typescript-eslint/eslint-plugin": "^4.12.0", + "@typescript-eslint/eslint-plugin": "^4.14.2", "@typescript-eslint/parser": "^4.0.0", - "ace-builds": "^1.4.11", - "ansi_up": "^4.0.4", + "ace-builds": "^1.4.12", + "ansi_up": "^5.0.0", "chart.js": "^2.9.3", "circular-dependency-plugin": "^5.2.2", "color": "^3.1.2", "concurrently": "^5.2.0", "css-element-queries": "^1.2.3", "css-loader": "^3.5.3", + "deepdash": "^5.3.5", "dompurify": "^2.0.11", - "electron": "^9.4.0", - "electron-builder": "^22.7.0", + "electron": "^9.4.4", + "electron-builder": "^22.10.5", "electron-notarize": "^0.3.0", "eslint": "^7.7.0", "eslint-plugin-react": "^7.21.5", @@ -313,12 +337,12 @@ "jest-canvas-mock": "^2.3.0", "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^1.0.10", - "make-plural": "^6.2.1", + "make-plural": "^6.2.2", "mini-css-extract-plugin": "^0.9.0", - "moment": "^2.26.0", "node-loader": "^0.6.0", "node-sass": "^4.14.1", "nodemon": "^2.0.4", + "open": "^7.3.1", "patch-package": "^6.2.2", "postinstall-postinstall": "^2.1.0", "prettier": "^2.2.0", @@ -328,6 +352,7 @@ "react-refresh": "^0.9.0", "react-router-dom": "^5.2.0", "react-select": "^3.1.0", + "react-select-event": "^5.1.0", "react-window": "^1.8.5", "sass-loader": "^8.0.2", "sharp": "^0.26.1", @@ -348,7 +373,7 @@ "webpack-dev-server": "^3.11.0", "webpack-node-externals": "^1.7.2", "what-input": "^5.2.10", - "xterm": "^4.6.0", + "xterm": "^4.10.0", "xterm-addon-fit": "^0.4.0" } } diff --git a/src/common/__tests__/catalog-entity-registry.test.ts b/src/common/__tests__/catalog-entity-registry.test.ts new file mode 100644 index 0000000000..da85af3e3d --- /dev/null +++ b/src/common/__tests__/catalog-entity-registry.test.ts @@ -0,0 +1,63 @@ +import { observable, reaction } from "mobx"; +import { WebLink } from "../catalog-entities"; +import { CatalogEntityRegistry } from "../catalog-entity-registry"; + +describe("CatalogEntityRegistry", () => { + let registry: CatalogEntityRegistry; + const entity = new WebLink({ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "WebLink", + metadata: { + uid: "test", + name: "test-link", + source: "test", + labels: {} + }, + spec: { + url: "https://k8slens.dev" + }, + status: { + phase: "ok" + } + }); + + beforeEach(() => { + registry = new CatalogEntityRegistry(); + }); + + describe("addSource", () => { + it ("allows to add an observable source", () => { + const source = observable.array([]); + + registry.addSource("test", source); + expect(registry.items.length).toEqual(0); + + source.push(entity); + + expect(registry.items.length).toEqual(1); + }); + + it ("added source change triggers reaction", (done) => { + const source = observable.array([]); + + registry.addSource("test", source); + reaction(() => registry.items, () => { + done(); + }); + + source.push(entity); + }); + }); + + describe("removeSource", () => { + it ("removes source", () => { + const source = observable.array([]); + + registry.addSource("test", source); + source.push(entity); + registry.removeSource("test"); + + expect(registry.items.length).toEqual(0); + }); + }); +}); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index fcbd5ebd6b..ec8e244fd2 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -2,17 +2,40 @@ import fs from "fs"; import mockFs from "mock-fs"; import yaml from "js-yaml"; import { Cluster } from "../../main/cluster"; -import { ClusterStore } from "../cluster-store"; -import { workspaceStore } from "../workspace-store"; +import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); +const kubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test +contexts: +- context: + cluster: test + user: test + name: foo +- context: + cluster: test + user: test + name: foo2 +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; jest.mock("electron", () => { return { app: { getVersion: () => "99.99.99", getPath: () => "tmp", - getLocale: () => "en" + getLocale: () => "en", + setLoginItemSettings: jest.fn(), }, ipcMain: { handle: jest.fn(), @@ -47,14 +70,13 @@ describe("empty config", () => { clusterStore.addCluster( new Cluster({ id: "foo", - contextName: "minikube", + contextName: "foo", preferences: { terminalCWD: "/tmp", icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"), - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig) }) ); }); @@ -68,21 +90,14 @@ describe("empty config", () => { expect(storedCluster.enabled).toBe(true); }); - it("adds cluster to default workspace", () => { - const storedCluster = clusterStore.getById("foo"); - - expect(storedCluster.workspace).toBe("default"); - }); - it("removes cluster from store", async () => { await clusterStore.removeById("foo"); - expect(clusterStore.getById("foo")).toBeUndefined(); + expect(clusterStore.getById("foo")).toBeNull(); }); it("sets active cluster", () => { clusterStore.setActive("foo"); expect(clusterStore.active.id).toBe("foo"); - expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo"); }); }); @@ -91,21 +106,19 @@ describe("empty config", () => { clusterStore.addClusters( new Cluster({ id: "prod", - contextName: "prod", + contextName: "foo", preferences: { clusterName: "prod" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"), - workspace: "workstation" + kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig) }), new Cluster({ id: "dev", - contextName: "dev", + contextName: "foo2", preferences: { clusterName: "dev" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"), - workspace: "workstation" + kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig) }) ); }); @@ -115,51 +128,11 @@ describe("empty config", () => { expect(clusterStore.clusters.size).toBe(2); }); - it("gets clusters by workspaces", () => { - const wsClusters = clusterStore.getByWorkspaceId("workstation"); - const defaultClusters = clusterStore.getByWorkspaceId("default"); - - expect(defaultClusters.length).toBe(0); - expect(wsClusters.length).toBe(2); - expect(wsClusters[0].id).toBe("prod"); - expect(wsClusters[1].id).toBe("dev"); - }); - it("check if cluster's kubeconfig file saved", () => { const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig"); expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); }); - - it("check if reorderring works for same from and to", () => { - clusterStore.swapIconOrders("workstation", 1, 1); - - const clusters = clusterStore.getByWorkspaceId("workstation"); - - expect(clusters[0].id).toBe("prod"); - expect(clusters[0].preferences.iconOrder).toBe(0); - expect(clusters[1].id).toBe("dev"); - expect(clusters[1].preferences.iconOrder).toBe(1); - }); - - it("check if reorderring works for different from and to", () => { - clusterStore.swapIconOrders("workstation", 0, 1); - - const clusters = clusterStore.getByWorkspaceId("workstation"); - - expect(clusters[0].id).toBe("dev"); - expect(clusters[0].preferences.iconOrder).toBe(0); - expect(clusters[1].id).toBe("prod"); - expect(clusters[1].preferences.iconOrder).toBe(1); - }); - - it("check if after icon reordering, changing workspaces still works", () => { - clusterStore.swapIconOrders("workstation", 1, 1); - clusterStore.getById("prod").workspace = "default"; - - expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1); - expect(clusterStore.getByWorkspaceId("default").length).toBe(1); - }); }); }); @@ -177,20 +150,20 @@ describe("config with existing clusters", () => { clusters: [ { id: "cluster1", - kubeConfig: "foo", + kubeConfigPath: kubeconfig, contextName: "foo", preferences: { terminalCWD: "/foo" }, workspace: "default" }, { id: "cluster2", - kubeConfig: "foo2", + kubeConfigPath: kubeconfig, contextName: "foo2", preferences: { terminalCWD: "/foo2" } }, { id: "cluster3", - kubeConfig: "foo", + kubeConfigPath: kubeconfig, contextName: "foo", preferences: { terminalCWD: "/foo" }, workspace: "foo", @@ -225,7 +198,7 @@ describe("config with existing clusters", () => { expect(storedCluster).toBeTruthy(); const storedCluster2 = clusterStore.getById("cluster2"); - expect(storedCluster2).toBeUndefined(); + expect(storedCluster2).toBeNull(); }); it("allows getting all of the clusters", async () => { @@ -247,6 +220,78 @@ describe("config with existing clusters", () => { }); }); +describe("config with invalid cluster kubeconfig", () => { + beforeEach(() => { + const invalidKubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test2 +contexts: +- context: + cluster: test + user: test + name: test +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; + + ClusterStore.resetInstance(); + const mockOpts = { + "tmp": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "99.99.99" + } + }, + clusters: [ + { + id: "cluster1", + kubeConfigPath: invalidKubeconfig, + contextName: "test", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + }, + { + id: "cluster2", + kubeConfigPath: kubeconfig, + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default" + }, + + ] + }) + } + }; + + mockFs(mockOpts); + clusterStore = ClusterStore.getInstance(); + + return clusterStore.load(); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("does not enable clusters with invalid kubeconfig", () => { + const storedClusters = clusterStore.clustersList; + + expect(storedClusters.length).toBe(2); + expect(storedClusters[0].enabled).toBeFalsy; + expect(storedClusters[1].id).toBe("cluster2"); + expect(storedClusters[1].enabled).toBeTruthy; + }); +}); + describe("pre 2.0 config with an existing cluster", () => { beforeEach(() => { ClusterStore.resetInstance(); @@ -390,12 +435,6 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => { afterEach(() => { mockFs.restore(); }); - - it("adds cluster to default workspace", async () => { - const storedClusterData = clusterStore.clustersList[0]; - - expect(storedClusterData.workspace).toBe("default"); - }); }); describe("pre 3.6.0-beta.1 config with an existing cluster", () => { @@ -446,3 +485,27 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { expect(icon.startsWith("data:;base64,")).toBe(true); }); }); + +describe("getClusterIdFromHost", () => { + const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a"; + + it("should return undefined for non cluster frame hosts", () => { + expect(getClusterIdFromHost("localhost:45345")).toBeUndefined(); + }); + + it("should return ClusterId for cluster frame hosts", () => { + expect(getClusterIdFromHost(`${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + }); + + it("should return ClusterId for cluster frame hosts with additional subdomains", () => { + expect(getClusterIdFromHost(`abc.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + }); +}); diff --git a/src/common/__tests__/kube-helpers.test.ts b/src/common/__tests__/kube-helpers.test.ts new file mode 100644 index 0000000000..a782772d34 --- /dev/null +++ b/src/common/__tests__/kube-helpers.test.ts @@ -0,0 +1,101 @@ +import { KubeConfig } from "@kubernetes/client-node"; +import { validateKubeConfig } from "../kube-helpers"; + +const kubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test +contexts: +- context: + cluster: test + user: test + name: valid +- context: + cluster: test2 + user: test + name: invalidCluster +- context: + cluster: test + user: test2 + name: invalidUser +- context: + cluster: test + user: invalidExec + name: invalidExec +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + exec: + command: echo +- name: invalidExec + user: + exec: + command: foo +`; + +const kc = new KubeConfig(); + +describe("validateKubeconfig", () => { + beforeAll(() => { + kc.loadFromString(kubeconfig); + }); + describe("with default validation options", () => { + describe("with valid kubeconfig", () => { + it("does not raise exceptions", () => { + expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow(); + }); + }); + describe("with invalid context object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'"); + }); + }); + + describe("with invalid cluster object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'"); + }); + }); + + describe("with invalid user object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'"); + }); + }); + + describe("with invalid exec command", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig"); + }); + }); + }); + + describe("with validateCluster as false", () => { + describe("with invalid cluster object", () => { + it("does not raise exception", () => { + expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow(); + }); + }); + }); + + describe("with validateUser as false", () => { + describe("with invalid user object", () => { + it("does not raise excpetions", () => { + expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow(); + }); + }); + }); + + describe("with validateExec as false", () => { + describe("with invalid exec object", () => { + it("does not raise excpetions", () => { + expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow(); + }); + }); + }); +}); diff --git a/src/common/__tests__/search-store.test.ts b/src/common/__tests__/search-store.test.ts index 7939ef1d8c..6193863192 100644 --- a/src/common/__tests__/search-store.test.ts +++ b/src/common/__tests__/search-store.test.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - import { SearchStore } from "../search-store"; let searchStore: SearchStore = null; @@ -29,28 +25,28 @@ describe("search store tests", () => { expect(searchStore.occurrences).toEqual([]); }); - it("find 3 occurences across 3 lines", () => { + it("find 3 occurrences across 3 lines", () => { searchStore.onSearch(logs, "172"); expect(searchStore.occurrences).toEqual([0, 1, 2]); }); - it("find occurences within 1 line (case-insensitive)", () => { + it("find occurrences within 1 line (case-insensitive)", () => { searchStore.onSearch(logs, "Starting"); expect(searchStore.occurrences).toEqual([2, 2]); }); - it("sets overlay index equal to first occurence", () => { + it("sets overlay index equal to first occurrence", () => { searchStore.onSearch(logs, "Replica"); expect(searchStore.activeOverlayIndex).toBe(0); }); - it("set overlay index to next occurence", () => { + it("set overlay index to next occurrence", () => { searchStore.onSearch(logs, "172"); searchStore.setNextOverlayActive(); expect(searchStore.activeOverlayIndex).toBe(1); }); - it("sets overlay to last occurence", () => { + it("sets overlay to last occurrence", () => { searchStore.onSearch(logs, "172"); searchStore.setPrevOverlayActive(); expect(searchStore.activeOverlayIndex).toBe(2); @@ -62,7 +58,7 @@ describe("search store tests", () => { }); it("escapes string for using in regex", () => { - const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]"); + const regex = SearchStore.escapeRegex("some.interesting-query\\#?()[]"); expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]"); }); @@ -77,4 +73,4 @@ describe("search store tests", () => { searchStore.onSearch(logs, "Starting"); expect(searchStore.totalFinds).toBe(2); }); -}); \ No newline at end of file +}); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 08ca359ce5..1715a1a593 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -1,3 +1,7 @@ +import { Console } from "console"; + +console = new Console(process.stdout, process.stderr); + import mockFs from "mock-fs"; jest.mock("electron", () => { @@ -5,7 +9,8 @@ jest.mock("electron", () => { app: { getVersion: () => "99.99.99", getPath: () => "tmp", - getLocale: () => "en" + getLocale: () => "en", + setLoginItemSettings: jest.fn(), } }; }); @@ -101,4 +106,4 @@ describe("user store tests", () => { expect(us.lastSeenAppVersion).toBe("0.0.0"); }); }); -}); \ No newline at end of file +}); diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts deleted file mode 100644 index e69ebda0aa..0000000000 --- a/src/common/__tests__/workspace-store.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import mockFs from "mock-fs"; - -jest.mock("electron", () => { - return { - app: { - getVersion: () => "99.99.99", - getPath: () => "tmp", - getLocale: () => "en" - }, - ipcMain: { - handle: jest.fn(), - on: jest.fn() - } - }; -}); - -import { Workspace, WorkspaceStore } from "../workspace-store"; - -describe("workspace store tests", () => { - describe("for an empty config", () => { - beforeEach(async () => { - WorkspaceStore.resetInstance(); - mockFs({ tmp: { "lens-workspace-store.json": "{}" } }); - - await WorkspaceStore.getInstance().load(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("default workspace should always exist", () => { - const ws = WorkspaceStore.getInstance(); - - expect(ws.workspaces.size).toBe(1); - expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null); - }); - - it("cannot remove the default workspace", () => { - const ws = WorkspaceStore.getInstance(); - - expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); - }); - - it("can update workspace description", () => { - const ws = WorkspaceStore.getInstance(); - const workspace = ws.addWorkspace(new Workspace({ - id: "foobar", - name: "foobar", - })); - - workspace.description = "Foobar description"; - ws.updateWorkspace(workspace); - - expect(ws.getById("foobar").description).toBe("Foobar description"); - }); - - it("can add workspaces", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "123", - name: "foobar", - })); - - const workspace = ws.getById("123"); - - expect(workspace.name).toBe("foobar"); - expect(workspace.enabled).toBe(true); - }); - - it("cannot set a non-existent workspace to be active", () => { - const ws = WorkspaceStore.getInstance(); - - expect(() => ws.setActive("abc")).toThrow("doesn't exist"); - }); - - it("can set a existent workspace to be active", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "abc", - name: "foobar", - })); - - expect(() => ws.setActive("abc")).not.toThrowError(); - }); - - it("can remove a workspace", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "123", - name: "foobar", - })); - ws.addWorkspace(new Workspace({ - id: "1234", - name: "foobar 1", - })); - ws.removeWorkspaceById("123"); - - expect(ws.workspaces.size).toBe(2); - }); - - it("cannot create workspace with existent name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "someid", - name: "default", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("cannot create workspace with empty name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: "", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("cannot create workspace with ' ' name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: " ", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("trim workspace name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: "default ", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - }); - - describe("for a non-empty config", () => { - beforeEach(async () => { - WorkspaceStore.resetInstance(); - mockFs({ - tmp: { - "lens-workspace-store.json": JSON.stringify({ - currentWorkspace: "abc", - workspaces: [{ - id: "abc", - name: "test" - }, { - id: "default", - name: "default" - }] - }) - } - }); - - await WorkspaceStore.getInstance().load(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("doesn't revert to default workspace", async () => { - const ws = WorkspaceStore.getInstance(); - - expect(ws.currentWorkspaceId).toBe("abc"); - }); - }); -}); diff --git a/src/common/base-store.ts b/src/common/base-store.ts index f7ad946bfd..91d3ef8312 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -19,7 +19,7 @@ export interface BaseStoreParams extends ConfOptions { * Note: T should only contain base JSON serializable types. */ export abstract class BaseStore extends Singleton { - protected storeConfig: Config; + protected storeConfig?: Config; protected syncDisposers: Function[] = []; whenLoaded = when(() => this.isLoaded); @@ -36,7 +36,7 @@ export abstract class BaseStore extends Singleton { } get name() { - return path.basename(this.storeConfig.path); + return path.basename(this.path); } protected get syncRendererChannel() { @@ -48,7 +48,7 @@ export abstract class BaseStore extends Singleton { } get path() { - return this.storeConfig.path; + return this.storeConfig?.path || ""; } protected async init() { @@ -82,10 +82,13 @@ export abstract class BaseStore extends Singleton { protected async saveToFile(model: T) { logger.info(`[STORE]: SAVING ${this.path}`); + // todo: update when fixed https://github.com/sindresorhus/conf/issues/114 - Object.entries(model).forEach(([key, value]) => { - this.storeConfig.set(key, value); - }); + if (this.storeConfig) { + for (const [key, value] of Object.entries(model)) { + this.storeConfig.set(key, value); + } + } } enableSync() { diff --git a/src/common/catalog-category-registry.ts b/src/common/catalog-category-registry.ts new file mode 100644 index 0000000000..d8d65564de --- /dev/null +++ b/src/common/catalog-category-registry.ts @@ -0,0 +1,56 @@ +import { action, computed, observable, toJS } from "mobx"; +import { CatalogCategory, CatalogEntityData } from "./catalog-entity"; + +export class CatalogCategoryRegistry { + @observable protected categories: CatalogCategory[] = []; + + @action add(category: CatalogCategory) { + this.categories.push(category); + } + + @action remove(category: CatalogCategory) { + this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind); + } + + @computed get items() { + return toJS(this.categories); + } + + getForGroupKind(group: string, kind: string) { + return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T; + } + + getEntityForData(data: CatalogEntityData) { + const category = this.getCategoryForEntity(data); + + if (!category) { + return null; + } + + const splitApiVersion = data.apiVersion.split("/"); + const version = splitApiVersion[1]; + + const specVersion = category.spec.versions.find((v) => v.name === version); + + if (!specVersion) { + return null; + } + + return new specVersion.entityClass(data); + } + + getCategoryForEntity(data: CatalogEntityData) { + const splitApiVersion = data.apiVersion.split("/"); + const group = splitApiVersion[0]; + + const category = this.categories.find((category) => { + return category.spec.group === group && category.spec.names.kind === data.kind; + }); + + if (!category) return null; + + return category as T; + } +} + +export const catalogCategoryRegistry = new CatalogCategoryRegistry(); diff --git a/src/common/catalog-entities/index.ts b/src/common/catalog-entities/index.ts new file mode 100644 index 0000000000..a42e68606d --- /dev/null +++ b/src/common/catalog-entities/index.ts @@ -0,0 +1,2 @@ +export * from "./kubernetes-cluster"; +export * from "./web-link"; diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts new file mode 100644 index 0000000000..78b99d3f5c --- /dev/null +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -0,0 +1,105 @@ +import { EventEmitter } from "events"; +import { observable } from "mobx"; +import { catalogCategoryRegistry } from "../catalog-category-registry"; +import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; +import { clusterDisconnectHandler } from "../cluster-ipc"; +import { clusterStore } from "../cluster-store"; +import { requestMain } from "../ipc"; + +export type KubernetesClusterSpec = { + kubeconfigPath: string; + kubeconfigContext: string; +}; + +export interface KubernetesClusterStatus extends CatalogEntityStatus { + phase: "connected" | "disconnected"; +} + +export class KubernetesCluster implements CatalogEntity { + public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public readonly kind = "KubernetesCluster"; + @observable public metadata: CatalogEntityMetadata; + @observable public status: KubernetesClusterStatus; + @observable public spec: KubernetesClusterSpec; + + constructor(data: CatalogEntityData) { + this.metadata = data.metadata; + this.status = data.status as KubernetesClusterStatus; + this.spec = data.spec as KubernetesClusterSpec; + } + + getId() { + return this.metadata.uid; + } + + getName() { + return this.metadata.name; + } + + async onRun(context: CatalogEntityActionContext) { + context.navigate(`/cluster/${this.metadata.uid}`); + } + + async onDetailsOpen() { + // + } + + async onContextMenuOpen(context: CatalogEntityContextMenuContext) { + context.menuItems = [ + { + icon: "settings", + title: "Settings", + onClick: async () => context.navigate(`/cluster/${this.metadata.uid}/settings`) + }, + { + icon: "delete", + title: "Delete", + onClick: async () => clusterStore.removeById(this.metadata.uid), + confirm: { + message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?` + } + }, + ]; + + if (this.status.active) { + context.menuItems.unshift({ + icon: "link_off", + title: "Disconnect", + onClick: async () => { + clusterStore.deactivate(this.metadata.uid); + requestMain(clusterDisconnectHandler, this.metadata.uid); + } + }); + } + + const category = catalogCategoryRegistry.getCategoryForEntity(this); + + if (category) category.emit("contextMenuOpen", this, context); + } +} + +export class KubernetesClusterCategory extends EventEmitter implements CatalogCategory { + public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; + public readonly kind = "CatalogCategory"; + public metadata = { + name: "Kubernetes Clusters" + }; + public spec = { + group: "entity.k8slens.dev", + versions: [ + { + name: "v1alpha1", + entityClass: KubernetesCluster + } + ], + names: { + kind: "KubernetesCluster" + } + }; + + getId() { + return `${this.spec.group}/${this.spec.names.kind}`; + } +} + +catalogCategoryRegistry.add(new KubernetesClusterCategory()); diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts new file mode 100644 index 0000000000..db52fe0eb6 --- /dev/null +++ b/src/common/catalog-entities/web-link.ts @@ -0,0 +1,71 @@ +import { observable } from "mobx"; +import { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; +import { catalogCategoryRegistry } from "../catalog-category-registry"; + +export interface WebLinkStatus extends CatalogEntityStatus { + phase: "valid" | "invalid"; +} + +export type WebLinkSpec = { + url: string; +}; + +export class WebLink implements CatalogEntity { + public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public readonly kind = "KubernetesCluster"; + @observable public metadata: CatalogEntityMetadata; + @observable public status: WebLinkStatus; + @observable public spec: WebLinkSpec; + + constructor(data: CatalogEntityData) { + this.metadata = data.metadata; + this.status = data.status as WebLinkStatus; + this.spec = data.spec as WebLinkSpec; + } + + getId() { + return this.metadata.uid; + } + + getName() { + return this.metadata.name; + } + + async onRun() { + window.open(this.spec.url, "_blank"); + } + + async onDetailsOpen() { + // + } + + async onContextMenuOpen() { + // + } +} + +export class WebLinkCategory implements CatalogCategory { + public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; + public readonly kind = "CatalogCategory"; + public metadata = { + name: "Web Links" + }; + public spec = { + group: "entity.k8slens.dev", + versions: [ + { + name: "v1alpha1", + entityClass: WebLink + } + ], + names: { + kind: "WebLink" + } + }; + + getId() { + return `${this.spec.group}/${this.spec.names.kind}`; + } +} + +catalogCategoryRegistry.add(new WebLinkCategory()); diff --git a/src/common/catalog-entity-registry.ts b/src/common/catalog-entity-registry.ts new file mode 100644 index 0000000000..28afa55bac --- /dev/null +++ b/src/common/catalog-entity-registry.ts @@ -0,0 +1,26 @@ +import { action, computed, observable, IObservableArray } from "mobx"; +import { CatalogEntity } from "./catalog-entity"; + +export class CatalogEntityRegistry { + protected sources = observable.map>([], { deep: true }); + + @action addSource(id: string, source: IObservableArray) { + this.sources.set(id, source); + } + + @action removeSource(id: string) { + this.sources.delete(id); + } + + @computed get items(): CatalogEntity[] { + return Array.from(this.sources.values()).flat(); + } + + getItemsForApiKind(apiVersion: string, kind: string): T[] { + const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + + return items as T[]; + } +} + +export const catalogEntityRegistry = new CatalogEntityRegistry(); diff --git a/src/common/catalog-entity.ts b/src/common/catalog-entity.ts new file mode 100644 index 0000000000..8a00297d3c --- /dev/null +++ b/src/common/catalog-entity.ts @@ -0,0 +1,75 @@ +export interface CatalogCategoryVersion { + name: string; + entityClass: { new(data: CatalogEntityData): CatalogEntity }; +} + +export interface CatalogCategory { + apiVersion: string; + kind: string; + metadata: { + name: string; + } + spec: { + group: string; + versions: CatalogCategoryVersion[]; + names: { + kind: string; + } + } + getId: () => string; +} + +export type CatalogEntityMetadata = { + uid: string; + name: string; + description?: string; + source?: string; + labels: { + [key: string]: string; + } + [key: string]: string | object; +}; + +export type CatalogEntityStatus = { + phase: string; + reason?: string; + message?: string; + active?: boolean; +}; + +export interface CatalogEntityActionContext { + navigate: (url: string) => void; + setCommandPaletteContext: (context?: CatalogEntity) => void; +} + +export type CatalogEntityContextMenu = { + icon: string; + title: string; + onClick: () => Promise; + confirm?: { + message: string; + } +}; + +export interface CatalogEntityContextMenuContext { + navigate: (url: string) => void; + menuItems: CatalogEntityContextMenu[]; +} + +export type CatalogEntityData = { + apiVersion: string; + kind: string; + metadata: CatalogEntityMetadata; + status: CatalogEntityStatus; + spec: { + [key: string]: any; + } +}; + +export interface CatalogEntity extends CatalogEntityData { + getId: () => string; + getName: () => string; + onRun: (context: CatalogEntityActionContext) => Promise; + onDetailsOpen: (context: CatalogEntityActionContext) => Promise; + onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise; +} diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d8bd28f1e8..d1f11d8372 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,4 +1,3 @@ -import { workspaceStore } from "./workspace-store"; import path from "path"; import { app, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; @@ -12,9 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; -import _ from "lodash"; -import move from "array-move"; -import type { WorkspaceId } from "./workspace-store"; +import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting"; export interface ClusterIconUpload { clusterId: string; @@ -34,7 +31,7 @@ export type ClusterPrometheusMetadata = { export interface ClusterStoreModel { activeCluster?: ClusterId; // last opened cluster - clusters?: ClusterModel[] + clusters?: ClusterModel[]; } export type ClusterId = string; @@ -46,8 +43,12 @@ export interface ClusterModel { /** Path to cluster kubeconfig */ kubeConfigPath: string; - /** Workspace id */ - workspace?: WorkspaceId; + /** + * Workspace id + * + * @deprecated + */ + workspace?: string; /** User context in kubeconfig */ contextName?: string; @@ -70,12 +71,13 @@ export interface ClusterModel { kubeConfig?: string; // yaml } -export interface ClusterPreferences extends ClusterPrometheusPreferences{ +export interface ClusterPreferences extends ClusterPrometheusPreferences { terminalCWD?: string; clusterName?: string; iconOrder?: number; icon?: string; httpsProxy?: string; + hiddenMetrics?: string[]; } export interface ClusterPrometheusPreferences { @@ -211,27 +213,24 @@ export class ClusterStore extends BaseStore { return this.activeCluster === id; } - @action - setActive(id: ClusterId) { - const clusterId = this.clusters.has(id) ? id : null; - - this.activeCluster = clusterId; - workspaceStore.setLastActiveClusterId(clusterId); + isMetricHidden(resource: ResourceType) { + return Boolean(this.active?.preferences.hiddenMetrics?.includes(resource)); } @action - swapIconOrders(workspace: WorkspaceId, from: number, to: number) { - const clusters = this.getByWorkspaceId(workspace); + setActive(clusterId: ClusterId) { + const cluster = this.clusters.get(clusterId); - if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) { - throw new Error(`invalid from<->to arguments`); + if (!cluster?.enabled) { + clusterId = null; } - move.mutate(clusters, from, to); + this.activeCluster = clusterId; + } - for (const i in clusters) { - // This resets the iconOrder to the current display order - clusters[i].preferences.iconOrder = +i; + deactivate(id: ClusterId) { + if (this.isActive(id)) { + this.setActive(null); } } @@ -239,15 +238,8 @@ export class ClusterStore extends BaseStore { return this.clusters.size > 0; } - getById(id: ClusterId): Cluster { - return this.clusters.get(id); - } - - getByWorkspaceId(workspaceId: string): Cluster[] { - const clusters = Array.from(this.clusters.values()) - .filter(cluster => cluster.workspace === workspaceId); - - return _.sortBy(clusters, cluster => cluster.preferences.iconOrder); + getById(id: ClusterId): Cluster | null { + return this.clusters.get(id) ?? null; } @action @@ -301,13 +293,6 @@ export class ClusterStore extends BaseStore { } } - @action - removeByWorkspaceId(workspaceId: string) { - this.getByWorkspaceId(workspaceId).forEach(cluster => { - this.removeById(cluster.id); - }); - } - @action protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { const currentClusters = this.clusters.toJS(); @@ -323,7 +308,7 @@ export class ClusterStore extends BaseStore { } else { cluster = new Cluster(clusterModel); - if (!cluster.isManaged) { + if (!cluster.isManaged && cluster.apiUrl) { cluster.enabled = true; } } @@ -337,7 +322,7 @@ export class ClusterStore extends BaseStore { } }); - this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; + this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } @@ -354,10 +339,11 @@ export class ClusterStore extends BaseStore { export const clusterStore = ClusterStore.getInstance(); -export function getClusterIdFromHost(hostname: string): ClusterId { - const subDomains = hostname.split(":")[0].split("."); +export function getClusterIdFromHost(host: string): ClusterId | undefined { + // e.g host == "%clusterId.localhost:45345" + const subDomains = host.split(":")[0].split("."); - return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345" + return subDomains.slice(-2, -1)[0]; // ClusterId or undefined } export function getClusterFrameUrl(clusterId: ClusterId) { @@ -365,7 +351,7 @@ export function getClusterFrameUrl(clusterId: ClusterId) { } export function getHostedClusterId() { - return getClusterIdFromHost(location.hostname); + return getClusterIdFromHost(location.host); } export function getHostedCluster(): Cluster { diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts index 9bcf3a998a..177ef7578f 100644 --- a/src/common/custom-errors.ts +++ b/src/common/custom-errors.ts @@ -10,4 +10,4 @@ export class ExecValidationNotFoundError extends Error { this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } -} \ No newline at end of file +} diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts new file mode 100644 index 0000000000..42cdc0a8b1 --- /dev/null +++ b/src/common/hotbar-store.ts @@ -0,0 +1,67 @@ +import { action, comparer, observable, toJS } from "mobx"; +import { BaseStore } from "./base-store"; +import migrations from "../migrations/hotbar-store"; + +export interface HotbarItem { + entity: { + uid: string; + }; + params?: { + [key: string]: string; + } +} + +export interface Hotbar { + name: string; + items: HotbarItem[]; +} + +export interface HotbarStoreModel { + hotbars: Hotbar[]; +} + +export class HotbarStore extends BaseStore { + @observable hotbars: Hotbar[] = []; + + private constructor() { + super({ + configName: "lens-hotbar-store", + accessPropertiesByDotNotation: false, // To make dots safe in cluster context names + syncOptions: { + equals: comparer.structural, + }, + migrations, + }); + } + + @action protected async fromStore(data: Partial = {}) { + this.hotbars = data.hotbars || [{ + name: "default", + items: [] + }]; + } + + getByName(name: string) { + return this.hotbars.find((hotbar) => hotbar.name === name); + } + + add(hotbar: Hotbar) { + this.hotbars.push(hotbar); + } + + remove(hotbar: Hotbar) { + this.hotbars = this.hotbars.filter((h) => h !== hotbar); + } + + toJSON(): HotbarStoreModel { + const model: HotbarStoreModel = { + hotbars: this.hotbars + }; + + return toJS(model, { + recurseEverything: true, + }); + } +} + +export const hotbarStore = HotbarStore.getInstance(); diff --git a/src/common/ipc/__tests__/type-enforced-ipc.test.ts b/src/common/ipc/__tests__/type-enforced-ipc.test.ts new file mode 100644 index 0000000000..4e20249392 --- /dev/null +++ b/src/common/ipc/__tests__/type-enforced-ipc.test.ts @@ -0,0 +1,126 @@ +import { EventEmitter } from "events"; +import { onCorrect, onceCorrect } from "../type-enforced-ipc"; + +describe("type enforced ipc tests", () => { + describe("onCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler twice if the args are valid on two emits", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + + it("should call the handler twice if the args are [valid, invalid, valid]", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const results = [true, false, true]; + const verifier = (args: unknown[]): args is [] => results.pop(); + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + }); + + describe("onceCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler only once even if args are valid multiple times", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(1); + }); + + it("should call the handler on only the first valid set of args", () => { + let called = ""; + let verifierCalled = 0; + const source = new EventEmitter(); + const listener = (info: any, arg: string) => called = arg; + const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel, {}, "a"); + source.emit(channel, {}, "b"); + source.emit(channel, {}, "c"); + source.emit(channel, {}, "d"); + source.emit(channel, {}, "e"); + source.emit(channel, {}, "f"); + source.emit(channel, {}, "g"); + source.emit(channel, {}, "h"); + source.emit(channel, {}, "i"); + expect(called).toBe("c"); + }); + }); +}); diff --git a/src/common/ipc/cluster.ipc.ts b/src/common/ipc/cluster.ipc.ts new file mode 100644 index 0000000000..a7b13ba290 --- /dev/null +++ b/src/common/ipc/cluster.ipc.ts @@ -0,0 +1,11 @@ +/** + * This channel is broadcast on whenever the cluster fails to list namespaces + * during a refresh and no `accessibleNamespaces` have been set. + */ +export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden"; + +export type ListNamespaceForbiddenArgs = [clusterId: string]; + +export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs { + return args.length === 1 && typeof args[0] === "string"; +} diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts new file mode 100644 index 0000000000..f67d794626 --- /dev/null +++ b/src/common/ipc/index.ts @@ -0,0 +1,5 @@ +export * from "./ipc"; +export * from "./invalid-kubeconfig"; +export * from "./update-available.ipc"; +export * from "./cluster.ipc"; +export * from "./type-enforced-ipc"; diff --git a/src/common/ipc/invalid-kubeconfig/index.ts b/src/common/ipc/invalid-kubeconfig/index.ts new file mode 100644 index 0000000000..9e8e7921d7 --- /dev/null +++ b/src/common/ipc/invalid-kubeconfig/index.ts @@ -0,0 +1,3 @@ +export const InvalidKubeconfigChannel = "invalid-kubeconfig"; + +export type InvalidKubeConfigArgs = [clusterId: string]; diff --git a/src/common/ipc.ts b/src/common/ipc/ipc.ts similarity index 53% rename from src/common/ipc.ts rename to src/common/ipc/ipc.ts index 369221815b..b104b31f4a 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -3,10 +3,13 @@ // https://www.electronjs.org/docs/api/ipc-renderer import { ipcMain, ipcRenderer, webContents, remote } from "electron"; -import logger from "../main/logger"; -import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames"; +import { toJS } from "mobx"; +import logger from "../../main/logger"; +import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; -export function handleRequest(channel: string, listener: (...args: any[]) => any) { +const subFramesChannel = "ipc:get-sub-frames"; + +export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(channel, listener); } @@ -14,38 +17,39 @@ export async function requestMain(channel: string, ...args: any[]) { return ipcRenderer.invoke(channel, ...args); } -async function getSubFrames(): Promise { - const subFrames: ClusterFrameInfo[] = []; - - clusterFrameMap.forEach(frameInfo => { - subFrames.push(frameInfo); - }); - - return subFrames; +function getSubFrames(): ClusterFrameInfo[] { + return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); } -export function broadcastMessage(channel: string, ...args: any[]) { +export async function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; - views.forEach(webContent => { - const type = webContent.getType(); - - logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); - webContent.send(channel, ...args); - getSubFrames().then((frames) => { - frames.map((frameInfo) => { - webContent.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); - }); - }).catch((e) => e); - }); - if (ipcRenderer) { ipcRenderer.send(channel, ...args); } else { ipcMain.emit(channel, ...args); } + + for (const view of views) { + const type = view.getType(); + + logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); + view.send(channel, ...args); + + try { + const subFrames: ClusterFrameInfo[] = ipcRenderer + ? await requestMain(subFramesChannel) + : getSubFrames(); + + for (const frameInfo of subFrames) { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } + } catch (error) { + logger.error("[IPC]: failed to send IPC message", { error: String(error) }); + } + } } export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { @@ -73,3 +77,9 @@ export function unsubscribeAllFromBroadcast(channel: string) { ipcMain.removeAllListeners(channel); } } + +export function bindBroadcastHandlers() { + handleRequest(subFramesChannel, () => { + return getSubFrames(); + }); +} diff --git a/src/common/ipc/type-enforced-ipc.ts b/src/common/ipc/type-enforced-ipc.ts new file mode 100644 index 0000000000..be54992008 --- /dev/null +++ b/src/common/ipc/type-enforced-ipc.ts @@ -0,0 +1,71 @@ +import { EventEmitter } from "events"; +import logger from "../../main/logger"; + +export type HandlerEvent = Parameters[1]>[0]; +export type ListVerifier = (args: unknown[]) => args is T; +export type Rest = T extends [any, ...infer R] ? R : []; + +/** + * Adds a listener to `source` that waits for the first IPC message with the correct + * argument data is sent. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onceCorrect< + EM extends EventEmitter, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + function handler(event: HandlerEvent, ...args: unknown[]): void { + if (verifier(args)) { + source.removeListener(channel, handler); // remove immediately + + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); + } + } + + source.on(channel, handler); +} + +/** + * Adds a listener to `source` that checks to verify the arguments before calling the handler. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onCorrect< + EM extends EventEmitter, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + source.on(channel, (event, ...args: unknown[]) => { + if (verifier(args)) { + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); + } + }); +} diff --git a/src/common/ipc/update-available.ipc.ts b/src/common/ipc/update-available.ipc.ts new file mode 100644 index 0000000000..8571c08512 --- /dev/null +++ b/src/common/ipc/update-available.ipc.ts @@ -0,0 +1,45 @@ +import { UpdateInfo } from "electron-updater"; + +export const UpdateAvailableChannel = "update-available"; +export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; + +export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo]; + +export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { + if (args.length !== 2) { + return false; + } + + if (typeof args[0] !== "string") { + return false; + } + + if (typeof args[1] !== "object" || args[1] === null) { + // TODO: improve this checking + return false; + } + + return true; +} + +export type BackchannelArg = { + doUpdate: false; +} | { + doUpdate: true; + now: boolean; +}; + +export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg]; + +export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { + if (args.length !== 1) { + return false; + } + + if (typeof args[0] !== "object" || args[0] === null) { + // TODO: improve this checking + return false; + } + + return true; +} diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index bb0e6b86d2..c2a2a8df93 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -7,6 +7,12 @@ import logger from "../main/logger"; import commandExists from "command-exists"; import { ExecValidationNotFoundError } from "./custom-errors"; +export type KubeConfigValidationOpts = { + validateCluster?: boolean; + validateUser?: boolean; + validateExec?: boolean; +}; + export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config"); function resolveTilde(filePath: string) { @@ -151,28 +157,43 @@ export function getNodeWarningConditions(node: V1Node) { } /** - * Validates kubeconfig supplied in the add clusters screen. At present this will just validate - * the User struct, specifically the command passed to the exec substructure. - */ -export function validateKubeConfig (config: KubeConfig) { + * Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required) + */ +export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) { // we only receive a single context, cluster & user object here so lets validate them as this // will be called when we add a new cluster to Lens - logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`); + + const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts; + + const contextObject = config.getContextObject(contextName); + + // Validate the Context Object + if (!contextObject) { + throw new Error(`No valid context object provided in kubeconfig for context '${contextName}'`); + } + + // Validate the Cluster Object + if (validateCluster && !config.getCluster(contextObject.cluster)) { + throw new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`); + } + + const user = config.getUser(contextObject.user); // Validate the User Object - const user = config.getCurrentUser(); - - if (user.exec) { + if (validateUser && !user) { + throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`); + } + + // Validate exec command if present + if (validateExec && user?.exec) { const execCommand = user.exec["command"]; // check if the command is absolute or not const isAbsolute = path.isAbsolute(execCommand); // validate the exec struct in the user object, start with the command field - logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`); - if (!commandExists.sync(execCommand)) { - logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`); + logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`); throw new ExecValidationNotFoundError(execCommand, isAbsolute); } } -} \ No newline at end of file +} diff --git a/src/common/prometheus-providers.ts b/src/common/prometheus-providers.ts index a5c515b338..5496163c38 100644 --- a/src/common/prometheus-providers.ts +++ b/src/common/prometheus-providers.ts @@ -10,4 +10,4 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry PrometheusProviderRegistry.registerProvider(provider.id, provider); }); -export const prometheusProviders = PrometheusProviderRegistry.getProviders(); \ No newline at end of file +export const prometheusProviders = PrometheusProviderRegistry.getProviders(); diff --git a/src/common/protocol-handler/error.ts b/src/common/protocol-handler/error.ts new file mode 100644 index 0000000000..ebe7adccd7 --- /dev/null +++ b/src/common/protocol-handler/error.ts @@ -0,0 +1,36 @@ +import Url from "url-parse"; + +export enum RoutingErrorType { + INVALID_PROTOCOL = "invalid-protocol", + INVALID_HOST = "invalid-host", + INVALID_PATHNAME = "invalid-pathname", + NO_HANDLER = "no-handler", + NO_EXTENSION_ID = "no-ext-id", + MISSING_EXTENSION = "missing-ext", +} + +export class RoutingError extends Error { + /** + * Will be set if the routing error originated in an extension route table + */ + public extensionName?: string; + + constructor(public type: RoutingErrorType, public url: Url) { + super("routing error"); + } + + toString(): string { + switch (this.type) { + case RoutingErrorType.INVALID_HOST: + return "invalid host"; + case RoutingErrorType.INVALID_PROTOCOL: + return "invalid protocol"; + case RoutingErrorType.INVALID_PATHNAME: + return "invalid pathname"; + case RoutingErrorType.NO_EXTENSION_ID: + return "no extension ID"; + case RoutingErrorType.MISSING_EXTENSION: + return "extension not found"; + } + } +} diff --git a/src/common/protocol-handler/index.ts b/src/common/protocol-handler/index.ts new file mode 100644 index 0000000000..887f549507 --- /dev/null +++ b/src/common/protocol-handler/index.ts @@ -0,0 +1,2 @@ +export * from "./error"; +export * from "./router"; diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts new file mode 100644 index 0000000000..7b7659992f --- /dev/null +++ b/src/common/protocol-handler/router.ts @@ -0,0 +1,220 @@ +import { match, matchPath } from "react-router"; +import { countBy } from "lodash"; +import { Singleton } from "../utils"; +import { pathToRegexp } from "path-to-regexp"; +import logger from "../../main/logger"; +import Url from "url-parse"; +import { RoutingError, RoutingErrorType } from "./error"; +import { extensionsStore } from "../../extensions/extensions-store"; +import { extensionLoader } from "../../extensions/extension-loader"; +import { LensExtension } from "../../extensions/lens-extension"; +import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry"; + +// IPC channel for protocol actions. Main broadcasts the open-url events to this channel. +export const ProtocolHandlerIpcPrefix = "protocol-handler"; + +export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`; +export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`; + +/** + * These two names are long and cumbersome by design so as to decrease the chances + * of an extension using the same names. + * + * Though under the current (2021/01/18) implementation, these are never matched + * against in the final matching so their names are less of a concern. + */ +const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH"; +const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH"; + +export abstract class LensProtocolRouter extends Singleton { + // Map between path schemas and the handlers + protected internalRoutes = new Map(); + + public static readonly LoggingPrefix = "[PROTOCOL ROUTER]"; + + protected static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; + + /** + * + * @param url the parsed URL that initiated the `lens://` protocol + */ + protected _routeToInternal(url: Url): void { + this._route(Array.from(this.internalRoutes.entries()), url); + } + + /** + * match against all matched URIs, returning either the first exact match or + * the most specific match if none are exact. + * @param routes the array of path schemas, handler pairs to match against + * @param url the url (in its current state) + */ + protected _findMatchingRoute(routes: [string, RouteHandler][], url: Url): null | [match>, RouteHandler] { + const matches: [match>, RouteHandler][] = []; + + for (const [schema, handler] of routes) { + const match = matchPath(url.pathname, { path: schema }); + + if (!match) { + continue; + } + + // prefer an exact match + if (match.isExact) { + return [match, handler]; + } + + matches.push([match, handler]); + } + + // if no exact match pick the one that is the most specific + return matches.sort(([a], [b]) => compareMatches(a, b))[0] ?? null; + } + + /** + * find the most specific matching handler and call it + * @param routes the array of (path schemas, handler) paris to match against + * @param url the url (in its current state) + */ + protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void { + const route = this._findMatchingRoute(routes, url); + + if (!route) { + const data: Record = { url: url.toString() }; + + if (extensionName) { + data.extensionName = extensionName; + } + + return void logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data); + } + + const [match, handler] = route; + + const params: RouteParams = { + pathname: match.params, + search: url.query, + }; + + if (!match.isExact) { + params.tail = url.pathname.slice(match.url.length); + } + + handler(params); + } + + /** + * Tries to find the matching LensExtension instance + * + * Note: this needs to be async so that `main`'s overloaded version can also be async + * @param url the protocol request URI that was "open"-ed + * @returns either the found name or the instance of `LensExtension` + */ + protected async _findMatchingExtensionByName(url: Url): Promise { + interface ExtensionUrlMatch { + [EXTENSION_PUBLISHER_MATCH]: string; + [EXTENSION_NAME_MATCH]: string; + } + + const match = matchPath(url.pathname, LensProtocolRouter.ExtensionUrlSchema); + + if (!match) { + throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url); + } + + const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params; + const name = [publisher, partialName].filter(Boolean).join("/"); + + const extension = extensionLoader.userExtensionsByName.get(name); + + if (!extension) { + logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`); + + return name; + } + + if (!extensionsStore.isEnabled(extension.id)) { + logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); + + return name; + } + + logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`); + + return extension; + } + + /** + * Find a matching extension by the first one or two path segments of `url` and then try to `_route` + * its correspondingly registered handlers. + * + * If no handlers are found or the extension is not enabled then `_missingHandlers` is called before + * checking if more handlers have been added. + * + * Note: this function modifies its argument, do not reuse + * @param url the protocol request URI that was "open"-ed + */ + protected async _routeToExtension(url: Url): Promise { + const extension = await this._findMatchingExtensionByName(url); + + if (typeof extension === "string") { + // failed to find an extension, it returned its name + return; + } + + // remove the extension name from the path name so we don't need to match on it anymore + url.set("pathname", url.pathname.slice(extension.name.length + 1)); + + const handlers = extension + .protocolHandlers + .map<[string, RouteHandler]>(({ pathSchema, handler }) => [pathSchema, handler]); + + try { + this._route(handlers, url, extension.name); + } catch (error) { + if (error instanceof RoutingError) { + error.extensionName = extension.name; + } + + throw error; + } + } + + /** + * Add a handler under the `lens://app` tree of routing. + * @param pathSchema the URI path schema to match against for this handler + * @param handler a function that will be called if a protocol path matches + */ + public addInternalHandler(urlSchema: string, handler: RouteHandler): this { + pathToRegexp(urlSchema); // verify now that the schema is valid + logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`); + this.internalRoutes.set(urlSchema, handler); + + return this; + } + + /** + * Remove an internal protocol handler. + * @param pathSchema the path schema that the handler was registered under + */ + public removeInternalHandler(urlSchema: string): void { + this.internalRoutes.delete(urlSchema); + } +} + +/** + * a comparison function for `array.sort(...)`. Sort order should be most path + * parts to least path parts. + * @param a the left side to compare + * @param b the right side to compare + */ +function compareMatches(a: match, b: match): number { + if (a.path === "/") { + return 1; + } + + if (b.path === "/") { + return -1; + } + + return countBy(b.path)["/"] - countBy(a.path)["/"]; +} diff --git a/src/common/rbac.ts b/src/common/rbac.ts index bd003e87a1..7d02e3be51 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -1,42 +1,44 @@ import { getHostedCluster } from "./cluster-store"; export type KubeResource = - "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | + "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; export interface KubeApiResource { - resource: KubeResource; // valid resource name + kind: string; // resource type (e.g. "Namespace") + apiName: KubeResource; // valid api resource name (e.g. "namespaces") group?: string; // api-group } // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) export const apiResources: KubeApiResource[] = [ - { resource: "configmaps" }, - { resource: "cronjobs", group: "batch" }, - { resource: "customresourcedefinitions", group: "apiextensions.k8s.io" }, - { resource: "daemonsets", group: "apps" }, - { resource: "deployments", group: "apps" }, - { resource: "endpoints" }, - { resource: "events" }, - { resource: "horizontalpodautoscalers" }, - { resource: "ingresses", group: "networking.k8s.io" }, - { resource: "jobs", group: "batch" }, - { resource: "namespaces" }, - { resource: "networkpolicies", group: "networking.k8s.io" }, - { resource: "nodes" }, - { resource: "persistentvolumes" }, - { resource: "persistentvolumeclaims" }, - { resource: "pods" }, - { resource: "poddisruptionbudgets" }, - { resource: "podsecuritypolicies" }, - { resource: "resourcequotas" }, - { resource: "replicasets", group: "apps" }, - { resource: "secrets" }, - { resource: "services" }, - { resource: "statefulsets", group: "apps" }, - { resource: "storageclasses", group: "storage.k8s.io" }, + { kind: "ConfigMap", apiName: "configmaps" }, + { kind: "CronJob", apiName: "cronjobs", group: "batch" }, + { kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" }, + { kind: "DaemonSet", apiName: "daemonsets", group: "apps" }, + { kind: "Deployment", apiName: "deployments", group: "apps" }, + { kind: "Endpoint", apiName: "endpoints" }, + { kind: "Event", apiName: "events" }, + { kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" }, + { kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" }, + { kind: "Job", apiName: "jobs", group: "batch" }, + { kind: "Namespace", apiName: "namespaces" }, + { kind: "LimitRange", apiName: "limitranges" }, + { kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" }, + { kind: "Node", apiName: "nodes" }, + { kind: "PersistentVolume", apiName: "persistentvolumes" }, + { kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" }, + { kind: "Pod", apiName: "pods" }, + { kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets", group: "policy" }, + { kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" }, + { kind: "ResourceQuota", apiName: "resourcequotas" }, + { kind: "ReplicaSet", apiName: "replicasets", group: "apps" }, + { kind: "Secret", apiName: "secrets" }, + { kind: "Service", apiName: "services" }, + { kind: "StatefulSet", apiName: "statefulsets", group: "apps" }, + { kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" }, ]; export function isAllowedResource(resources: KubeResource | KubeResource[]) { diff --git a/src/common/search-store.ts b/src/common/search-store.ts index a3aba9dcbe..c4e34ba5a2 100644 --- a/src/common/search-store.ts +++ b/src/common/search-store.ts @@ -1,10 +1,42 @@ -import { action, computed, observable } from "mobx"; +import { action, computed, observable,reaction } from "mobx"; +import { dockStore } from "../renderer/components/dock/dock.store"; import { autobind } from "../renderer/utils"; export class SearchStore { - @observable searchQuery = ""; // Text in the search input - @observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...] - @observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located + /** + * An utility methods escaping user string to safely pass it into new Regex(variable) + * @param value Unescaped string + */ + public static escapeRegex(value?: string): string { + return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : ""; + } + + /** + * Text in the search input + * + * @observable + */ + @observable searchQuery = ""; + + /** + * Array with line numbers, eg [0, 0, 10, 21, 21, 40...] + * + * @observable + */ + @observable occurrences: number[] = []; + + /** + * Index within the occurrences array. Showing where is activeOverlay currently located + * + * @observable + */ + @observable activeOverlayIndex = -1; + + constructor() { + reaction(() => dockStore.selectedTabId, () => { + searchStore.reset(); + }); + } /** * Sets default activeOverlayIndex @@ -12,49 +44,45 @@ export class SearchStore { * @param query Search query from input */ @action - onSearch(text: string[], query = this.searchQuery) { + public onSearch(text?: string[] | null, query = this.searchQuery): void { this.searchQuery = query; if (!query) { - this.reset(); + return this.reset(); + } + this.occurrences = this.findOccurrences(text ?? [], query); + + if (!this.occurrences.length) { return; } - this.occurrences = this.findOccurences(text, query); - if (!this.occurrences.length) return; // If new highlighted keyword in exact same place as previous one, then no changing in active overlay - if (this.occurrences[this.activeOverlayIndex] !== undefined) return; - this.activeOverlayIndex = this.getNextOverlay(true); + if (this.occurrences[this.activeOverlayIndex] === undefined) { + this.activeOverlayIndex = this.getNextOverlay(true); + } } /** - * Does searching within text array, create a list of search keyword occurences. - * Each keyword "occurency" is saved as index of the the line where keyword founded - * @param text An array of any textual data (logs, for example) + * Does searching within text array, create a list of search keyword occurrences. + * Each keyword "occurrence" is saved as index of the line where keyword was found + * @param lines An array of any textual data (logs, for example) * @param query Search query from input - * @returns {Array} Array of line indexes [0, 0, 14, 17, 17, 17, 20...] + * @returns Array of line indexes [0, 0, 14, 17, 17, 17, 20...] */ - findOccurences(text: string[], query: string) { - if (!text) return []; - const occurences: number[] = []; + private findOccurrences(lines: string[], query?: string): number[] { + const regex = new RegExp(SearchStore.escapeRegex(query), "gi"); - text.forEach((line, index) => { - const regex = new RegExp(this.escapeRegex(query), "gi"); - const matches = [...line.matchAll(regex)]; - - matches.forEach(() => occurences.push(index)); - }); - - return occurences; + return lines + .flatMap((line, index) => Array.from(line.matchAll(regex), () => index)); } /** - * Getting next overlay index within the occurences array + * Getting next overlay index within the occurrences array * @param loopOver Allows to jump from last element to first - * @returns {number} next overlay index + * @returns next overlay index */ - getNextOverlay(loopOver = false) { + private getNextOverlay(loopOver = false): number { const next = this.activeOverlayIndex + 1; if (next > this.occurrences.length - 1) { @@ -65,11 +93,11 @@ export class SearchStore { } /** - * Getting previous overlay index within the occurences array of occurences + * Getting previous overlay index within the occurrences array of occurrences * @param loopOver Allows to jump from first element to last one - * @returns {number} prev overlay index + * @returns previous overlay index */ - getPrevOverlay(loopOver = false) { + private getPrevOverlay(loopOver = false): number { const prev = this.activeOverlayIndex - 1; if (prev < 0) { @@ -80,18 +108,18 @@ export class SearchStore { } @autobind() - setNextOverlayActive() { + public setNextOverlayActive(): void { this.activeOverlayIndex = this.getNextOverlay(true); } @autobind() - setPrevOverlayActive() { + public setPrevOverlayActive(): void { this.activeOverlayIndex = this.getPrevOverlay(true); } /** * Gets line index of where active overlay is located - * @returns {number} A line index within the text/logs array + * @returns A line index within the text/logs array */ @computed get activeOverlayLine(): number { return this.occurrences[this.activeOverlayIndex]; @@ -108,29 +136,21 @@ export class SearchStore { /** * Checks if overlay is active (to highlight it with orange background usually) * @param line Index of the line where overlay is located - * @param occurence Number of the overlay within one line + * @param occurrence Number of the overlay within one line */ @autobind() - isActiveOverlay(line: number, occurence: number) { + public isActiveOverlay(line: number, occurrence: number): boolean { const firstLineIndex = this.occurrences.findIndex(item => item === line); - return firstLineIndex + occurence === this.activeOverlayIndex; - } - - /** - * An utility methods escaping user string to safely pass it into new Regex(variable) - * @param value Unescaped string - */ - escapeRegex(value: string) { - return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + return firstLineIndex + occurrence === this.activeOverlayIndex; } @action - reset() { + private reset(): void { this.searchQuery = ""; this.activeOverlayIndex = -1; this.occurrences = []; } } -export const searchStore = new SearchStore; \ No newline at end of file +export const searchStore = new SearchStore; diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 1e9f7646f3..c7ad21f988 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -10,6 +10,7 @@ import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { appEventBus } from "./event-bus"; import logger from "../main/logger"; import path from "path"; +import { fileNameMigration } from "../migrations/user-store"; export interface UserStoreModel { kubeConfigPath: string; @@ -20,6 +21,7 @@ export interface UserStoreModel { export interface UserPreferences { httpsProxy?: string; + shell?: string; colorTheme?: string; allowUntrustedCAs?: boolean; allowTelemetry?: boolean; @@ -28,6 +30,7 @@ export interface UserPreferences { downloadBinariesPath?: string; kubectlBinariesPath?: string; openAtLogin?: boolean; + hiddenTableColumns?: Record; } export class UserStore extends BaseStore { @@ -35,7 +38,7 @@ export class UserStore extends BaseStore { private constructor() { super({ - // configName: "lens-user-store", // todo: migrate from default "config.json" + configName: "lens-user-store", migrations, }); @@ -54,6 +57,7 @@ export class UserStore extends BaseStore { downloadMirror: "default", downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version openAtLogin: false, + hiddenTableColumns: {}, }; protected async handleOnLoad() { @@ -71,17 +75,40 @@ export class UserStore extends BaseStore { // open at system start-up reaction(() => this.preferences.openAtLogin, openAtLogin => { - app.setLoginItemSettings({ openAtLogin }); + app.setLoginItemSettings({ + openAtLogin, + openAsHidden: true, + args: ["--hidden"] + }); }, { fireImmediately: true, }); } } + async load(): Promise { + /** + * 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(); + + return super.load(); + } + get isNewVersion() { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } + @action + setHiddenTableColumns(tableId: string, names: Set | string[]) { + this.preferences.hiddenTableColumns[tableId] = Array.from(names); + } + + getHiddenTableColumns(tableId: string): Set { + return new Set(this.preferences.hiddenTableColumns[tableId]); + } + @action resetKubeConfigPath() { this.kubeConfigPath = kubeConfigDefaultPath; diff --git a/src/common/utils/__tests__/splitArray.test.ts b/src/common/utils/__tests__/splitArray.test.ts index a401e07701..1e1589fee2 100644 --- a/src/common/utils/__tests__/splitArray.test.ts +++ b/src/common/utils/__tests__/splitArray.test.ts @@ -28,4 +28,4 @@ describe("split array on element tests", () => { test("ten elements, in end array", () => { expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]); }); -}); \ No newline at end of file +}); diff --git a/src/common/utils/app-version.ts b/src/common/utils/app-version.ts index a45f5a55bb..d625f44cd7 100644 --- a/src/common/utils/app-version.ts +++ b/src/common/utils/app-version.ts @@ -1,3 +1,4 @@ +import requestPromise from "request-promise-native"; import packageInfo from "../../../package.json"; export function getAppVersion(): string { @@ -11,3 +12,13 @@ export function getBundledKubectlVersion(): string { export function getBundledExtensions(): string[] { return packageInfo.lens?.extensions || []; } + +export async function getAppVersionFromProxyServer(proxyPort: number): Promise { + const response = await requestPromise({ + method: "GET", + uri: `http://localhost:${proxyPort}/version`, + resolveWithFullResponse: true + }); + + return JSON.parse(response.body).version; +} diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts index e3bad9b302..ba2b31d2d0 100644 --- a/src/common/utils/buildUrl.ts +++ b/src/common/utils/buildUrl.ts @@ -3,14 +3,20 @@ import { compile } from "path-to-regexp"; export interface IURLParams

{ params?: P; query?: Q; + fragment?: string; } export function buildURL

(path: string | any) { const pathBuilder = compile(String(path)); - return function ({ params, query }: IURLParams = {}) { + return function ({ params, query, fragment }: IURLParams = {}): string { const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""; + const parts = [ + pathBuilder(params), + queryParams && `?${queryParams}`, + fragment && `#${fragment}`, + ]; - return pathBuilder(params) + (queryParams ? `?${queryParams}` : ""); + return parts.filter(Boolean).join(""); }; } diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts new file mode 100644 index 0000000000..f19839538a --- /dev/null +++ b/src/common/utils/delay.ts @@ -0,0 +1,19 @@ +import { AbortController } from "abort-controller"; + +/** + * Return a promise that will be resolved after at least `timeout` ms have + * passed. If `failFast` is provided then the promise is also resolved if it has + * been aborted. + * @param timeout The number of milliseconds before resolving + * @param failFast An abort controller instance to cause the delay to short-circuit + */ +export function delay(timeout = 1000, failFast?: AbortController): Promise { + return new Promise(resolve => { + const timeoutId = setTimeout(resolve, timeout); + + failFast?.signal.addEventListener("abort", () => { + clearTimeout(timeoutId); + resolve(); + }); + }); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 582135d7f0..6f26bab2da 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -7,6 +7,7 @@ export * from "./autobind"; export * from "./base64"; export * from "./camelCase"; export * from "./cloneJson"; +export * from "./delay"; export * from "./debouncePromise"; export * from "./defineGlobal"; export * from "./getRandId"; @@ -17,3 +18,4 @@ export * from "./openExternal"; export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./tar"; +export * from "./type-narrowing"; diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index 61269d10b1..caa5471072 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -26,4 +26,4 @@ class Singleton { } export { Singleton }; -export default Singleton; \ No newline at end of file +export default Singleton; diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts new file mode 100644 index 0000000000..6a239c43ee --- /dev/null +++ b/src/common/utils/type-narrowing.ts @@ -0,0 +1,13 @@ +/** + * Narrows `val` to include the property `key` (if true is returned) + * @param val The object to be tested + * @param key The key to test if it is present on the object + */ +export function hasOwnProperty(val: V, key: K): val is (V & { [key in K]: unknown }) { + // this call syntax is for when `val` was created by `Object.create(null)` + return Object.prototype.hasOwnProperty.call(val, key); +} + +export function hasOwnProperties(val: V, ...keys: K[]): val is (V & { [key in K]: unknown}) { + return keys.every(key => hasOwnProperty(val, key)); +} diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts deleted file mode 100644 index 7688516af2..0000000000 --- a/src/common/workspace-store.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { ipcRenderer } from "electron"; -import { action, computed, observable, toJS, reaction } from "mobx"; -import { BaseStore } from "./base-store"; -import { clusterStore } from "./cluster-store"; -import { appEventBus } from "./event-bus"; -import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; -import logger from "../main/logger"; -import type { ClusterId } from "./cluster-store"; - -export type WorkspaceId = string; - -export interface WorkspaceStoreModel { - workspaces: WorkspaceModel[]; - currentWorkspace?: WorkspaceId; -} - -export interface WorkspaceModel { - id: WorkspaceId; - name: string; - description?: string; - ownerRef?: string; - lastActiveClusterId?: ClusterId; -} - -export interface WorkspaceState { - enabled: boolean; -} - -/** - * Workspace - * - * @beta - */ -export class Workspace implements WorkspaceModel, WorkspaceState { - /** - * Unique id for workspace - * - * @observable - */ - @observable id: WorkspaceId; - /** - * Workspace name - * - * @observable - */ - @observable name: string; - /** - * Workspace description - * - * @observable - */ - @observable description?: string; - /** - * Workspace owner reference - * - * If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved) - * - * @observable - */ - @observable ownerRef?: string; - /** - * Is workspace enabled - * - * Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace. - * - * @observable - */ - @observable enabled: boolean; - /** - * Last active cluster id - * - * @observable - */ - @observable lastActiveClusterId?: ClusterId; - - constructor(data: WorkspaceModel) { - Object.assign(this, data); - - if (!ipcRenderer) { - reaction(() => this.getState(), () => { - this.pushState(); - }); - } - } - - /** - * Is workspace managed by an extension - */ - get isManaged(): boolean { - return !!this.ownerRef; - } - - /** - * Get workspace state - * - */ - getState(): WorkspaceState { - return toJS({ - enabled: this.enabled - }); - } - - /** - * Push state - * - * @interal - * @param state workspace state - */ - pushState(state = this.getState()) { - logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}); - broadcastMessage("workspace:state", this.id, toJS(state)); - } - - /** - * - * @param state workspace state - */ - @action setState(state: WorkspaceState) { - Object.assign(this, state); - } - - toJSON(): WorkspaceModel { - return toJS({ - id: this.id, - name: this.name, - description: this.description, - ownerRef: this.ownerRef, - lastActiveClusterId: this.lastActiveClusterId - }); - } -} - -export class WorkspaceStore extends BaseStore { - static readonly defaultId: WorkspaceId = "default"; - private static stateRequestChannel = "workspace:states"; - - private constructor() { - super({ - configName: "lens-workspace-store", - }); - } - - async load() { - await super.load(); - type workspaceStateSync = { - id: string; - state: WorkspaceState; - }; - - if (ipcRenderer) { - logger.info("[WORKSPACE-STORE] requesting initial state sync"); - const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel); - - workspaceStates.forEach((workspaceState) => { - const workspace = this.getById(workspaceState.id); - - if (workspace) { - workspace.setState(workspaceState.state); - } - }); - } else { - handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => { - const states: workspaceStateSync[] = []; - - this.workspacesList.forEach((workspace) => { - states.push({ - state: workspace.getState(), - id: workspace.id - }); - }); - - return states; - }); - } - } - - registerIpcListener() { - logger.info("[WORKSPACE-STORE] starting to listen state events"); - ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => { - this.getById(workspaceId)?.setState(state); - }); - } - - unregisterIpcListener() { - super.unregisterIpcListener(); - ipcRenderer.removeAllListeners("workspace:state"); - } - - @observable currentWorkspaceId = WorkspaceStore.defaultId; - - @observable workspaces = observable.map({ - [WorkspaceStore.defaultId]: new Workspace({ - id: WorkspaceStore.defaultId, - name: "default" - }) - }); - - @computed get currentWorkspace(): Workspace { - return this.getById(this.currentWorkspaceId); - } - - @computed get workspacesList() { - return Array.from(this.workspaces.values()); - } - - @computed get enabledWorkspacesList() { - return this.workspacesList.filter((w) => w.enabled); - } - - pushState() { - this.workspaces.forEach((w) => { - w.pushState(); - }); - } - - isDefault(id: WorkspaceId) { - return id === WorkspaceStore.defaultId; - } - - getById(id: WorkspaceId): Workspace { - return this.workspaces.get(id); - } - - getByName(name: string): Workspace { - return this.workspacesList.find(workspace => workspace.name === name); - } - - @action - setActive(id = WorkspaceStore.defaultId) { - if (id === this.currentWorkspaceId) return; - - if (!this.getById(id)) { - throw new Error(`workspace ${id} doesn't exist`); - } - this.currentWorkspaceId = id; - } - - @action - addWorkspace(workspace: Workspace) { - const { id, name } = workspace; - - if (!name.trim() || this.getByName(name.trim())) { - return; - } - this.workspaces.set(id, workspace); - - if (!workspace.isManaged) { - workspace.enabled = true; - } - - appEventBus.emit({name: "workspace", action: "add"}); - - return workspace; - } - - @action - updateWorkspace(workspace: Workspace) { - this.workspaces.set(workspace.id, workspace); - appEventBus.emit({name: "workspace", action: "update"}); - } - - @action - removeWorkspace(workspace: Workspace) { - this.removeWorkspaceById(workspace.id); - } - - @action - removeWorkspaceById(id: WorkspaceId) { - const workspace = this.getById(id); - - if (!workspace) return; - - if (this.isDefault(id)) { - throw new Error("Cannot remove default workspace"); - } - - if (this.currentWorkspaceId === id) { - this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default - } - this.workspaces.delete(id); - appEventBus.emit({name: "workspace", action: "remove"}); - clusterStore.removeByWorkspaceId(id); - } - - @action - setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) { - this.getById(workspaceId).lastActiveClusterId = clusterId; - } - - @action - protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { - if (currentWorkspace) { - this.currentWorkspaceId = currentWorkspace; - } - - if (workspaces.length) { - this.workspaces.clear(); - workspaces.forEach(ws => { - const workspace = new Workspace(ws); - - if (!workspace.isManaged) { - workspace.enabled = true; - } - this.workspaces.set(workspace.id, workspace); - }); - } - } - - toJSON(): WorkspaceStoreModel { - return toJS({ - currentWorkspace: this.currentWorkspaceId, - workspaces: this.workspacesList.map((w) => w.toJSON()), - }, { - recurseEverything: true - }); - } -} - -export const workspaceStore = WorkspaceStore.getInstance(); diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index 36a2f0bfb8..ad3a258b0e 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -3,11 +3,12 @@ import path from "path"; import hb from "handlebars"; import { observable } from "mobx"; import { ResourceApplier } from "../main/resource-applier"; -import { Cluster } from "../main/cluster"; +import { KubernetesCluster } from "./core-api/stores"; import logger from "../main/logger"; import { app } from "electron"; import { requestMain } from "../common/ipc"; import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; +import { clusterStore } from "../common/cluster-store"; export interface ClusterFeatureStatus { /** feature's current version, as set by the implementation */ @@ -44,7 +45,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be installed on */ - abstract async install(cluster: Cluster): Promise; + abstract install(cluster: KubernetesCluster): Promise; /** * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation @@ -52,7 +53,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be upgraded on */ - abstract async upgrade(cluster: Cluster): Promise; + abstract upgrade(cluster: KubernetesCluster): Promise; /** * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation @@ -60,7 +61,7 @@ export abstract class ClusterFeature { * * @param cluster the cluster that the feature is to be uninstalled from */ - abstract async uninstall(cluster: Cluster): Promise; + abstract uninstall(cluster: KubernetesCluster): Promise; /** * to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation @@ -72,7 +73,7 @@ export abstract class ClusterFeature { * * @return a promise, resolved with the updated ClusterFeatureStatus */ - abstract async updateStatus(cluster: Cluster): Promise; + abstract updateStatus(cluster: KubernetesCluster): Promise; /** * this is a helper method that conveniently applies kubernetes resources to the cluster. @@ -82,9 +83,15 @@ export abstract class ClusterFeature { * files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the * cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster */ - protected async applyResources(cluster: Cluster, resourceSpec: string | string[]) { + protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) { let resources: string[]; + const clusterModel = clusterStore.getById(cluster.metadata.uid); + + if (!clusterModel) { + throw new Error(`cluster not found`); + } + if ( typeof resourceSpec === "string" ) { resources = this.renderTemplates(resourceSpec); } else { @@ -92,9 +99,9 @@ export abstract class ClusterFeature { } if (app) { - await new ResourceApplier(cluster).kubectlApplyAll(resources); + await new ResourceApplier(clusterModel).kubectlApplyAll(resources); } else { - await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources); + await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources); } } diff --git a/src/extensions/core-api/catalog.ts b/src/extensions/core-api/catalog.ts new file mode 100644 index 0000000000..a1dfcb41d0 --- /dev/null +++ b/src/extensions/core-api/catalog.ts @@ -0,0 +1,12 @@ + +import { computed } from "mobx"; +import { CatalogEntity } from "../../common/catalog-entity"; +import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry"; + +export class CatalogEntityRegistry { + @computed getItemsForApiKind(apiVersion: string, kind: string): T[] { + return registry.getItemsForApiKind(apiVersion, kind); + } +} + +export const catalogEntities = new CatalogEntityRegistry(); diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index 706c336fdf..cd01d6ae93 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,8 +1,4 @@ export { ExtensionStore } from "../extension-store"; - -export { clusterStore, Cluster, ClusterStore } from "../stores/cluster-store"; -export type { ClusterModel, ClusterId } from "../stores/cluster-store"; - -export { workspaceStore, Workspace, WorkspaceStore } from "../stores/workspace-store"; -export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store"; - +export { KubernetesCluster, KubernetesClusterCategory } from "../../common/catalog-entities/kubernetes-cluster"; +export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry"; +export { catalogEntities } from "./catalog"; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 98697d252c..d36123b95a 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -14,7 +14,7 @@ import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; import fs from "fs"; -// lazy load so that we get correct userData + export function extensionPackagesRoot() { return path.join((app || remote.app).getPath("userData")); } @@ -52,6 +52,30 @@ export class ExtensionLoader { return extensions; } + @computed get userExtensionsByName(): Map { + const extensions = new Map(); + + for (const [, val] of this.instances.toJS()) { + if (val.isBundled) { + continue; + } + + extensions.set(val.manifest.name, val); + } + + return extensions; + } + + getExtensionByName(name: string): LensExtension | null { + for (const [, val] of this.instances) { + if (val.name === name) { + return val; + } + } + + return null; + } + // Transform userExtensions to a state object for storing into ExtensionsStore @computed get storeState() { return Object.fromEntries( @@ -102,7 +126,6 @@ export class ExtensionLoader { } catch (error) { logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); } - } removeExtension(lensExtensionId: LensExtensionId) { @@ -190,8 +213,8 @@ export class ExtensionLoader { registries.globalPageRegistry.add(extension.globalPages, extension), registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension), registries.appPreferenceRegistry.add(extension.appPreferences), - registries.clusterFeatureRegistry.add(extension.clusterFeatures), registries.statusBarRegistry.add(extension.statusBarItems), + registries.commandRegistry.add(extension.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { @@ -220,7 +243,8 @@ export class ExtensionLoader { registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension), registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems), registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems), - registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts) + registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts), + registries.commandRegistry.add(extension.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 0885bbb730..8e88d22f38 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -27,7 +27,7 @@ export class ExtensionsStore extends BaseStore { protected state = observable.map(); - isEnabled(extId: LensExtensionId) { + isEnabled(extId: LensExtensionId): boolean { const state = this.state.get(extId); // By default false, so that copied extensions are disabled by default. diff --git a/src/extensions/interfaces/catalog.ts b/src/extensions/interfaces/catalog.ts new file mode 100644 index 0000000000..916742076b --- /dev/null +++ b/src/extensions/interfaces/catalog.ts @@ -0,0 +1 @@ +export * from "../../common/catalog-entity"; diff --git a/src/extensions/interfaces/index.ts b/src/extensions/interfaces/index.ts index c91d8cdd19..8bfca77884 100644 --- a/src/extensions/interfaces/index.ts +++ b/src/extensions/interfaces/index.ts @@ -1 +1,2 @@ -export * from "./registrations"; \ No newline at end of file +export * from "./registrations"; +export * from "./catalog"; diff --git a/src/extensions/interfaces/registrations.ts b/src/extensions/interfaces/registrations.ts index 47c63062ea..6af60d97d9 100644 --- a/src/extensions/interfaces/registrations.ts +++ b/src/extensions/interfaces/registrations.ts @@ -1,8 +1,8 @@ export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry"; -export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"; -export type { StatusBarRegistration } from "../registries/status-bar-registry"; \ No newline at end of file +export type { StatusBarRegistration } from "../registries/status-bar-registry"; +export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler-registry"; diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index aaa6f60ac5..a00e289e17 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -2,6 +2,7 @@ import type { InstalledExtension } from "./extension-discovery"; import { action, observable, reaction } from "mobx"; import { filesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; +import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -21,6 +22,8 @@ export class LensExtension { readonly manifestPath: string; readonly isBundled: boolean; + protocolHandlers: ProtocolHandlerRegistration[] = []; + @observable private isEnabled = false; constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index f0e943540d..0ebb3d90f4 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -2,6 +2,9 @@ import type { MenuRegistration } from "./registries/menu-registry"; import { LensExtension } from "./lens-extension"; import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; +import { CatalogEntity } from "../common/catalog-entity"; +import { IObservableArray } from "mobx"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; @@ -16,4 +19,12 @@ export class LensMainExtension extends LensExtension { await windowManager.navigate(pageUrl, frameId); } + + addCatalogSource(id: string, source: IObservableArray) { + catalogEntityRegistry.addSource(`${this.name}:${id}`, source); + } + + removeCatalogSource(id: string) { + catalogEntityRegistry.removeSource(`${this.name}:${id}`); + } } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 25afaa76fe..bf1f8cbeb3 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,7 +1,8 @@ -import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; +import type { AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; import type { Cluster } from "../main/cluster"; import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; +import { CommandRegistration } from "./registries/command-registry"; export class LensRendererExtension extends LensExtension { globalPages: PageRegistration[] = []; @@ -10,10 +11,10 @@ export class LensRendererExtension extends LensExtension { clusterPageMenus: ClusterPageMenuRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; appPreferences: AppPreferenceRegistration[] = []; - clusterFeatures: ClusterFeatureRegistration[] = []; statusBarItems: StatusBarRegistration[] = []; kubeObjectDetailItems: KubeObjectDetailRegistration[] = []; kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; + commands: CommandRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); @@ -29,8 +30,7 @@ export class LensRendererExtension extends LensExtension { /** * Defines if extension is enabled for a given cluster. Defaults to `true`. */ - // eslint-disable-next-line unused-imports/no-unused-vars-ts async isEnabledForCluster(cluster: Cluster): Promise { - return true; + return (void cluster) || true; } } diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index 5b2b4475ae..77fcc25a11 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -3,7 +3,7 @@ "productName": "Lens extensions", "description": "Lens - The Kubernetes IDE: extensions", "version": "0.0.0", - "copyright": "© 2020, Mirantis, Inc.", + "copyright": "© 2021, Mirantis, Inc.", "license": "MIT", "main": "dist/src/extensions/extension-api.js", "types": "dist/src/extensions/extension-api.d.ts", diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts index 338f93b5bc..0bbb537fa5 100644 --- a/src/extensions/registries/app-preference-registry.ts +++ b/src/extensions/registries/app-preference-registry.ts @@ -8,10 +8,22 @@ export interface AppPreferenceComponents { export interface AppPreferenceRegistration { title: string; + id?: string; + showInPreferencesTab?: string; components: AppPreferenceComponents; } -export class AppPreferenceRegistry extends BaseRegistry { +export interface RegisteredAppPreference extends AppPreferenceRegistration { + id: string; +} + +export class AppPreferenceRegistry extends BaseRegistry { + getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference { + return { + id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"), + ...item, + }; + } } export const appPreferenceRegistry = new AppPreferenceRegistry(); diff --git a/src/extensions/registries/cluster-feature-registry.ts b/src/extensions/registries/cluster-feature-registry.ts deleted file mode 100644 index 5017ad27a6..0000000000 --- a/src/extensions/registries/cluster-feature-registry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type React from "react"; -import { BaseRegistry } from "./base-registry"; -import { ClusterFeature } from "../cluster-feature"; - -export interface ClusterFeatureComponents { - Description: React.ComponentType; -} - -export interface ClusterFeatureRegistration { - title: string; - components: ClusterFeatureComponents - feature: ClusterFeature -} - -export class ClusterFeatureRegistry extends BaseRegistry { -} - -export const clusterFeatureRegistry = new ClusterFeatureRegistry(); diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts new file mode 100644 index 0000000000..45af9121a1 --- /dev/null +++ b/src/extensions/registries/command-registry.ts @@ -0,0 +1,37 @@ +// Extensions API -> Commands + +import { BaseRegistry } from "./base-registry"; +import { action, observable } from "mobx"; +import { LensExtension } from "../lens-extension"; +import { CatalogEntity } from "../../common/catalog-entity"; + +export type CommandContext = { + entity?: CatalogEntity; +}; + +export interface CommandRegistration { + id: string; + title: string; + scope: "entity" | "global"; + action: (context: CommandContext) => void; + isActive?: (context: CommandContext) => boolean; +} + +export class CommandRegistry extends BaseRegistry { + @observable activeEntity: CatalogEntity; + + @action + add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) { + const itemArray = [items].flat(); + + const newIds = itemArray.map((item) => item.id); + const currentIds = this.getItems().map((item) => item.id); + + const filteredIds = newIds.filter((id) => !currentIds.includes(id)); + const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id)); + + return super.add(filteredItems, extension); + } +} + +export const commandRegistry = new CommandRegistry(); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 8e343742ec..9f0aba5f0d 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -7,5 +7,5 @@ export * from "./app-preference-registry"; export * from "./status-bar-registry"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; -export * from "./cluster-feature-registry"; export * from "./kube-object-status-registry"; +export * from "./command-registry"; diff --git a/src/extensions/registries/protocol-handler-registry.ts b/src/extensions/registries/protocol-handler-registry.ts new file mode 100644 index 0000000000..dd637818a3 --- /dev/null +++ b/src/extensions/registries/protocol-handler-registry.ts @@ -0,0 +1,44 @@ +/** + * ProtocolHandlerRegistration is the data required for an extension to register + * a handler to a specific path or dynamic path. + */ +export interface ProtocolHandlerRegistration { + pathSchema: string; + handler: RouteHandler; +} + +/** + * The collection of the dynamic parts of a URI which initiated a `lens://` + * protocol request + */ +export interface RouteParams { + /** + * the parts of the URI query string + */ + search: Record; + + /** + * the matching parts of the path. The dynamic parts of the URI path. + */ + pathname: Record; + + /** + * if the most specific path schema that is matched does not cover the whole + * of the URI's path. Then this field will be set to the remaining path + * segments. + * + * Example: + * + * If the path schema `/landing/:type` is the matched schema for the URI + * `/landing/soft/easy` then this field will be set to `"/easy"`. + */ + tail?: string; +} + +/** + * RouteHandler represents the function signature of the handler function for + * `lens://` protocol routing. + */ +export interface RouteHandler { + (params: RouteParams): void; +} diff --git a/src/extensions/registries/status-bar-registry.ts b/src/extensions/registries/status-bar-registry.ts index 88c4132d30..e0454fe77e 100644 --- a/src/extensions/registries/status-bar-registry.ts +++ b/src/extensions/registries/status-bar-registry.ts @@ -3,7 +3,18 @@ import React from "react"; import { BaseRegistry } from "./base-registry"; -export interface StatusBarRegistration { +interface StatusBarComponents { + Item?: React.ComponentType; +} + +interface StatusBarRegistrationV2 { + components?: StatusBarComponents; // has to be optional for backwards compatability +} + +export interface StatusBarRegistration extends StatusBarRegistrationV2 { + /** + * @deprecated use components.Item instead + */ item?: React.ReactNode; } diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index 68dd5d6510..62a3687fdb 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -1,4 +1,5 @@ // Common UI components +export * from "../../renderer/components/layout/sub-title"; // layouts export * from "../../renderer/components/layout/page-layout"; @@ -13,6 +14,9 @@ export * from "../../renderer/components/select"; export * from "../../renderer/components/slider"; export * from "../../renderer/components/input/input"; +// command-overlay +export { CommandOverlay } from "../../renderer/components/command-palette"; + // other components export * from "../../renderer/components/icon"; export * from "../../renderer/components/tooltip"; @@ -38,4 +42,4 @@ export * from "../../renderer/components/+events/kube-event-details"; // specific exports export * from "../../renderer/components/status-brick"; export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store"; -export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store"; +export { logTabStore } from "../../renderer/components/dock/log-tab.store"; diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index fe04550fb7..071d8365ab 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -14,6 +14,7 @@ export { ConfigMap, configMapApi } from "../../renderer/api/endpoints"; export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints"; export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints"; export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints"; +export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints"; export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints"; export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints"; export { Service, serviceApi } from "../../renderer/api/endpoints"; @@ -46,6 +47,7 @@ export type { ConfigMapsStore } from "../../renderer/components/+config-maps/con export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store"; export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store"; export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store"; +export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store"; export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store"; export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store"; export type { ServiceStore } from "../../renderer/components/+network-services/services.store"; diff --git a/src/extensions/renderer-api/kube-object-status.ts b/src/extensions/renderer-api/kube-object-status.ts index f609d736fe..616ead1bb2 100644 --- a/src/extensions/renderer-api/kube-object-status.ts +++ b/src/extensions/renderer-api/kube-object-status.ts @@ -8,4 +8,4 @@ export enum KubeObjectStatusLevel { INFO = 1, WARNING = 2, CRITICAL = 3 -} \ No newline at end of file +} diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts index f819036803..b3da69bdbc 100644 --- a/src/extensions/renderer-api/theming.ts +++ b/src/extensions/renderer-api/theming.ts @@ -2,4 +2,4 @@ import { themeStore } from "../../renderer/theme.store"; export function getActiveTheme() { return themeStore.activeTheme; -} \ No newline at end of file +} diff --git a/src/extensions/stores/cluster-store.ts b/src/extensions/stores/cluster-store.ts deleted file mode 100644 index b2aba41c06..0000000000 --- a/src/extensions/stores/cluster-store.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store"; -import type { ClusterModel } from "../../common/cluster-store"; -import { Cluster } from "../../main/cluster"; -import { Singleton } from "../core-api/utils"; -import { ObservableMap } from "mobx"; - -export { Cluster } from "../../main/cluster"; -export type { ClusterModel, ClusterId } from "../../common/cluster-store"; - -/** - * Store for all added clusters - * - * @beta - */ -export class ClusterStore extends Singleton { - - /** - * Active cluster id - */ - get activeClusterId(): string { - return internalClusterStore.activeCluster; - } - - /** - * Set active cluster id - */ - set activeClusterId(id : ClusterId) { - internalClusterStore.activeCluster = id; - } - - /** - * Map of all clusters - */ - get clusters(): ObservableMap { - return internalClusterStore.clusters; - } - - /** - * Get active cluster (a cluster which is currently visible) - */ - get activeCluster(): Cluster { - if (!this.activeClusterId) { - return null; - } - - return this.getById(this.activeClusterId); - } - - /** - * Array of all clusters - */ - get clustersList(): Cluster[] { - return internalClusterStore.clustersList; - } - - /** - * Array of all enabled clusters - */ - get enabledClustersList(): Cluster[] { - return internalClusterStore.enabledClustersList; - } - - /** - * Array of all clusters that have active connection to a Kubernetes cluster - */ - get connectedClustersList(): Cluster[] { - return internalClusterStore.connectedClustersList; - } - - /** - * Get cluster object by cluster id - * @param id cluster id - */ - getById(id: ClusterId): Cluster { - return internalClusterStore.getById(id); - } - - /** - * Get all clusters belonging to a workspace - * @param workspaceId workspace id - */ - getByWorkspaceId(workspaceId: string): Cluster[] { - return internalClusterStore.getByWorkspaceId(workspaceId); - } - - /** - * Add clusters to store - * @param models list of cluster models - */ - addClusters(...models: ClusterModel[]): Cluster[] { - return internalClusterStore.addClusters(...models); - } - - /** - * Add a cluster to store - * @param model cluster - */ - addCluster(model: ClusterModel | Cluster): Cluster { - return internalClusterStore.addCluster(model); - } - - /** - * Remove a cluster from store - * @param model cluster - */ - async removeCluster(model: ClusterModel) { - return this.removeById(model.id); - } - - /** - * Remove a cluster from store by id - * @param clusterId cluster id - */ - async removeById(clusterId: ClusterId) { - return internalClusterStore.removeById(clusterId); - } - - /** - * Remove all clusters belonging to a workspaces - * @param workspaceId workspace id - */ - removeByWorkspaceId(workspaceId: string) { - return internalClusterStore.removeByWorkspaceId(workspaceId); - } -} - - -export const clusterStore = ClusterStore.getInstance(); diff --git a/src/extensions/stores/workspace-store.ts b/src/extensions/stores/workspace-store.ts deleted file mode 100644 index 2ff4a830fd..0000000000 --- a/src/extensions/stores/workspace-store.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Singleton } from "../core-api/utils"; -import { workspaceStore as internalWorkspaceStore, WorkspaceStore as InternalWorkspaceStore, Workspace, WorkspaceId } from "../../common/workspace-store"; -import { ObservableMap } from "mobx"; - -export { Workspace } from "../../common/workspace-store"; -export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store"; - -/** - * Stores all workspaces - * - * @beta - */ -export class WorkspaceStore extends Singleton { - /** - * Default workspace id, this workspace is always present - */ - static readonly defaultId: WorkspaceId = InternalWorkspaceStore.defaultId; - - /** - * Currently active workspace id - */ - get currentWorkspaceId(): string { - return internalWorkspaceStore.currentWorkspaceId; - } - - /** - * Set active workspace id - */ - set currentWorkspaceId(id: string) { - internalWorkspaceStore.currentWorkspaceId = id; - } - - /** - * Map of all workspaces - */ - get workspaces(): ObservableMap { - return internalWorkspaceStore.workspaces; - } - - /** - * Currently active workspace - */ - get currentWorkspace(): Workspace { - return internalWorkspaceStore.currentWorkspace; - } - - /** - * Array of all workspaces - */ - get workspacesList(): Workspace[] { - return internalWorkspaceStore.workspacesList; - } - - /** - * Array of all enabled (visible) workspaces - */ - get enabledWorkspacesList(): Workspace[] { - return internalWorkspaceStore.enabledWorkspacesList; - } - - /** - * Get workspace by id - * @param id workspace id - */ - getById(id: WorkspaceId): Workspace { - return internalWorkspaceStore.getById(id); - } - - /** - * Get workspace by name - * @param name workspace name - */ - getByName(name: string): Workspace { - return internalWorkspaceStore.getByName(name); - } - - /** - * Set active workspace - * @param id workspace id - */ - setActive(id = WorkspaceStore.defaultId) { - return internalWorkspaceStore.setActive(id); - } - - /** - * Add a workspace to store - * @param workspace workspace - */ - addWorkspace(workspace: Workspace) { - return internalWorkspaceStore.addWorkspace(workspace); - } - - /** - * Update a workspace in store - * @param workspace workspace - */ - updateWorkspace(workspace: Workspace) { - return internalWorkspaceStore.updateWorkspace(workspace); - } - - /** - * Remove workspace from store - * @param workspace workspace - */ - removeWorkspace(workspace: Workspace) { - return internalWorkspaceStore.removeWorkspace(workspace); - } - - /** - * Remove workspace by id - * @param id workspace - */ - removeWorkspaceById(id: WorkspaceId) { - return internalWorkspaceStore.removeWorkspaceById(id); - } -} - -export const workspaceStore = WorkspaceStore.getInstance(); diff --git a/src/jest.setup.ts b/src/jest.setup.ts index ef6565c907..d8c6ce9161 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -4,3 +4,7 @@ fetchMock.enableMocks(); // Mock __non_webpack_require__ for tests globalThis.__non_webpack_require__ = jest.fn(); + +process.on("unhandledRejection", (err) => { + fail(err); +}); diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index b3f0442cc2..113bb49a0c 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -31,7 +31,6 @@ jest.mock("request-promise-native"); import { Console } from "console"; import mockFs from "mock-fs"; -import { workspaceStore } from "../../common/workspace-store"; import { Cluster } from "../cluster"; import { ContextHandler } from "../context-handler"; import { getFreePort } from "../port"; @@ -81,8 +80,7 @@ describe("create clusters", () => { c = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); }); @@ -126,6 +124,7 @@ describe("create clusters", () => { }; jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true)); + jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true)); jest.spyOn(Cluster.prototype, "canI") .mockImplementation((attr: V1ResourceAttributes): Promise => { expect(attr.namespace).toBe("default"); @@ -161,8 +160,7 @@ describe("create clusters", () => { }({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); await c.init(port); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 89c882b109..1cc39a4ada 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -26,7 +26,6 @@ jest.mock("winston", () => ({ import { KubeconfigManager } from "../kubeconfig-manager"; import mockFs from "mock-fs"; import { Cluster } from "../cluster"; -import { workspaceStore } from "../../common/workspace-store"; import { ContextHandler } from "../context-handler"; import { getFreePort } from "../port"; import fse from "fs-extra"; @@ -77,16 +76,17 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port); expect(logger.error).not.toBeCalled(); - expect(kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); - const file = await fse.readFile(kubeConfManager.getPath()); + expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); + // this causes an intermittent "ENXIO: no such device or address, read" error + // const file = await fse.readFile(await kubeConfManager.getPath()); + const file = fse.readFileSync(await kubeConfManager.getPath()); const yml = loadYaml(file.toString()); expect(yml["current-context"]).toBe("minikube"); @@ -98,18 +98,17 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port); - const configPath = kubeConfManager.getPath(); + const configPath = await kubeConfManager.getPath(); expect(await fse.pathExists(configPath)).toBe(true); await kubeConfManager.unlink(); expect(await fse.pathExists(configPath)).toBe(false); await kubeConfManager.unlink(); // doesn't throw - expect(kubeConfManager.getPath()).toBeUndefined(); + expect(await kubeConfManager.getPath()).toBeUndefined(); }); }); diff --git a/src/main/__test__/router.test.ts b/src/main/__test__/router.test.ts new file mode 100644 index 0000000000..8c2fa9c822 --- /dev/null +++ b/src/main/__test__/router.test.ts @@ -0,0 +1,40 @@ +import { Router } from "../router"; + +const staticRoot = __dirname; + +class TestRouter extends Router { + protected resolveStaticRootPath() { + return staticRoot; + } +} + +describe("Router", () => { + it("blocks path traversal attacks", async () => { + const router = new TestRouter(); + const res = { + statusCode: 200, + end: jest.fn() + }; + + await router.handleStaticFile("../index.ts", res as any, {} as any, 0); + + expect(res.statusCode).toEqual(404); + }); + + it("serves files under static root", async () => { + const router = new TestRouter(); + const res = { + statusCode: 200, + write: jest.fn(), + setHeader: jest.fn(), + end: jest.fn() + }; + const req = { + url: "" + }; + + await router.handleStaticFile("router.test.ts", res as any, req as any, 0); + + expect(res.statusCode).toEqual(200); + }); +}); diff --git a/src/main/__test__/shell-session.test.ts b/src/main/__test__/shell-session.test.ts new file mode 100644 index 0000000000..d16b5453b9 --- /dev/null +++ b/src/main/__test__/shell-session.test.ts @@ -0,0 +1,19 @@ +/** + * @jest-environment jsdom + */ + +import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; + +describe("clearKubeconfigEnvVars tests", () => { + it("should not touch non kubeconfig keys", () => { + expect(clearKubeconfigEnvVars({ a: 1 })).toStrictEqual({ a: 1 }); + }); + + it("should remove a single kubeconfig key", () => { + expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1" })).toStrictEqual({ a: 1 }); + }); + + it("should remove a two kubeconfig key", () => { + expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: 1 }); + }); +}); diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index dd9ed97e69..8f31df8b57 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -1,20 +1,99 @@ -import { autoUpdater } from "electron-updater"; +import { autoUpdater, UpdateInfo } from "electron-updater"; import logger from "./logger"; +import { isDevelopment, isTestEnv } from "../common/vars"; +import { delay } from "../common/utils"; +import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; +import { once } from "lodash"; +import { app, ipcMain } from "electron"; -export class AppUpdater { - static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day +let installVersion: null | string = null; - static checkForUpdates() { - return autoUpdater.checkForUpdatesAndNotify(); - } - - constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { - autoUpdater.logger = logger; - } - - public start() { - setInterval(AppUpdater.checkForUpdates, this.updateInterval); - - return AppUpdater.checkForUpdates(); +function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { + if (arg.doUpdate) { + if (arg.now) { + logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); + autoUpdater.quitAndInstall(true, true); + app.exit(); // this is needed for the installer not to fail on windows. + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); + autoUpdater.autoInstallOnAppQuit = true; + } + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose not to update`); + } +} + +/** + * starts the automatic update checking + * @param interval milliseconds between interval to check on, defaults to 24h + */ +export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24): void { + if (isDevelopment || isTestEnv) { + return; + } + + autoUpdater.logger = logger; + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + + autoUpdater + .on("update-available", (info: UpdateInfo) => { + if (autoUpdater.autoInstallOnAppQuit) { + // a previous auto-update loop was completed with YES+LATER, check if same version + if (installVersion === info.version) { + // same version, don't broadcast + return; + } + } + + /** + * This should be always set to false here because it is the reasonable + * default. Namely, if a don't auto update to a version that the user + * didn't ask for. + */ + autoUpdater.autoInstallOnAppQuit = false; + installVersion = info.version; + + autoUpdater.downloadUpdate() + .catch(error => logger.error(`${AutoUpdateLogPrefix}: failed to download update`, { error: String(error) })); + }) + .on("update-downloaded", (info: UpdateInfo) => { + try { + const backchannel = `auto-update:${info.version}`; + + ipcMain.removeAllListeners(backchannel); // only one handler should be present + + // make sure that the handler is in place before broadcasting (prevent race-condition) + onceCorrect({ + source: ipcMain, + channel: backchannel, + listener: handleAutoUpdateBackChannel, + verifier: areArgsUpdateAvailableToBackchannel, + }); + logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: info.version }); + broadcastMessage(UpdateAvailableChannel, backchannel, info); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); + installVersion = undefined; + } + }); + + async function helper() { + while (true) { + await checkForUpdates(); + await delay(interval); + } + } + + helper(); +}); + +export async function checkForUpdates(): Promise { + try { + logger.info(`📡 Checking for app updates`); + + await autoUpdater.checkForUpdates(); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) }); } } diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts new file mode 100644 index 0000000000..ffe8cc17d0 --- /dev/null +++ b/src/main/catalog-pusher.ts @@ -0,0 +1,32 @@ +import { autorun, toJS } from "mobx"; +import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc"; +import { CatalogEntityRegistry} from "../common/catalog-entity-registry"; +import "../common/catalog-entities/kubernetes-cluster"; + +export class CatalogPusher { + static init(catalog: CatalogEntityRegistry) { + new CatalogPusher(catalog).init(); + } + + private constructor(private catalog: CatalogEntityRegistry) {} + + init() { + const disposers: { (): void; }[] = []; + + disposers.push(autorun(() => { + this.broadcast(); + })); + + const listener = subscribeToBroadcast("catalog:broadcast", () => { + this.broadcast(); + }); + + disposers.push(() => unsubscribeFromBroadcast("catalog:broadcast", listener)); + + return disposers; + } + + broadcast() { + broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true })); + } +} diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 9d52e1a70e..885f96c33e 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -31,4 +31,4 @@ export class BaseClusterDetector { }, }); } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts index 2e0cc694ff..810955afae 100644 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ b/src/main/cluster-detectors/cluster-id-detector.ts @@ -23,4 +23,4 @@ export class ClusterIdDetector extends BaseClusterDetector { return response.metadata.uid; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts index 43c56153c9..b1d1b73447 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster-detectors/detector-registry.ts @@ -48,4 +48,4 @@ detectorRegistry.add(ClusterIdDetector); detectorRegistry.add(LastSeenDetector); detectorRegistry.add(VersionDetector); detectorRegistry.add(DistributionDetector); -detectorRegistry.add(NodesCountDetector); \ No newline at end of file +detectorRegistry.add(NodesCountDetector); diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts index e648d5f2f9..0a9bcf9f74 100644 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ b/src/main/cluster-detectors/last-seen-detector.ts @@ -11,4 +11,4 @@ export class LastSeenDetector extends BaseClusterDetector { return { value: new Date().toJSON(), accuracy: 100 }; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts index 0ece5dd080..45584df5bd 100644 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ b/src/main/cluster-detectors/nodes-count-detector.ts @@ -16,4 +16,4 @@ export class NodesCountDetector extends BaseClusterDetector { return response.items.length; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts index 8080ef57a1..b19979db8a 100644 --- a/src/main/cluster-detectors/version-detector.ts +++ b/src/main/cluster-detectors/version-detector.ts @@ -16,4 +16,4 @@ export class VersionDetector extends BaseClusterDetector { return response.gitVersion; } -} \ No newline at end of file +} diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 5717c7278d..047fb0ca95 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,26 +1,45 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; -import { autorun } from "mobx"; +import { action, autorun, observable, reaction, toJS } from "mobx"; import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { Singleton } from "../common/utils"; +import { CatalogEntity } from "../common/catalog-entity"; +import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; + +const clusterOwnerRef = "ClusterManager"; export class ClusterManager extends Singleton { + catalogSource = observable.array([]); + constructor(public readonly port: number) { super(); + + catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource); // auto-init clusters - autorun(() => { - clusterStore.enabledClustersList.forEach(cluster => { - if (!cluster.initialized) { + reaction(() => clusterStore.enabledClustersList, (clusters) => { + clusters.forEach((cluster) => { + if (!cluster.initialized && !cluster.initializing) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); cluster.init(port); } }); + + }, { fireImmediately: true }); + + reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => { + this.updateCatalogSource(clusterStore.enabledClustersList); + }, { fireImmediately: true }); + + reaction(() => catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { + this.syncClustersFromCatalog(entities); }); + // auto-stop removed clusters autorun(() => { const removedClusters = Array.from(clusterStore.removedClusters.values()); @@ -40,6 +59,90 @@ export class ClusterManager extends Singleton { ipcMain.on("network:online", () => { this.onNetworkOnline(); }); } + @action protected updateCatalogSource(clusters: Cluster[]) { + this.catalogSource.forEach((entity, index) => { + const clusterIndex = clusters.findIndex((cluster) => entity.metadata.uid === cluster.id); + + if (clusterIndex === -1) { + this.catalogSource.splice(index, 1); + } + }); + + clusters.filter((c) => !c.ownerRef).forEach((cluster) => { + const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id); + const newEntity = this.catalogEntityFromCluster(cluster); + + if (entityIndex === -1) { + this.catalogSource.push(newEntity); + } else { + const oldEntity = this.catalogSource[entityIndex]; + + newEntity.status.phase = cluster.disconnected ? "disconnected" : "connected"; + newEntity.status.active = !cluster.disconnected; + newEntity.metadata.labels = { + ...newEntity.metadata.labels, + ...oldEntity.metadata.labels + }; + this.catalogSource.splice(entityIndex, 1, newEntity); + } + }); + } + + @action syncClustersFromCatalog(entities: KubernetesCluster[]) { + entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: KubernetesCluster) => { + const cluster = clusterStore.getById(entity.metadata.uid); + + if (!cluster) { + clusterStore.addCluster({ + id: entity.metadata.uid, + enabled: true, + ownerRef: clusterOwnerRef, + preferences: { + clusterName: entity.metadata.name + }, + kubeConfigPath: entity.spec.kubeconfigPath, + contextName: entity.spec.kubeconfigContext + }); + } else { + cluster.enabled = true; + if (!cluster.ownerRef) cluster.ownerRef = clusterOwnerRef; + cluster.preferences.clusterName = entity.metadata.name; + cluster.kubeConfigPath = entity.spec.kubeconfigPath; + cluster.contextName = entity.spec.kubeconfigContext; + + entity.status = { + phase: cluster.disconnected ? "disconnected" : "connected", + active: !cluster.disconnected + }; + } + }); + } + + protected catalogEntityFromCluster(cluster: Cluster) { + return new KubernetesCluster(toJS({ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: cluster.id, + name: cluster.name, + source: "local", + labels: { + "distro": (cluster.metadata["distribution"] || "unknown").toString() + } + }, + spec: { + kubeconfigPath: cluster.kubeConfigPath, + kubeconfigContext: cluster.contextName + }, + status: { + phase: cluster.disconnected ? "disconnected" : "connected", + reason: "", + message: "", + active: !cluster.disconnected + } + })); + } + protected onNetworkOffline() { logger.info("[CLUSTER-MANAGER]: network is offline"); clusterStore.enabledClustersList.forEach((cluster) => { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b9ff62e8ac..7a02c3e0c9 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,15 +1,14 @@ import { ipcMain } from "electron"; import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; -import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastMessage } from "../common/ipc"; +import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; -import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; +import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; -import { loadConfig } from "../common/kube-helpers"; +import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; import request, { RequestPromiseOptions } from "request-promise-native"; import { apiResources, KubeApiResource } from "../common/rbac"; import logger from "./logger"; @@ -48,6 +47,7 @@ export interface ClusterState { isAdmin: boolean; allowedNamespaces: string[] allowedResources: string[] + isGlobalWatchEnabled: boolean; } /** @@ -84,6 +84,13 @@ export class Cluster implements ClusterModel, ClusterState { whenInitialized = when(() => this.initialized); whenReady = when(() => this.ready); + /** + * Is cluster object initializinng on-going + * + * @observable + */ + @observable initializing = false; + /** * Is cluster object initialized * @@ -96,18 +103,16 @@ export class Cluster implements ClusterModel, ClusterState { * @observable */ @observable contextName: string; - /** - * Workspace id - * - * @observable - */ - @observable workspace: WorkspaceId; /** * Path to kubeconfig * * @observable */ @observable kubeConfigPath: string; + /** + * @deprecated + */ + @observable workspace: string; /** * Kubernetes API server URL * @@ -169,6 +174,13 @@ export class Cluster implements ClusterModel, ClusterState { * @observable */ @observable isAdmin = false; + + /** + * Global watch-api accessibility , e.g. "/api/v1/services?watch=1" + * + * @observable + */ + @observable isGlobalWatchEnabled = false; /** * Preferences * @@ -182,7 +194,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable metadata: ClusterMetadata = {}; /** - * List of allowed namespaces + * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api * * @observable */ @@ -195,7 +207,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable allowedResources: string[] = []; /** - * List of accessible namespaces + * List of accessible namespaces provided by user in the Cluster Settings * * @observable */ @@ -216,7 +228,7 @@ export class Cluster implements ClusterModel, ClusterState { * @computed */ @computed get name() { - return this.preferences.clusterName || this.contextName; + return this.preferences.clusterName || this.contextName; } /** @@ -237,15 +249,21 @@ export class Cluster implements ClusterModel, ClusterState { * Kubernetes version */ get version(): string { - return String(this.metadata?.version) || ""; + return String(this.metadata?.version ?? ""); } constructor(model: ClusterModel) { this.updateModel(model); - const kubeconfig = this.getKubeconfig(); - if (kubeconfig.getContextObject(this.contextName)) { + try { + const kubeconfig = this.getKubeconfig(); + + validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false}); this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; + } catch(err) { + logger.error(err); + logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`); + broadcastMessage(InvalidKubeconfigChannel, model.id); } } @@ -271,8 +289,10 @@ export class Cluster implements ClusterModel, ClusterState { * @param port port where internal auth proxy is listening * @internal */ - @action async init(port: number) { + @action + async init(port: number) { try { + this.initializing = true; this.contextHandler = new ContextHandler(this); this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port); this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; @@ -287,6 +307,8 @@ export class Cluster implements ClusterModel, ClusterState { id: this.id, error: err, }); + } finally { + this.initializing = false; } } @@ -323,7 +345,8 @@ export class Cluster implements ClusterModel, ClusterState { * @param force force activation * @internal */ - @action async activate(force = false) { + @action + async activate(force = false) { if (this.activated && !force) { return this.pushState(); } @@ -340,9 +363,7 @@ export class Cluster implements ClusterModel, ClusterState { await this.refreshConnectionStatus(); if (this.accessible) { - await this.refreshAllowedResources(); - this.isAdmin = await this.isClusterAdmin(); - this.ready = true; + await this.refreshAccessibility(); this.ensureKubectl(); } this.activated = true; @@ -362,7 +383,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async reconnect() { + @action + async reconnect() { logger.info(`[CLUSTER]: reconnect`, this.getMeta()); this.contextHandler?.stopServer(); await this.contextHandler?.ensureServer(); @@ -381,6 +403,7 @@ export class Cluster implements ClusterModel, ClusterState { this.accessible = false; this.ready = false; this.activated = false; + this.allowedNamespaces = []; this.resourceAccessStatuses.clear(); this.pushState(); } @@ -389,19 +412,18 @@ export class Cluster implements ClusterModel, ClusterState { * @internal * @param opts refresh options */ - @action async refresh(opts: ClusterRefreshOptions = {}) { + @action + async refresh(opts: ClusterRefreshOptions = {}) { logger.info(`[CLUSTER]: refresh`, this.getMeta()); await this.whenInitialized; await this.refreshConnectionStatus(); if (this.accessible) { - this.isAdmin = await this.isClusterAdmin(); - await this.refreshAllowedResources(); + await this.refreshAccessibility(); if (opts.refreshMetadata) { this.refreshMetadata(); } - this.ready = true; } this.pushState(); } @@ -409,7 +431,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshMetadata() { + @action + async refreshMetadata() { logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); const metadata = await detectorRegistry.detectForCluster(this); const existingMetadata = this.metadata; @@ -420,7 +443,20 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshConnectionStatus() { + private async refreshAccessibility(): Promise { + this.isAdmin = await this.isClusterAdmin(); + this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" }); + + await this.refreshAllowedResources(); + + this.ready = true; + } + + /** + * @internal + */ + @action + async refreshConnectionStatus() { const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; @@ -430,7 +466,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshAllowedResources() { + @action + async refreshAllowedResources() { this.allowedNamespaces = await this.getAllowedNamespaces(); this.allowedResources = await this.getAllowedResources(); } @@ -442,14 +479,16 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - getProxyKubeconfig(): KubeConfig { - return loadConfig(this.getProxyKubeconfigPath()); + async getProxyKubeconfig(): Promise { + const kubeconfigPath = await this.getProxyKubeconfigPath(); + + return loadConfig(kubeconfigPath); } /** * @internal */ - getProxyKubeconfigPath(): string { + async getProxyKubeconfigPath(): Promise { return this.kubeconfigManager.getPath(); } @@ -476,7 +515,8 @@ export class Cluster implements ClusterModel, ClusterState { timeout: 0, resolveWithFullResponse: false, json: true, - qs: queryParams, + method: "POST", + form: queryParams, }); } @@ -525,7 +565,7 @@ export class Cluster implements ClusterModel, ClusterState { * @param resourceAttributes resource attributes */ async canI(resourceAttributes: V1ResourceAttributes): Promise { - const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api); + const authApi = (await this.getProxyKubeconfig()).makeApiClient(AuthorizationV1Api); try { const accessReview = await authApi.createSelfSubjectAccessReview({ @@ -553,6 +593,17 @@ export class Cluster implements ClusterModel, ClusterState { }); } + /** + * @internal + */ + async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise { + return this.canI({ + verb: "watch", + resource: "*", + ...customizeResource, + }); + } + toJSON(): ClusterModel { const model: ClusterModel = { id: this.id, @@ -586,6 +637,7 @@ export class Cluster implements ClusterModel, ClusterState { isAdmin: this.isAdmin, allowedNamespaces: this.allowedNamespaces, allowedResources: this.allowedResources, + isGlobalWatchEnabled: this.isGlobalWatchEnabled, }; return toJS(state, { @@ -628,18 +680,22 @@ export class Cluster implements ClusterModel, ClusterState { return this.accessibleNamespaces; } - const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api); + const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api); try { const namespaceList = await api.listNamespace(); return namespaceList.body.items.map(ns => ns.metadata.name); } catch (error) { - const ctx = this.getProxyKubeconfig().getContextObject(this.contextName); + const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); + const namespaceList = [ctx.namespace].filter(Boolean); - if (ctx.namespace) return [ctx.namespace]; + if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { + logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id }); + broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); + } - return []; + return namespaceList; } } @@ -657,7 +713,7 @@ export class Cluster implements ClusterModel, ClusterState { for (const namespace of this.allowedNamespaces.slice(0, 10)) { if (!this.resourceAccessStatuses.get(apiResource)) { const result = await this.canI({ - resource: apiResource.resource, + resource: apiResource.apiName, group: apiResource.group, verb: "list", namespace @@ -672,9 +728,19 @@ export class Cluster implements ClusterModel, ClusterState { return apiResources .filter((resource) => this.resourceAccessStatuses.get(resource)) - .map(apiResource => apiResource.resource); + .map(apiResource => apiResource.apiName); } catch (error) { return []; } } + + isAllowedResource(kind: string): boolean { + const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind); + + if (apiResource) { + return this.allowedResources.includes(apiResource.apiName); + } + + return true; // allowed by default for other resources + } } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index d67c495a84..e94520b9be 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -59,7 +59,7 @@ export class ContextHandler { async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { - const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api); + const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); return await provider.getPrometheusService(apiClient); }); diff --git a/src/main/developer-tools.ts b/src/main/developer-tools.ts index 3a3d5009f8..d0d7e2ae01 100644 --- a/src/main/developer-tools.ts +++ b/src/main/developer-tools.ts @@ -1,9 +1,12 @@ +import logger from "./logger"; + /** * Installs Electron developer tools in the development build. * The dependency is not bundled to the production build. */ export const installDeveloperTools = async () => { if (process.env.NODE_ENV === "development") { + logger.info("🤓 Installing developer tools"); const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer"); return devToolsInstaller([REACT_DEVELOPER_TOOLS]); diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts new file mode 100644 index 0000000000..e832a937cb --- /dev/null +++ b/src/main/helm/__mocks__/helm-chart-manager.ts @@ -0,0 +1,108 @@ +import { HelmRepo, HelmRepoManager } from "../helm-repo-manager"; + +export class HelmChartManager { + private cache: any = {}; + private repo: HelmRepo; + + constructor(repo: HelmRepo){ + this.cache = HelmRepoManager.cache; + this.repo = repo; + } + + public async charts(): Promise { + switch (this.repo.name) { + case "stable": + return Promise.resolve({ + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }); + case "experiment": + return Promise.resolve({ + "fairwind": [ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true + } + ] + }); + case "bitnami": + return Promise.resolve({ + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + } + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test" + } + ] + }); + default: + return Promise.resolve({}); + } + } +} diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts new file mode 100644 index 0000000000..8c1e82ef0a --- /dev/null +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -0,0 +1,104 @@ +import { helmService } from "../helm-service"; +import { repoManager } from "../helm-repo-manager"; + +jest.spyOn(repoManager, "init").mockImplementation(); + +jest.mock("../helm-chart-manager"); + +describe("Helm Service tests", () => { + test("list charts without deprecated ones", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "stable", url: "stableurl" }, + { name: "experiment", url: "experimenturl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + stable: { + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }, + experiment: {} + }); + }); + + test("list charts sorted by version in descending order", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "bitnami", url: "bitnamiurl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + bitnami: { + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test" + } + ] + } + }); + }); +}); diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index cf4a8e5ace..69619a56d4 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -4,9 +4,10 @@ import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"; import logger from "../logger"; import { promiseExec } from "../promise-exec"; import { helmCli } from "./helm-cli"; +import type { RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; type CachedYaml = { - entries: any; // todo: types + entries: RepoHelmChartList }; export class HelmChartManager { @@ -24,15 +25,15 @@ export class HelmChartManager { return charts[name]; } - public async charts(): Promise { + public async charts(): Promise { try { const cachedYaml = await this.cachedYaml(); return cachedYaml["entries"]; } catch(error) { - logger.error(error); + logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); - return []; + return {}; } } diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 220d665ae0..58a4e99798 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -56,11 +56,12 @@ export class HelmReleaseManager { public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){ const helm = await helmCli.binaryPath(); const fileName = tempy.file({name: "values.yaml"}); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); await fs.promises.writeFile(fileName, yaml.safeDump(values)); try { - const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);}); + const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); return { log: stdout, @@ -73,7 +74,9 @@ export class HelmReleaseManager { public async getRelease(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);}); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + + const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); const release = JSON.parse(stdout); release.resources = await this.getResources(name, namespace, cluster); @@ -112,7 +115,7 @@ export class HelmReleaseManager { protected async getResources(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath(); const kubectl = await cluster.kubeCtl.getPath(); - const pathToKubeconfig = cluster.getProxyKubeconfigPath(); + const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => { return { stdout: JSON.stringify({items: []})}; }); diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 1918268075..9682e58a84 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -1,16 +1,20 @@ +import semver from "semver"; import { Cluster } from "../cluster"; import logger from "../logger"; import { repoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; import { releaseManager } from "./helm-release-manager"; +import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { - return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath()); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + + return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig); } public async listCharts() { - const charts: any = {}; + const charts: HelmChartList = {}; await repoManager.init(); const repositories = await repoManager.repositories(); @@ -18,14 +22,10 @@ class HelmService { for (const repo of repositories) { charts[repo.name] = {}; const manager = new HelmChartManager(repo); - let entries = await manager.charts(); + const sortedCharts = this.sortChartsByVersion(await manager.charts()); + const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts); - entries = this.excludeDeprecated(entries); - - for (const key in entries) { - entries[key] = entries[key][0]; - } - charts[repo.name] = entries; + charts[repo.name] = enabledCharts; } return charts; @@ -55,8 +55,9 @@ class HelmService { public async listReleases(cluster: Cluster, namespace: string = null) { await repoManager.init(); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace); + return await releaseManager.listReleases(proxyKubeconfig, namespace); } public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { @@ -66,21 +67,27 @@ class HelmService { } public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Fetch release values"); - return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig); } public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Fetch release history"); - return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig); } public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Delete release"); - return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig); } public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { @@ -90,26 +97,38 @@ class HelmService { } public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Rollback release"); - const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath()); + const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig); return { message: output }; } - protected excludeDeprecated(entries: any) { - for (const key in entries) { - entries[key] = entries[key].filter((entry: any) => { - if (Array.isArray(entry)) { - return entry[0]["deprecated"] != true; - } + private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) { + const groups = new Map(Object.entries(chartGroups)); - return entry["deprecated"] != true; + for (const [chartName, charts] of groups) { + if (charts[0].deprecated) { + groups.delete(chartName); + } + } + + return Object.fromEntries(groups); + } + + private sortChartsByVersion(chartGroups: RepoHelmChartList) { + for (const key in chartGroups) { + chartGroups[key] = chartGroups[key].sort((first, second) => { + const firstVersion = semver.coerce(first.version || 0); + const secondVersion = semver.coerce(second.version || 0); + + return semver.compare(secondVersion, firstVersion); }); } - return entries; + return chartGroups; } - } export const helmService = new HelmService(); diff --git a/src/main/index.ts b/src/main/index.ts index 265c91f6d4..2ba462ce0c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,13 +4,12 @@ import "../common/system-ca"; import "../common/prometheus-providers"; import * as Mobx from "mobx"; import * as LensExtensions from "../extensions/core-api"; -import { app, dialog, powerMonitor } from "electron"; -import { appName } from "../common/vars"; +import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron"; +import { appName, isMac } from "../common/vars"; import path from "path"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; -import { AppUpdater } from "./app-updater"; import { shellSync } from "./shell-sync"; import { getFreePort } from "./port"; import { mangleProxyEnv } from "./proxy-env"; @@ -18,7 +17,6 @@ import { registerFileProtocol } from "../common/register-protocol"; import logger from "./logger"; import { clusterStore } from "../common/cluster-store"; import { userStore } from "../common/user-store"; -import { workspaceStore } from "../common/workspace-store"; import { appEventBus } from "../common/event-bus"; import { extensionLoader } from "../extensions/extension-loader"; import { extensionsStore } from "../extensions/extensions-store"; @@ -26,6 +24,14 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension- import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; +import { LensProtocolRouterMain } from "./protocol-handler"; +import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; +import { bindBroadcastHandlers } from "../common/ipc"; +import { startUpdateChecking } from "./app-updater"; +import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; +import { CatalogPusher } from "./catalog-pusher"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; +import { hotbarStore } from "../common/hotbar-store"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -35,57 +41,82 @@ let windowManager: WindowManager; app.setName(appName); +logger.info("📟 Setting Lens as protocol client for lens://"); + +if (app.setAsDefaultProtocolClient("lens")) { + logger.info("📟 succeeded ✅"); +} else { + logger.info("📟 failed ❗"); +} + if (!process.env.CICD) { app.setPath("userData", workingDir); } +if (process.env.LENS_DISABLE_GPU) { + app.disableHardwareAcceleration(); +} + mangleProxyEnv(); if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); } -const instanceLock = app.requestSingleInstanceLock(); - -if (!instanceLock) { +if (!app.requestSingleInstanceLock()) { app.exit(); +} else { + const lprm = LensProtocolRouterMain.getInstance(); + + for (const arg of process.argv) { + if (arg.toLowerCase().startsWith("lens://")) { + lprm.route(arg) + .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg })); + } + } } -app.on("second-instance", () => { +app.on("second-instance", (event, argv) => { + const lprm = LensProtocolRouterMain.getInstance(); + + for (const arg of argv) { + if (arg.toLowerCase().startsWith("lens://")) { + lprm.route(arg) + .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg })); + } + } + windowManager?.ensureMainWindow(); }); -if (process.env.LENS_DISABLE_GPU) { - app.disableHardwareAcceleration(); -} - app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`); + logger.info("🐚 Syncing shell environment"); await shellSync(); + bindBroadcastHandlers(); + powerMonitor.on("shutdown", () => { app.exit(); }); - const updater = new AppUpdater(); - - updater.start(); - registerFileProtocol("static", __static); await installDeveloperTools(); + logger.info("💾 Loading stores"); // preload await Promise.all([ userStore.load(), clusterStore.load(), - workspaceStore.load(), + hotbarStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), ]); // find free port try { + logger.info("🔑 Getting free port for LensProxy server"); proxyPort = await getFreePort(); } catch (error) { logger.error(error); @@ -98,6 +129,7 @@ app.on("ready", async () => { // run proxy try { + logger.info("🔌 Starting LensProxy"); // eslint-disable-next-line unused-imports/no-unused-vars-ts proxyServer = LensProxy.create(proxyPort, clusterManager); } catch (error) { @@ -106,10 +138,49 @@ app.on("ready", async () => { app.exit(); } + // test proxy connection + try { + logger.info("🔎 Testing LensProxy connection ..."); + const versionFromProxy = await getAppVersionFromProxyServer(proxyPort); + + if (getAppVersion() !== versionFromProxy) { + logger.error(`Proxy server responded with invalid response`); + } + logger.info("⚡ LensProxy connection OK"); + } catch (error) { + logger.error("Checking proxy server connection failed", error); + } + extensionLoader.init(); extensionDiscovery.init(); + + // Start the app without showing the main window when auto starting on login + // (On Windows and Linux, we get a flag. On MacOS, we get special API.) + const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); + + logger.info("🖥️ Starting WindowManager"); windowManager = WindowManager.getInstance(proxyPort); + if (!startHidden) { + windowManager.initMainWindow(); + } + + ipcMain.on(IpcRendererNavigationEvents.LOADED, () => { + CatalogPusher.init(catalogEntityRegistry); + startUpdateChecking(); + LensProtocolRouterMain + .getInstance() + .rendererLoaded = true; + }); + + extensionLoader.whenLoaded.then(() => { + LensProtocolRouterMain + .getInstance() + .extensionsLoaded = true; + }); + + logger.info("🧩 Initializing extensions"); + // call after windowManager to see splash earlier try { const extensions = await extensionDiscovery.load(); @@ -145,14 +216,35 @@ app.on("activate", (event, hasVisibleWindows) => { } }); -// Quit app on Cmd+Q (MacOS) +/** + * This variable should is used so that `autoUpdater.installAndQuit()` works + */ +let blockQuit = true; + +autoUpdater.on("before-quit-for-update", () => blockQuit = false); + app.on("will-quit", (event) => { + // Quit app on Cmd+Q (MacOS) logger.info("APP:QUIT"); appEventBus.emit({name: "app", action: "close"}); - event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) + clusterManager?.stop(); // close cluster connections - return; // skip exit to make tray work, to quit go to app's global menu or tray's menu + if (blockQuit) { + event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) + + return; // skip exit to make tray work, to quit go to app's global menu or tray's menu + } +}); + +app.on("open-url", (event, rawUrl) => { + // lens:// protocol handler + event.preventDefault(); + + LensProtocolRouterMain + .getInstance() + .route(rawUrl) + .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl })); }); // Extensions-api runtime exports diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index bd1b32c1f5..f88ce87840 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -24,14 +24,24 @@ export class KubeconfigManager { protected async init() { try { await this.contextHandler.ensurePort(); - await this.createProxyKubeconfig(); + this.tempFile = await this.createProxyKubeconfig(); } catch (err) { logger.error(`Failed to created temp config for auth-proxy`, { err }); } } - getPath() { + async getPath() { + // create proxy kubeconfig if it is removed + if (this.tempFile !== undefined && !(await fs.pathExists(this.tempFile))) { + try { + this.tempFile = await this.createProxyKubeconfig(); + } catch (err) { + logger.error(`Failed to created temp config for auth-proxy`, { err }); + } + } + return this.tempFile; + } protected resolveProxyUrl() { @@ -71,9 +81,8 @@ export class KubeconfigManager { // write const configYaml = dumpConfigYaml(proxyConfig); - fs.ensureDir(path.dirname(tempFile)); - fs.writeFileSync(tempFile, configYaml, { mode: 0o600 }); - this.tempFile = tempFile; + await fs.ensureDir(path.dirname(tempFile)); + await fs.writeFile(tempFile, configYaml, { mode: 0o600 }); logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); return tempFile; diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index ebfd2a6a98..7e0d6ed5c7 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -23,10 +23,10 @@ const kubectlMap: Map = new Map([ ["1.14", "1.14.10"], ["1.15", "1.15.11"], ["1.16", "1.16.15"], - ["1.17", bundledVersion], - ["1.18", "1.18.14"], - ["1.19", "1.19.5"], - ["1.20", "1.20.0"] + ["1.17", "1.17.17"], + ["1.18", bundledVersion], + ["1.19", "1.19.7"], + ["1.20", "1.20.2"] ]); const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index e4f6ab4a34..98bdd97f54 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -5,11 +5,11 @@ import httpProxy from "http-proxy"; import url from "url"; import * as WebSocket from "ws"; import { apiPrefix, apiKubePrefix } from "../common/vars"; -import { openShell } from "./node-shell-session"; import { Router } from "./router"; import { ClusterManager } from "./cluster-manager"; import { ContextHandler } from "./context-handler"; import logger from "./logger"; +import { NodeShellSession, LocalShellSession } from "./shell-session"; export class LensProxy { protected origin: string; @@ -28,8 +28,8 @@ export class LensProxy { } listen(port = this.port): this { - this.proxyServer = this.buildCustomProxy().listen(port); - logger.info(`LensProxy server has started at ${this.origin}`); + this.proxyServer = this.buildCustomProxy().listen(port, "127.0.0.1"); + logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`); return this; } @@ -120,12 +120,18 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); - proxy.on("proxyRes", (proxyRes, req) => { + proxy.on("proxyRes", (proxyRes, req, res) => { const retryCounterId = this.getRequestId(req); if (this.retryCounters.has(retryCounterId)) { this.retryCounters.delete(retryCounterId); } + + if (!res.headersSent && req.url) { + const url = new URL(req.url, "http://localhost"); + + if (url.searchParams.has("watch")) res.flushHeaders(); + } }); proxy.on("error", (error, req, res, target) => { @@ -167,8 +173,12 @@ export class LensProxy { return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { const cluster = this.clusterManager.getClusterForRequest(req); const nodeParam = url.parse(req.url, true).query["node"]?.toString(); + const shell = nodeParam + ? new NodeShellSession(socket, cluster, nodeParam) + : new LocalShellSession(socket, cluster); - openShell(socket, cluster, nodeParam); + shell.open() + .catch(error => logger.error(`[SHELL-SESSION]: failed to open: ${error}`, { error })); })); } @@ -194,7 +204,8 @@ export class LensProxy { if (proxyTarget) { // allow to fetch apis in "clusterId.localhost:port" from "localhost:port" - res.setHeader("Access-Control-Allow-Origin", this.origin); + // this should be safe because we have already validated cluster uuid + res.setHeader("Access-Control-Allow-Origin", "*"); return proxy.web(req, res, proxyTarget); } diff --git a/src/main/menu.ts b/src/main/menu.ts index 2cddbb1b01..1dcd320584 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -7,9 +7,12 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences. import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; import { extensionsURL } from "../renderer/components/+extensions/extensions.route"; +import { catalogURL } from "../renderer/components/+catalog/catalog.route"; import { menuRegistry } from "../extensions/registries/menu-registry"; import logger from "./logger"; import { exitApp } from "./exit-app"; +import { broadcastMessage } from "../common/ipc"; +import * as packageJson from "../../package.json"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"; @@ -25,7 +28,7 @@ export function showAbout(browserWindow: BrowserWindow) { `Electron: ${process.versions.electron}`, `Chrome: ${process.versions.chrome}`, `Node: ${process.versions.node}`, - `Copyright 2020 Mirantis, Inc.`, + packageJson.copyright, ]; dialog.showMessageBoxSync(browserWindow, { @@ -173,6 +176,21 @@ export function buildMenu(windowManager: WindowManager) { const viewMenu: MenuItemConstructorOptions = { label: "View", submenu: [ + { + label: "Catalog", + accelerator: "Shift+CmdOrCtrl+C", + click() { + navigate(catalogURL()); + } + }, + { + label: "Command Palette...", + accelerator: "Shift+CmdOrCtrl+P", + click() { + broadcastMessage("command-palette:open"); + } + }, + { type: "separator" }, { label: "Back", accelerator: "CmdOrCtrl+[", diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts deleted file mode 100644 index b67e776725..0000000000 --- a/src/main/node-shell-session.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as WebSocket from "ws"; -import * as pty from "node-pty"; -import { ShellSession } from "./shell-session"; -import { v4 as uuid } from "uuid"; -import * as k8s from "@kubernetes/client-node"; -import { KubeConfig } from "@kubernetes/client-node"; -import { Cluster } from "./cluster"; -import logger from "./logger"; -import { appEventBus } from "../common/event-bus"; - -export class NodeShellSession extends ShellSession { - protected nodeName: string; - protected podId: string; - protected kc: KubeConfig; - - constructor(socket: WebSocket, cluster: Cluster, nodeName: string) { - super(socket, cluster); - this.nodeName = nodeName; - this.podId = `node-shell-${uuid()}`; - this.kc = cluster.getProxyKubeconfig(); - } - - public async open() { - const shell = await this.kubectl.getPath(); - let args = []; - - if (this.createNodeShellPod(this.podId, this.nodeName)) { - await this.waitForRunningPod(this.podId).catch(() => { - this.exit(1001); - }); - } - args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]; - - const shellEnv = await this.getCachedShellEnv(); - - this.shellProcess = pty.spawn(shell, args, { - cols: 80, - cwd: this.cwd() || shellEnv["HOME"], - env: shellEnv, - name: "xterm-256color", - rows: 30, - }); - this.running = true; - this.pipeStdout(); - this.pipeStdin(); - this.closeWebsocketOnProcessExit(); - this.exitProcessOnWebsocketClose(); - - appEventBus.emit({name: "node-shell", action: "open"}); - } - - protected exit(code = 1000) { - if (this.podId) { - this.deleteNodeShellPod(); - } - super.exit(code); - } - - protected async createNodeShellPod(podId: string, nodeName: string) { - const kc = this.getKubeConfig(); - const k8sApi = kc.makeApiClient(k8s.CoreV1Api); - const pod = { - metadata: { - name: podId, - namespace: "kube-system" - }, - spec: { - restartPolicy: "Never", - terminationGracePeriodSeconds: 0, - hostPID: true, - hostIPC: true, - hostNetwork: true, - tolerations: [{ - operator: "Exists" - }], - containers: [{ - name: "shell", - image: "docker.io/alpine:3.12", - securityContext: { - privileged: true, - }, - command: ["nsenter"], - args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"] - }], - nodeSelector: { - "kubernetes.io/hostname": nodeName - } - } - } as k8s.V1Pod; - - await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => { - logger.error(error); - - return false; - }); - - return true; - } - - protected getKubeConfig() { - if (this.kc) { - return this.kc; - } - this.kc = new k8s.KubeConfig(); - this.kc.loadFromFile(this.kubeconfigPath); - - return this.kc; - } - - protected waitForRunningPod(podId: string) { - return new Promise(async (resolve, reject) => { - const kc = this.getKubeConfig(); - const watch = new k8s.Watch(kc); - const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {}, - // callback is called for each received object. - (type, obj) => { - if (obj.metadata.name == podId && obj.status.phase === "Running") { - resolve(true); - } - }, - // done callback is called if the watch terminates normally - (err) => { - logger.error(err); - reject(false); - } - ); - - setTimeout(() => { - req.abort(); - reject(false); - }, 120 * 1000); - }); - } - - protected deleteNodeShellPod() { - const kc = this.getKubeConfig(); - const k8sApi = kc.makeApiClient(k8s.CoreV1Api); - - k8sApi.deleteNamespacedPod(this.podId, "kube-system"); - } -} - -export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise { - let shell: ShellSession; - - if (nodeName) { - shell = new NodeShellSession(socket, cluster, nodeName); - } else { - shell = new ShellSession(socket, cluster); - } - shell.open(); - - return shell; -} diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts new file mode 100644 index 0000000000..6b3f668079 --- /dev/null +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -0,0 +1,259 @@ +import { LensProtocolRouterMain } from "../router"; +import { noop } from "../../../common/utils"; +import { extensionsStore } from "../../../extensions/extensions-store"; +import { extensionLoader } from "../../../extensions/extension-loader"; +import * as uuid from "uuid"; +import { LensMainExtension } from "../../../extensions/core-api"; +import { broadcastMessage } from "../../../common/ipc"; +import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler"; + +jest.mock("../../../common/ipc"); + +function throwIfDefined(val: any): void { + if (val != null) { + throw val; + } +} + +describe("protocol router tests", () => { + let lpr: LensProtocolRouterMain; + + beforeEach(() => { + jest.clearAllMocks(); + (extensionsStore as any).state.clear(); + (extensionLoader as any).instances.clear(); + LensProtocolRouterMain.resetInstance(); + lpr = LensProtocolRouterMain.getInstance(); + lpr.extensionsLoaded = true; + lpr.rendererLoaded = true; + }); + + it("should throw on non-lens URLS", async () => { + try { + expect(await lpr.route("https://google.ca")).toBeUndefined(); + } catch (error) { + expect(error).toBeInstanceOf(Error); + } + }); + + it("should throw when host not internal or extension", async () => { + try { + expect(await lpr.route("lens://foobar")).toBeUndefined(); + } catch (error) { + expect(error).toBeInstanceOf(Error); + } + }); + + it("should not throw when has valid host", async () => { + const extId = uuid.v4(); + const ext = new LensMainExtension({ + id: extId, + manifestPath: "/foo/bar", + manifest: { + name: "@mirantis/minikube", + version: "0.1.1", + }, + isBundled: false, + isEnabled: true, + absolutePath: "/foo/bar", + }); + + ext.protocolHandlers.push({ + pathSchema: "/", + handler: noop, + }); + + (extensionLoader as any).instances.set(extId, ext); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); + + lpr.addInternalHandler("/", noop); + + try { + expect(await lpr.route("lens://app")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + + try { + expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app"); + expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, "lens://extension/@mirantis/minikube"); + }); + + it("should call handler if matches", async () => { + let called = false; + + lpr.addInternalHandler("/page", () => { called = true; }); + + try { + expect(await lpr.route("lens://app/page")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe(true); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page"); + }); + + it("should call most exact handler", async () => { + let called: any = 0; + + lpr.addInternalHandler("/page", () => { called = 1; }); + lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; }); + + try { + expect(await lpr.route("lens://app/page/foo")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe("foo"); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo"); + }); + + it("should call most exact handler for an extension", async () => { + let called: any = 0; + + const extId = uuid.v4(); + const ext = new LensMainExtension({ + id: extId, + manifestPath: "/foo/bar", + manifest: { + name: "@foobar/icecream", + version: "0.1.1", + }, + isBundled: false, + isEnabled: true, + absolutePath: "/foo/bar", + }); + + ext.protocolHandlers + .push({ + pathSchema: "/page", + handler: () => { called = 1; }, + }, { + pathSchema: "/page/:id", + handler: params => { called = params.pathname.id; }, + }); + + (extensionLoader as any).instances.set(extId, ext); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + + try { + expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe("foob"); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob"); + }); + + it("should work with non-org extensions", async () => { + let called: any = 0; + + { + const extId = uuid.v4(); + const ext = new LensMainExtension({ + id: extId, + manifestPath: "/foo/bar", + manifest: { + name: "@foobar/icecream", + version: "0.1.1", + }, + isBundled: false, + isEnabled: true, + absolutePath: "/foo/bar", + }); + + ext.protocolHandlers + .push({ + pathSchema: "/page/:id", + handler: params => { called = params.pathname.id; }, + }); + + (extensionLoader as any).instances.set(extId, ext); + (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + } + + { + const extId = uuid.v4(); + const ext = new LensMainExtension({ + id: extId, + manifestPath: "/foo/bar", + manifest: { + name: "icecream", + version: "0.1.1", + }, + isBundled: false, + isEnabled: true, + absolutePath: "/foo/bar", + }); + + ext.protocolHandlers + .push({ + pathSchema: "/page", + handler: () => { called = 1; }, + }); + + (extensionLoader as any).instances.set(extId, ext); + (extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" }); + } + + (extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); + (extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" }); + + try { + expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe(1); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page"); + }); + + it("should throw if urlSchema is invalid", () => { + expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError(); + }); + + it("should call most exact handler with 3 found handlers", async () => { + let called: any = 0; + + lpr.addInternalHandler("/", () => { called = 2; }); + lpr.addInternalHandler("/page", () => { called = 1; }); + lpr.addInternalHandler("/page/foo", () => { called = 3; }); + lpr.addInternalHandler("/page/bar", () => { called = 4; }); + + try { + expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe(3); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat"); + }); + + it("should call most exact handler with 2 found handlers", async () => { + let called: any = 0; + + lpr.addInternalHandler("/", () => { called = 2; }); + lpr.addInternalHandler("/page", () => { called = 1; }); + lpr.addInternalHandler("/page/bar", () => { called = 4; }); + + try { + expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); + } catch (error) { + expect(throwIfDefined(error)).not.toThrow(); + } + + expect(called).toBe(1); + expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat"); + }); +}); diff --git a/src/main/protocol-handler/index.ts b/src/main/protocol-handler/index.ts new file mode 100644 index 0000000000..9ca8201129 --- /dev/null +++ b/src/main/protocol-handler/index.ts @@ -0,0 +1 @@ +export * from "./router"; diff --git a/src/main/protocol-handler/router.ts b/src/main/protocol-handler/router.ts new file mode 100644 index 0000000000..20962372b2 --- /dev/null +++ b/src/main/protocol-handler/router.ts @@ -0,0 +1,112 @@ +import logger from "../logger"; +import * as proto from "../../common/protocol-handler"; +import Url from "url-parse"; +import { LensExtension } from "../../extensions/lens-extension"; +import { broadcastMessage } from "../../common/ipc"; +import { observable, when } from "mobx"; + +export interface FallbackHandler { + (name: string): Promise; +} + +export class LensProtocolRouterMain extends proto.LensProtocolRouter { + private missingExtensionHandlers: FallbackHandler[] = []; + + @observable rendererLoaded = false; + @observable extensionsLoaded = false; + + /** + * Find the most specific registered handler, if it exists, and invoke it. + * + * This will send an IPC message to the renderer router to do the same + * in the renderer. + */ + public async route(rawUrl: string): Promise { + try { + const url = new Url(rawUrl, true); + + if (url.protocol.toLowerCase() !== "lens:") { + throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url); + } + + logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); + + switch (url.host) { + case "app": + return this._routeToInternal(url); + case "extension": + await when(() => this.extensionsLoaded); + + return this._routeToExtension(url); + default: + throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url); + } + + } catch (error) { + if (error instanceof proto.RoutingError) { + logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url }); + } else { + logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl }); + } + } + } + + protected async _executeMissingExtensionHandlers(extensionName: string): Promise { + for (const handler of this.missingExtensionHandlers) { + if (await handler(extensionName)) { + return true; + } + } + + return false; + } + + protected async _findMatchingExtensionByName(url: Url): Promise { + const firstAttempt = await super._findMatchingExtensionByName(url); + + if (typeof firstAttempt !== "string") { + return firstAttempt; + } + + if (await this._executeMissingExtensionHandlers(firstAttempt)) { + return super._findMatchingExtensionByName(url); + } + + return ""; + } + + protected async _routeToInternal(url: Url): Promise { + const rawUrl = url.toString(); // for sending to renderer + + super._routeToInternal(url); + + await when(() => this.rendererLoaded); + + return broadcastMessage(proto.ProtocolHandlerInternal, rawUrl); + } + + protected async _routeToExtension(url: Url): Promise { + const rawUrl = url.toString(); // for sending to renderer + + /** + * This needs to be done first, so that the missing extension handlers can + * be called before notifying the renderer. + * + * Note: this needs to clone the url because _routeToExtension modifies its + * argument. + */ + await super._routeToExtension(new Url(url.toString(), true)); + await when(() => this.rendererLoaded); + + return broadcastMessage(proto.ProtocolHandlerExtension, rawUrl); + } + + /** + * Add a function to the list which will be sequentially called if an extension + * is not found while routing to the extensions + * @param handler A function that tries to find an extension + */ + public addMissingExtensionHandler(handler: FallbackHandler): void { + this.missingExtensionHandlers.push(handler); + } +} diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index d4070f2378..6f1b0a8e0f 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -23,12 +23,13 @@ export class ResourceApplier { protected async kubectlApply(content: string): Promise { const { kubeCtl } = this.cluster; const kubectlPath = await kubeCtl.getPath(); + const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); return new Promise((resolve, reject) => { const fileName = tempy.file({ name: "resource.yaml" }); fs.writeFileSync(fileName, content); - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`; + const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`; logger.debug(`shooting manifests with: ${cmd}`); const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env); @@ -54,6 +55,7 @@ export class ResourceApplier { public async kubectlApplyAll(resources: string[]): Promise { const { kubeCtl } = this.cluster; const kubectlPath = await kubeCtl.getPath(); + const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); return new Promise((resolve, reject) => { const tmpDir = tempy.directory(); @@ -62,7 +64,7 @@ export class ResourceApplier { resources.forEach((resource, index) => { fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource); }); - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`; + const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`; console.log("shooting manifests with:", cmd); exec(cmd, (error, stdout, stderr) => { diff --git a/src/main/router.ts b/src/main/router.ts index 896893a592..6fa14e1444 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -5,7 +5,7 @@ import path from "path"; import { readFile } from "fs-extra"; import { Cluster } from "./cluster"; import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars"; -import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes"; +import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, versionRoute } from "./routes"; import logger from "./logger"; export interface RouterRequestOpts { @@ -40,10 +40,16 @@ export interface LensApiRequest

{ export class Router { protected router: any; + protected staticRootPath: string; public constructor() { this.router = new Call.Router(); this.addRoutes(); + this.staticRootPath = this.resolveStaticRootPath(); + } + + protected resolveStaticRootPath() { + return path.resolve(__static); } public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { @@ -102,7 +108,15 @@ export class Router { } async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) { - const asset = path.join(__static, filePath); + const asset = path.join(this.staticRootPath, filePath); + const normalizedFilePath = path.resolve(asset); + + if (!normalizedFilePath.startsWith(this.staticRootPath)) { + res.statusCode = 404; + res.end(); + + return; + } try { const filename = path.basename(req.url); @@ -143,11 +157,9 @@ export class Router { this.handleStaticFile(params.path, response, req); }); + this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute)); this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)); - // Watch API - this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute)); - // Metrics API this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)); diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts index 4d1cae8bc2..c3c0bdb353 100644 --- a/src/main/routes/helm-route.ts +++ b/src/main/routes/helm-route.ts @@ -70,7 +70,7 @@ class HelmApiRoute extends LensApi { this.respondJson(response, result); } catch (error) { logger.debug(error); - this.respondText(response, error); + this.respondText(response, error, 422); } } @@ -83,7 +83,7 @@ class HelmApiRoute extends LensApi { this.respondJson(response, result); } catch(error) { logger.debug(error); - this.respondText(response, error); + this.respondText(response, error, 422); } } diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index 5bc5b3f3dd..c194d8f8b2 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -1,6 +1,6 @@ export * from "./kubeconfig-route"; export * from "./metrics-route"; export * from "./port-forward-route"; -export * from "./watch-route"; export * from "./helm-route"; export * from "./resource-applier-route"; +export * from "./version-route"; diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index 7fe5bcb9bc..bad5ecd57a 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -44,7 +44,7 @@ class KubeconfigRoute extends LensApi { public async routeServiceAccountRoute(request: LensApiRequest) { const { params, response, cluster} = request; - const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api); + const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); const secretList = await client.listNamespacedSecret(params.namespace); const secret = secretList.body.items.find(secret => { const { annotations } = secret.metadata; diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 33c34758a4..0b5954948d 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -92,7 +92,7 @@ class PortForwardRoute extends LensApi { namespace, name: resourceName, port, - kubeConfig: cluster.getProxyKubeconfigPath() + kubeConfig: await cluster.getProxyKubeconfigPath() }); const started = await portForward.start(); diff --git a/src/main/routes/version-route.ts b/src/main/routes/version-route.ts new file mode 100644 index 0000000000..81ada9eca7 --- /dev/null +++ b/src/main/routes/version-route.ts @@ -0,0 +1,13 @@ +import { LensApiRequest } from "../router"; +import { LensApi } from "../lens-api"; +import { getAppVersion } from "../../common/utils"; + +class VersionRoute extends LensApi { + public async getVersion(request: LensApiRequest) { + const { response } = request; + + this.respondJson(response, { version: getAppVersion()}, 200); + } +} + +export const versionRoute = new VersionRoute(); diff --git a/src/main/routes/watch-route.ts b/src/main/routes/watch-route.ts deleted file mode 100644 index eb9f007eae..0000000000 --- a/src/main/routes/watch-route.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { LensApiRequest } from "../router"; -import { LensApi } from "../lens-api"; -import { Watch, KubeConfig } from "@kubernetes/client-node"; -import { ServerResponse } from "http"; -import { Request } from "request"; -import logger from "../logger"; - -class ApiWatcher { - private apiUrl: string; - private response: ServerResponse; - private watchRequest: Request; - private watch: Watch; - private processor: NodeJS.Timeout; - private eventBuffer: any[] = []; - - constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) { - this.apiUrl = apiUrl; - this.watch = new Watch(kubeConfig); - this.response = response; - } - - public async start() { - if (this.processor) { - clearInterval(this.processor); - } - this.processor = setInterval(() => { - const events = this.eventBuffer.splice(0); - - events.map(event => this.sendEvent(event)); - this.response.flushHeaders(); - }, 50); - this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this)); - } - - public stop() { - if (!this.watchRequest) { return; } - - if (this.processor) { - clearInterval(this.processor); - } - logger.debug(`Stopping watcher for api: ${this.apiUrl}`); - - try { - this.watchRequest.abort(); - this.sendEvent({ - type: "STREAM_END", - url: this.apiUrl, - status: 410, - }); - logger.debug("watch aborted"); - } catch (error) { - logger.error(`Watch abort errored:${error}`); - } - } - - private watchHandler(phase: string, obj: any) { - this.eventBuffer.push({ - type: phase, - object: obj - }); - } - - private doneHandler(error: Error) { - if (error) logger.warn(`watch ended: ${error.toString()}`); - this.watchRequest.abort(); - } - - private sendEvent(evt: any) { - // convert to "text/event-stream" format - this.response.write(`data: ${JSON.stringify(evt)}\n\n`); - } -} - -class WatchRoute extends LensApi { - - public async routeWatch(request: LensApiRequest) { - const { response, cluster} = request; - const apis: string[] = request.query.getAll("api"); - const watchers: ApiWatcher[] = []; - - if (!apis.length) { - this.respondJson(response, { - message: "Empty request. Query params 'api' are not provided.", - example: "?api=/api/v1/pods&api=/api/v1/nodes", - }, 400); - - return; - } - - response.setHeader("Content-Type", "text/event-stream"); - response.setHeader("Cache-Control", "no-cache"); - response.setHeader("Connection", "keep-alive"); - logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`); - - apis.forEach(apiUrl => { - const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response); - - watcher.start(); - watchers.push(watcher); - }); - - request.raw.req.on("close", () => { - logger.debug("Watch request closed"); - watchers.map(watcher => watcher.stop()); - }); - - request.raw.req.on("end", () => { - logger.debug("Watch request ended"); - watchers.map(watcher => watcher.stop()); - }); - - } -} - -export const watchRoute = new WatchRoute(); diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts deleted file mode 100644 index be04649a31..0000000000 --- a/src/main/shell-session.ts +++ /dev/null @@ -1,219 +0,0 @@ -import * as pty from "node-pty"; -import * as WebSocket from "ws"; -import { EventEmitter } from "events"; -import path from "path"; -import shellEnv from "shell-env"; -import { app } from "electron"; -import { Kubectl } from "./kubectl"; -import { Cluster } from "./cluster"; -import { ClusterPreferences } from "../common/cluster-store"; -import { helmCli } from "./helm/helm-cli"; -import { isWindows } from "../common/vars"; -import { appEventBus } from "../common/event-bus"; -import { userStore } from "../common/user-store"; - -export class ShellSession extends EventEmitter { - static shellEnvs: Map = new Map(); - - protected websocket: WebSocket; - protected shellProcess: pty.IPty; - protected kubeconfigPath: string; - protected nodeShellPod: string; - protected kubectl: Kubectl; - protected kubectlBinDir: string; - protected kubectlPathDir: string; - protected helmBinDir: string; - protected preferences: ClusterPreferences; - protected running = false; - protected clusterId: string; - - constructor(socket: WebSocket, cluster: Cluster) { - super(); - this.websocket = socket; - this.kubeconfigPath = cluster.getProxyKubeconfigPath(); - this.kubectl = new Kubectl(cluster.version); - this.preferences = cluster.preferences || {}; - this.clusterId = cluster.id; - } - - public async open() { - this.kubectlBinDir = await this.kubectl.binDir(); - const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); - - this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences); - this.helmBinDir = helmCli.getBinaryDir(); - const env = await this.getCachedShellEnv(); - const shell = env.PTYSHELL; - const args = await this.getShellArgs(shell); - - this.shellProcess = pty.spawn(shell, args, { - cols: 80, - cwd: this.cwd() || env.HOME, - env, - name: "xterm-256color", - rows: 30, - }); - this.running = true; - - this.pipeStdout(); - this.pipeStdin(); - this.closeWebsocketOnProcessExit(); - this.exitProcessOnWebsocketClose(); - - appEventBus.emit({name: "shell", action: "open"}); - } - - protected cwd(): string { - if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") { - return null; - } - - return this.preferences.terminalCWD; - } - - protected async getShellArgs(shell: string): Promise> { - switch(path.basename(shell)) { - case "powershell.exe": - return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`]; - case "bash": - return ["--init-file", path.join(this.kubectlBinDir, ".bash_set_path")]; - case "fish": - return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]; - case "zsh": - return ["--login"]; - default: - return []; - } - } - - protected async getCachedShellEnv() { - let env = ShellSession.shellEnvs.get(this.clusterId); - - if (!env) { - env = await this.getShellEnv(); - ShellSession.shellEnvs.set(this.clusterId, env); - } else { - // refresh env in the background - this.getShellEnv().then((shellEnv: any) => { - ShellSession.shellEnvs.set(this.clusterId, shellEnv); - }); - } - - return env; - } - - protected async getShellEnv() { - const env = JSON.parse(JSON.stringify(await shellEnv())); - const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter); - - if(isWindows) { - env["SystemRoot"] = process.env.SystemRoot; - env["PTYSHELL"] = "powershell.exe"; - env["PATH"] = pathStr; - } else if(typeof(process.env.SHELL) != "undefined") { - env["PTYSHELL"] = process.env.SHELL; - env["PATH"] = pathStr; - } else { - env["PTYSHELL"] = ""; // blank runs the system default shell - } - - if(path.basename(env["PTYSHELL"]) === "zsh") { - env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME; - env["ZDOTDIR"] = this.kubectlBinDir; - env["DISABLE_AUTO_UPDATE"] = "true"; - } - - env["PTYPID"] = process.pid.toString(); - env["KUBECONFIG"] = this.kubeconfigPath; - env["TERM_PROGRAM"] = app.getName(); - env["TERM_PROGRAM_VERSION"] = app.getVersion(); - - if (this.preferences.httpsProxy) { - env["HTTPS_PROXY"] = this.preferences.httpsProxy; - } - const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]; - - env["NO_PROXY"] = no_proxy.filter(address => !!address).join(); - - if (env.DEBUG) { // do not pass debug option to bash - delete env["DEBUG"]; - } - - return(env); - } - - protected pipeStdout() { - // send shell output to websocket - this.shellProcess.onData(((data: string) => { - this.sendResponse(data); - })); - } - - protected pipeStdin() { - // write websocket messages to shellProcess - this.websocket.on("message", (data: string) => { - if (!this.running) { return; } - - const message = Buffer.from(data.slice(1, data.length), "base64").toString(); - - switch (data[0]) { - case "0": - this.shellProcess.write(message); - break; - case "4": - const resizeMsgObj = JSON.parse(message); - - this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"]); - break; - case "9": - this.emit("newToken", message); - break; - } - }); - } - - protected exit(code = 1000) { - if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code); - this.emit("exit"); - } - - protected closeWebsocketOnProcessExit() { - this.shellProcess.onExit(({ exitCode }) => { - this.running = false; - let timeout = 0; - - if (exitCode > 0) { - this.sendResponse("Terminal will auto-close in 15 seconds ..."); - timeout = 15*1000; - } - setTimeout(() => { - this.exit(); - }, timeout); - }); - } - - protected exitProcessOnWebsocketClose() { - this.websocket.on("close", () => { - this.killShellProcess(); - }); - } - - protected killShellProcess(){ - if(this.running) { - // On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()` - if (isWindows) { - try { - process.kill(this.shellProcess.pid); - } catch(e) { - return; - } - } else { - this.shellProcess.kill(); - } - } - } - - protected sendResponse(msg: string) { - this.websocket.send(`1${Buffer.from(msg).toString("base64")}`); - } -} diff --git a/src/main/shell-session/index.ts b/src/main/shell-session/index.ts new file mode 100644 index 0000000000..96743adcc6 --- /dev/null +++ b/src/main/shell-session/index.ts @@ -0,0 +1,2 @@ +export * from "./node-shell-session"; +export * from "./local-shell-session"; diff --git a/src/main/shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session.ts new file mode 100644 index 0000000000..c131ea651c --- /dev/null +++ b/src/main/shell-session/local-shell-session.ts @@ -0,0 +1,40 @@ +import path from "path"; +import { helmCli } from "../helm/helm-cli"; +import { userStore } from "../../common/user-store"; +import { ShellSession } from "./shell-session"; + +export class LocalShellSession extends ShellSession { + ShellType = "shell"; + + protected getPathEntries(): string[] { + return [helmCli.getBinaryDir()]; + } + + public async open() { + + const env = await this.getCachedShellEnv(); + const shell = env.PTYSHELL; + const args = await this.getShellArgs(shell); + + super.open(env.PTYSHELL, args, env); + } + + protected async getShellArgs(shell: string): Promise { + const helmpath = helmCli.getBinaryDir(); + const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); + const kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); + + switch(path.basename(shell)) { + case "powershell.exe": + return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${helmpath};${kubectlPathDir};$Env:PATH"}`]; + case "bash": + return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")]; + case "fish": + return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPathP}"`]; + case "zsh": + return ["--login"]; + default: + return []; + } + } +} diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session.ts new file mode 100644 index 0000000000..16d467138d --- /dev/null +++ b/src/main/shell-session/node-shell-session.ts @@ -0,0 +1,108 @@ +import * as WebSocket from "ws"; +import { v4 as uuid } from "uuid"; +import * as k8s from "@kubernetes/client-node"; +import { KubeConfig } from "@kubernetes/client-node"; +import { Cluster } from "../cluster"; +import { ShellOpenError, ShellSession } from "./shell-session"; + +export class NodeShellSession extends ShellSession { + ShellType = "node-shell"; + + protected podId = `node-shell-${uuid()}`; + protected kc: KubeConfig; + + constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) { + super(socket, cluster); + } + + public async open() { + this.kc = await this.cluster.getProxyKubeconfig(); + const shell = await this.kubectl.getPath(); + + try { + await this.createNodeShellPod(); + await this.waitForRunningPod(); + } catch (error) { + this.deleteNodeShellPod(); + this.sendResponse("Error occurred. "); + + throw new ShellOpenError("failed to create node pod", error); + } + + const args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]; + const env = await this.getCachedShellEnv(); + + super.open(shell, args, env); + } + + protected createNodeShellPod() { + return this + .kc + .makeApiClient(k8s.CoreV1Api) + .createNamespacedPod("kube-system", { + metadata: { + name: this.podId, + namespace: "kube-system" + }, + spec: { + nodeName: this.nodeName, + restartPolicy: "Never", + terminationGracePeriodSeconds: 0, + hostPID: true, + hostIPC: true, + hostNetwork: true, + tolerations: [{ + operator: "Exists" + }], + containers: [{ + name: "shell", + image: "docker.io/alpine:3.12", + securityContext: { + privileged: true, + }, + command: ["nsenter"], + args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"] + }], + } + }); + } + + protected waitForRunningPod(): Promise { + return new Promise((resolve, reject) => { + const watch = new k8s.Watch(this.kc); + + watch + .watch(`/api/v1/namespaces/kube-system/pods`, + {}, + // callback is called for each received object. + (type, obj) => { + if (obj.metadata.name == this.podId && obj.status.phase === "Running") { + resolve(); + } + }, + // done callback is called if the watch terminates normally + (err) => { + console.log(err); + reject(err); + } + ) + .then(req => { + setTimeout(() => { + console.log("aborting"); + req.abort(); + }, 2 * 60 * 1000); + }) + .catch(err => { + console.log("watch failed"); + reject(err); + }); + }); + } + + protected deleteNodeShellPod() { + this + .kc + .makeApiClient(k8s.CoreV1Api) + .deleteNamespacedPod(this.podId, "kube-system"); + } +} diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts new file mode 100644 index 0000000000..eebaed0605 --- /dev/null +++ b/src/main/shell-session/shell-session.ts @@ -0,0 +1,179 @@ +import { Cluster } from "../cluster"; +import { Kubectl } from "../kubectl"; +import * as WebSocket from "ws"; +import shellEnv from "shell-env"; +import { app } from "electron"; +import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; +import path from "path"; +import { isWindows } from "../../common/vars"; +import { userStore } from "../../common/user-store"; +import * as pty from "node-pty"; +import { appEventBus } from "../../common/event-bus"; + +export class ShellOpenError extends Error { + constructor(message: string, public cause: Error) { + super(`${message}: ${cause}`); + this.name = this.constructor.name; + Error.captureStackTrace(this); + } +} + +export abstract class ShellSession { + abstract ShellType: string; + + static shellEnvs: Map> = new Map(); + + protected kubectl: Kubectl; + protected running = false; + protected shellProcess: pty.IPty; + protected kubectlBinDirP: Promise; + protected kubeconfigPathP: Promise; + + protected get cwd(): string | undefined { + return this.cluster.preferences?.terminalCWD; + } + + constructor(protected websocket: WebSocket, protected cluster: Cluster) { + this.kubectl = new Kubectl(cluster.version); + this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath(); + this.kubectlBinDirP = this.kubectl.binDir(); + } + + open(shell: string, args: string[], env: Record): void { + this.shellProcess = pty.spawn(shell, args, { + cols: 80, + cwd: this.cwd || env.HOME, + env, + name: "xterm-256color", + rows: 30, + }); + this.running = true; + + this.shellProcess.onData(data => this.sendResponse(data)); + this.shellProcess.onExit(({ exitCode }) => { + this.running = false; + + if (exitCode > 0) { + this.sendResponse("Terminal will auto-close in 15 seconds ..."); + setTimeout(() => this.exit(), 15 * 1000); + } else { + this.exit(); + } + }); + + this.websocket + .on("message", (data: string) => { + if (!this.running) { + return; + } + + const message = Buffer.from(data.slice(1, data.length), "base64").toString(); + + switch (data[0]) { + case "0": + this.shellProcess.write(message); + break; + case "4": + const { Width, Height } = JSON.parse(message); + + this.shellProcess.resize(Width, Height); + break; + } + }) + .on("close", () => { + if (this.running) { + try { + process.kill(this.shellProcess.pid); + } catch (e) { + } + } + + this.running = false; + }); + + appEventBus.emit({ name: this.ShellType, action: "open" }); + } + + protected getPathEntries(): string[] { + return []; + } + + protected async getCachedShellEnv() { + const { id: clusterId } = this.cluster; + + let env = ShellSession.shellEnvs.get(clusterId); + + if (!env) { + env = await this.getShellEnv(); + ShellSession.shellEnvs.set(clusterId, env); + } else { + // refresh env in the background + this.getShellEnv().then((shellEnv: any) => { + ShellSession.shellEnvs.set(clusterId, shellEnv); + }); + } + + return env; + } + + protected async getShellEnv() { + const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); + const pathStr = [...this.getPathEntries(), await this.kubectlBinDirP, process.env.PATH].join(path.delimiter); + const shell = userStore.preferences.shell || process.env.SHELL || process.env.PTYSHELL; + + delete env.DEBUG; // don't pass DEBUG into shells + + if (isWindows) { + env.SystemRoot = process.env.SystemRoot; + env.PTYSHELL = shell || "powershell.exe"; + env.PATH = pathStr; + env.LENS_SESSION = "true"; + env.WSLENV = [ + process.env.WSLENV, + "KUBECONFIG/up:LENS_SESSION/u" + ] + .filter(Boolean) + .join(":"); + } else if (shell !== undefined) { + env.PTYSHELL = shell; + env.PATH = pathStr; + } else { + env.PTYSHELL = ""; // blank runs the system default shell + } + + if (path.basename(env.PTYSHELL) === "zsh") { + env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME; + env.ZDOTDIR = this.kubectlBinDirP; + env.DISABLE_AUTO_UPDATE = "true"; + } + + env.PTYPID = process.pid.toString(); + env.KUBECONFIG = await this.kubeconfigPathP; + env.TERM_PROGRAM = app.getName(); + env.TERM_PROGRAM_VERSION = app.getVersion(); + + if (this.cluster.preferences.httpsProxy) { + env.HTTPS_PROXY = this.cluster.preferences.httpsProxy; + } + + env.NO_PROXY = [ + "localhost", + "127.0.0.1", + env.NO_PROXY + ] + .filter(Boolean) + .join(); + + return env; + } + + protected exit(code = 1000) { + if (this.websocket.readyState == this.websocket.OPEN) { + this.websocket.close(code); + } + } + + protected sendResponse(msg: string) { + this.websocket.send(`1${Buffer.from(msg).toString("base64")}`); + } +} diff --git a/src/main/tray.ts b/src/main/tray.ts index 47f641ad72..a15fa845d3 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,18 +1,17 @@ import path from "path"; import packageInfo from "../../package.json"; -import { dialog, Menu, NativeImage, Tray } from "electron"; +import { Menu, Tray } from "electron"; import { autorun } from "mobx"; import { showAbout } from "./menu"; -import { AppUpdater } from "./app-updater"; +import { checkForUpdates } from "./app-updater"; import { WindowManager } from "./window-manager"; -import { clusterStore } from "../common/cluster-store"; -import { workspaceStore } from "../common/workspace-store"; import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; -import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route"; import logger from "./logger"; import { isDevelopment, isWindows } from "../common/vars"; import { exitApp } from "./exit-app"; +const TRAY_LOG_PREFIX = "[TRAY]"; + // note: instance of Tray should be saved somewhere, otherwise it disappears export let tray: Tray; @@ -25,103 +24,70 @@ export function getTrayIcon(): string { } export function initTray(windowManager: WindowManager) { - const dispose = autorun(() => { - try { - const menu = createTrayMenu(windowManager); + const icon = getTrayIcon(); - buildTray(getTrayIcon(), menu, windowManager); - } catch (err) { - logger.error(`[TRAY]: building failed: ${err}`); - } - }); + tray = new Tray(icon); + tray.setToolTip(packageInfo.description); + tray.setIgnoreDoubleClickEvents(true); + + if (isWindows) { + tray.on("click", () => { + windowManager + .ensureMainWindow() + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); + }); + } + + const disposers = [ + autorun(() => { + try { + const menu = createTrayMenu(windowManager); + + tray.setContextMenu(menu); + } catch (error) { + logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error }); + } + }), + ]; return () => { - dispose(); + disposers.forEach(disposer => disposer()); tray?.destroy(); tray = null; }; } -function buildTray(icon: string | NativeImage, menu: Menu, windowManager: WindowManager) { - if (!tray) { - tray = new Tray(icon); - tray.setToolTip(packageInfo.description); - tray.setIgnoreDoubleClickEvents(true); - tray.setImage(icon); - tray.setContextMenu(menu); - - if (isWindows) { - tray.on("click", () => { - windowManager.ensureMainWindow(); - }); - } - } - - return tray; -} - function createTrayMenu(windowManager: WindowManager): Menu { return Menu.buildFromTemplate([ - { - label: "About Lens", - async click() { - // note: argument[1] (browserWindow) not available when app is not focused / hidden - const browserWindow = await windowManager.ensureMainWindow(); - - showAbout(browserWindow); - }, - }, - { type: "separator" }, { label: "Open Lens", - async click() { - await windowManager.ensureMainWindow(); + click() { + windowManager + .ensureMainWindow() + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); }, }, { label: "Preferences", click() { - windowManager.navigate(preferencesURL()); + windowManager + .navigate(preferencesURL()) + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error })); }, }, { - label: "Clusters", - submenu: workspaceStore.enabledWorkspacesList - .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces - .map(workspace => { - const clusters = clusterStore.getByWorkspaceId(workspace.id); - - return { - label: workspace.name, - toolTip: workspace.description, - submenu: clusters.map(cluster => { - const { id: clusterId, name: label, online, workspace } = cluster; - - return { - label: `${online ? "✓" : "\x20".repeat(3)/*offset*/}${label}`, - toolTip: clusterId, - async click() { - workspaceStore.setActive(workspace); - windowManager.navigate(clusterViewURL({ params: { clusterId } })); - } - }; - }) - }; - }), + label: "Check for updates", + click() { + checkForUpdates() + .then(() => windowManager.ensureMainWindow()); + }, }, { - label: "Check for updates", - async click() { - const result = await AppUpdater.checkForUpdates(); - - if (!result) { - const browserWindow = await windowManager.ensureMainWindow(); - - dialog.showMessageBoxSync(browserWindow, { - message: "No updates available", - type: "info", - }); - } + label: "About Lens", + click() { + windowManager.ensureMainWindow() + .then(showAbout) + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error })); }, }, { type: "separator" }, diff --git a/src/main/utils/clear-kube-env-vars.ts b/src/main/utils/clear-kube-env-vars.ts new file mode 100644 index 0000000000..ef14b1a331 --- /dev/null +++ b/src/main/utils/clear-kube-env-vars.ts @@ -0,0 +1,16 @@ +const anyKubeconfig = /^kubeconfig$/i; + +/** + * This function deletes all keys of the form /^kubeconfig$/i, returning a new + * object. + * + * This is needed because `kubectl` checks for other version of kubeconfig + * before KUBECONFIG and we only set KUBECONFIG. + * @param env The current copy of env + */ +export function clearKubeconfigEnvVars(env: Record): Record { + return Object.fromEntries( + Object.entries(env) + .filter(([key]) => anyKubeconfig.exec(key) === null) + ); +} diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index bf7458afa0..afd6b03670 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -8,6 +8,8 @@ import { initMenu } from "./menu"; import { initTray } from "./tray"; import { Singleton } from "../common/utils"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; +import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; +import logger from "./logger"; export class WindowManager extends Singleton { protected mainWindow: BrowserWindow; @@ -22,7 +24,6 @@ export class WindowManager extends Singleton { this.bindEvents(); this.initMenu(); this.initTray(); - this.initMainWindow(); } get mainUrl() { @@ -65,13 +66,13 @@ export class WindowManager extends Singleton { shell.openExternal(url); }); this.mainWindow.webContents.on("dom-ready", () => { - appEventBus.emit({name: "app", action: "dom-ready"}); + appEventBus.emit({ name: "app", action: "dom-ready" }); }); this.mainWindow.on("focus", () => { - appEventBus.emit({name: "app", action: "focus"}); + appEventBus.emit({ name: "app", action: "focus" }); }); this.mainWindow.on("blur", () => { - appEventBus.emit({name: "app", action: "blur"}); + appEventBus.emit({ name: "app", action: "blur" }); }); // clean up @@ -81,10 +82,19 @@ export class WindowManager extends Singleton { this.splashWindow = null; app.dock?.hide(); // hide icon in dock (mac-os) }); + + this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => { + logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc }); + }); + + this.mainWindow.webContents.on("did-finish-load", () => { + logger.info("[WINDOW-MANAGER]: Main window loaded"); + }); } try { if (showSplash) await this.showSplash(); + logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainUrl} ...`); await this.mainWindow.loadURL(this.mainUrl); this.mainWindow.show(); this.splashWindow?.close(); @@ -106,7 +116,7 @@ export class WindowManager extends Singleton { protected bindEvents() { // track visible cluster from ui - subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => { + subscribeToBroadcast(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => { this.activeClusterId = clusterId; }); } @@ -128,13 +138,14 @@ export class WindowManager extends Singleton { async navigate(url: string, frameId?: number) { await this.ensureMainWindow(); - let frameInfo: ClusterFrameInfo; - if (frameId) { - frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId); - } + const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId); + const channel = frameInfo + ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER + : IpcRendererNavigationEvents.NAVIGATE_IN_APP; + this.sendToView({ - channel: "renderer:navigate", + channel, frameInfo, data: [url], }); @@ -144,7 +155,7 @@ export class WindowManager extends Singleton { const frameInfo = clusterFrameMap.get(this.activeClusterId); if (frameInfo) { - this.sendToView({ channel: "renderer:reload", frameInfo }); + this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo }); } else { webContents.getFocusedWebContents()?.reload(); } diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts index c546fdaeda..4a71d4f7ad 100644 --- a/src/migrations/cluster-store/index.ts +++ b/src/migrations/cluster-store/index.ts @@ -18,4 +18,4 @@ export default { ...version270Beta1, ...version360Beta1, ...snap -}; \ No newline at end of file +}; diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts new file mode 100644 index 0000000000..22087ab569 --- /dev/null +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -0,0 +1,28 @@ +// Cleans up a store that had the state related data stored +import { Hotbar } from "../../common/hotbar-store"; +import { clusterStore } from "../../common/cluster-store"; +import { migration } from "../migration-wrapper"; + +export default migration({ + version: "5.0.0-alpha.0", + run(store) { + const hotbars: Hotbar[] = []; + + clusterStore.enabledClustersList.forEach((cluster: any) => { + const name = cluster.workspace || "default"; + let hotbar = hotbars.find((h) => h.name === name); + + if (!hotbar) { + hotbar = { name, items: [] }; + hotbars.push(hotbar); + } + + hotbar.items.push({ + entity: { uid: cluster.id }, + params: {} + }); + }); + + store.set("hotbars", hotbars); + } +}); diff --git a/src/migrations/hotbar-store/index.ts b/src/migrations/hotbar-store/index.ts new file mode 100644 index 0000000000..ae9d4bc125 --- /dev/null +++ b/src/migrations/hotbar-store/index.ts @@ -0,0 +1,7 @@ +// Hotbar store migrations + +import version500alpha0 from "./5.0.0-alpha.0"; + +export default { + ...version500alpha0, +}; diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts new file mode 100644 index 0000000000..0abe6b280b --- /dev/null +++ b/src/migrations/user-store/file-name-migration.ts @@ -0,0 +1,22 @@ +import fse from "fs-extra"; +import { app, remote } from "electron"; +import path from "path"; + +export async function fileNameMigration() { + const userDataPath = (app || remote.app).getPath("userData"); + const configJsonPath = path.join(userDataPath, "config.json"); + const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); + + try { + await fse.move(configJsonPath, lensUserStoreJsonPath); + } catch (error) { + if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory) + return; // file already moved + } else if (error.message === "dest already exists.") { + await fse.remove(configJsonPath); + } else { + // pass other errors along + throw error; + } + } +} diff --git a/src/migrations/user-store/index.ts b/src/migrations/user-store/index.ts index e1e7b8ffc9..5f5085b475 100644 --- a/src/migrations/user-store/index.ts +++ b/src/migrations/user-store/index.ts @@ -1,6 +1,11 @@ // User store migrations import version210Beta4 from "./2.1.0-beta.4"; +import { fileNameMigration } from "./file-name-migration"; + +export { + fileNameMigration +}; export default { ...version210Beta4, diff --git a/src/renderer/api/__tests__/api-manager.test.ts b/src/renderer/api/__tests__/api-manager.test.ts new file mode 100644 index 0000000000..29ce7bd189 --- /dev/null +++ b/src/renderer/api/__tests__/api-manager.test.ts @@ -0,0 +1,40 @@ +import { ingressStore } from "../../components/+network-ingresses/ingress.store"; +import { apiManager } from "../api-manager"; +import { KubeApi } from "../kube-api"; + +class TestApi extends KubeApi { + + protected async checkPreferredVersion() { + return; + } +} + +describe("ApiManager", () => { + describe("registerApi", () => { + it("re-register store if apiBase changed", async () => { + const apiBase = "apis/v1/foo"; + const fallbackApiBase = "/apis/extensions/v1beta1/foo"; + const kubeApi = new TestApi({ + apiBase, + fallbackApiBases: [fallbackApiBase], + checkPreferredVersion: true, + }); + + apiManager.registerApi(apiBase, kubeApi); + + // Define to use test api for ingress store + Object.defineProperty(ingressStore, "api", { value: kubeApi }); + apiManager.registerStore(ingressStore, [kubeApi]); + + // Test that store is returned with original apiBase + expect(apiManager.getStore(kubeApi)).toBe(ingressStore); + + // Change apiBase similar as checkPreferredVersion does + Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase }); + apiManager.registerApi(fallbackApiBase, kubeApi); + + // Test that store is returned with new apiBase + expect(apiManager.getStore(kubeApi)).toBe(ingressStore); + }); + }); +}); diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts new file mode 100644 index 0000000000..2b36086856 --- /dev/null +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -0,0 +1,135 @@ +import { CatalogEntityRegistry } from "../catalog-entity-registry"; +import "../../../common/catalog-entities"; +import { catalogCategoryRegistry } from "../../../common/catalog-category-registry"; + +describe("CatalogEntityRegistry", () => { + describe("updateItems", () => { + it("adds new catalog item", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + + items.push({ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }); + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(2); + }); + + it("ignores unknown items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "FooBar", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(0); + }); + + it("updates existing items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].status.phase).toEqual("disconnected"); + + items[0].status.phase = "connected"; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].status.phase).toEqual("connected"); + }); + + it("removes deleted items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [ + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }, + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + } + ]; + + catalog.updateItems(items); + items.splice(0, 1); + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].metadata.uid).toEqual("456"); + }); + }); +}); diff --git a/src/renderer/api/__tests__/kube-api-parse.test.ts b/src/renderer/api/__tests__/kube-api-parse.test.ts index c2aec7fd58..bc4528ad4e 100644 --- a/src/renderer/api/__tests__/kube-api-parse.test.ts +++ b/src/renderer/api/__tests__/kube-api-parse.test.ts @@ -1,134 +1,115 @@ +jest.mock("../kube-object"); +jest.mock("../kube-api"); +jest.mock("../api-manager", () => ({ + apiManager() { + return { + registerStore: jest.fn(), + }; + } +})); + import { IKubeApiParsed, parseKubeApi } from "../kube-api-parse"; -interface KubeApiParseTestData { - url: string; - expected: Required; -} +/** + * [, ] + */ +type KubeApiParseTestData = [string, Required]; const tests: KubeApiParseTestData[] = [ - { - url: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", - expected: { - apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", - apiPrefix: "/apis", - apiGroup: "apiextensions.k8s.io", - apiVersion: "v1beta1", - apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", - namespace: undefined, - resource: "customresourcedefinitions", - name: "prometheuses.monitoring.coreos.com" - }, - }, - { - url: "/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", - expected: { - apiBase: "/api/v1/pods", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - namespace: "kube-system", - resource: "pods", - name: "coredns-6955765f44-v8p27" - }, - }, - { - url: "/apis/stable.example.com/foo1/crontabs", - expected: { - apiBase: "/apis/stable.example.com/foo1/crontabs", - apiPrefix: "/apis", - apiGroup: "stable.example.com", - apiVersion: "foo1", - apiVersionWithGroup: "stable.example.com/foo1", - resource: "crontabs", - name: undefined, - namespace: undefined, - }, - }, - { - url: "/apis/cluster.k8s.io/v1alpha1/clusters", - expected: { - apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters", - apiPrefix: "/apis", - apiGroup: "cluster.k8s.io", - apiVersion: "v1alpha1", - apiVersionWithGroup: "cluster.k8s.io/v1alpha1", - resource: "clusters", - name: undefined, - namespace: undefined, - }, - }, - { - url: "/api/v1/namespaces", - expected: { - apiBase: "/api/v1/namespaces", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "namespaces", - name: undefined, - namespace: undefined, - }, - }, - { - url: "/api/v1/secrets", - expected: { - apiBase: "/api/v1/secrets", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "secrets", - name: undefined, - namespace: undefined, - }, - }, - { - url: "/api/v1/nodes/minikube", - expected: { - apiBase: "/api/v1/nodes", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "nodes", - name: "minikube", - namespace: undefined, - }, - }, - { - url: "/api/foo-bar/nodes/minikube", - expected: { - apiBase: "/api/foo-bar/nodes", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "foo-bar", - apiVersionWithGroup: "foo-bar", - resource: "nodes", - name: "minikube", - namespace: undefined, - }, - }, - { - url: "/api/v1/namespaces/kube-public", - expected: { - apiBase: "/api/v1/namespaces", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "namespaces", - name: "kube-public", - namespace: undefined, - }, - }, + ["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { + apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", + apiPrefix: "/apis", + apiGroup: "apiextensions.k8s.io", + apiVersion: "v1beta1", + apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", + namespace: undefined, + resource: "customresourcedefinitions", + name: "prometheuses.monitoring.coreos.com" + }], + ["/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", { + apiBase: "/api/v1/pods", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + namespace: "kube-system", + resource: "pods", + name: "coredns-6955765f44-v8p27" + }], + ["/apis/stable.example.com/foo1/crontabs", { + apiBase: "/apis/stable.example.com/foo1/crontabs", + apiPrefix: "/apis", + apiGroup: "stable.example.com", + apiVersion: "foo1", + apiVersionWithGroup: "stable.example.com/foo1", + resource: "crontabs", + name: undefined, + namespace: undefined, + }], + ["/apis/cluster.k8s.io/v1alpha1/clusters", { + apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters", + apiPrefix: "/apis", + apiGroup: "cluster.k8s.io", + apiVersion: "v1alpha1", + apiVersionWithGroup: "cluster.k8s.io/v1alpha1", + resource: "clusters", + name: undefined, + namespace: undefined, + }], + ["/api/v1/namespaces", { + apiBase: "/api/v1/namespaces", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "namespaces", + name: undefined, + namespace: undefined, + }], + ["/api/v1/secrets", { + apiBase: "/api/v1/secrets", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "secrets", + name: undefined, + namespace: undefined, + }], + ["/api/v1/nodes/minikube", { + apiBase: "/api/v1/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "nodes", + name: "minikube", + namespace: undefined, + }], + ["/api/foo-bar/nodes/minikube", { + apiBase: "/api/foo-bar/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "foo-bar", + apiVersionWithGroup: "foo-bar", + resource: "nodes", + name: "minikube", + namespace: undefined, + }], + ["/api/v1/namespaces/kube-public", { + apiBase: "/api/v1/namespaces", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "namespaces", + name: "kube-public", + namespace: undefined, + }], ]; describe("parseApi unit tests", () => { - for (const { url, expected } of tests) { - test(`testing "${url}"`, () => { - expect(parseKubeApi(url)).toStrictEqual(expected); - }); - } + it.each(tests)("testing %s", (url, expected) => { + expect(parseKubeApi(url)).toStrictEqual(expected); + }); }); diff --git a/src/renderer/api/__tests__/kube-api.test.ts b/src/renderer/api/__tests__/kube-api.test.ts index 41078e77a3..9d3c41869d 100644 --- a/src/renderer/api/__tests__/kube-api.test.ts +++ b/src/renderer/api/__tests__/kube-api.test.ts @@ -28,7 +28,7 @@ describe("KubeApi", () => { }; } }); - + const apiBase = "/apis/networking.k8s.io/v1/ingresses"; const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; const kubeApi = new KubeApi({ @@ -36,7 +36,7 @@ describe("KubeApi", () => { fallbackApiBases: [fallbackApiBase], checkPreferredVersion: true, }); - + await kubeApi.get(); expect(kubeApi.apiPrefix).toEqual("/apis"); expect(kubeApi.apiGroup).toEqual("networking.k8s.io"); @@ -79,4 +79,4 @@ describe("KubeApi", () => { expect(kubeApi.apiPrefix).toEqual("/apis"); expect(kubeApi.apiGroup).toEqual("extensions"); }); -}); \ No newline at end of file +}); diff --git a/src/renderer/api/__tests__/pods.test.ts b/src/renderer/api/__tests__/pods.test.ts new file mode 100644 index 0000000000..9a257bb724 --- /dev/null +++ b/src/renderer/api/__tests__/pods.test.ts @@ -0,0 +1,303 @@ +import { Pod } from "../endpoints"; + +interface GetDummyPodOptions { + running?: number; + dead?: number; + initRunning?: number; + initDead?: number; +} + +function getDummyPodDefaultOptions(): Required { + return { + running: 0, + dead: 0, + initDead: 0, + initRunning: 0, + }; +} + +function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod { + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + uid: "1", + name: "test", + resourceVersion: "v1", + selfLink: "http" + } + }); + + pod.spec = { + containers: [], + initContainers: [], + serviceAccount: "dummy", + serviceAccountName: "dummy", + }; + + pod.status = { + phase: "Running", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], + }; + + for (let i = 0; i < opts.running; i += 1) { + const name = `container_r_${i}`; + + pod.spec.containers.push({ + image: "dummy", + imagePullPolicy: "dummy", + name, + }); + pod.status.containerStatuses.push({ + image: "dummy", + imageID: "dummy", + name, + ready: true, + restartCount: i, + state: { + running: { + startedAt: "before" + }, + } + }); + } + + for (let i = 0; i < opts.dead; i += 1) { + const name = `container_d_${i}`; + + pod.spec.containers.push({ + image: "dummy", + imagePullPolicy: "dummy", + name, + }); + pod.status.containerStatuses.push({ + image: "dummy", + imageID: "dummy", + name, + ready: false, + restartCount: i, + state: { + terminated: { + startedAt: "before", + exitCode: i+1, + finishedAt: "later", + reason: `reason_${i}` + } + } + }); + } + + for (let i = 0; i < opts.initRunning; i += 1) { + const name = `container_ir_${i}`; + + pod.spec.initContainers.push({ + image: "dummy", + imagePullPolicy: "dummy", + name, + }); + pod.status.initContainerStatuses.push({ + image: "dummy", + imageID: "dummy", + name, + ready: true, + restartCount: i, + state: { + running: { + startedAt: "before" + } + } + }); + } + + for (let i = 0; i < opts.initDead; i += 1) { + const name = `container_id_${i}`; + + pod.spec.initContainers.push({ + image: "dummy", + imagePullPolicy: "dummy", + name, + }); + pod.status.initContainerStatuses.push({ + image: "dummy", + imageID: "dummy", + name, + ready: false, + restartCount: i, + state: { + terminated: { + startedAt: "before", + exitCode: i+1, + finishedAt: "later", + reason: `reason_${i}` + } + } + }); + } + + return pod; +} + +describe("Pods", () => { + const podTests = []; + + for (let r = 0; r < 3; r += 1) { + for (let d = 0; d < 3; d += 1) { + for (let ir = 0; ir < 3; ir += 1) { + for (let id = 0; id < 3; id += 1) { + podTests.push([r, d, ir, id]); + } + } + } + } + + describe.each(podTests)("for [%d running, %d dead] & initial [%d running, %d dead]", (running, dead, initRunning, initDead) => { + const pod = getDummyPod({ running, dead, initRunning, initDead }); + + function getNamedContainer(name: string) { + return { + image: "dummy", + imagePullPolicy: "dummy", + name + }; + } + + it("getRunningContainers should return only running and init running", () => { + const res = [ + ...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)), + ...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)), + ]; + + expect(pod.getRunningContainers()).toStrictEqual(res); + }); + + it("getAllContainers should return all containers", () => { + const res = [ + ...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)), + ...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_d_${index}`)), + ...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)), + ...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_id_${index}`)), + ]; + + expect(pod.getAllContainers()).toStrictEqual(res); + }); + + it("getRestartsCount should return total restart counts", () => { + function sum(len: number): number { + let res = 0; + + for (let i = 0; i < len; i += 1) { + res += i; + } + + return res; + } + + expect(pod.getRestartsCount()).toStrictEqual(sum(running) + sum(dead)); + }); + + it("hasIssues should return false", () => { + expect(pod.hasIssues()).toStrictEqual(false); + }); + }); + + describe("getSelectedNodeOs", () => { + it("should return stable", () => { + const pod = getDummyPod(); + + pod.spec.nodeSelector = { + "kubernetes.io/os": "foobar" + }; + + expect(pod.getSelectedNodeOs()).toStrictEqual("foobar"); + }); + + it("should return beta", () => { + const pod = getDummyPod(); + + pod.spec.nodeSelector = { + "beta.kubernetes.io/os": "foobar1" + }; + + expect(pod.getSelectedNodeOs()).toStrictEqual("foobar1"); + }); + + it("should return stable over beta", () => { + const pod = getDummyPod(); + + pod.spec.nodeSelector = { + "kubernetes.io/os": "foobar2", + "beta.kubernetes.io/os": "foobar3" + }; + + expect(pod.getSelectedNodeOs()).toStrictEqual("foobar2"); + }); + + it("should return undefined if none set", () => { + const pod = getDummyPod(); + + expect(pod.getSelectedNodeOs()).toStrictEqual(undefined); + }); + }); + + describe("hasIssues", () => { + it("should return true if a condition isn't ready", () => { + const pod = getDummyPod({ running: 1 }); + + pod.status.conditions.push({ + type: "Ready", + status: "foobar", + lastProbeTime: 1, + lastTransitionTime: "longer ago" + }); + + expect(pod.hasIssues()).toStrictEqual(true); + }); + + it("should return false if a condition is non-ready", () => { + const pod = getDummyPod({ running: 1 }); + + pod.status.conditions.push({ + type: "dummy", + status: "foobar", + lastProbeTime: 1, + lastTransitionTime: "longer ago" + }); + + expect(pod.hasIssues()).toStrictEqual(false); + }); + + it("should return true if a current container is in a crash loop back off", () => { + const pod = getDummyPod({ running: 1 }); + + pod.status.containerStatuses[0].state = { + waiting: { + reason: "CrashLookBackOff", + message: "too much foobar" + } + }; + + expect(pod.hasIssues()).toStrictEqual(true); + }); + + it("should return true if a current phase isn't running", () => { + const pod = getDummyPod({ running: 1 }); + + pod.status.phase = "not running"; + + expect(pod.hasIssues()).toStrictEqual(true); + }); + + it("should return false if a current phase is running", () => { + const pod = getDummyPod({ running: 1 }); + + pod.status.phase = "Running"; + + expect(pod.hasIssues()).toStrictEqual(false); + }); + }); +}); diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 629a0f29c2..90e63f692b 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -2,16 +2,16 @@ import type { KubeObjectStore } from "../kube-object.store"; import { action, observable } from "mobx"; import { autobind } from "../utils"; -import { KubeApi } from "./kube-api"; +import { KubeApi, parseKubeApi } from "./kube-api"; @autobind() export class ApiManager { private apis = observable.map(); - private stores = observable.map(); + private stores = observable.map(); getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { if (typeof pathOrCallback === "string") { - return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); + return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); } return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true)); @@ -23,6 +23,12 @@ export class ApiManager { registerApi(apiBase: string, api: KubeApi) { if (!this.apis.has(apiBase)) { + this.stores.forEach((store) => { + if(store.api === api) { + this.stores.set(apiBase, store); + } + }); + this.apis.set(apiBase, api); } } @@ -46,12 +52,12 @@ export class ApiManager { @action registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) { apis.forEach(api => { - this.stores.set(api, store); + this.stores.set(api.apiBase, store); }); } getStore(api: string | KubeApi): S { - return this.stores.get(this.resolveApi(api)) as S; + return this.stores.get(this.resolveApi(api)?.apiBase) as S; } } diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts new file mode 100644 index 0000000000..afed7a88cf --- /dev/null +++ b/src/renderer/api/catalog-entity-registry.ts @@ -0,0 +1,61 @@ +import { action, observable } from "mobx"; +import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; +import { CatalogCategory, CatalogEntity, CatalogEntityData } from "../../common/catalog-entity"; +import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../common/catalog-category-registry"; +import "../../common/catalog-entities"; + +export class CatalogEntityRegistry { + @observable protected _items: CatalogEntity[] = observable.array([], { deep: true }); + + constructor(private categoryRegistry: CatalogCategoryRegistry) {} + + init() { + subscribeToBroadcast("catalog:items", (ev, items: CatalogEntityData[]) => { + this.updateItems(items); + }); + broadcastMessage("catalog:broadcast"); + } + + @action updateItems(items: CatalogEntityData[]) { + this._items.forEach((item, index) => { + const foundIndex = items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid); + + if (foundIndex === -1) { + this._items.splice(index, 1); + } + }); + + items.forEach((data) => { + const item = this.categoryRegistry.getEntityForData(data); + + if (!item) return; // invalid data + + const index = this._items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid); + + if (index === -1) { + this._items.push(item); + } else { + this._items.splice(index, 1, item); + } + }); + } + + get items() { + return this._items; + } + + getItemsForApiKind(apiVersion: string, kind: string): T[] { + const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + + return items as T[]; + } + + getItemsForCategory(category: CatalogCategory): T[] { + const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`); + const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind); + + return items as T[]; + } +} + +export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); diff --git a/src/renderer/api/catalog-entity.ts b/src/renderer/api/catalog-entity.ts new file mode 100644 index 0000000000..a8006be0f7 --- /dev/null +++ b/src/renderer/api/catalog-entity.ts @@ -0,0 +1,12 @@ +import { navigate } from "../navigation"; +import { commandRegistry } from "../../extensions/registries"; +import { CatalogEntity } from "../../common/catalog-entity"; + +export { CatalogEntity, CatalogEntityData, CatalogEntityActionContext, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../common/catalog-entity"; + +export const catalogEntityRunContext = { + navigate: (url: string) => navigate(url), + setCommandPaletteContext: (entity?: CatalogEntity) => { + commandRegistry.activeEntity = entity; + } +}; diff --git a/src/renderer/api/endpoints/events.api.ts b/src/renderer/api/endpoints/events.api.ts index df9aa540a4..4fb21aaaa5 100644 --- a/src/renderer/api/endpoints/events.api.ts +++ b/src/renderer/api/endpoints/events.api.ts @@ -28,7 +28,7 @@ export class KubeEvent extends KubeObject { firstTimestamp: string; lastTimestamp: string; count: number; - type: string; + type: "Normal" | "Warning" | string; eventTime: null; reportingComponent: string; reportingInstance: string; diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index a1fd497798..093adf9aef 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -3,11 +3,8 @@ import { apiBase } from "../index"; import { stringify } from "querystring"; import { autobind } from "../../utils"; -interface IHelmChartList { - [repo: string]: { - [name: string]: HelmChart; - }; -} +export type RepoHelmChartList = Record; +export type HelmChartList = Record; export interface IHelmChartDetails { readme: string; @@ -22,12 +19,12 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { export const helmChartsApi = { list() { return apiBase - .get(endpoint()) + .get(endpoint()) .then(data => { return Object .values(data) .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), []) - .map(HelmChart.create); + .map(([chart]) => HelmChart.create(chart)); }); }, @@ -86,7 +83,7 @@ export class HelmChart { tillerVersion?: string; getId() { - return this.digest; + return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`; } getName() { diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 84e095721b..9a5a6dadd0 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -57,6 +57,7 @@ export interface IReleaseRevision { updated: string; status: string; chart: string; + app_version: string; description: string; } @@ -186,7 +187,7 @@ export class HelmRelease implements ItemObject { } getVersion() { - const versions = this.chart.match(/(v?\d+)[^-].*$/); + const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/); if (versions) { return versions[0]; @@ -197,10 +198,9 @@ export class HelmRelease implements ItemObject { } getUpdated(humanize = true, compact = true) { - const now = new Date().getTime(); const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date() const updatedDate = new Date(updated).getTime(); - const diff = now - updatedDate; + const diff = Date.now() - updatedDate; if (humanize) { return formatDuration(diff, compact); diff --git a/src/renderer/api/endpoints/index.ts b/src/renderer/api/endpoints/index.ts index f1202b9122..5ab54e7c3a 100644 --- a/src/renderer/api/endpoints/index.ts +++ b/src/renderer/api/endpoints/index.ts @@ -14,6 +14,7 @@ export * from "./events.api"; export * from "./hpa.api"; export * from "./ingress.api"; export * from "./job.api"; +export * from "./limit-range.api"; export * from "./namespaces.api"; export * from "./network-policy.api"; export * from "./nodes.api"; diff --git a/src/renderer/api/endpoints/limit-range.api.ts b/src/renderer/api/endpoints/limit-range.api.ts new file mode 100644 index 0000000000..bbb3941c87 --- /dev/null +++ b/src/renderer/api/endpoints/limit-range.api.ts @@ -0,0 +1,57 @@ +import { KubeObject } from "../kube-object"; +import { KubeApi } from "../kube-api"; +import { autobind } from "../../utils"; + +export enum LimitType { + CONTAINER = "Container", + POD = "Pod", + PVC = "PersistentVolumeClaim", +} + +export enum Resource { + MEMORY = "memory", + CPU = "cpu", + STORAGE = "storage", + EPHEMERAL_STORAGE = "ephemeral-storage", +} + +export enum LimitPart { + MAX = "max", + MIN = "min", + DEFAULT = "default", + DEFAULT_REQUEST = "defaultRequest", + MAX_LIMIT_REQUEST_RATIO = "maxLimitRequestRatio", +} + +type LimitRangeParts = Partial>>; + +export interface LimitRangeItem extends LimitRangeParts { + type: string +} + +@autobind() +export class LimitRange extends KubeObject { + static kind = "LimitRange"; + static namespaced = true; + static apiBase = "/api/v1/limitranges"; + + spec: { + limits: LimitRangeItem[]; + }; + + getContainerLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); + } + + getPodLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.POD); + } + + getPVCLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.PVC); + } +} + +export const limitRangeApi = new KubeApi({ + objectConstructor: LimitRange, +}); diff --git a/src/renderer/api/endpoints/persistent-volume.api.ts b/src/renderer/api/endpoints/persistent-volume.api.ts index dd5dbb616e..db286db062 100644 --- a/src/renderer/api/endpoints/persistent-volume.api.ts +++ b/src/renderer/api/endpoints/persistent-volume.api.ts @@ -63,10 +63,16 @@ export class PersistentVolume extends KubeObject { return this.status.phase || "-"; } - getClaimRefName() { - const { claimRef } = this.spec; + getStorageClass(): string { + return this.spec.storageClassName; + } - return claimRef ? claimRef.name : ""; + getClaimRefName(): string { + return this.spec.claimRef?.name ?? ""; + } + + getStorageClassName() { + return this.spec.storageClassName || ""; } } diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 9c80301bb0..ea2f45023f 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -104,6 +104,9 @@ export interface IPodContainer { configMapRef?: { name: string; }; + secretRef?: { + name: string; + } }[]; volumeMounts?: { name: string; @@ -267,60 +270,55 @@ export class Pod extends WorkloadKubeObject { } getAllContainers() { - return this.getContainers().concat(this.getInitContainers()); + return [...this.getContainers(), ...this.getInitContainers()]; } getRunningContainers() { - const statuses = this.getContainerStatuses(); - - return this.getAllContainers().filter(container => { - return statuses.find(status => status.name === container.name && !!status.state["running"]); - } + const runningContainerNames = new Set( + this.getContainerStatuses() + .filter(({ state }) => state.running) + .map(({ name }) => name) ); + + return this.getAllContainers() + .filter(({ name }) => runningContainerNames.has(name)); } getContainerStatuses(includeInitContainers = true) { - const statuses: IPodContainerStatus[] = []; - const { containerStatuses, initContainerStatuses } = this.status; + const { containerStatuses = [], initContainerStatuses = [] } = this.status ?? {}; - if (containerStatuses) { - statuses.push(...containerStatuses); + if (includeInitContainers) { + return [...containerStatuses, ...initContainerStatuses]; } - if (includeInitContainers && initContainerStatuses) { - statuses.push(...initContainerStatuses); - } - - return statuses; + return [...containerStatuses]; } getRestartsCount(): number { - const { containerStatuses } = this.status; + const { containerStatuses = [] } = this.status ?? {}; - if (!containerStatuses) return 0; - - return containerStatuses.reduce((count, item) => count + item.restartCount, 0); + return containerStatuses.reduce((totalCount, { restartCount }) => totalCount + restartCount, 0); } getQosClass() { - return this.status.qosClass || ""; + return this.status?.qosClass || ""; } getReason() { - return this.status.reason || ""; + return this.status?.reason || ""; } getPriorityClassName() { return this.spec.priorityClassName || ""; } - // Returns one of 5 statuses: Running, Succeeded, Pending, Failed, Evicted - getStatus() { + getStatus(): PodStatus { const phase = this.getStatusPhase(); const reason = this.getReason(); - const goodConditions = ["Initialized", "Ready"].every(condition => - !!this.getConditions().find(item => item.type === condition && item.status === "True") - ); + const trueConditionTypes = new Set(this.getConditions() + .filter(({ status }) => status === "True") + .map(({ type }) => type)); + const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition)); if (reason === PodStatus.EVICTED) { return PodStatus.EVICTED; @@ -334,7 +332,7 @@ export class Pod extends WorkloadKubeObject { return PodStatus.SUCCEEDED; } - if (phase === PodStatus.RUNNING && goodConditions) { + if (phase === PodStatus.RUNNING && isInGoodCondition) { return PodStatus.RUNNING; } @@ -346,37 +344,27 @@ export class Pod extends WorkloadKubeObject { if (this.getReason() === PodStatus.EVICTED) return "Evicted"; if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) return "Terminating"; - let message = ""; const statuses = this.getContainerStatuses(false); // not including initContainers - if (statuses.length) { - statuses.forEach(status => { - const { state } = status; + for (const { state } of statuses.reverse()) { + if (state.waiting) { + return state.waiting.reason || "Waiting"; + } - if (state.waiting) { - const { reason } = state.waiting; - - message = reason ? reason : "Waiting"; - } - - if (state.terminated) { - const { reason } = state.terminated; - - message = reason ? reason : "Terminated"; - } - }); + if (state.terminated) { + return state.terminated.reason || "Terminated"; + } } - if (message) return message; return this.getStatusPhase(); } getStatusPhase() { - return this.status.phase; + return this.status?.phase; } getConditions() { - return this.status.conditions || []; + return this.status?.conditions || []; } getVolumes() { @@ -390,9 +378,7 @@ export class Pod extends WorkloadKubeObject { } getNodeSelectors(): string[] { - const { nodeSelector } = this.spec; - - if (!nodeSelector) return []; + const { nodeSelector = {} } = this.spec; return Object.entries(nodeSelector).map(values => values.join(": ")); } @@ -406,20 +392,19 @@ export class Pod extends WorkloadKubeObject { } hasIssues() { - const notReady = !!this.getConditions().find(condition => { - return condition.type == "Ready" && condition.status !== "True"; - }); - const crashLoop = !!this.getContainerStatuses().find(condition => { - const waiting = condition.state.waiting; + for (const { type, status } of this.getConditions()) { + if (type === "Ready" && status !== "True") { + return true; + } + } - return (waiting && waiting.reason == "CrashLoopBackOff"); - }); + for (const { state } of this.getContainerStatuses()) { + if (state?.waiting?.reason === "CrashLookBackOff") { + return true; + } + } - return ( - notReady || - crashLoop || - this.getStatusPhase() !== "Running" - ); + return this.getStatusPhase() !== "Running"; } getLivenessProbe(container: IPodContainer) { @@ -473,14 +458,11 @@ export class Pod extends WorkloadKubeObject { } getNodeName() { - return this.spec?.nodeName; + return this.spec.nodeName; } - getSelectedNodeOs() { - if (!this.spec.nodeSelector) return; - if (!this.spec.nodeSelector["kubernetes.io/os"] && !this.spec.nodeSelector["beta.kubernetes.io/os"]) return; - - return this.spec.nodeSelector["kubernetes.io/os"] || this.spec.nodeSelector["beta.kubernetes.io/os"]; + getSelectedNodeOs(): string | undefined { + return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"]; } } diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index 1a82ce45f8..7c2b55d0e1 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -1,11 +1,11 @@ import { JsonApi, JsonApiErrorParsed } from "./json-api"; import { KubeJsonApi } from "./kube-json-api"; import { Notifications } from "../components/notifications"; -import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars"; +import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars"; export const apiBase = new JsonApi({ apiBase: apiPrefix, - debug: isDevelopment, + debug: isDevelopment || isDebugging, }); export const apiKube = new KubeJsonApi({ apiBase: apiKubePrefix, diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts index 49c2cb1a28..df12b08ab7 100644 --- a/src/renderer/api/json-api.ts +++ b/src/renderer/api/json-api.ts @@ -3,7 +3,7 @@ import { stringify } from "querystring"; import { EventEmitter } from "../../common/event-emitter"; import { cancelableFetch } from "../utils/cancelableFetch"; - +import { randomBytes } from "crypto"; export interface JsonApiData { } @@ -55,6 +55,34 @@ export class JsonApi { return this.request(path, params, { ...reqInit, method: "get" }); } + getResponse(path: string, params?: P, init: RequestInit = {}): Promise { + const reqPath = `${this.config.apiBase}${path}`; + const subdomain = randomBytes(2).toString("hex"); + let reqUrl = `http://${subdomain}.${window.location.host}${reqPath}`; // hack around browser connection limits (chromium allows 6 per domain) + const reqInit: RequestInit = { ...init }; + const { query } = params || {} as P; + + if (!reqInit.method) { + reqInit.method = "get"; + } + + if (query) { + const queryString = stringify(query); + + reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; + } + + const infoLog: JsonApiLog = { + method: reqInit.method.toUpperCase(), + reqUrl: reqPath, + reqInit, + }; + + this.writeLog({ ...infoLog }); + + return fetch(reqUrl, reqInit); + } + post(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "post" }); } diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 8a3a2517c2..98833e3d4d 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -8,8 +8,10 @@ import { apiManager } from "./api-manager"; import { apiKube } from "./index"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } from "./kube-json-api"; -import { IKubeObjectConstructor, KubeObject } from "./kube-object"; -import { kubeWatchApi } from "./kube-watch-api"; +import { IKubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object"; +import byline from "byline"; +import { IKubeWatchEvent } from "./kube-watch-api"; +import { ReadableWebToNodeStream } from "../utils/readableStream"; export interface IKubeApiOptions { /** @@ -60,7 +62,9 @@ export interface IKubeResourceList { } export interface IKubeApiCluster { - id: string; + metadata: { + uid: string; + } } export function forCluster(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor): KubeApi { @@ -69,7 +73,7 @@ export function forCluster(cluster: IKubeApiCluster, kubeC debug: isDevelopment, }, { headers: { - "X-Cluster-ID": cluster.id + "X-Cluster-ID": cluster.metadata.uid } }); @@ -91,15 +95,15 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { } } +export type KubeApiWatchCallback = (data: IKubeWatchEvent, error: any) => void; + +export type KubeApiWatchOptions = { + namespace: string; + callback?: KubeApiWatchCallback; + abortController?: AbortController +}; + export class KubeApi { - static parseApi = parseKubeApi; - - static watchAll(...apis: KubeApi[]) { - const disposers = apis.map(api => api.watch()); - - return () => disposers.forEach(unwatch => unwatch()); - } - readonly kind: string; readonly apiBase: string; readonly apiPrefix: string; @@ -112,6 +116,7 @@ export class KubeApi { public objectConstructor: IKubeObjectConstructor; protected request: KubeJsonApi; protected resourceVersions = new Map(); + protected watchDisposer: () => void; constructor(protected options: IKubeApiOptions) { const { @@ -124,7 +129,7 @@ export class KubeApi { if (!options.apiBase) { options.apiBase = objectConstructor.apiBase; } - const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase); + const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase); this.kind = kind; this.isNamespaced = isNamespaced; @@ -157,7 +162,7 @@ export class KubeApi { for (const apiUrl of apiBases) { // Split e.g. "/apis/extensions/v1beta1/ingresses" to parts - const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl); + const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl); // Request available resources try { @@ -269,6 +274,7 @@ export class KubeApi { } protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any { + if (!data) return; const KubeObjectConstructor = this.objectConstructor; if (KubeObject.isJsonApiData(data)) { @@ -365,8 +371,98 @@ export class KubeApi { }); } - watch(): () => void { - return kubeWatchApi.subscribe(this); + watch(opts: KubeApiWatchOptions = { namespace: "" }): () => void { + if (!opts.abortController) { + opts.abortController = new AbortController(); + } + let errorReceived = false; + let timedRetry: NodeJS.Timeout; + const { abortController, namespace, callback } = opts; + + abortController.signal.addEventListener("abort", () => { + clearTimeout(timedRetry); + }); + + const watchUrl = this.getWatchUrl(namespace); + const responsePromise = this.request.getResponse(watchUrl, null, { + signal: abortController.signal + }); + + responsePromise.then((response) => { + if (!response.ok && !abortController.signal.aborted) { + callback?.(null, response); + + return; + } + const nodeStream = new ReadableWebToNodeStream(response.body); + + ["end", "close", "error"].forEach((eventName) => { + nodeStream.on(eventName, () => { + if (errorReceived) return; // kubernetes errors should be handled in a callback + + clearTimeout(timedRetry); + timedRetry = setTimeout(() => { // we did not get any kubernetes errors so let's retry + if (abortController.signal.aborted) return; + + this.watch({...opts, namespace, callback}); + }, 1000); + }); + }); + + const stream = byline(nodeStream); + + stream.on("data", (line) => { + try { + const event: IKubeWatchEvent = JSON.parse(line); + + if (event.type === "ERROR" && event.object.kind === "Status") { + errorReceived = true; + callback(null, new KubeStatus(event.object as any)); + + return; + } + + this.modifyWatchEvent(event); + + if (callback) { + callback(event, null); + } + } catch (ignore) { + // ignore parse errors + } + }); + }, (error) => { + if (error instanceof DOMException) return; // AbortController rejects, we can ignore it + + callback?.(null, error); + }).catch((error) => { + callback?.(null, error); + }); + + const disposer = () => { + abortController.abort(); + }; + + return disposer; + } + + protected modifyWatchEvent(event: IKubeWatchEvent) { + + switch (event.type) { + case "ADDED": + case "DELETED": + + case "MODIFIED": { + ensureObjectSelfLink(this, event.object); + + const { namespace, resourceVersion } = event.object.metadata; + + this.setResourceVersion(namespace, resourceVersion); + this.setResourceVersion("", resourceVersion); + + break; + } + } } } diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts index 3026a9a956..362ee5438e 100644 --- a/src/renderer/api/kube-json-api.ts +++ b/src/renderer/api/kube-json-api.ts @@ -21,7 +21,7 @@ export interface KubeJsonApiData extends JsonApiData { resourceVersion: string; continue?: string; finalizers?: string[]; - selfLink: string; + selfLink?: string; labels?: { [label: string]: string; }; diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index 08bd6401b9..7d0c34de33 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -40,6 +40,29 @@ export interface IKubeObjectMetadata { }[]; } +export interface IKubeStatus { + kind: string; + apiVersion: string; + code: number; + message?: string; + reason?: string; +} + +export class KubeStatus { + public readonly kind = "Status"; + public readonly apiVersion: string; + public readonly code: number; + public readonly message: string; + public readonly reason: string; + + constructor(data: IKubeStatus) { + this.apiVersion = data.apiVersion; + this.code = data.code; + this.message = data.message || ""; + this.reason = data.reason || ""; + } +} + export type IKubeMetaField = keyof IKubeObjectMetadata; @autobind() @@ -99,12 +122,15 @@ export class KubeObject implements ItemObject { return this.metadata.namespace || undefined; } - // todo: refactor with named arguments - getAge(humanize = true, compact = true, fromNow = false) { + getTimeDiffFromNow(): number { + return Date.now() - new Date(this.metadata.creationTimestamp).getTime(); + } + + getAge(humanize = true, compact = true, fromNow = false): string | number { if (fromNow) { - return moment(this.metadata.creationTimestamp).fromNow(); + return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used } - const diff = new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime(); + const diff = this.getTimeDiffFromNow(); if (humanize) { return formatDuration(diff, compact); diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 78ca25256e..f2f50193db 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -1,183 +1,131 @@ -// Kubernetes watch-api consumer +// Kubernetes watch-api client +// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams -import { computed, observable, reaction } from "mobx"; -import { stringify } from "querystring"; -import { autobind, EventEmitter } from "../utils"; -import { KubeJsonApiData } from "./kube-json-api"; import type { KubeObjectStore } from "../kube-object.store"; -import { ensureObjectSelfLink, KubeApi } from "./kube-api"; -import { apiManager } from "./api-manager"; -import { apiPrefix, isDevelopment } from "../../common/vars"; -import { getHostedCluster } from "../../common/cluster-store"; +import type { ClusterContext } from "../components/context"; -export interface IKubeWatchEvent { - type: "ADDED" | "MODIFIED" | "DELETED"; +import plimit from "p-limit"; +import { comparer, IReactionDisposer, observable, reaction, when } from "mobx"; +import { autobind, noop } from "../utils"; +import { KubeApi } from "./kube-api"; +import { KubeJsonApiData } from "./kube-json-api"; +import { isDebugging, isProduction } from "../../common/vars"; + +export interface IKubeWatchEvent { + type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; object?: T; } -export interface IKubeWatchRouteEvent { - type: "STREAM_END"; - url: string; - status: number; +export interface IKubeWatchSubscribeStoreOptions { + namespaces?: string[]; // default: all accessible namespaces + preload?: boolean; // preload store items, default: true + waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true + loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false } -export interface IKubeWatchRouteQuery { - api: string | string[]; +export interface IKubeWatchLog { + message: string | string[] | Error; + meta?: object; + cssStyle?: string; } @autobind() export class KubeWatchApi { - protected evtSource: EventSource; - protected onData = new EventEmitter<[IKubeWatchEvent]>(); - protected subscribers = observable.map(); - protected reconnectTimeoutMs = 5000; - protected maxReconnectsOnError = 10; - protected reconnectAttempts = this.maxReconnectsOnError; + @observable context: ClusterContext = null; - constructor() { - reaction(() => this.activeApis, () => this.connect(), { - fireImmediately: true, - delay: 500, - }); + contextReady = when(() => Boolean(this.context)); + + isAllowedApi(api: KubeApi): boolean { + return Boolean(this.context?.cluster.isAllowedResource(api.kind)); } - @computed get activeApis() { - return Array.from(this.subscribers.keys()); - } + preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) { + const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages + const preloading: Promise[] = []; - getSubscribersCount(api: KubeApi) { - return this.subscribers.get(api) || 0; - } + for (const store of stores) { + preloading.push(limitRequests(async () => { + if (store.isLoaded && opts.loadOnce) return; // skip - subscribe(...apis: KubeApi[]) { - apis.forEach(api => { - this.subscribers.set(api, this.getSubscribersCount(api) + 1); - }); - - return () => apis.forEach(api => { - const count = this.getSubscribersCount(api) - 1; - - if (count <= 0) this.subscribers.delete(api); - else this.subscribers.set(api, count); - }); - } - - protected getQuery(): Partial { - const { isAdmin, allowedNamespaces } = getHostedCluster(); + return store.loadAll({ namespaces: opts.namespaces }); + })); + } return { - api: this.activeApis.map(api => { - if (isAdmin) return api.getWatchUrl(); - - return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)); - }).flat() + loading: Promise.allSettled(preloading), + cancelLoading: () => limitRequests.clearQueue(), }; } - // todo: maybe switch to websocket to avoid often reconnects - @autobind() - protected connect() { - if (this.evtSource) this.disconnect(); // close previous connection + subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void { + const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts; + const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? []; + const unsubscribeList: Function[] = []; + let isUnsubscribed = false; - if (!this.activeApis.length) { + const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce }); + let preloading = preload && load(); + let cancelReloading: IReactionDisposer = noop; + + const subscribe = () => { + if (isUnsubscribed) return; + + stores.forEach((store) => { + unsubscribeList.push(store.subscribe()); + }); + }; + + if (preloading) { + if (waitUntilLoaded) { + preloading.loading.then(subscribe, error => { + this.log({ + message: new Error("Loading stores has failed"), + meta: { stores, error, options: opts }, + }); + }); + } else { + subscribe(); + } + + // reload stores only for context namespaces change + cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => { + preloading?.cancelLoading(); + unsubscribeList.forEach(unsubscribe => unsubscribe()); + unsubscribeList.length = 0; + preloading = load(namespaces); + preloading.loading.then(subscribe); + }, { + equals: comparer.shallow, + }); + } + + // unsubscribe + return () => { + if (isUnsubscribed) return; + isUnsubscribed = true; + cancelReloading(); + preloading?.cancelLoading(); + unsubscribeList.forEach(unsubscribe => unsubscribe()); + unsubscribeList.length = 0; + }; + } + + protected log({ message, cssStyle = "", meta = {} }: IKubeWatchLog) { + if (isProduction && !isDebugging) { return; } - const query = this.getQuery(); - const apiUrl = `${apiPrefix}/watch?${stringify(query)}`; - this.evtSource = new EventSource(apiUrl); - this.evtSource.onmessage = this.onMessage; - this.evtSource.onerror = this.onError; - this.writeLog("CONNECTING", query.api); - } - - reconnect() { - if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) { - this.reconnectAttempts = this.maxReconnectsOnError; - this.connect(); - } - } - - protected disconnect() { - if (!this.evtSource) return; - this.evtSource.close(); - this.evtSource.onmessage = null; - this.evtSource = null; - } - - protected onMessage(evt: MessageEvent) { - if (!evt.data) return; - const data = JSON.parse(evt.data); - - if ((data as IKubeWatchEvent).object) { - this.onData.emit(data); - } else { - this.onRouteEvent(data); - } - } - - protected async onRouteEvent(event: IKubeWatchRouteEvent) { - if (event.type === "STREAM_END") { - this.disconnect(); - const { apiBase, namespace } = KubeApi.parseApi(event.url); - const api = apiManager.getApi(apiBase); - - if (api) { - try { - await api.refreshResourceVersion({ namespace }); - this.reconnect(); - } catch (error) { - console.error("failed to refresh resource version", error); - - if (this.subscribers.size > 0) { - setTimeout(() => { - this.onRouteEvent(event); - }, 1000); - } - } - } - } - } - - protected onError(evt: MessageEvent) { - const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this; - - if (evt.eventPhase === EventSource.CLOSED) { - if (attemptsRemain > 0) { - this.reconnectAttempts--; - setTimeout(() => this.connect(), reconnectTimeoutMs); - } - } - } - - protected writeLog(...data: any[]) { - if (isDevelopment) { - console.log("%cKUBE-WATCH-API:", `font-weight: bold`, ...data); - } - } - - addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { - const listener = (evt: IKubeWatchEvent) => { - const { namespace, resourceVersion } = evt.object.metadata; - const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion); - - api.setResourceVersion(namespace, resourceVersion); - api.setResourceVersion("", resourceVersion); - - ensureObjectSelfLink(api, evt.object); - - if (store == apiManager.getStore(api)) { - callback(evt); - } + const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String); + const logMeta = { + time: new Date().toLocaleString(), + ...meta, }; - this.onData.addListener(listener); - - return () => this.onData.removeListener(listener); - } - - reset() { - this.subscribers.clear(); + if (message instanceof Error) { + console.error(...logInfo, logMeta); + } else { + console.info(...logInfo, logMeta); + } } } diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index e0c6d3f121..185d3d502c 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -1,7 +1,7 @@ import get from "lodash/get"; import { KubeObject } from "./kube-object"; -interface IToleration { +export interface IToleration { key?: string; operator?: string; effect?: string; @@ -82,4 +82,4 @@ export class WorkloadKubeObject extends KubeObject { return Object.keys(affinity).length; } -} \ No newline at end of file +} diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 4d46011442..c9ef607871 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -8,17 +8,29 @@ import * as ReactRouterDom from "react-router-dom"; import { render, unmountComponentAtNode } from "react-dom"; import { clusterStore } from "../common/cluster-store"; import { userStore } from "../common/user-store"; -import { isMac } from "../common/vars"; -import { workspaceStore } from "../common/workspace-store"; +import { delay } from "../common/utils"; +import { isMac, isDevelopment } from "../common/vars"; import * as LensExtensions from "../extensions/extension-api"; import { extensionDiscovery } from "../extensions/extension-discovery"; import { extensionLoader } from "../extensions/extension-loader"; import { extensionsStore } from "../extensions/extensions-store"; +import { hotbarStore } from "../common/hotbar-store"; import { filesystemProvisionerStore } from "../main/extension-filesystem"; import { App } from "./components/app"; import { LensApp } from "./lens-app"; import { themeStore } from "./theme.store"; +/** + * If this is a development buid, wait a second to attach + * Chrome Debugger to renderer process + * https://stackoverflow.com/questions/52844870/debugging-electron-renderer-process-with-vscode + */ +async function attachChromeDebugger() { + if (isDevelopment) { + await delay(1000); + } +} + type AppComponent = React.ComponentType & { init?(): Promise; }; @@ -35,6 +47,7 @@ export { export async function bootstrap(App: AppComponent) { const rootElem = document.getElementById("app"); + await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); extensionLoader.init(); @@ -43,7 +56,7 @@ export async function bootstrap(App: AppComponent) { // preload common stores await Promise.all([ userStore.load(), - workspaceStore.load(), + hotbarStore.load(), clusterStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), @@ -52,7 +65,6 @@ export async function bootstrap(App: AppComponent) { // Register additional store listeners clusterStore.registerIpcListener(); - workspaceStore.registerIpcListener(); // init app's dependencies if any if (App.init) { @@ -61,7 +73,6 @@ export async function bootstrap(App: AppComponent) { window.addEventListener("message", (ev: MessageEvent) => { if (ev.data === "teardown") { userStore.unregisterIpcListener(); - workspaceStore.unregisterIpcListener(); clusterStore.unregisterIpcListener(); unmountComponentAtNode(rootElem); window.location.href = "about:blank"; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index ae4a3e6ace..b9392e2b95 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -12,11 +12,9 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; -import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid"; import { navigate } from "../../navigation"; import { userStore } from "../../../common/user-store"; -import { clusterViewURL } from "../cluster-manager/cluster-view.route"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Tab, Tabs } from "../tabs"; @@ -24,6 +22,7 @@ import { ExecValidationNotFoundError } from "../../../common/custom-errors"; import { appEventBus } from "../../../common/event-bus"; import { PageLayout } from "../layout/page-layout"; import { docsUrl } from "../../../common/vars"; +import { catalogURL } from "../+catalog"; enum KubeConfigSourceTab { FILE = "file", @@ -147,7 +146,7 @@ export class AddCluster extends React.Component { try { const kubeConfig = this.kubeContexts.get(context); - validateKubeConfig(kubeConfig); + validateKubeConfig(kubeConfig, context); return true; } catch (err) { @@ -171,7 +170,6 @@ export class AddCluster extends React.Component { return { id: clusterId, kubeConfigPath, - workspace: workspaceStore.currentWorkspaceId, contextName: kubeConfig.currentContext, preferences: { clusterName: kubeConfig.currentContext, @@ -183,18 +181,11 @@ export class AddCluster extends React.Component { runInAction(() => { clusterStore.addClusters(...newClusters); - if (newClusters.length === 1) { - const clusterId = newClusters[0].id; + Notifications.ok( + <>Successfully imported {newClusters.length} cluster(s) + ); - clusterStore.setActive(clusterId); - navigate(clusterViewURL({ params: { clusterId } })); - } else { - if (newClusters.length > 1) { - Notifications.ok( - <>Successfully imported {newClusters.length} cluster(s) - ); - } - } + navigate(catalogURL()); }); this.refreshContexts(); } catch (err) { @@ -352,7 +343,7 @@ export class AddCluster extends React.Component { return ( - Add Clusters}> +

Add Clusters

} showOnTop={true}>

Add Clusters from Kubeconfig

{this.renderInfo()} {this.renderKubeConfigSource()} diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index a33e9d2503..d31efc5438 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -82,9 +82,9 @@ export class HelmChartDetails extends Component {
{selectedChart.getDescription()} -
- + +
diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx index 1aed2c9d24..3414b74d6a 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx +++ b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx @@ -10,7 +10,7 @@ import { resourceQuotaStore } from "./resource-quotas.store"; import { IResourceQuotaRouteParams } from "./resource-quotas.route"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age" @@ -25,11 +25,13 @@ export class ResourceQuotas extends React.Component { return ( <> item.getName(), - [sortBy.namespace]: (item: ResourceQuota) => item.getNs(), - [sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp, + [columnId.name]: (item: ResourceQuota) => item.getName(), + [columnId.namespace]: (item: ResourceQuota) => item.getNs(), + [columnId.age]: (item: ResourceQuota) => item.getTimeDiffFromNow(), }} searchFilters={[ (item: ResourceQuota) => item.getSearchFields(), @@ -37,10 +39,10 @@ export class ResourceQuotas extends React.Component { ]} renderHeaderTitle="Resource Quotas" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(resourceQuota: ResourceQuota) => [ resourceQuota.getName(), diff --git a/src/renderer/components/+config-secrets/add-secret-dialog.tsx b/src/renderer/components/+config-secrets/add-secret-dialog.tsx index 5071c908bf..2fae17d30d 100644 --- a/src/renderer/components/+config-secrets/add-secret-dialog.tsx +++ b/src/renderer/components/+config-secrets/add-secret-dialog.tsx @@ -133,7 +133,7 @@ export class AddSecretDialog extends React.Component { this.addField(field)} /> @@ -146,7 +146,7 @@ export class AddSecretDialog extends React.Component {
{ multiLine maxRows={5} required={required} className="value" - placeholder={`Value`} + placeholder="Value" value={value} onChange={v => item.value = v} /> { this.name = v} /> diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index 92a58141ac..ab7bc59e46 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -69,7 +69,7 @@ export class SecretDetails extends React.Component { {!isEmpty(this.data) && ( <> - + { Object.entries(this.data).map(([name, value]) => { const revealSecret = this.revealSecret[name]; @@ -107,7 +107,7 @@ export class SecretDetails extends React.Component { } diff --git a/src/renderer/components/+preferences/helm-charts.scss b/src/renderer/components/+preferences/helm-charts.scss new file mode 100644 index 0000000000..574fec28d9 --- /dev/null +++ b/src/renderer/components/+preferences/helm-charts.scss @@ -0,0 +1,21 @@ +.HelmCharts { + .repos { + margin-top: 20px; + + .repo { + background: var(--inputControlBackground); + border-radius: 4px; + padding: 12px 16px; + box-shadow: 0 0 0 1px var(--secondaryBackground); + + .repoName { + font-weight: 500; + margin-bottom: 8px; + } + + .repoUrl { + color: var(--textColorDimmed); + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+preferences/helm-charts.tsx b/src/renderer/components/+preferences/helm-charts.tsx new file mode 100644 index 0000000000..1b0d80bf26 --- /dev/null +++ b/src/renderer/components/+preferences/helm-charts.tsx @@ -0,0 +1,136 @@ +import "./helm-charts.scss"; + +import React from "react"; +import { action, computed, observable } from "mobx"; + +import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; +import { Button } from "../button"; +import { Icon } from "../icon"; +import { Notifications } from "../notifications"; +import { Select, SelectOption } from "../select"; +import { AddHelmRepoDialog } from "./add-helm-repo-dialog"; +import { observer } from "mobx-react"; + +@observer +export class HelmCharts extends React.Component { + @observable loading = false; + @observable repos: HelmRepo[] = []; + @observable addedRepos = observable.map(); + + @computed get options(): SelectOption[] { + return this.repos.map(repo => ({ + label: repo.name, + value: repo, + })); + } + + async componentDidMount() { + await this.loadRepos(); + } + + @action + async loadRepos() { + this.loading = true; + + try { + if (!this.repos.length) { + this.repos = await repoManager.loadAvailableRepos(); // via https://helm.sh + } + const repos = await repoManager.repositories(); // via helm-cli + + this.addedRepos.clear(); + repos.forEach(repo => this.addedRepos.set(repo.name, repo)); + } catch (err) { + Notifications.error(String(err)); + } + + this.loading = false; + } + + async addRepo(repo: HelmRepo) { + try { + await repoManager.addRepo(repo); + this.addedRepos.set(repo.name, repo); + } catch (err) { + Notifications.error(<>Adding helm branch {repo.name} has failed: {String(err)}); + } + } + + async removeRepo(repo: HelmRepo) { + try { + await repoManager.removeRepo(repo); + this.addedRepos.delete(repo.name); + } catch (err) { + Notifications.error( + <>Removing helm branch {repo.name} has failed: {String(err)} + ); + } + } + + onRepoSelect = async ({ value: repo }: SelectOption) => { + const isAdded = this.addedRepos.has(repo.name); + + if (isAdded) { + Notifications.ok(<>Helm branch {repo.name} already in use); + + return; + } + this.loading = true; + await this.addRepo(repo); + this.loading = false; + }; + + formatOptionLabel = ({ value: repo }: SelectOption) => { + const isAdded = this.addedRepos.has(repo.name); + + return ( +
+ {repo.name} + {isAdded && } +
+ ); + }; + + render() { + return ( +
+
+ preferences.downloadMirror = value} - disabled={!preferences.downloadKubectlBinaries} - /> - - - - The directory to download binaries into. - - - - - The path to the kubectl binary on the system. - +
+ + preferences.downloadKubectlBinaries = v.target.checked} + name="kubectl-download" + /> + } + label="Download kubectl binaries matching the Kubernetes cluster version" + /> +
+ +
+ +
+ + +
+ The directory to download binaries into. +
+
+ +
+ +
+ + +
); }); diff --git a/src/renderer/components/+preferences/preferences.route.ts b/src/renderer/components/+preferences/preferences.route.ts index 7c880bd5fc..2915b68a28 100644 --- a/src/renderer/components/+preferences/preferences.route.ts +++ b/src/renderer/components/+preferences/preferences.route.ts @@ -1,8 +1,17 @@ import type { RouteProps } from "react-router"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; import { buildURL } from "../../../common/utils/buildUrl"; +import { navigate } from "../../navigation"; export const preferencesRoute: RouteProps = { path: "/preferences" }; export const preferencesURL = buildURL(preferencesRoute.path); + +commandRegistry.add({ + id: "app.showPreferences", + title: "Preferences: Open", + scope: "global", + action: () => navigate(preferencesURL()) +}); diff --git a/src/renderer/components/+preferences/preferences.scss b/src/renderer/components/+preferences/preferences.scss index cb0d866e15..5c02312432 100644 --- a/src/renderer/components/+preferences/preferences.scss +++ b/src/renderer/components/+preferences/preferences.scss @@ -1,27 +1,2 @@ .Preferences { - $spacing: $padding * 2; - - .repos { - position: relative; - - .Badge { - display: flex; - margin-bottom: 1px; - padding: $padding $spacing; - } - } - - .extensions { - h2 { - margin: $spacing 0; - } - - &:empty { - display: none; - } - } - - .Checkbox { - align-self: start; // limit clickable area to checkbox + text - } } \ No newline at end of file diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 56e881d9f5..3eb98cc4d7 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -1,30 +1,37 @@ import "./preferences.scss"; import React from "react"; -import { observer } from "mobx-react"; -import { action, computed, observable } from "mobx"; -import { Icon } from "../icon"; -import { Select, SelectOption } from "../select"; +import { computed, observable, reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; + import { userStore } from "../../../common/user-store"; -import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; -import { Input } from "../input"; -import { Checkbox } from "../checkbox"; -import { Notifications } from "../notifications"; -import { Badge } from "../badge"; -import { Button } from "../button"; +import { isWindows } from "../../../common/vars"; +import { appPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry"; import { themeStore } from "../../theme.store"; -import { Tooltip } from "../tooltip"; -import { KubectlBinaries } from "./kubectl-binaries"; -import { appPreferenceRegistry } from "../../../extensions/registries/app-preference-registry"; +import { Input } from "../input"; import { PageLayout } from "../layout/page-layout"; -import { AddHelmRepoDialog } from "./add-helm-repo-dialog"; +import { SubTitle } from "../layout/sub-title"; +import { Select, SelectOption } from "../select"; +import { HelmCharts } from "./helm-charts"; +import { KubectlBinaries } from "./kubectl-binaries"; +import { navigation } from "../../navigation"; +import { Tab, Tabs } from "../tabs"; +import { FormSwitch, Switcher } from "../switch"; + +enum Pages { + Application = "application", + Proxy = "proxy", + Kubernetes = "kubernetes", + Telemetry = "telemetry", + Extensions = "extensions", + Other = "other" +} @observer export class Preferences extends React.Component { - @observable helmLoading = false; - @observable helmRepos: HelmRepo[] = []; - @observable helmAddedRepos = observable.map(); @observable httpProxy = userStore.preferences.httpsProxy || ""; + @observable shell = userStore.preferences.shell || ""; + @observable activeTab = Pages.Application; @computed get themeOptions(): SelectOption[] { return themeStore.themes.map(theme => ({ @@ -33,177 +40,188 @@ export class Preferences extends React.Component { })); } - @computed get helmOptions(): SelectOption[] { - return this.helmRepos.map(repo => ({ - label: repo.name, - value: repo, - })); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => navigation.location.hash, hash => { + const fragment = hash.slice(1); // hash is /^(#\w.)?$/ + + if (fragment) { + // ignore empty framents + document.getElementById(fragment)?.scrollIntoView(); + } + }, { + fireImmediately: true + }) + ]); } - async componentDidMount() { - await this.loadHelmRepos(); - } - - @action - async loadHelmRepos() { - this.helmLoading = true; - - try { - if (!this.helmRepos.length) { - this.helmRepos = await repoManager.loadAvailableRepos(); // via https://helm.sh - } - const repos = await repoManager.repositories(); // via helm-cli - - this.helmAddedRepos.clear(); - repos.forEach(repo => this.helmAddedRepos.set(repo.name, repo)); - } catch (err) { - Notifications.error(String(err)); - } - this.helmLoading = false; - } - - async addRepo(repo: HelmRepo) { - try { - await repoManager.addRepo(repo); - this.helmAddedRepos.set(repo.name, repo); - } catch (err) { - Notifications.error(<>Adding helm branch {repo.name} has failed: {String(err)}); - } - } - - async removeRepo(repo: HelmRepo) { - try { - await repoManager.removeRepo(repo); - this.helmAddedRepos.delete(repo.name); - } catch (err) { - Notifications.error( - <>Removing helm branch {repo.name} has failed: {String(err)} - ); - } - } - - onRepoSelect = async ({ value: repo }: SelectOption) => { - const isAdded = this.helmAddedRepos.has(repo.name); - - if (isAdded) { - Notifications.ok(<>Helm branch {repo.name} already in use); - - return; - } - this.helmLoading = true; - await this.addRepo(repo); - this.helmLoading = false; + onTabChange = (tabId: Pages) => { + this.activeTab = tabId; }; - formatHelmOptionLabel = ({ value: repo }: SelectOption) => { - const isAdded = this.helmAddedRepos.has(repo.name); + renderNavigation() { + const extensions = appPreferenceRegistry.getItems().filter(e => !e.showInPreferencesTab); return ( -
- {repo.name} - {isAdded && } -
+ +
Preferences
+ + + + + {extensions.length > 0 && + + } +
); - }; + } + + renderExtension({ title, id, components: { Hint, Input } }: RegisteredAppPreference) { + return ( + +
+ + +
+ +
+
+
+
+ ); + } render() { const { preferences } = userStore; - const header =

Preferences

; + const extensions = appPreferenceRegistry.getItems(); + const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry); + let defaultShell = process.env.SHELL || process.env.PTYSHELL; + + if (!defaultShell) { + if (isWindows) { + defaultShell = "powershell.exe"; + } else { + defaultShell = "System default shell"; + } + } return ( - -

Color Theme

- preferences.colorTheme = value} + themeName="lens" + /> + -

HTTP Proxy

- this.httpProxy = v} - onBlur={() => preferences.httpsProxy = this.httpProxy} - /> - - Proxy is used only for non-cluster communication. - +
- +
+ + this.shell = v} + onBlur={() => preferences.shell = this.shell} + /> +
-

Helm

-
- this.httpProxy = v} + onBlur={() => preferences.httpsProxy = this.httpProxy} + /> + + Proxy is used only for non-cluster communication. + + -

Auto start-up

- preferences.openAtLogin = v} - /> +
-

Certificate Trust

- preferences.allowUntrustedCAs = v} - /> - - This will make Lens to trust ANY certificate authority without any validations.{" "} - Needed with some corporate proxies that do certificate re-writing.{" "} - Does not affect cluster communications! - +
+ + preferences.allowUntrustedCAs = v.target.checked} + name="startup" + /> + } + label="Allow untrusted Certificate Authorities" + /> + + This will make Lens to trust ANY certificate authority without any validations.{" "} + Needed with some corporate proxies that do certificate re-writing.{" "} + Does not affect cluster communications! + +
+ + )} -
- {appPreferenceRegistry.getItems().map(({ title, components: { Hint, Input } }, index) => { - return ( -
-

{title}

- - - - -
- ); - })} -
+ {this.activeTab == Pages.Kubernetes && ( +
+
+

Kubernetes

+ +
+
+
+

Helm Charts

+ +
+
+ )} + + {this.activeTab == Pages.Telemetry && ( +
+

Telemetry

+ {telemetryExtensions.map(this.renderExtension)} +
+ )} + + {this.activeTab == Pages.Extensions && ( +
+

Extensions

+ {extensions.filter(e => !e.showInPreferencesTab).map(this.renderExtension)} +
+ )} ); } diff --git a/src/renderer/components/+storage-classes/storage-class-details.tsx b/src/renderer/components/+storage-classes/storage-class-details.tsx index 95f09bc6bf..1d47df8ecb 100644 --- a/src/renderer/components/+storage-classes/storage-class-details.tsx +++ b/src/renderer/components/+storage-classes/storage-class-details.tsx @@ -10,14 +10,22 @@ import { KubeObjectDetailsProps } from "../kube-object"; import { StorageClass } from "../../api/endpoints"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { storageClassStore } from "./storage-class.store"; +import { VolumeDetailsList } from "../+storage-volumes/volume-details-list"; +import { volumesStore } from "../+storage-volumes/volumes.store"; interface Props extends KubeObjectDetailsProps { } @observer export class StorageClassDetails extends React.Component { + async componentDidMount() { + volumesStore.reloadAll(); + } + render() { const { object: storageClass } = this.props; + const persistentVolumes = storageClassStore.getPersistentVolumes(storageClass); if (!storageClass) return null; const { provisioner, parameters, mountOptions } = storageClass; @@ -45,7 +53,7 @@ export class StorageClassDetails extends React.Component { )} {parameters && ( <> - + { Object.entries(parameters).map(([name, value]) => ( @@ -55,6 +63,7 @@ export class StorageClassDetails extends React.Component { } )} +
); } diff --git a/src/renderer/components/+storage-classes/storage-class.store.ts b/src/renderer/components/+storage-classes/storage-class.store.ts index 9e388456a5..0050c432b5 100644 --- a/src/renderer/components/+storage-classes/storage-class.store.ts +++ b/src/renderer/components/+storage-classes/storage-class.store.ts @@ -2,10 +2,15 @@ import { KubeObjectStore } from "../../kube-object.store"; import { autobind } from "../../utils"; import { StorageClass, storageClassApi } from "../../api/endpoints/storage-class.api"; import { apiManager } from "../../api/api-manager"; +import { volumesStore } from "../+storage-volumes/volumes.store"; @autobind() export class StorageClassStore extends KubeObjectStore { api = storageClassApi; + + getPersistentVolumes(storageClass: StorageClass) { + return volumesStore.getByStorageClass(storageClass); + } } export const storageClassStore = new StorageClassStore(); diff --git a/src/renderer/components/+storage-classes/storage-classes.tsx b/src/renderer/components/+storage-classes/storage-classes.tsx index ec7e1c8e05..f4f0432afd 100644 --- a/src/renderer/components/+storage-classes/storage-classes.tsx +++ b/src/renderer/components/+storage-classes/storage-classes.tsx @@ -9,10 +9,11 @@ import { IStorageClassesRouteParams } from "./storage-classes.route"; import { storageClassStore } from "./storage-class.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", age = "age", provisioner = "provision", + default = "default", reclaimPolicy = "reclaim", } @@ -24,13 +25,15 @@ export class StorageClasses extends React.Component { render() { return ( item.getName(), - [sortBy.age]: (item: StorageClass) => item.metadata.creationTimestamp, - [sortBy.provisioner]: (item: StorageClass) => item.provisioner, - [sortBy.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy, + [columnId.name]: (item: StorageClass) => item.getName(), + [columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(), + [columnId.provisioner]: (item: StorageClass) => item.provisioner, + [columnId.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy, }} searchFilters={[ (item: StorageClass) => item.getSearchFields(), @@ -38,12 +41,12 @@ export class StorageClasses extends React.Component { ]} renderHeaderTitle="Storage Classes" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Provisioner", className: "provisioner", sortBy: sortBy.provisioner }, - { title: "Reclaim Policy", className: "reclaim-policy", sortBy: sortBy.reclaimPolicy }, - { title: "Default", className: "is-default" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Provisioner", className: "provisioner", sortBy: columnId.provisioner, id: columnId.provisioner }, + { title: "Reclaim Policy", className: "reclaim-policy", sortBy: columnId.reclaimPolicy, id: columnId.reclaimPolicy }, + { title: "Default", className: "is-default", id: columnId.default }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(storageClass: StorageClass) => [ storageClass.getName(), diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index ea1ec71778..2aebba52b3 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -14,6 +14,8 @@ import { VolumeClaimDiskChart } from "./volume-claim-disk-chart"; import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-object"; import { PersistentVolumeClaim } from "../../api/endpoints"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -41,15 +43,18 @@ export class PersistentVolumeClaimDetails extends React.Component { const metricTabs = [ "Disk" ]; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim); return (
- volumeClaimStore.loadMetrics(volumeClaim)} - tabs={metricTabs} object={volumeClaim} params={{ metrics }} - > - - + {!isMetricHidden && ( + volumeClaimStore.loadMetrics(volumeClaim)} + tabs={metricTabs} object={volumeClaim} params={{ metrics }} + > + + + )} {accessModes.join(", ")} @@ -71,7 +76,7 @@ export class PersistentVolumeClaimDetails extends React.Component { {volumeClaim.getStatus()} - + {volumeClaim.getMatchLabels().map(label => )} diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.tsx b/src/renderer/components/+storage-volume-claims/volume-claims.tsx index bb9a4a05a7..8a69f6ee60 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claims.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claims.tsx @@ -13,7 +13,7 @@ import { stopPropagation } from "../../utils"; import { storageClassApi } from "../../api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", pods = "pods", @@ -31,17 +31,19 @@ export class PersistentVolumeClaims extends React.Component { render() { return ( pvc.getName(), - [sortBy.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(), - [sortBy.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()), - [sortBy.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(), - [sortBy.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()), - [sortBy.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName, - [sortBy.age]: (pvc: PersistentVolumeClaim) => pvc.metadata.creationTimestamp, + [columnId.name]: (pvc: PersistentVolumeClaim) => pvc.getName(), + [columnId.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(), + [columnId.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()), + [columnId.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(), + [columnId.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()), + [columnId.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName, + [columnId.age]: (pvc: PersistentVolumeClaim) => pvc.getTimeDiffFromNow(), }} searchFilters={[ (item: PersistentVolumeClaim) => item.getSearchFields(), @@ -49,14 +51,14 @@ export class PersistentVolumeClaims extends React.Component { ]} renderHeaderTitle="Persistent Volume Claims" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Storage class", className: "storageClass", sortBy: sortBy.storageClass }, - { title: "Size", className: "size", sortBy: sortBy.size }, - { title: "Pods", className: "pods", sortBy: sortBy.pods }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Storage class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Size", className: "size", sortBy: columnId.size, id: columnId.size }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(pvc: PersistentVolumeClaim) => { const pods = pvc.getPods(podsStore.items); diff --git a/src/renderer/components/+storage-volumes/volume-details-list.scss b/src/renderer/components/+storage-volumes/volume-details-list.scss new file mode 100644 index 0000000000..aed61b4b5f --- /dev/null +++ b/src/renderer/components/+storage-volumes/volume-details-list.scss @@ -0,0 +1,31 @@ +@import "../+storage/storage-mixins"; + +.VolumeDetailsList { + position: relative; + + .Table { + margin: 0 (-$margin * 3); + + &.virtual { + height: 500px; // applicable for 100+ items + } + } + + .TableCell { + &:first-child { + margin-left: $margin; + } + + &:last-child { + margin-right: $margin; + } + + &.name { + flex-grow: 2; + } + + &.status { + @include pv-status-colors; + } + } +} diff --git a/src/renderer/components/+storage-volumes/volume-details-list.tsx b/src/renderer/components/+storage-volumes/volume-details-list.tsx new file mode 100644 index 0000000000..a9fb091050 --- /dev/null +++ b/src/renderer/components/+storage-volumes/volume-details-list.tsx @@ -0,0 +1,89 @@ +import "./volume-details-list.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { PersistentVolume } from "../../api/endpoints/persistent-volume.api"; +import { autobind } from "../../../common/utils/autobind"; +import { TableRow } from "../table/table-row"; +import { cssNames, prevDefault } from "../../utils"; +import { showDetails } from "../kube-object/kube-object-details"; +import { TableCell } from "../table/table-cell"; +import { Spinner } from "../spinner/spinner"; +import { DrawerTitle } from "../drawer/drawer-title"; +import { Table } from "../table/table"; +import { TableHead } from "../table/table-head"; +import { volumesStore } from "./volumes.store"; +import kebabCase from "lodash/kebabCase"; + +interface Props { + persistentVolumes: PersistentVolume[]; +} + +enum sortBy { + name = "name", + status = "status", + capacity = "capacity", +} + +@observer +export class VolumeDetailsList extends React.Component { + private sortingCallbacks = { + [sortBy.name]: (volume: PersistentVolume) => volume.getName(), + [sortBy.capacity]: (volume: PersistentVolume) => volume.getCapacity(), + [sortBy.status]: (volume: PersistentVolume) => volume.getStatus(), + }; + + @autobind() + getTableRow(uid: string) { + const { persistentVolumes } = this.props; + const volume = persistentVolumes.find(volume => volume.getId() === uid); + + return ( + showDetails(volume.selfLink, false))} + > + {volume.getName()} + {volume.getCapacity()} + {volume.getStatus()} + + ); + } + + render() { + const { persistentVolumes } = this.props; + const virtual = persistentVolumes.length > 100; + + if (!persistentVolumes.length) { + return !volumesStore.isLoaded && ; + } + + return ( +
+ + + + Name + Capacity + Status + + { + !virtual && persistentVolumes.map(volume => this.getTableRow(volume.getId())) + } +
+
+ ); + } +} diff --git a/src/renderer/components/+storage-volumes/volumes.scss b/src/renderer/components/+storage-volumes/volumes.scss index 272aa3fd89..9aa1e616b1 100644 --- a/src/renderer/components/+storage-volumes/volumes.scss +++ b/src/renderer/components/+storage-volumes/volumes.scss @@ -26,7 +26,7 @@ flex-grow: 3; } - .status { + &.status { @include pv-status-colors; } diff --git a/src/renderer/components/+storage-volumes/volumes.store.ts b/src/renderer/components/+storage-volumes/volumes.store.ts index ea61525735..b2601f9b12 100644 --- a/src/renderer/components/+storage-volumes/volumes.store.ts +++ b/src/renderer/components/+storage-volumes/volumes.store.ts @@ -2,10 +2,17 @@ import { KubeObjectStore } from "../../kube-object.store"; import { autobind } from "../../utils"; import { PersistentVolume, persistentVolumeApi } from "../../api/endpoints/persistent-volume.api"; import { apiManager } from "../../api/api-manager"; +import { StorageClass } from "../../api/endpoints/storage-class.api"; @autobind() export class PersistentVolumesStore extends KubeObjectStore { api = persistentVolumeApi; + + getByStorageClass(storageClass: StorageClass): PersistentVolume[] { + return this.items.filter(volume => + volume.getStorageClassName() === storageClass.getName() + ); + } } export const volumesStore = new PersistentVolumesStore(); diff --git a/src/renderer/components/+storage-volumes/volumes.tsx b/src/renderer/components/+storage-volumes/volumes.tsx index 412093a7ad..55a9eb753e 100644 --- a/src/renderer/components/+storage-volumes/volumes.tsx +++ b/src/renderer/components/+storage-volumes/volumes.tsx @@ -11,10 +11,11 @@ import { volumesStore } from "./volumes.store"; import { pvcApi, storageClassApi } from "../../api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", storageClass = "storage-class", capacity = "capacity", + claim = "claim", status = "status", age = "age", } @@ -27,14 +28,16 @@ export class PersistentVolumes extends React.Component { render() { return ( item.getName(), - [sortBy.storageClass]: (item: PersistentVolume) => item.spec.storageClassName, - [sortBy.capacity]: (item: PersistentVolume) => item.getCapacity(true), - [sortBy.status]: (item: PersistentVolume) => item.getStatus(), - [sortBy.age]: (item: PersistentVolume) => item.metadata.creationTimestamp, + [columnId.name]: (item: PersistentVolume) => item.getName(), + [columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(), + [columnId.capacity]: (item: PersistentVolume) => item.getCapacity(true), + [columnId.status]: (item: PersistentVolume) => item.getStatus(), + [columnId.age]: (item: PersistentVolume) => item.getTimeDiffFromNow(), }} searchFilters={[ (item: PersistentVolume) => item.getSearchFields(), @@ -42,13 +45,13 @@ export class PersistentVolumes extends React.Component { ]} renderHeaderTitle="Persistent Volumes" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Storage Class", className: "storageClass", sortBy: sortBy.storageClass }, - { title: "Capacity", className: "capacity", sortBy: sortBy.capacity }, - { title: "Claim", className: "claim" }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Storage Class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Capacity", className: "capacity", sortBy: columnId.capacity, id: columnId.capacity }, + { title: "Claim", className: "claim", id: columnId.claim }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(volume: PersistentVolume) => { const { claimRef, storageClassName } = volume.spec; diff --git a/src/renderer/components/+storage/storage.route.ts b/src/renderer/components/+storage/storage.route.ts index 6ad17a4fdf..174eaea080 100644 --- a/src/renderer/components/+storage/storage.route.ts +++ b/src/renderer/components/+storage/storage.route.ts @@ -1,12 +1,15 @@ import { RouteProps } from "react-router"; -import { volumeClaimsURL } from "../+storage-volume-claims"; -import { Storage } from "./storage"; +import { storageClassesRoute } from "../+storage-classes"; +import { volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims"; +import { volumesRoute } from "../+storage-volumes"; import { IURLParams } from "../../../common/utils/buildUrl"; export const storageRoute: RouteProps = { - get path() { - return Storage.tabRoutes.map(({ routePath }) => routePath).flat(); - } + path: [ + volumeClaimsRoute, + volumesRoute, + storageClassesRoute + ].map(route => route.path.toString()) }; export const storageURL = (params?: IURLParams) => volumeClaimsURL(params); diff --git a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx index ab06126bd2..e7a676365c 100644 --- a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx +++ b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx @@ -7,7 +7,7 @@ import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; import { Select, SelectOption } from "../select"; import { SubTitle } from "../layout/sub-title"; -import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } from "../../api/endpoints"; +import { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints"; import { Icon } from "../icon"; import { Input } from "../input"; import { NamespaceSelect } from "../+namespaces/namespace-select"; @@ -19,6 +19,7 @@ import { namespaceStore } from "../+namespaces/namespace.store"; import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store"; import { roleBindingsStore } from "./role-bindings.store"; import { showDetails } from "../kube-object"; +import { KubeObjectStore } from "../../kube-object.store"; interface BindingSelectOption extends SelectOption { value: string; // binding name @@ -73,14 +74,14 @@ export class AddRoleBindingDialog extends React.Component { }; async loadData() { - const stores = [ + const stores: KubeObjectStore[] = [ namespaceStore, rolesStore, serviceAccountsStore, ]; this.isLoading = true; - await Promise.all(stores.map(store => store.loadAll())); + await Promise.all(stores.map(store => store.reloadAll())); this.isLoading = false; } @@ -136,8 +137,7 @@ export class AddRoleBindingDialog extends React.Component { roleBinding: this.roleBinding, addSubjects: subjects, }); - } - else { + } else { const name = useRoleForBindingName ? selectedRole.getName() : bindingName; roleBinding = await roleBindingsStore.create({ name, namespace }, { @@ -205,7 +205,7 @@ export class AddRoleBindingDialog extends React.Component { this.bindingName = v} @@ -239,7 +239,7 @@ export class AddRoleBindingDialog extends React.Component { this.roleName = v} /> + + this.namespace = value} + /> diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index 6af33deacb..82b0e66612 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -1,14 +1,14 @@ import { clusterRoleApi, Role, roleApi } from "../../api/endpoints"; import { autobind } from "../../utils"; -import { KubeObjectStore } from "../../kube-object.store"; +import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { apiManager } from "../../api/api-manager"; @autobind() export class RolesStore extends KubeObjectStore { api = clusterRoleApi; - subscribe() { - return super.subscribe([roleApi, clusterRoleApi]); + getSubscribeApis() { + return [roleApi, clusterRoleApi]; } protected sortItems(items: Role[]) { @@ -24,15 +24,13 @@ export class RolesStore extends KubeObjectStore { return clusterRoleApi.get(params); } - protected loadItems(namespaces?: string[]): Promise { - if (namespaces) { - return Promise.all( - namespaces.map(namespace => roleApi.list({ namespace })) - ).then(items => items.flat()); - } else { - return Promise.all([clusterRoleApi.list(), roleApi.list()]) - .then(items => items.flat()); - } + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { + const items = await Promise.all([ + super.loadItems({ ...params, api: clusterRoleApi }), + super.loadItems({ ...params, api: roleApi }), + ]); + + return items.flat(); } protected async createItem(params: { name: string; namespace?: string }, data?: Partial) { @@ -49,4 +47,4 @@ export const rolesStore = new RolesStore(); apiManager.registerStore(rolesStore, [ roleApi, clusterRoleApi, -]); \ No newline at end of file +]); diff --git a/src/renderer/components/+user-management-roles/roles.tsx b/src/renderer/components/+user-management-roles/roles.tsx index 21ad3bdf8a..8d916c9d42 100644 --- a/src/renderer/components/+user-management-roles/roles.tsx +++ b/src/renderer/components/+user-management-roles/roles.tsx @@ -10,7 +10,7 @@ import { KubeObjectListLayout } from "../kube-object"; import { AddRoleDialog } from "./add-role-dialog"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -25,22 +25,24 @@ export class Roles extends React.Component { return ( <> role.getName(), - [sortBy.namespace]: (role: Role) => role.getNs(), - [sortBy.age]: (role: Role) => role.metadata.creationTimestamp, + [columnId.name]: (role: Role) => role.getName(), + [columnId.namespace]: (role: Role) => role.getNs(), + [columnId.age]: (role: Role) => role.getTimeDiffFromNow(), }} searchFilters={[ (role: Role) => role.getSearchFields(), ]} renderHeaderTitle="Roles" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(role: Role) => [ role.getName(), diff --git a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx index c56888f6f4..e9a27979cf 100644 --- a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx +++ b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx @@ -66,7 +66,7 @@ export class CreateServiceAccountDialog extends React.Component { this.name = v.toLowerCase()} /> diff --git a/src/renderer/components/+user-management-service-accounts/index.ts b/src/renderer/components/+user-management-service-accounts/index.ts index fd45e28288..bd81292bf1 100644 --- a/src/renderer/components/+user-management-service-accounts/index.ts +++ b/src/renderer/components/+user-management-service-accounts/index.ts @@ -1,3 +1,3 @@ export * from "./service-accounts"; export * from "./service-accounts-details"; -export * from "./create-service-account-dialog"; \ No newline at end of file +export * from "./create-service-account-dialog"; diff --git a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx index 37bed40ba9..34b3fc8d0a 100644 --- a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx +++ b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx @@ -15,7 +15,7 @@ import { CreateServiceAccountDialog } from "./create-service-account-dialog"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -30,21 +30,23 @@ export class ServiceAccounts extends React.Component { return ( <> account.getName(), - [sortBy.namespace]: (account: ServiceAccount) => account.getNs(), - [sortBy.age]: (account: ServiceAccount) => account.metadata.creationTimestamp, + [columnId.name]: (account: ServiceAccount) => account.getName(), + [columnId.namespace]: (account: ServiceAccount) => account.getNs(), + [columnId.age]: (account: ServiceAccount) => account.getTimeDiffFromNow(), }} searchFilters={[ (account: ServiceAccount) => account.getSearchFields(), ]} renderHeaderTitle="Service Accounts" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(account: ServiceAccount) => [ account.getName(), diff --git a/src/renderer/components/+user-management/index.ts b/src/renderer/components/+user-management/index.ts index 6f29869b9b..4ff825df97 100644 --- a/src/renderer/components/+user-management/index.ts +++ b/src/renderer/components/+user-management/index.ts @@ -1,2 +1,2 @@ export * from "./user-management"; -export * from "./user-management.route"; \ No newline at end of file +export * from "./user-management.route"; diff --git a/src/renderer/components/+user-management/user-management.route.ts b/src/renderer/components/+user-management/user-management.route.ts index 9dc17cbbd8..3acebb7899 100644 --- a/src/renderer/components/+user-management/user-management.route.ts +++ b/src/renderer/components/+user-management/user-management.route.ts @@ -1,12 +1,5 @@ import type { RouteProps } from "react-router"; import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; -import { UserManagement } from "./user-management"; - -export const usersManagementRoute: RouteProps = { - get path() { - return UserManagement.tabRoutes.map(({ routePath }) => routePath).flat(); - } -}; // Routes export const serviceAccountsRoute: RouteProps = { @@ -18,6 +11,18 @@ export const rolesRoute: RouteProps = { export const roleBindingsRoute: RouteProps = { path: "/role-bindings" }; +export const podSecurityPoliciesRoute: RouteProps = { + path: "/pod-security-policies" +}; + +export const usersManagementRoute: RouteProps = { + path: [ + serviceAccountsRoute, + roleBindingsRoute, + rolesRoute, + podSecurityPoliciesRoute + ].map(route => route.path.toString()) +}; // Route params export interface IServiceAccountsRouteParams { @@ -34,3 +39,4 @@ export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(pa export const serviceAccountsURL = buildURL(serviceAccountsRoute.path); export const roleBindingsURL = buildURL(roleBindingsRoute.path); export const rolesURL = buildURL(rolesRoute.path); +export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path); diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/user-management.tsx index e851d50424..480808d6a1 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/user-management.tsx @@ -5,9 +5,9 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { Roles } from "../+user-management-roles"; import { RoleBindings } from "../+user-management-roles-bindings"; import { ServiceAccounts } from "../+user-management-service-accounts"; -import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; +import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; import { namespaceUrlParam } from "../+namespaces/namespace.store"; -import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies"; +import { PodSecurityPolicies } from "../+pod-security-policies"; import { isAllowedResource } from "../../../common/rbac"; @observer diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx index 6fd04ffe7e..1b473e90c1 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx @@ -20,9 +20,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class CronJobDetails extends React.Component { async componentDidMount() { - if (!jobStore.isLoaded) { - jobStore.loadAll(); - } + jobStore.reloadAll(); } render() { diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 19d35e4b3a..44c87cfba3 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -18,12 +18,13 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { ConfirmDialog } from "../confirm-dialog/confirm-dialog"; import { Notifications } from "../notifications/notifications"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + schedule = "schedule", suspend = "suspend", active = "active", - lastSchedule = "schedule", + lastSchedule = "last-schedule", age = "age", } @@ -35,15 +36,17 @@ export class CronJobs extends React.Component { render() { return ( cronJob.getName(), - [sortBy.namespace]: (cronJob: CronJob) => cronJob.getNs(), - [sortBy.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(), - [sortBy.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob), - [sortBy.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(), - [sortBy.age]: (cronJob: CronJob) => cronJob.metadata.creationTimestamp, + [columnId.name]: (cronJob: CronJob) => cronJob.getName(), + [columnId.namespace]: (cronJob: CronJob) => cronJob.getNs(), + [columnId.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(), + [columnId.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob), + [columnId.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(), + [columnId.age]: (cronJob: CronJob) => cronJob.getTimeDiffFromNow(), }} searchFilters={[ (cronJob: CronJob) => cronJob.getSearchFields(), @@ -51,14 +54,14 @@ export class CronJobs extends React.Component { ]} renderHeaderTitle="Cron Jobs" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Schedule", className: "schedule" }, - { title: "Suspend", className: "suspend", sortBy: sortBy.suspend }, - { title: "Active", className: "active", sortBy: sortBy.active }, - { title: "Last schedule", className: "last-schedule", sortBy: sortBy.lastSchedule }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Schedule", className: "schedule", id: columnId.schedule }, + { title: "Suspend", className: "suspend", sortBy: columnId.suspend, id: columnId.suspend }, + { title: "Active", className: "active", sortBy: columnId.active, id: columnId.active }, + { title: "Last schedule", className: "last-schedule", sortBy: columnId.lastSchedule, id: columnId.lastSchedule }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(cronJob: CronJob) => [ cronJob.getName(), @@ -84,7 +87,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { return ( <> CronJobTriggerDialog.open(object)}> - + Trigger @@ -103,7 +106,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { Resume CronJob {object.getName()}?

), })}> - + Resume @@ -121,7 +124,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { Suspend CronJob {object.getName()}?

), })}> - + Suspend } diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 44ccbaac56..9e613c3eff 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -18,6 +18,8 @@ import { reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -30,9 +32,7 @@ export class DaemonSetDetails extends React.Component { }); componentDidMount() { - if (!podsStore.isLoaded) { - podsStore.loadAll(); - } + podsStore.reloadAll(); } componentWillUnmount() { @@ -49,10 +49,11 @@ export class DaemonSetDetails extends React.Component { const nodeSelector = daemonSet.getNodeSelectors(); const childPods = daemonSetStore.getChildPods(daemonSet); const metrics = daemonSetStore.metrics; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet); return (
- {podsStore.isLoaded && ( + {!isMetricHidden && podsStore.isLoaded && ( daemonSetStore.loadMetrics(daemonSet)} tabs={podMetricTabs} object={daemonSet} params={{ metrics }} diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts index e5327fcd49..b6b669488d 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts @@ -16,7 +16,7 @@ export class DaemonSetStore extends KubeObjectStore { } getChildPods(daemonSet: DaemonSet): Pod[] { - return podsStore.getPodsByOwner(daemonSet); + return podsStore.getPodsByOwnerId(daemonSet.getId()); } getStatuses(daemonSets?: DaemonSet[]) { diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index ff061f7877..866d561036 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -13,10 +13,11 @@ import { IDaemonSetsRouteParams } from "../+workloads"; import { Badge } from "../badge"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", pods = "pods", + labels = "labels", age = "age", } @@ -38,13 +39,15 @@ export class DaemonSets extends React.Component { render() { return ( daemonSet.getName(), - [sortBy.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(), - [sortBy.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet), - [sortBy.age]: (daemonSet: DaemonSet) => daemonSet.metadata.creationTimestamp, + [columnId.name]: (daemonSet: DaemonSet) => daemonSet.getName(), + [columnId.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(), + [columnId.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet), + [columnId.age]: (daemonSet: DaemonSet) => daemonSet.getTimeDiffFromNow(), }} searchFilters={[ (daemonSet: DaemonSet) => daemonSet.getSearchFields(), @@ -52,12 +55,12 @@ export class DaemonSets extends React.Component { ]} renderHeaderTitle="Daemon Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods", sortBy: sortBy.pods }, - { className: "warning" }, - { title: "Node Selector", className: "labels" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { className: "warning", showWithColumn: columnId.pods }, + { title: "Node Selector", className: "labels", id: columnId.labels }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(daemonSet: DaemonSet) => [ daemonSet.getName(), diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 26cd4c0b23..7cd2350a17 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -19,6 +19,8 @@ import { reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -31,9 +33,7 @@ export class DeploymentDetails extends React.Component { }); componentDidMount() { - if (!podsStore.isLoaded) { - podsStore.loadAll(); - } + podsStore.reloadAll(); } componentWillUnmount() { @@ -49,10 +49,11 @@ export class DeploymentDetails extends React.Component { const selectors = deployment.getSelectors(); const childPods = deploymentStore.getChildPods(deployment); const metrics = deploymentStore.metrics; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment); return (
- {podsStore.isLoaded && ( + {!isMetricHidden && podsStore.isLoaded && ( deploymentStore.loadMetrics(deployment)} tabs={podMetricTabs} object={deployment} params={{ metrics }} diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx index e3d18669f9..da3ac55e07 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx @@ -4,9 +4,9 @@ import "@testing-library/jest-dom/extend-expect"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; jest.mock("../../api/endpoints"); -import { deploymentApi } from "../../api/endpoints"; +import { Deployment, deploymentApi } from "../../api/endpoints"; -const dummyDeployment = { +const dummyDeployment: Deployment = { apiVersion: "v1", kind: "dummy", metadata: { @@ -83,6 +83,7 @@ const dummyDeployment = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index b84cd7b340..a2510327ee 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -23,9 +23,10 @@ import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-obje import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Notifications } from "../notifications"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + pods = "pods", replicas = "replicas", age = "age", condition = "condition", @@ -55,14 +56,16 @@ export class Deployments extends React.Component { render() { return ( deployment.getName(), - [sortBy.namespace]: (deployment: Deployment) => deployment.getNs(), - [sortBy.replicas]: (deployment: Deployment) => deployment.getReplicas(), - [sortBy.age]: (deployment: Deployment) => deployment.metadata.creationTimestamp, - [sortBy.condition]: (deployment: Deployment) => deployment.getConditionsText(), + [columnId.name]: (deployment: Deployment) => deployment.getName(), + [columnId.namespace]: (deployment: Deployment) => deployment.getNs(), + [columnId.replicas]: (deployment: Deployment) => deployment.getReplicas(), + [columnId.age]: (deployment: Deployment) => deployment.getTimeDiffFromNow(), + [columnId.condition]: (deployment: Deployment) => deployment.getConditionsText(), }} searchFilters={[ (deployment: Deployment) => deployment.getSearchFields(), @@ -70,13 +73,13 @@ export class Deployments extends React.Component { ]} renderHeaderTitle="Deployments" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods" }, - { title: "Replicas", className: "replicas", sortBy: sortBy.replicas }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Conditions", className: "conditions", sortBy: sortBy.condition }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.condition, id: columnId.condition }, ]} renderTableContents={(deployment: Deployment) => [ deployment.getName(), @@ -101,7 +104,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) { return ( <> DeploymentScaleDialog.open(object)}> - + Scale ConfirmDialog.open({ @@ -123,7 +126,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {

), })}> - + Restart
diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx index 0f7da2ca22..8c5354b3e4 100644 --- a/src/renderer/components/+workloads-jobs/job-details.tsx +++ b/src/renderer/components/+workloads-jobs/job-details.tsx @@ -27,9 +27,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class JobDetails extends React.Component { async componentDidMount() { - if (!podsStore.isLoaded) { - podsStore.loadAll(); - } + podsStore.reloadAll(); } render() { diff --git a/src/renderer/components/+workloads-jobs/job.store.ts b/src/renderer/components/+workloads-jobs/job.store.ts index cf2eaf8d21..08d40a2a38 100644 --- a/src/renderer/components/+workloads-jobs/job.store.ts +++ b/src/renderer/components/+workloads-jobs/job.store.ts @@ -17,7 +17,7 @@ export class JobStore extends KubeObjectStore { } getChildPods(job: Job): Pod[] { - return podsStore.getPodsByOwner(job); + return podsStore.getPodsByOwnerId(job.getId()); } getJobsByOwner(cronJob: CronJob) { diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx index 00c1ee0db5..5961c17376 100644 --- a/src/renderer/components/+workloads-jobs/jobs.tsx +++ b/src/renderer/components/+workloads-jobs/jobs.tsx @@ -12,9 +12,10 @@ import { IJobsRouteParams } from "../+workloads"; import kebabCase from "lodash/kebabCase"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + completions = "completions", conditions = "conditions", age = "age", } @@ -27,25 +28,27 @@ export class Jobs extends React.Component { render() { return ( job.getName(), - [sortBy.namespace]: (job: Job) => job.getNs(), - [sortBy.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "", - [sortBy.age]: (job: Job) => job.metadata.creationTimestamp, + [columnId.name]: (job: Job) => job.getName(), + [columnId.namespace]: (job: Job) => job.getNs(), + [columnId.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "", + [columnId.age]: (job: Job) => job.getTimeDiffFromNow(), }} searchFilters={[ (job: Job) => job.getSearchFields(), ]} renderHeaderTitle="Jobs" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Completions", className: "completions" }, - { className: "warning" }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Conditions", className: "conditions", sortBy: sortBy.conditions }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Completions", className: "completions", id: columnId.completions }, + { className: "warning", showWithColumn: columnId.completions }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions }, ]} renderTableContents={(job: Job) => { const condition = job.getCondition(); diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 78adecb6df..bc0484dadc 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -6,10 +6,9 @@ import { OverviewWorkloadStatus } from "./overview-workload-status"; import { Link } from "react-router-dom"; import { workloadURL, workloadStores } from "../+workloads"; import { namespaceStore } from "../+namespaces/namespace.store"; -import { PageFiltersList } from "../item-object-list/page-filters-list"; -import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; +import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { isAllowedResource, KubeResource } from "../../../common/rbac"; -import { ResourceNames } from "../../../renderer/utils/rbac"; +import { ResourceNames } from "../../utils/rbac"; import { autobind } from "../../utils"; const resources: KubeResource[] = [ @@ -27,7 +26,7 @@ export class OverviewStatuses extends React.Component { @autobind() renderWorkload(resource: KubeResource): React.ReactElement { const store = workloadStores[resource]; - const items = store.getAllByNs(namespaceStore.contextNs); + const items = store.getAllByNs(namespaceStore.contextNamespaces); return (
@@ -50,7 +49,6 @@ export class OverviewStatuses extends React.Component {
Overview
-
{workloads}
diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 318ad53f77..92bc569307 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -1,8 +1,7 @@ import "./overview.scss"; import React from "react"; -import { observable, when } from "mobx"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { OverviewStatuses } from "./overview-statuses"; import { RouteComponentProps } from "react-router"; import { IWorkloadsOverviewRouteParams } from "../+workloads"; @@ -15,83 +14,32 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Events } from "../+events"; -import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; +import { kubeWatchApi } from "../../api/kube-watch-api"; +import { clusterContext } from "../context"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { - @observable isUnmounting = false; - - async componentDidMount() { - const stores: KubeObjectStore[] = []; - - if (isAllowedResource("pods")) { - stores.push(podsStore); - } - - if (isAllowedResource("deployments")) { - stores.push(deploymentStore); - } - - if (isAllowedResource("daemonsets")) { - stores.push(daemonSetStore); - } - - if (isAllowedResource("statefulsets")) { - stores.push(statefulSetStore); - } - - if (isAllowedResource("replicasets")) { - stores.push(replicaSetStore); - } - - if (isAllowedResource("jobs")) { - stores.push(jobStore); - } - - if (isAllowedResource("cronjobs")) { - stores.push(cronJobStore); - } - - if (isAllowedResource("events")) { - stores.push(eventStore); - } - - const unsubscribeList: Array<() => void> = []; - - for (const store of stores) { - await store.loadAll(); - unsubscribeList.push(store.subscribe()); - } - - await when(() => this.isUnmounting); - unsubscribeList.forEach(dispose => dispose()); - } - - componentWillUnmount() { - this.isUnmounting = true; - } - - get contents() { - return ( - <> - - { isAllowedResource("events") && } - - ); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([ + podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore, + jobStore, cronJobStore, eventStore, + ], { + preload: true, + namespaces: clusterContext.contextNamespaces, + }), + ]); } render() { return (
- {this.contents} + + {isAllowedResource("events") && }
); } diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx new file mode 100644 index 0000000000..8071254940 --- /dev/null +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { fireEvent, render } from "@testing-library/react"; +import { IToleration } from "../../../api/workload-kube-object"; +import { PodTolerations } from "../pod-tolerations"; + +const tolerations: IToleration[] =[ + { + key: "CriticalAddonsOnly", + operator: "Exist", + effect: "NoExecute", + tolerationSeconds: 3600 + }, + { + key: "node.kubernetes.io/not-ready", + operator: "NoExist", + effect: "NoSchedule", + tolerationSeconds: 7200 + }, +]; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("shows all tolerations", () => { + const { container } = render(); + const rows = container.querySelectorAll(".TableRow"); + + expect(rows[0].querySelector(".key").textContent).toBe("CriticalAddonsOnly"); + expect(rows[0].querySelector(".operator").textContent).toBe("Exist"); + expect(rows[0].querySelector(".effect").textContent).toBe("NoExecute"); + expect(rows[0].querySelector(".seconds").textContent).toBe("3600"); + + expect(rows[1].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready"); + expect(rows[1].querySelector(".operator").textContent).toBe("NoExist"); + expect(rows[1].querySelector(".effect").textContent).toBe("NoSchedule"); + expect(rows[1].querySelector(".seconds").textContent).toBe("7200"); + }); + + it("sorts table properly", () => { + const { container, getByText } = render(); + const headCell = getByText("Key"); + + fireEvent.click(headCell); + fireEvent.click(headCell); + + const rows = container.querySelectorAll(".TableRow"); + + expect(rows[0].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready"); + }); +}); diff --git a/src/renderer/components/+workloads-pods/index.ts b/src/renderer/components/+workloads-pods/index.ts index f3181cb3a2..cc7782911c 100644 --- a/src/renderer/components/+workloads-pods/index.ts +++ b/src/renderer/components/+workloads-pods/index.ts @@ -1,2 +1,2 @@ export * from "./pods"; -export * from "./pod-details"; \ No newline at end of file +export * from "./pod-details"; diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx index 38af50a457..2e96b142f9 100644 --- a/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -30,7 +30,11 @@ export const ContainerEnvironment = observer((props: Props) => { } }); envFrom && envFrom.forEach(item => { - const { configMapRef } = item; + const { configMapRef, secretRef } = item; + + if (secretRef && secretRef.name) { + secretsStore.load({ name: secretRef.name, namespace }); + } if (configMapRef && configMapRef.name) { configMapsStore.load({ name: configMapRef.name, namespace }); @@ -89,21 +93,54 @@ export const ContainerEnvironment = observer((props: Props) => { const renderEnvFrom = () => { const envVars = envFrom.map(vars => { - if (!vars.configMapRef || !vars.configMapRef.name) return; - const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace); - - if (!configMap) return; - - return Object.entries(configMap.data).map(([name, value]) => ( -
- {name}: {value} -
- )); + if (vars.configMapRef?.name) { + return renderEnvFromConfigMap(vars.configMapRef.name); + } else if (vars.secretRef?.name ) { + return renderEnvFromSecret(vars.secretRef.name); + } }); return _.flatten(envVars); }; + const renderEnvFromConfigMap = (configMapName: string) => { + const configMap = configMapsStore.getByName(configMapName, namespace); + + if (!configMap) return; + + return Object.entries(configMap.data).map(([name, value]) => ( +
+ {name}: {value} +
+ )); + }; + + const renderEnvFromSecret = (secretName: string) => { + const secret = secretsStore.getByName(secretName, namespace); + + if (!secret) return; + + return Object.keys(secret.data).map(key => { + const secretKeyRef = { + name: secret.getName(), + key + }; + + const value = ( + + ); + + return ( +
+ {key}: {value} +
+ ); + }); + }; + return ( {env && renderEnv()} diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index ce5c27c462..4f2124ec3d 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -43,7 +43,7 @@ export class PodContainerPort extends React.Component { return (
- this.portForward() }> + this.portForward() }> {text} {this.waiting && ( diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index b1596c60f5..99238aa5ad 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -11,6 +11,8 @@ import { PodContainerPort } from "./pod-container-port"; import { ResourceMetrics } from "../resource-metrics"; import { IMetrics } from "../../api/endpoints/metrics.api"; import { ContainerCharts } from "./container-charts"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props { pod: Pod; @@ -53,6 +55,7 @@ export class PodDetailsContainer extends React.Component { const state = status ? Object.keys(status.state)[0] : ""; const lastState = status ? Object.keys(status.lastState)[0] : ""; const ready = status ? status.ready : ""; + const imageId = status? status.imageID : ""; const liveness = pod.getLivenessProbe(container); const readiness = pod.getReadinessProbe(container); const startup = pod.getStartupProbe(container); @@ -62,13 +65,14 @@ export class PodDetailsContainer extends React.Component { "Memory", "Filesystem", ]; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container); return (
{name}
- {!isInitContainer && + {!isMetricHidden && !isInitContainer && @@ -84,7 +88,7 @@ export class PodDetailsContainer extends React.Component { } - {image} + {imagePullPolicy && imagePullPolicy !== "IfNotPresent" && diff --git a/src/renderer/components/+workloads-pods/pod-details-list.tsx b/src/renderer/components/+workloads-pods/pod-details-list.tsx index 6e2bd64c1d..c5d9023334 100644 --- a/src/renderer/components/+workloads-pods/pod-details-list.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-list.tsx @@ -135,6 +135,7 @@ export class PodDetailsList extends React.Component {
{showTitle && } * { display: block; - margin-bottom: $margin; + margin-bottom: var(--margin); &:last-child { margin-bottom: 0; diff --git a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx index af1515c1b4..b2c7cd14c3 100644 --- a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx @@ -13,33 +13,49 @@ interface Props { @observer export class PodDetailsSecrets extends Component { - @observable secrets: Secret[] = []; + @observable secrets: Map = observable.map(); @disposeOnUnmount secretsLoader = autorun(async () => { const { pod } = this.props; - this.secrets = await Promise.all( + const secrets = await Promise.all( pod.getSecrets().map(secretName => secretsApi.get({ name: secretName, namespace: pod.getNs(), })) ); + + secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret)); }); render() { + const { pod } = this.props; + return (
{ - this.secrets.map(secret => { - return ( - - {secret.getName()} - - ); + pod.getSecrets().map(secretName => { + const secret = this.secrets.get(secretName); + + if (secret) { + return this.renderSecretLink(secret); + } else { + return ( + {secretName} + ); + } }) }
); } + + protected renderSecretLink(secret: Secret) { + return ( + + {secret.getName()} + + ); + } } diff --git a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx index 5ce8465e72..1e0f765381 100644 --- a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx @@ -27,4 +27,4 @@ export class PodDetailsStatuses extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss index 0aa68fa1d6..1ac932cd9d 100644 --- a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss +++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss @@ -1,5 +1,23 @@ .PodDetailsTolerations { - .toleration { - margin-bottom: $margin; + grid-template-columns: auto; + + .PodTolerations { + margin-top: var(--margin); + } + + // Expanding value cell to cover 2 columns (whole Drawer width) + + > .name { + grid-row-start: 1; + grid-column-start: 1; + } + + > .value { + grid-row-start: 1; + grid-column-start: 1; + } + + .DrawerParamToggler > .params { + margin-left: var(--drawer-item-title-width); } } \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx index 8b67502e26..67bd5a07d0 100644 --- a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx @@ -1,10 +1,11 @@ import "./pod-details-tolerations.scss"; import React from "react"; -import { Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job } from "../../api/endpoints"; import { DrawerParamToggler, DrawerItem } from "../drawer"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; +import { PodTolerations } from "./pod-tolerations"; interface Props { - workload: Pod | Deployment | DaemonSet | StatefulSet | ReplicaSet | Job; + workload: WorkloadKubeObject; } export class PodDetailsTolerations extends React.Component { @@ -17,20 +18,7 @@ export class PodDetailsTolerations extends React.Component { return ( - { - tolerations.map((toleration, index) => { - const { key, operator, effect, tolerationSeconds } = toleration; - - return ( -
- {key} - {operator && {operator}} - {effect && {effect}} - {!!tolerationSeconds && {tolerationSeconds}} -
- ); - }) - } +
); diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 5db26ca9a4..6736e1e326 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -22,6 +22,8 @@ import { getItemMetrics } from "../../api/endpoints/metrics.api"; import { PodCharts, podMetricTabs } from "./pod-charts"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -66,15 +68,18 @@ export class PodDetails extends React.Component { const nodeSelector = pod.getNodeSelectors(); const volumes = pod.getVolumes(); const metrics = podsStore.metrics; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod); return (
- podsStore.loadMetrics(pod)} - tabs={podMetricTabs} object={pod} params={{ metrics }} - > - - + {!isMetricHidden && ( + podsStore.loadMetrics(pod)} + tabs={podMetricTabs} object={pod} params={{ metrics }} + > + + + )} {pod.getStatusMessage()} diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.scss b/src/renderer/components/+workloads-pods/pod-tolerations.scss new file mode 100644 index 0000000000..b840697685 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-tolerations.scss @@ -0,0 +1,14 @@ +.PodTolerations { + .TableHead { + background-color: var(--drawerSubtitleBackground); + } + + .TableCell { + white-space: normal; + word-break: normal; + + &.key { + flex-grow: 3; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-tolerations.tsx new file mode 100644 index 0000000000..ff464fe1d3 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-tolerations.tsx @@ -0,0 +1,64 @@ +import "./pod-tolerations.scss"; +import React from "react"; +import uniqueId from "lodash/uniqueId"; + +import { IToleration } from "../../api/workload-kube-object"; +import { Table, TableCell, TableHead, TableRow } from "../table"; + +interface Props { + tolerations: IToleration[]; +} + +enum sortBy { + Key = "key", + Operator = "operator", + Effect = "effect", + Seconds = "seconds", +} + +const sortingCallbacks = { + [sortBy.Key]: (toleration: IToleration) => toleration.key, + [sortBy.Operator]: (toleration: IToleration) => toleration.operator, + [sortBy.Effect]: (toleration: IToleration) => toleration.effect, + [sortBy.Seconds]: (toleration: IToleration) => toleration.tolerationSeconds, +}; + +const getTableRow = (toleration: IToleration) => { + const { key, operator, effect, tolerationSeconds } = toleration; + + return ( + + {key} + {operator} + {effect} + {tolerationSeconds} + + ); +}; + +export function PodTolerations({ tolerations }: Props) { + return ( +
+ + Key + Operator + Effect + Seconds + + { + tolerations.map(getTableRow) + } +
+ ); +} diff --git a/src/renderer/components/+workloads-pods/pods.store.ts b/src/renderer/components/+workloads-pods/pods.store.ts index 9cd3c3b2f9..5a535cec66 100644 --- a/src/renderer/components/+workloads-pods/pods.store.ts +++ b/src/renderer/components/+workloads-pods/pods.store.ts @@ -3,8 +3,8 @@ import { action, observable } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints"; -import { WorkloadKubeObject } from "../../api/workload-kube-object"; import { apiManager } from "../../api/api-manager"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; @autobind() export class PodsStore extends KubeObjectStore { @@ -44,6 +44,12 @@ export class PodsStore extends KubeObjectStore { }); } + getPodsByOwnerId(workloadId: string): Pod[] { + return this.items.filter(pod => { + return pod.getOwnerRefs().find(owner => owner.uid === workloadId); + }); + } + getPodsByNode(node: string) { if (!this.isLoaded) return []; diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 88981b968f..fc2a0930af 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -19,8 +19,7 @@ import { lookupApiLink } from "../../api/kube-api"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge"; - -enum sortBy { +enum columnId { name = "name", namespace = "namespace", containers = "containers", @@ -74,16 +73,18 @@ export class Pods extends React.Component { pod.getName(), - [sortBy.namespace]: (pod: Pod) => pod.getNs(), - [sortBy.containers]: (pod: Pod) => pod.getContainers().length, - [sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(), - [sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), - [sortBy.qos]: (pod: Pod) => pod.getQosClass(), - [sortBy.node]: (pod: Pod) => pod.getNodeName(), - [sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp, - [sortBy.status]: (pod: Pod) => pod.getStatusMessage(), + [columnId.name]: (pod: Pod) => pod.getName(), + [columnId.namespace]: (pod: Pod) => pod.getNs(), + [columnId.containers]: (pod: Pod) => pod.getContainers().length, + [columnId.restarts]: (pod: Pod) => pod.getRestartsCount(), + [columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), + [columnId.qos]: (pod: Pod) => pod.getQosClass(), + [columnId.node]: (pod: Pod) => pod.getNodeName(), + [columnId.age]: (pod: Pod) => pod.getTimeDiffFromNow(), + [columnId.status]: (pod: Pod) => pod.getStatusMessage(), }} searchFilters={[ (pod: Pod) => pod.getSearchFields(), @@ -93,16 +94,16 @@ export class Pods extends React.Component { ]} renderHeaderTitle="Pods" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Containers", className: "containers", sortBy: sortBy.containers }, - { title: "Restarts", className: "restarts", sortBy: sortBy.restarts }, - { title: "Controlled By", className: "owners", sortBy: sortBy.owners }, - { title: "Node", className: "node", sortBy: sortBy.node }, - { title: "QoS", className: "qos", sortBy: sortBy.qos }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, + { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, + { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, + { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, + { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(pod: Pod) => [ , diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index 8c28d81a83..4510d0add7 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -17,6 +17,8 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -29,9 +31,7 @@ export class ReplicaSetDetails extends React.Component { }); async componentDidMount() { - if (!podsStore.isLoaded) { - podsStore.loadAll(); - } + podsStore.reloadAll(); } componentWillUnmount() { @@ -49,10 +49,11 @@ export class ReplicaSetDetails extends React.Component { const nodeSelector = replicaSet.getNodeSelectors(); const images = replicaSet.getImages(); const childPods = replicaSetStore.getChildPods(replicaSet); + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet); return (
- {podsStore.isLoaded && ( + {!isMetricHidden && podsStore.isLoaded && ( replicaSetStore.loadMetrics(replicaSet)} tabs={podMetricTabs} object={replicaSet} params={{ metrics }} diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx index 804b7c344f..131bc08ca2 100755 --- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx @@ -4,9 +4,9 @@ jest.mock("../../api/endpoints"); import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; import { render, waitFor, fireEvent } from "@testing-library/react"; import React from "react"; -import { replicaSetApi } from "../../api/endpoints/replica-set.api"; +import { ReplicaSet, replicaSetApi } from "../../api/endpoints/replica-set.api"; -const dummyReplicaSet = { +const dummyReplicaSet: ReplicaSet = { apiVersion: "v1", kind: "dummy", metadata: { @@ -67,7 +67,6 @@ const dummyReplicaSet = { getCurrent: jest.fn(), getReady: jest.fn(), getImages: jest.fn(), - getReplicas: jest.fn(), getSelectors: jest.fn(), getTemplateLabels: jest.fn(), getAffinity: jest.fn(), @@ -79,6 +78,7 @@ const dummyReplicaSet = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), diff --git a/src/renderer/components/+workloads-replicasets/replicasets.store.ts b/src/renderer/components/+workloads-replicasets/replicasets.store.ts index 815397aaec..cf6c20bf1d 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.store.ts +++ b/src/renderer/components/+workloads-replicasets/replicasets.store.ts @@ -16,7 +16,7 @@ export class ReplicaSetStore extends KubeObjectStore { } getChildPods(replicaSet: ReplicaSet) { - return podsStore.getPodsByOwner(replicaSet); + return podsStore.getPodsByOwnerId(replicaSet.getId()); } getStatuses(replicaSets: ReplicaSet[]) { diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 55f607e3c3..1caa394df6 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -14,7 +14,7 @@ import { Icon } from "../icon/icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", desired = "desired", @@ -31,27 +31,29 @@ export class ReplicaSets extends React.Component { render() { return ( replicaSet.getName(), - [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), - [sortBy.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(), - [sortBy.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(), - [sortBy.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(), - [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, + [columnId.name]: (replicaSet: ReplicaSet) => replicaSet.getName(), + [columnId.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), + [columnId.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(), + [columnId.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(), + [columnId.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(), + [columnId.age]: (replicaSet: ReplicaSet) => replicaSet.getTimeDiffFromNow(), }} searchFilters={[ (replicaSet: ReplicaSet) => replicaSet.getSearchFields(), ]} renderHeaderTitle="Replica Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Desired", className: "desired", sortBy: sortBy.desired }, - { title: "Current", className: "current", sortBy: sortBy.current }, - { title: "Ready", className: "ready", sortBy: sortBy.ready }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Desired", className: "desired", sortBy: columnId.desired, id: columnId.desired }, + { title: "Current", className: "current", sortBy: columnId.current, id: columnId.current }, + { title: "Ready", className: "ready", sortBy: columnId.ready, id: columnId.ready }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(replicaSet: ReplicaSet) => [ replicaSet.getName(), @@ -76,7 +78,7 @@ export function ReplicaSetMenu(props: KubeObjectMenuProps) { return ( <> ReplicaSetScaleDialog.open(object)}> - + Scale diff --git a/src/renderer/components/+workloads-statefulsets/index.ts b/src/renderer/components/+workloads-statefulsets/index.ts index 1cb72d701a..af942b604f 100644 --- a/src/renderer/components/+workloads-statefulsets/index.ts +++ b/src/renderer/components/+workloads-statefulsets/index.ts @@ -1,2 +1,2 @@ export * from "./statefulsets"; -export * from "./statefulset-details"; \ No newline at end of file +export * from "./statefulset-details"; diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index d30633ff32..97e83807f9 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -18,6 +18,8 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; +import { clusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -30,9 +32,7 @@ export class StatefulSetDetails extends React.Component { }); componentDidMount() { - if (!podsStore.isLoaded) { - podsStore.loadAll(); - } + podsStore.reloadAll(); } componentWillUnmount() { @@ -48,10 +48,11 @@ export class StatefulSetDetails extends React.Component { const nodeSelector = statefulSet.getNodeSelectors(); const childPods = statefulSetStore.getChildPods(statefulSet); const metrics = statefulSetStore.metrics; + const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet); return (
- {podsStore.isLoaded && ( + {!isMetricHidden && podsStore.isLoaded && ( statefulSetStore.loadMetrics(statefulSet)} tabs={podMetricTabs} object={statefulSet} params={{ metrics }} diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx index c0a26dbd52..faf94b995d 100755 --- a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx @@ -1,12 +1,12 @@ import "@testing-library/jest-dom/extend-expect"; jest.mock("../../api/endpoints"); -import { statefulSetApi } from "../../api/endpoints"; +import { StatefulSet, statefulSetApi } from "../../api/endpoints"; import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; import { render, waitFor, fireEvent } from "@testing-library/react"; import React from "react"; -const dummyStatefulSet = { +const dummyStatefulSet: StatefulSet = { apiVersion: "v1", kind: "dummy", metadata: { @@ -88,6 +88,7 @@ const dummyStatefulSet = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), diff --git a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts index 699603d1f0..4694a1dbbf 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts +++ b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts @@ -15,7 +15,7 @@ export class StatefulSetStore extends KubeObjectStore { } getChildPods(statefulSet: StatefulSet) { - return podsStore.getPodsByOwner(statefulSet); + return podsStore.getPodsByOwnerId(statefulSet.getId()); } getStatuses(statefulSets: StatefulSet[]) { diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 9e6011e156..868a6afc45 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -17,9 +17,10 @@ import { MenuItem } from "../menu/menu"; import { Icon } from "../icon/icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + pods = "pods", age = "age", replicas = "replicas", } @@ -38,25 +39,27 @@ export class StatefulSets extends React.Component { render() { return ( statefulSet.getName(), - [sortBy.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(), - [sortBy.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp, - [sortBy.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(), + [columnId.name]: (statefulSet: StatefulSet) => statefulSet.getName(), + [columnId.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(), + [columnId.age]: (statefulSet: StatefulSet) => statefulSet.getTimeDiffFromNow(), + [columnId.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(), }} searchFilters={[ (statefulSet: StatefulSet) => statefulSet.getSearchFields(), ]} renderHeaderTitle="Stateful Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods" }, - { title: "Replicas", className: "replicas", sortBy: sortBy.replicas }, - { className: "warning" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { className: "warning", showWithColumn: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(statefulSet: StatefulSet) => [ statefulSet.getName(), @@ -80,7 +83,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps) { return ( <> StatefulSetScaleDialog.open(object)}> - + Scale diff --git a/src/renderer/components/+workloads/index.ts b/src/renderer/components/+workloads/index.ts index a6e1904b84..7e40e91f98 100644 --- a/src/renderer/components/+workloads/index.ts +++ b/src/renderer/components/+workloads/index.ts @@ -1,3 +1,4 @@ export * from "./workloads.route"; export * from "./workloads"; export * from "./workloads.stores"; +export * from "./workloads.command"; diff --git a/src/renderer/components/+workloads/workloads.command.ts b/src/renderer/components/+workloads/workloads.command.ts new file mode 100644 index 0000000000..dfe774b204 --- /dev/null +++ b/src/renderer/components/+workloads/workloads.command.ts @@ -0,0 +1,45 @@ +import { navigate } from "../../navigation"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { cronJobsURL, daemonSetsURL, deploymentsURL, jobsURL, podsURL, statefulSetsURL } from "./workloads.route"; + +commandRegistry.add({ + id: "cluster.viewPods", + title: "Cluster: View Pods", + scope: "entity", + action: () => navigate(podsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewDeployments", + title: "Cluster: View Deployments", + scope: "entity", + action: () => navigate(deploymentsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewDaemonSets", + title: "Cluster: View DaemonSets", + scope: "entity", + action: () => navigate(daemonSetsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewStatefulSets", + title: "Cluster: View StatefulSets", + scope: "entity", + action: () => navigate(statefulSetsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewJobs", + title: "Cluster: View Jobs", + scope: "entity", + action: () => navigate(jobsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewCronJobs", + title: "Cluster: View CronJobs", + scope: "entity", + action: () => navigate(cronJobsURL()) +}); diff --git a/src/renderer/components/+workloads/workloads.route.ts b/src/renderer/components/+workloads/workloads.route.ts index 44c43c5ef9..14a0bbb07d 100644 --- a/src/renderer/components/+workloads/workloads.route.ts +++ b/src/renderer/components/+workloads/workloads.route.ts @@ -1,13 +1,6 @@ import type { RouteProps } from "react-router"; import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; import { KubeResource } from "../../../common/rbac"; -import { Workloads } from "./workloads"; - -export const workloadsRoute: RouteProps = { - get path() { - return Workloads.tabRoutes.map(({ routePath }) => routePath).flat(); - } -}; // Routes export const overviewRoute: RouteProps = { @@ -35,6 +28,19 @@ export const cronJobsRoute: RouteProps = { path: "/cronjobs" }; +export const workloadsRoute: RouteProps = { + path: [ + overviewRoute, + podsRoute, + deploymentsRoute, + daemonSetsRoute, + statefulSetsRoute, + replicaSetsRoute, + jobsRoute, + cronJobsRoute + ].map(route => route.path.toString()) +}; + // Route params export interface IWorkloadsOverviewRouteParams { } diff --git a/src/renderer/components/+workspaces/index.ts b/src/renderer/components/+workspaces/index.ts deleted file mode 100644 index db23faa3be..0000000000 --- a/src/renderer/components/+workspaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./workspaces.route"; -export * from "./workspaces"; diff --git a/src/renderer/components/+workspaces/workspace-menu.scss b/src/renderer/components/+workspaces/workspace-menu.scss deleted file mode 100644 index e7adf9ae6a..0000000000 --- a/src/renderer/components/+workspaces/workspace-menu.scss +++ /dev/null @@ -1,7 +0,0 @@ -.WorkspaceMenu { - border-radius: $radius; - - .workspaces-title { - padding: $padding; - } -} \ No newline at end of file diff --git a/src/renderer/components/+workspaces/workspace-menu.tsx b/src/renderer/components/+workspaces/workspace-menu.tsx deleted file mode 100644 index 30a68cc081..0000000000 --- a/src/renderer/components/+workspaces/workspace-menu.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import "./workspace-menu.scss"; -import React from "react"; -import { observer } from "mobx-react"; -import { Link } from "react-router-dom"; -import { workspacesURL } from "./workspaces.route"; -import { Menu, MenuItem, MenuProps } from "../menu"; -import { Icon } from "../icon"; -import { observable } from "mobx"; -import { WorkspaceId, workspaceStore } from "../../../common/workspace-store"; -import { cssNames } from "../../utils"; -import { navigate } from "../../navigation"; -import { clusterViewURL } from "../cluster-manager/cluster-view.route"; -import { landingURL } from "../+landing-page"; - -interface Props extends Partial { -} - -@observer -export class WorkspaceMenu extends React.Component { - @observable menuVisible = false; - - activateWorkspace = (id: WorkspaceId) => { - const clusterId = workspaceStore.getById(id).lastActiveClusterId; - - workspaceStore.setActive(id); - - if (clusterId) { - navigate(clusterViewURL({ params: { clusterId } })); - } else { - navigate(landingURL()); - } - }; - - render() { - const { className, ...menuProps } = this.props; - const { enabledWorkspacesList, currentWorkspace } = workspaceStore; - - return ( - this.menuVisible = true} - close={() => this.menuVisible = false} - > - - Workspaces - - {enabledWorkspacesList.map(({ id: workspaceId, name, description }) => { - return ( - this.activateWorkspace(workspaceId)} - > - - {name} - - ); - })} - - ); - } -} diff --git a/src/renderer/components/+workspaces/workspaces.route.ts b/src/renderer/components/+workspaces/workspaces.route.ts deleted file mode 100644 index 2429c5315e..0000000000 --- a/src/renderer/components/+workspaces/workspaces.route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RouteProps } from "react-router"; -import { buildURL } from "../../../common/utils/buildUrl"; - -export const workspacesRoute: RouteProps = { - path: "/workspaces" -}; - -export const workspacesURL = buildURL(workspacesRoute.path); diff --git a/src/renderer/components/+workspaces/workspaces.scss b/src/renderer/components/+workspaces/workspaces.scss deleted file mode 100644 index 95c036c304..0000000000 --- a/src/renderer/components/+workspaces/workspaces.scss +++ /dev/null @@ -1,14 +0,0 @@ -.Workspaces { - .workspace { - --flex-gap: #{$padding}; - padding: $padding / 2; - - &.default { - font-style: italic; - } - - > .description { - flex: 1; - } - } -} \ No newline at end of file diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx deleted file mode 100644 index 932306adab..0000000000 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import "./workspaces.scss"; -import React, { Fragment } from "react"; -import { observer } from "mobx-react"; -import { computed, observable, toJS } from "mobx"; -import { WizardLayout } from "../layout/wizard-layout"; -import { Workspace, WorkspaceId, workspaceStore } from "../../../common/workspace-store"; -import { v4 as uuid } from "uuid"; -import { ConfirmDialog } from "../confirm-dialog"; -import { Icon } from "../icon"; -import { Input } from "../input"; -import { cssNames, prevDefault } from "../../utils"; -import { Button } from "../button"; -import { isRequired, InputValidator } from "../input/input_validators"; -import { clusterStore } from "../../../common/cluster-store"; - -@observer -export class Workspaces extends React.Component { - @observable editingWorkspaces = observable.map(); - - @computed get workspaces(): Workspace[] { - const currentWorkspaces: Map = new Map(); - - workspaceStore.enabledWorkspacesList.forEach((w) => { - currentWorkspaces.set(w.id, w); - }); - const allWorkspaces = new Map([ - ...currentWorkspaces, - ...this.editingWorkspaces, - ]); - - return Array.from(allWorkspaces.values()); - } - - renderInfo() { - return ( - -

What is a Workspace?

-

- Workspaces are used to organize number of clusters into logical groups. -

-

- A single workspaces contains a list of clusters and their full configuration. -

-
- ); - } - - saveWorkspace = (id: WorkspaceId) => { - const workspace = new Workspace(this.editingWorkspaces.get(id)); - - if (workspaceStore.getById(id)) { - workspaceStore.updateWorkspace(workspace); - this.clearEditing(id); - - return; - } - - if (workspaceStore.addWorkspace(workspace)) { - this.clearEditing(id); - } - }; - - addWorkspace = () => { - const workspaceId = uuid(); - - this.editingWorkspaces.set(workspaceId, new Workspace({ - id: workspaceId, - name: "", - description: "" - })); - }; - - editWorkspace = (id: WorkspaceId) => { - const workspace = workspaceStore.getById(id); - - this.editingWorkspaces.set(id, toJS(workspace)); - }; - - activateWorkspace = (id: WorkspaceId) => { - const clusterId = workspaceStore.getById(id).lastActiveClusterId; - - workspaceStore.setActive(id); - clusterStore.setActive(clusterId); - }; - - clearEditing = (id: WorkspaceId) => { - this.editingWorkspaces.delete(id); - }; - - removeWorkspace = (id: WorkspaceId) => { - const workspace = workspaceStore.getById(id); - - ConfirmDialog.open({ - okButtonProps: { - label: `Remove Workspace`, - primary: false, - accent: true, - }, - ok: () => { - this.clearEditing(id); - workspaceStore.removeWorkspace(workspace); - }, - message: ( -
-

- Are you sure you want remove workspace {workspace.name}? -

-

- All clusters within workspace will be cleared as well -

-
- ), - }); - }; - - onInputKeypress = (evt: React.KeyboardEvent, workspaceId: WorkspaceId) => { - if (evt.key == "Enter") { - // Trigget input validation - evt.currentTarget.blur(); - evt.currentTarget.focus(); - this.saveWorkspace(workspaceId); - } - }; - - render() { - return ( - -

- Workspaces -

-
- {this.workspaces.map(({ id: workspaceId, name, description, ownerRef }) => { - const isActive = workspaceStore.currentWorkspaceId === workspaceId; - const isDefault = workspaceStore.isDefault(workspaceId); - const isEditing = this.editingWorkspaces.has(workspaceId); - const editingWorkspace = this.editingWorkspaces.get(workspaceId); - const managed = !!ownerRef; - const className = cssNames("workspace flex gaps align-center", { - active: isActive, - editing: isEditing, - default: isDefault, - }); - const existenceValidator: InputValidator = { - message: () => `Workspace '${name}' already exists`, - validate: value => !workspaceStore.getByName(value.trim()) - }; - - return ( -
- {!isEditing && ( - - - this.activateWorkspace(workspaceId))}>{name} - {isActive && (current)} - - {description} - {!isDefault && !managed && ( - - this.editWorkspace(workspaceId)} - /> - this.removeWorkspace(workspaceId)} - /> - - )} - - )} - {isEditing && ( - - editingWorkspace.name = v} - onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} - validators={[isRequired, existenceValidator]} - autoFocus - /> - editingWorkspace.description = v} - onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} - /> - this.saveWorkspace(workspaceId)} - /> - this.clearEditing(workspaceId)} - /> - - )} -
- ); - })} -
-
); } -} \ No newline at end of file +} diff --git a/src/renderer/components/ace-editor/index.ts b/src/renderer/components/ace-editor/index.ts index 7bfc7c01ea..173845abab 100644 --- a/src/renderer/components/ace-editor/index.ts +++ b/src/renderer/components/ace-editor/index.ts @@ -1 +1 @@ -export * from "./ace-editor"; \ No newline at end of file +export * from "./ace-editor"; diff --git a/src/renderer/components/add-remove-buttons/index.ts b/src/renderer/components/add-remove-buttons/index.ts index 825c59d7d2..fa2deb84ec 100644 --- a/src/renderer/components/add-remove-buttons/index.ts +++ b/src/renderer/components/add-remove-buttons/index.ts @@ -1 +1 @@ -export * from "./add-remove-buttons"; \ No newline at end of file +export * from "./add-remove-buttons"; diff --git a/src/renderer/components/animate/index.ts b/src/renderer/components/animate/index.ts index 080c5446c8..36d812de20 100644 --- a/src/renderer/components/animate/index.ts +++ b/src/renderer/components/animate/index.ts @@ -1 +1 @@ -export * from "./animate"; \ No newline at end of file +export * from "./animate"; diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 037b088efe..50cb9c1e38 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -50,6 +50,10 @@ color: white; } +*:target { + color: $textColorAccent; +} + html { font-size: 62.5%; // 1 rem == 10px color: $textColorPrimary; @@ -101,12 +105,6 @@ ol, ul { list-style: none; } -hr { - margin: $margin 0 !important; - height: 1px; - background: $grey-800; -} - h1 { color: $textColorPrimary; font-size: 28px; @@ -151,7 +149,7 @@ code { vertical-align: middle; border-radius: $radius; font-family: $font-monospace; - font-size: calc($font-size * .9); + font-size: calc(var(--font-size) * .9); color: #b4b5b4; &.block { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 495182b2a9..2110898885 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { observer } from "mobx-react"; +import { observable } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; import { history } from "../navigation"; import { Notifications } from "./notifications"; @@ -35,18 +36,20 @@ import { webFrame } from "electron"; import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry"; import { extensionLoader } from "../../extensions/extension-loader"; import { appEventBus } from "../../common/event-bus"; -import { broadcastMessage, requestMain } from "../../common/ipc"; +import { requestMain } from "../../common/ipc"; import whatInput from "what-input"; import { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; import { TabLayout, TabLayoutRoute } from "./layout/tab-layout"; import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog"; import { eventStore } from "./+events/event.store"; -import { computed, reaction } from "mobx"; import { nodesStore } from "./+nodes/nodes.store"; import { podsStore } from "./+workloads-pods/pods.store"; -import { sum } from "lodash"; +import { kubeWatchApi } from "../api/kube-watch-api"; import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog"; +import { CommandContainer } from "./command-palette/command-container"; +import { KubeObjectStore } from "../kube-object.store"; +import { clusterContext } from "./context"; @observer export class App extends React.Component { @@ -73,53 +76,21 @@ export class App extends React.Component { window.location.reload(); }); whatInput.ask(); // Start to monitor user input device + + // Setup hosted cluster context + KubeObjectStore.defaultContext = clusterContext; + kubeWatchApi.context = clusterContext; } - async componentDidMount() { - const cluster = getHostedCluster(); - const promises: Promise[] = []; - - if (isAllowedResource("events") && isAllowedResource("pods")) { - promises.push(eventStore.loadAll()); - promises.push(podsStore.loadAll()); - } - - if (isAllowedResource("nodes")) { - promises.push(nodesStore.loadAll()); - } - await Promise.all(promises); - - if (eventStore.isLoaded && podsStore.isLoaded) { - eventStore.subscribe(); - podsStore.subscribe(); - } - - if (nodesStore.isLoaded) { - nodesStore.subscribe(); - } - - reaction(() => this.warningsCount, (count) => { - broadcastMessage(`cluster-warning-event-count:${cluster.id}`, count); - }); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], { + preload: true, + }) + ]); } - @computed - get warningsCount() { - let warnings = sum(nodesStore.items - .map(node => node.getWarningConditions().length)); - - warnings = warnings + eventStore.getWarnings().length; - - return warnings; - } - - get startURL() { - if (isAllowedResource(["events", "nodes", "pods"])) { - return clusterURL(); - } - - return workloadsURL(); - } + @observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL(); getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { const routes: TabLayoutRoute[] = []; @@ -190,7 +161,7 @@ export class App extends React.Component { {this.renderExtensionTabLayoutRoutes()} {this.renderExtensionRoutes()} - + @@ -203,6 +174,7 @@ export class App extends React.Component { + ); diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss index b850a3be59..a874970d0b 100644 --- a/src/renderer/components/button/button.scss +++ b/src/renderer/components/button/button.scss @@ -3,6 +3,8 @@ position: relative; overflow: hidden; // required for transition effect on hover color: white; + font-family: var(--font-main); + font-weight: var(--font-weight-bold); text-align: center; text-decoration: none; cursor: pointer; @@ -21,10 +23,16 @@ &.primary { background: $buttonPrimaryBackground; } + &.accent { background: $buttonAccentBackground; } + &.light { + background-color: $buttonLightBackground; + color: #505050; + } + &.plain { color: inherit; background: transparent; diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index 9fa822b214..8bcb37bad4 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes, TooltipDecorator waiting?: boolean; primary?: boolean; accent?: boolean; + light?: boolean; plain?: boolean; outlined?: boolean; hidden?: boolean; @@ -24,13 +25,16 @@ export class Button extends React.PureComponent { private button: HTMLButtonElement; render() { - const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props; - const btnProps = props as Partial; + const { + className, waiting, label, primary, accent, plain, hidden, active, big, + round, outlined, tooltip, light, children, ...props + } = this.props; + const btnProps: Partial = props; if (hidden) return null; btnProps.className = cssNames("Button", className, { - waiting, primary, accent, plain, active, big, round, outlined + waiting, primary, accent, plain, active, big, round, outlined, light, }); const btnContent: ReactNode = ( diff --git a/src/renderer/components/chart/background-block.plugin.ts b/src/renderer/components/chart/background-block.plugin.ts deleted file mode 100644 index 1d39f71aed..0000000000 --- a/src/renderer/components/chart/background-block.plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import ChartJS from "chart.js"; -import get from "lodash/get"; - -const defaultOptions = { - interval: 61, - coverBars: 3, - borderColor: "#44474A", - backgroundColor: "#00000033" -}; - -export const BackgroundBlock = { - options: {}, - - getOptions(chart: ChartJS) { - return get(chart, "options.plugins.BackgroundBlock"); - }, - - afterInit(chart: ChartJS) { - this.options = { - ...defaultOptions, - ...this.getOptions(chart) - }; - }, - - beforeDraw(chart: ChartJS) { - if (!chart.chartArea) return; - const { interval, coverBars, borderColor, backgroundColor } = this.options; - const { ctx, chartArea } = chart; - const { left, right, top, bottom } = chartArea; - const blockWidth = (right - left) / interval * coverBars; - - ctx.save(); - ctx.fillStyle = backgroundColor; - ctx.strokeStyle = borderColor; - ctx.fillRect(right - blockWidth, top, blockWidth, bottom - top); - ctx.beginPath(); - ctx.moveTo(right - blockWidth + 1.5, top); - ctx.lineTo(right - blockWidth + 1.5, bottom); - ctx.stroke(); - ctx.restore(); - } -}; \ No newline at end of file diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 69b65b10c9..74ee203d4c 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -3,7 +3,7 @@ import merge from "lodash/merge"; import moment from "moment"; import Color from "color"; import { observer } from "mobx-react"; -import { ChartData, ChartOptions, ChartPoint, Scriptable } from "chart.js"; +import { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js"; import { Chart, ChartKind, ChartProps } from "./chart"; import { bytesToUnits, cssNames } from "../../utils"; import { ZebraStripes } from "./zebra-stripes.plugin"; @@ -50,6 +50,10 @@ export class BarChart extends React.Component { }) }; + if (chartData.datasets.length == 0) { + return ; + } + const formatTimeLabels = (timestamp: string, index: number) => { const label = moment(parseInt(timestamp)).format("HH:mm"); const offset = " "; @@ -111,12 +115,13 @@ export class BarChart extends React.Component { mode: "index", position: "cursor", callbacks: { - title: tooltipItems => { - const now = new Date().getTime(); + title([tooltip]: ChartTooltipItem[]) { + const xLabel = tooltip?.xLabel; + const skipLabel = xLabel == null || new Date(xLabel).getTime() > Date.now(); - if (new Date(tooltipItems[0].xLabel).getTime() > now) return ""; + if (skipLabel) return ""; - return `${tooltipItems[0].xLabel}`; + return String(xLabel); }, labelColor: ({ datasetIndex }) => { return { @@ -136,16 +141,13 @@ export class BarChart extends React.Component { }, plugins: { ZebraStripes: { - stripeColor: chartStripesColor + stripeColor: chartStripesColor, + interval: chartData.datasets[0].data.length } } }; const options = merge(barOptions, customOptions); - if (chartData.datasets.length == 0) { - return ; - } - return ( { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/chart/index.ts b/src/renderer/components/chart/index.ts index a9db66c298..d75ddf7a2f 100644 --- a/src/renderer/components/chart/index.ts +++ b/src/renderer/components/chart/index.ts @@ -1,3 +1,3 @@ export * from "./chart"; export * from "./pie-chart"; -export * from "./bar-chart"; \ No newline at end of file +export * from "./bar-chart"; diff --git a/src/renderer/components/chart/pie-chart.scss b/src/renderer/components/chart/pie-chart.scss index 16abb49933..1925786adb 100644 --- a/src/renderer/components/chart/pie-chart.scss +++ b/src/renderer/components/chart/pie-chart.scss @@ -1,6 +1,7 @@ .PieChart { .chart-container { width: 120px; + min-height: 150px; } .legend { diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index 1c629ab505..939d6bb612 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -64,4 +64,4 @@ export class PieChart extends React.Component { ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) { return position; -}; \ No newline at end of file +}; diff --git a/src/renderer/components/chart/useRealTimeMetrics.ts b/src/renderer/components/chart/useRealTimeMetrics.ts deleted file mode 100644 index 69e4e3da7f..0000000000 --- a/src/renderer/components/chart/useRealTimeMetrics.ts +++ /dev/null @@ -1,45 +0,0 @@ -import moment from "moment"; -import { useState, useEffect } from "react"; -import { useInterval } from "../../hooks"; - -type IMetricValues = [number, string][]; -type IChartData = { x: number; y: string }[]; - -const defaultParams = { - fetchInterval: 15, - updateInterval: 5 -}; - -export function useRealTimeMetrics(metrics: IMetricValues, chartData: IChartData, params = defaultParams) { - const [index, setIndex] = useState(0); - const { fetchInterval, updateInterval } = params; - const rangeMetrics = metrics.slice(-updateInterval); - const steps = fetchInterval / updateInterval; - const data = [...chartData]; - - useEffect(() => { - setIndex(0); - }, [metrics]); - - useInterval(() => { - if (index < steps + 1) { - setIndex(index + steps - 1); - } - }, updateInterval * 1000); - - if (data.length && metrics.length) { - const lastTime = data[data.length - 1].x; - const values = []; - - for (let i = 0; i < 3; i++) { - values[i] = moment.unix(lastTime).add(i + 1, "m").unix(); - } - data.push( - { x: values[0], y: "0" }, - { x: values[1], y: parseFloat(rangeMetrics[index][1]).toFixed(3) }, - { x: values[2], y: "0" } - ); - } - - return data; -} \ No newline at end of file diff --git a/src/renderer/components/chart/zebra-stripes.plugin.ts b/src/renderer/components/chart/zebra-stripes.plugin.ts index f934f88fb2..f190401066 100644 --- a/src/renderer/components/chart/zebra-stripes.plugin.ts +++ b/src/renderer/components/chart/zebra-stripes.plugin.ts @@ -6,8 +6,6 @@ import moment, { Moment } from "moment"; import get from "lodash/get"; const defaultOptions = { - interval: 61, - stripeMinutes: 10, stripeColor: "#ffffff08", }; @@ -36,12 +34,23 @@ export const ZebraStripes = { chart.canvas.parentElement.removeChild(elem); }, + updateOptions(chart: ChartJS) { + this.options = { + ...defaultOptions, + ...this.getOptions(chart) + }; + }, + + getStripeMinutes() { + return this.options.interval < 10 ? 0 : 10; + }, + renderStripes(chart: ChartJS) { if (!chart.data.datasets.length) return; - const { interval, stripeMinutes, stripeColor } = this.options; + const { interval, stripeColor } = this.options; const { top, left, bottom, right } = chart.chartArea; const step = (right - left) / interval; - const stripeWidth = step * stripeMinutes; + const stripeWidth = step * this.getStripeMinutes(); const cover = document.createElement("div"); const styles = cover.style; @@ -61,14 +70,12 @@ export const ZebraStripes = { afterInit(chart: ChartJS) { if (!chart.data.datasets.length) return; - this.options = { - ...defaultOptions, - ...this.getOptions(chart) - }; + this.updateOptions(chart); this.updated = this.getLastUpdate(chart); }, afterUpdate(chart: ChartJS) { + this.updateOptions(chart); this.renderStripes(chart); }, @@ -95,4 +102,4 @@ export const ZebraStripes = { cover.style.backgroundPositionX = `${-step * minutes}px`; } } -}; \ No newline at end of file +}; diff --git a/src/renderer/components/checkbox/checkbox.tsx b/src/renderer/components/checkbox/checkbox.tsx index 0831e6122f..8d452a1198 100644 --- a/src/renderer/components/checkbox/checkbox.tsx +++ b/src/renderer/components/checkbox/checkbox.tsx @@ -30,7 +30,7 @@ export class Checkbox extends React.PureComponent { render() { const { label, inline, className, value, theme, children, ...inputProps } = this.props; - const componentClass = cssNames("Checkbox flex", className, { + const componentClass = cssNames("Checkbox flex align-center", className, { inline, checked: value, disabled: this.props.disabled, @@ -50,4 +50,4 @@ export class Checkbox extends React.PureComponent { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/checkbox/index.ts b/src/renderer/components/checkbox/index.ts index 7af8873e06..057f167821 100644 --- a/src/renderer/components/checkbox/index.ts +++ b/src/renderer/components/checkbox/index.ts @@ -1 +1 @@ -export * from "./checkbox"; \ No newline at end of file +export * from "./checkbox"; diff --git a/src/renderer/components/cluster-icon/cluster-icon.scss b/src/renderer/components/cluster-icon/cluster-icon.scss deleted file mode 100644 index 540cecf9eb..0000000000 --- a/src/renderer/components/cluster-icon/cluster-icon.scss +++ /dev/null @@ -1,38 +0,0 @@ -.ClusterIcon { - --size: 37px; - - position: relative; - border-radius: $radius; - padding: $radius; - user-select: none; - cursor: pointer; - - &.interactive { - img { - opacity: .55; - } - } - - &.active, &.interactive:hover { - background-color: #fff; - - img { - opacity: 1; - } - } - - img { - width: var(--size); - height: var(--size); - } - - .Badge { - position: absolute; - right: 0; - bottom: 0; - margin: -$padding; - font-size: $font-size-small; - background: $colorError; - color: white; - } -} \ No newline at end of file diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx deleted file mode 100644 index c1b0ef3577..0000000000 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import "./cluster-icon.scss"; - -import React, { DOMAttributes } from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { Params as HashiconParams } from "@emeraldpay/hashicon"; -import { Hashicon } from "@emeraldpay/hashicon-react"; -import { Cluster } from "../../../main/cluster"; -import { cssNames, IClassName } from "../../utils"; -import { Badge } from "../badge"; -import { Tooltip } from "../tooltip"; -import { subscribeToBroadcast } from "../../../common/ipc"; -import { observable } from "mobx"; - -interface Props extends DOMAttributes { - cluster: Cluster; - className?: IClassName; - errorClass?: IClassName; - showErrors?: boolean; - showTooltip?: boolean; - interactive?: boolean; - isActive?: boolean; - options?: HashiconParams; -} - -const defaultProps: Partial = { - showErrors: true, - showTooltip: true, -}; - -@observer -export class ClusterIcon extends React.Component { - static defaultProps = defaultProps as object; - - @observable eventCount = 0; - - get eventCountBroadcast() { - return `cluster-warning-event-count:${this.props.cluster.id}`; - } - - componentDidMount() { - const subscriber = subscribeToBroadcast(this.eventCountBroadcast, (ev, eventCount) => { - this.eventCount = eventCount; - }); - - disposeOnUnmount(this, [ - subscriber - ]); - } - - render() { - const { - cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, - children, ...elemProps - } = this.props; - const { name, preferences, id: clusterId } = cluster; - const eventCount = this.eventCount; - const { icon } = preferences; - const clusterIconId = `cluster-icon-${clusterId}`; - const className = cssNames("ClusterIcon flex inline", this.props.className, { - interactive: interactive !== undefined ? interactive : !!this.props.onClick, - active: isActive, - }); - - return ( -
- {showTooltip && ( - {name} - )} - {icon && {name}/} - {!icon && } - {showErrors && eventCount > 0 && !isActive && ( - = 1000 ? `${Math.ceil(eventCount / 1000)}k+` : eventCount} - /> - )} - {children} -
- ); - } -} diff --git a/src/renderer/components/cluster-icon/index.ts b/src/renderer/components/cluster-icon/index.ts deleted file mode 100644 index 4e1858939f..0000000000 --- a/src/renderer/components/cluster-icon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./cluster-icon"; \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/bottom-bar.scss b/src/renderer/components/cluster-manager/bottom-bar.scss index a40146ad97..83ca2570db 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.scss +++ b/src/renderer/components/cluster-manager/bottom-bar.scss @@ -4,9 +4,9 @@ background-color: var(--blue); padding: 0 2px; - height: 22px; + height: var(--bottom-bar-height); - #current-workspace { + #catalog-link { font-size: var(--font-size-small); color: white; padding: $padding / 4 $padding / 2; diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index 6578f4ac20..f13121f3a5 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -14,14 +14,19 @@ describe("", () => { expect(container).toBeInstanceOf(HTMLElement); }); - // some defensive testing - it("renders w/o errors when .getItems() returns edge cases", async () => { + it("renders w/o errors when .getItems() returns unexpected (not type complient) data", async () => { statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => undefined); expect(() => render()).not.toThrow(); + statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => "hello"); + expect(() => render()).not.toThrow(); + statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => 6); + expect(() => render()).not.toThrow(); statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => null); expect(() => render()).not.toThrow(); statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => []); expect(() => render()).not.toThrow(); + statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => [{}]); + expect(() => render()).not.toThrow(); statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => { return {};}); expect(() => render()).not.toThrow(); }); diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx index 489d90769f..eb82fdb6b2 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.tsx @@ -2,41 +2,55 @@ import "./bottom-bar.scss"; import React from "react"; import { observer } from "mobx-react"; +import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries"; +import { navigate } from "../../navigation"; +import { catalogURL } from "../+catalog"; import { Icon } from "../icon"; -import { WorkspaceMenu } from "../+workspaces/workspace-menu"; -import { workspaceStore } from "../../../common/workspace-store"; -import { statusBarRegistry } from "../../../extensions/registries"; @observer export class BottomBar extends React.Component { - render() { - const { currentWorkspace } = workspaceStore; - // in case .getItems() returns undefined - const items = statusBarRegistry.getItems() ?? []; + renderRegisteredItem(registration: StatusBarRegistration) { + const { item } = registration; + + if (item) { + return typeof item === "function" ? item() : item; + } + + return ; + } + + renderRegisteredItems() { + const items = statusBarRegistry.getItems(); + + if (!Array.isArray(items)) { + return; + } return ( -
-
- - {currentWorkspace.name} -
- -
- {Array.isArray(items) && items.map(({ item }, index) => { - if (!item) return; +
+ {items.map((registration, index) => { + if (!registration?.item && !registration?.components?.Item) { + return; + } - return ( -
- {typeof item === "function" ? item() : item} -
- ); - })} + return ( +
+ {this.renderRegisteredItem(registration)} +
+ ); + })} +
+ ); + } + + render() { + return ( +
+ + {this.renderRegisteredItems()}
); } diff --git a/src/renderer/components/cluster-manager/cluster-actions.tsx b/src/renderer/components/cluster-manager/cluster-actions.tsx new file mode 100644 index 0000000000..5015abbcac --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-actions.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import uniqueId from "lodash/uniqueId"; +import { clusterSettingsURL } from "../+cluster-settings"; +import { catalogURL } from "../+catalog"; + +import { clusterStore } from "../../../common/cluster-store"; +import { broadcastMessage, requestMain } from "../../../common/ipc"; +import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Cluster } from "../../../main/cluster"; +import { Tooltip } from "../../components//tooltip"; +import { IpcRendererNavigationEvents } from "../../navigation/events"; + +const navigate = (route: string) => + broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, route); + +/** + * Creates handlers for high-level actions + * that could be performed on an individual cluster + * @param cluster Cluster + */ +export const ClusterActions = (cluster: Cluster) => ({ + showSettings: () => navigate(clusterSettingsURL({ + params: { clusterId: cluster.id } + })), + disconnect: async () => { + clusterStore.deactivate(cluster.id); + navigate(catalogURL()); + await requestMain(clusterDisconnectHandler, cluster.id); + }, + remove: () => { + const tooltipId = uniqueId("tooltip_target_"); + + return ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + label: "Remove" + }, + ok: () => { + clusterStore.deactivate(cluster.id); + clusterStore.removeById(cluster.id); + navigate(catalogURL()); + }, + message:

+ Are you sure want to remove cluster {cluster.name}? + {cluster.id} +

+ }); + } +}); diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index accc72ef40..a7d081cc4d 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -1,4 +1,6 @@ .ClusterManager { + --bottom-bar-height: 22px; + display: grid; grid-template-areas: "menu main" "menu main" "bottom-bar bottom-bar"; grid-template-rows: auto 1fr min-content; @@ -9,13 +11,9 @@ grid-area: main; position: relative; display: flex; - - > * { - z-index: 1; - } } - .ClustersMenu { + .HotbarMenu { grid-area: menu; } @@ -36,4 +34,4 @@ flex: 1; } } -} \ No newline at end of file +} diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 68a358766c..6fb6646d32 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -4,20 +4,19 @@ import React from "react"; import { Redirect, Route, Switch } from "react-router"; import { comparer, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; -import { LandingPage, landingRoute, landingURL } from "../+landing-page"; +import { Catalog, catalogRoute, catalogURL } from "../+catalog"; import { Preferences, preferencesRoute } from "../+preferences"; -import { Workspaces, workspacesRoute } from "../+workspaces"; import { AddCluster, addClusterRoute } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; -import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; +import { clusterViewRoute } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { Extensions, extensionsRoute } from "../+extensions"; import { getMatchedClusterId } from "../../navigation"; +import { HotbarMenu } from "../hotbar/hotbar-menu"; @observer export class ClusterManager extends React.Component { @@ -45,17 +44,7 @@ export class ClusterManager extends React.Component { } get startUrl() { - const { activeClusterId } = clusterStore; - - if (activeClusterId) { - return clusterViewURL({ - params: { - clusterId: activeClusterId - } - }); - } - - return landingURL(); + return catalogURL(); } render() { @@ -64,10 +53,9 @@ export class ClusterManager extends React.Component {
- + - @@ -77,7 +65,7 @@ export class ClusterManager extends React.Component {
- +
); diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index ddedf145e7..8d89b7a81d 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -8,6 +8,8 @@ import { ClusterStatus } from "./cluster-status"; import { hasLoadedView } from "./lens-views"; import { Cluster } from "../../../main/cluster"; import { clusterStore } from "../../../common/cluster-store"; +import { navigate } from "../../navigation"; +import { catalogURL } from "../+catalog"; interface Props extends RouteComponentProps { } @@ -26,6 +28,9 @@ export class ClusterView extends React.Component { disposeOnUnmount(this, [ reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { fireImmediately: true, + }), + reaction(() => this.cluster.online, (online) => { + if (!online) navigate(catalogURL()); }) ]); } diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss deleted file mode 100644 index b6a1e66d1a..0000000000 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ /dev/null @@ -1,79 +0,0 @@ -.ClustersMenu { - $spacing: $padding * 2; - - position: relative; - text-align: center; - background: $clusterMenuBackground; - border-right: 1px solid $clusterMenuBorderColor; - padding: $spacing 0; - min-width: 75px; - - .is-mac &:before { - content: ""; - height: 20px; // extra spacing for mac-os "traffic-light" buttons - } - - .clusters { - @include hidden-scrollbar; - padding: 0 $spacing; // extra spacing for cluster-icon's badge - margin-bottom: $margin; - - .ClusterIcon { - margin-bottom: $margin * 1.5; - } - - &:empty { - display: none; - } - } - - > .add-cluster { - position: relative; - - .Icon { - border-radius: $radius; - padding: $padding / 3; - color: $addClusterIconColor; - background: #ffffff66; - cursor: pointer; - - &.active { - opacity: 1; - } - - &:hover { - box-shadow: none; - background: #ffffff; - } - } - - .Badge { - $boxSize: 17px; - - position: absolute; - bottom: 0px; - transform: translateX(-50%) translateY(50%); - font-size: $font-size-small; - line-height: $boxSize; - min-width: $boxSize; - min-height: $boxSize; - text-align: center; - color: white; - background: $colorSuccess; - font-weight: normal; - border-radius: $radius; - padding: 0; - pointer-events: none; - } - } - - > .extensions { - &:not(:empty) { - padding-top: $spacing; - } - - .Icon { - --size: 40px; - } - } -} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx deleted file mode 100644 index 1e79b64f85..0000000000 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import "./clusters-menu.scss"; - -import React from "react"; -import { remote } from "electron"; -import { requestMain } from "../../../common/ipc"; -import type { Cluster } from "../../../main/cluster"; -import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; -import { observer } from "mobx-react"; -import { userStore } from "../../../common/user-store"; -import { ClusterId, clusterStore } from "../../../common/cluster-store"; -import { workspaceStore } from "../../../common/workspace-store"; -import { ClusterIcon } from "../cluster-icon"; -import { Icon } from "../icon"; -import { autobind, cssNames, IClassName } from "../../utils"; -import { Badge } from "../badge"; -import { isActiveRoute, navigate } from "../../navigation"; -import { addClusterURL } from "../+add-cluster"; -import { clusterSettingsURL } from "../+cluster-settings"; -import { landingURL } from "../+landing-page"; -import { Tooltip } from "../tooltip"; -import { ConfirmDialog } from "../confirm-dialog"; -import { clusterViewURL } from "./cluster-view.route"; -import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; -import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; - -interface Props { - className?: IClassName; -} - -@observer -export class ClustersMenu extends React.Component { - showCluster = (clusterId: ClusterId) => { - navigate(clusterViewURL({ params: { clusterId } })); - }; - - addCluster = () => { - navigate(addClusterURL()); - }; - - showContextMenu = (cluster: Cluster) => { - const { Menu, MenuItem } = remote; - const menu = new Menu(); - - menu.append(new MenuItem({ - label: `Settings`, - click: () => { - navigate(clusterSettingsURL({ - params: { - clusterId: cluster.id - } - })); - } - })); - - if (cluster.online) { - menu.append(new MenuItem({ - label: `Disconnect`, - click: async () => { - if (clusterStore.isActive(cluster.id)) { - navigate(landingURL()); - clusterStore.setActive(null); - } - await requestMain(clusterDisconnectHandler, cluster.id); - } - })); - } - - if (!cluster.isManaged) { - menu.append(new MenuItem({ - label: `Remove`, - click: () => { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - label: `Remove`, - }, - ok: () => { - if (clusterStore.activeClusterId === cluster.id) { - navigate(landingURL()); - clusterStore.setActive(null); - } - clusterStore.removeById(cluster.id); - }, - message:

Are you sure want to remove cluster {cluster.contextName}?

, - }); - } - })); - } - menu.popup({ - window: remote.getCurrentWindow() - }); - }; - - @autobind() - swapClusterIconOrder(result: DropResult) { - if (result.reason === "DROP") { - const { currentWorkspaceId } = workspaceStore; - const { - source: { index: from }, - destination: { index: to }, - } = result; - - clusterStore.swapIconOrders(currentWorkspaceId, from, to); - } - } - - render() { - const { className } = this.props; - const { newContexts } = userStore; - const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); - const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled); - const activeClusterId = clusterStore.activeCluster; - - return ( -
-
- - - {({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( -
- {clusters.map((cluster, index) => { - const isActive = cluster.id === activeClusterId; - - return ( - - {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( -
- this.showCluster(cluster.id)} - onContextMenu={() => this.showContextMenu(cluster)} - /> -
- )} -
- ); - })} - {placeholder} -
- )} -
-
-
-
- - Add Cluster - - - {newContexts.size > 0 && ( - - )} -
-
- {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { - const registeredPage = globalPageRegistry.getByPageTarget(target); - - if (!registeredPage){ - return; - } - const pageUrl = getExtensionPageUrl(target); - const isActive = isActiveRoute(registeredPage.url); - - return ( - navigate(pageUrl)} - /> - ); - })} -
-
- ); - } -} diff --git a/src/renderer/components/cluster-manager/index.tsx b/src/renderer/components/cluster-manager/index.tsx index 692f1676ef..74595583aa 100644 --- a/src/renderer/components/cluster-manager/index.tsx +++ b/src/renderer/components/cluster-manager/index.tsx @@ -1 +1,2 @@ export * from "./cluster-manager"; +export * from "./cluster-actions"; diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 17e6c9f00d..a3a92e4f64 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -24,6 +24,7 @@ export async function initView(clusterId: ClusterId) { if (!cluster) { return; } + logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`); const parentElem = document.getElementById("lens-views"); const iframe = document.createElement("iframe"); @@ -36,11 +37,21 @@ export async function initView(clusterId: ClusterId) { }, { once: true }); lensViews.set(clusterId, { clusterId, view: iframe }); parentElem.appendChild(iframe); + logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); + await cluster.whenReady; await autoCleanOnRemove(clusterId, iframe); } export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { - await when(() => !clusterStore.getById(clusterId)); + await when(() => { + const cluster = clusterStore.getById(clusterId); + + if (!cluster) return true; + + const view = lensViews.get(clusterId); + + return cluster.disconnected && view?.isLoaded; + }); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); lensViews.delete(clusterId); diff --git a/src/renderer/components/command-palette/command-container.scss b/src/renderer/components/command-palette/command-container.scss new file mode 100644 index 0000000000..c8f76a4698 --- /dev/null +++ b/src/renderer/components/command-palette/command-container.scss @@ -0,0 +1,11 @@ +#command-container { + position: absolute; + top: 20px; + width: 40%; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + padding: 10px; + background-color: var(--dockInfoBackground); +} diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx new file mode 100644 index 0000000000..140523bc61 --- /dev/null +++ b/src/renderer/components/command-palette/command-container.tsx @@ -0,0 +1,83 @@ + +import "./command-container.scss"; +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import React from "react"; +import { Dialog } from "../dialog"; +import { EventEmitter } from "../../../common/event-emitter"; +import { subscribeToBroadcast } from "../../../common/ipc"; +import { CommandDialog } from "./command-dialog"; +import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry"; + +export type CommandDialogEvent = { + component: React.ReactElement +}; + +const commandDialogBus = new EventEmitter<[CommandDialogEvent]>(); + +export class CommandOverlay { + static open(component: React.ReactElement) { + commandDialogBus.emit({ component }); + } + + static close() { + commandDialogBus.emit({ component: null }); + } +} + +@observer +export class CommandContainer extends React.Component<{ clusterId?: string }> { + @observable.ref commandComponent: React.ReactElement; + + private escHandler(event: KeyboardEvent) { + if (event.key === "Escape") { + event.stopPropagation(); + this.closeDialog(); + } + } + + @action + private closeDialog() { + this.commandComponent = null; + } + + private findCommandById(commandId: string) { + return commandRegistry.getItems().find((command) => command.id === commandId); + } + + private runCommand(command: CommandRegistration) { + command.action({ + entity: commandRegistry.activeEntity + }); + } + + componentDidMount() { + if (this.props.clusterId) { + subscribeToBroadcast(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => { + const command = this.findCommandById(commandId); + + if (command) { + this.runCommand(command); + } + }); + } else { + subscribeToBroadcast("command-palette:open", () => { + CommandOverlay.open(); + }); + } + window.addEventListener("keyup", (e) => this.escHandler(e), true); + commandDialogBus.addListener((event) => { + this.commandComponent = event.component; + }); + } + + render() { + return ( + this.commandComponent = null}> +
+ {this.commandComponent} +
+
+ ); + } +} diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx new file mode 100644 index 0000000000..c784612345 --- /dev/null +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -0,0 +1,85 @@ + +import { Select } from "../select"; +import { computed, observable, toJS } from "mobx"; +import { observer } from "mobx-react"; +import React from "react"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { clusterStore } from "../../../common/cluster-store"; +import { CommandOverlay } from "./command-container"; +import { broadcastMessage } from "../../../common/ipc"; +import { navigate } from "../../navigation"; +import { clusterViewURL } from "../cluster-manager/cluster-view.route"; + +@observer +export class CommandDialog extends React.Component { + @observable menuIsOpen = true; + + @computed get options() { + const context = { + entity: commandRegistry.activeEntity + }; + + return commandRegistry.getItems().filter((command) => { + if (command.scope === "entity" && !clusterStore.active) { + return false; + } + + if (!command.isActive) { + return true; + } + + try { + return command.isActive(context); + } catch(e) { + console.error(e); + + return false; + } + }).map((command) => { + return { value: command.id, label: command.title }; + }).sort((a, b) => a.label > b.label ? 1 : -1); + } + + private onChange(value: string) { + const command = commandRegistry.getItems().find((cmd) => cmd.id === value); + + if (!command) { + return; + } + + const action = toJS(command.action); + + try { + CommandOverlay.close(); + + if (command.scope === "global") { + action({ + entity: commandRegistry.activeEntity + }); + } else if(commandRegistry.activeEntity) { + navigate(clusterViewURL({ + params: { + clusterId: commandRegistry.activeEntity.metadata.uid + } + })); + broadcastMessage(`command-palette:run-action:${commandRegistry.activeEntity.metadata.uid}`, command.id); + } + } catch(error) { + console.error("[COMMAND-DIALOG] failed to execute command", command.id, error); + } + } + + render() { + return ( + { onChange={this.onNamespaceChange} /> { controls={panelControls} error={this.error} submit={install} - submitLabel={`Install`} - submittingMessage={`Installing...`} + submitLabel="Install" + submittingMessage="Installing..." showSubmitClose={false} /> ) => void + reload: () => void +} + +export const LogControls = observer((props: Props) => { + const { tabData, save, reload, logs } = props; + const { showTimestamps, previous } = tabData; + const since = logs.length ? logStore.getTimestamps(logs[0]) : null; + const pod = new Pod(tabData.selectedPod); + + const toggleTimestamps = () => { + save({ showTimestamps: !showTimestamps }); + }; + + const togglePrevious = () => { + save({ previous: !previous }); + reload(); + }; + + const downloadLogs = () => { + const fileName = pod.getName(); + const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps; + + saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain"); + }; + + return ( +
+
+ {since && ( + + Logs from{" "} + {new Date(since[0]).toLocaleString()} + + )} +
+
+ + + +
+
+ ); +}); diff --git a/src/renderer/components/dock/pod-log-list.scss b/src/renderer/components/dock/log-list.scss similarity index 99% rename from src/renderer/components/dock/pod-log-list.scss rename to src/renderer/components/dock/log-list.scss index 9b923b520b..8a39dcf925 100644 --- a/src/renderer/components/dock/pod-log-list.scss +++ b/src/renderer/components/dock/log-list.scss @@ -1,4 +1,4 @@ -.PodLogList { +.LogList { --overlay-bg: #8cc474b8; --overlay-active-bg: orange; diff --git a/src/renderer/components/dock/pod-log-list.tsx b/src/renderer/components/dock/log-list.tsx similarity index 90% rename from src/renderer/components/dock/pod-log-list.tsx rename to src/renderer/components/dock/log-list.tsx index c876d0c362..fc6efacdfa 100644 --- a/src/renderer/components/dock/pod-log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -1,4 +1,4 @@ -import "./pod-log-list.scss"; +import "./log-list.scss"; import React from "react"; import AnsiUp from "ansi_up"; @@ -8,13 +8,14 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Align, ListOnScrollProps } from "react-window"; -import { searchStore } from "../../../common/search-store"; +import { SearchStore, searchStore } from "../../../common/search-store"; import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; -import { podLogsStore } from "./pod-logs.store"; +import { logStore } from "./log.store"; +import { logTabStore } from "./log-tab.store"; interface Props { logs: string[] @@ -26,7 +27,7 @@ interface Props { const colorConverter = new AnsiUp(); @observer -export class PodLogList extends React.Component { +export class LogList extends React.Component { @observable isJumpButtonVisible = false; @observable isLastLineVisible = true; @@ -77,10 +78,10 @@ export class PodLogList extends React.Component { */ @computed get logs() { - const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps; + const showTimestamps = logTabStore.getData(this.props.id).showTimestamps; if (!showTimestamps) { - return podLogsStore.logsWithoutTimestamps; + return logStore.logsWithoutTimestamps; } return this.props.logs; @@ -163,7 +164,7 @@ export class PodLogList extends React.Component { if (searchQuery) { // If search is enabled, replace keyword with backgrounded // Case-insensitive search (lowercasing query and keywords in line) - const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi"); + const regex = new RegExp(SearchStore.escapeRegex(searchQuery), "gi"); const matches = item.matchAll(regex); const modified = item.replace(regex, match => match.toLowerCase()); // Splitting text line by keyword @@ -206,19 +207,23 @@ export class PodLogList extends React.Component { const rowHeights = new Array(this.logs.length).fill(this.lineHeight); if (isInitLoading) { - return ; + return ( +
+ +
+ ); } if (!this.logs.length) { return ( -
+
There are no logs available for container
); } return ( -
+
) => void + reload: () => void +} + +export const LogResourceSelector = observer((props: Props) => { + const { tabData, save, reload, tabId } = props; + const { selectedPod, selectedContainer, pods } = tabData; + const pod = new Pod(selectedPod); + const containers = pod.getContainers(); + const initContainers = pod.getInitContainers(); + + const onContainerChange = (option: SelectOption) => { + save({ + selectedContainer: containers + .concat(initContainers) + .find(container => container.name === option.value) + }); + reload(); + }; + + const onPodChange = (option: SelectOption) => { + const selectedPod = podsStore.getByName(option.value, pod.getNs()); + + save({ selectedPod }); + logTabStore.renameTab(tabId); + }; + + const getSelectOptions = (items: string[]) => { + return items.map(item => { + return { + value: item, + label: item + }; + }); + }; + + const containerSelectOptions = [ + { + label: `Containers`, + options: getSelectOptions(containers.map(container => container.name)) + }, + { + label: `Init Containers`, + options: getSelectOptions(initContainers.map(container => container.name)), + } + ]; + + const podSelectOptions = [ + { + label: pod.getOwnerRefs()[0]?.name, + options: getSelectOptions(pods.map(pod => pod.metadata.name)) + } + ]; + + useEffect(() => { + reload(); + }, [selectedPod]); + + return ( +
+ Namespace + Pod + +
+ ); +}); diff --git a/src/renderer/components/dock/pod-log-search.scss b/src/renderer/components/dock/log-search.scss similarity index 61% rename from src/renderer/components/dock/pod-log-search.scss rename to src/renderer/components/dock/log-search.scss index 7d3ea9d92a..eec9a74749 100644 --- a/src/renderer/components/dock/pod-log-search.scss +++ b/src/renderer/components/dock/log-search.scss @@ -1,10 +1,13 @@ -.PodLogsSearch { +.LogSearch { .SearchInput { min-width: 150px; - width: 150px; .find-count { margin-left: 2px; } + + label { + padding-bottom: 7px; + } } } \ No newline at end of file diff --git a/src/renderer/components/dock/pod-log-search.tsx b/src/renderer/components/dock/log-search.tsx similarity index 83% rename from src/renderer/components/dock/pod-log-search.tsx rename to src/renderer/components/dock/log-search.tsx index 675c556c3e..c2bccec59b 100644 --- a/src/renderer/components/dock/pod-log-search.tsx +++ b/src/renderer/components/dock/log-search.tsx @@ -1,4 +1,4 @@ -import "./pod-log-search.scss"; +import "./log-search.scss"; import React, { useEffect } from "react"; import { observer } from "mobx-react"; @@ -16,7 +16,7 @@ interface Props extends PodLogSearchProps { logs: string[] } -export const PodLogSearch = observer((props: Props) => { +export const LogSearch = observer((props: Props) => { const { logs, onSearch, toPrevOverlay, toNextOverlay } = props; const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore; const jumpDisabled = !searchQuery || !occurrences.length; @@ -57,32 +57,27 @@ export const PodLogSearch = observer((props: Props) => { }, [logs]); return ( -
+
0 && findCounts} onClear={onClear} onKeyDown={onKeyDown} /> -
); }); diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts new file mode 100644 index 0000000000..ca5d49873b --- /dev/null +++ b/src/renderer/components/dock/log-tab.store.ts @@ -0,0 +1,123 @@ +import uniqueId from "lodash/uniqueId"; +import { reaction } from "mobx"; +import { podsStore } from "../+workloads-pods/pods.store"; + +import { IPodContainer, Pod } from "../../api/endpoints"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; +import { DockTabStore } from "./dock-tab.store"; +import { dockStore, IDockTab, TabKind } from "./dock.store"; + +export interface LogTabData { + pods: Pod[]; + selectedPod: Pod; + selectedContainer: IPodContainer + showTimestamps?: boolean + previous?: boolean +} + +interface PodLogsTabData { + selectedPod: Pod + selectedContainer: IPodContainer +} + +interface WorkloadLogsTabData { + workload: WorkloadKubeObject +} + +export class LogTabStore extends DockTabStore { + constructor() { + super({ + storageKey: "pod_logs" + }); + + reaction(() => podsStore.items.length, () => { + this.updateTabsData(); + }); + } + + createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void { + const podOwner = selectedPod.getOwnerRefs()[0]; + const pods = podsStore.getPodsByOwnerId(podOwner?.uid); + const title = `Pod ${selectedPod.getName()}`; + + this.createLogsTab(title, { + pods: pods.length ? pods : [selectedPod], + selectedPod, + selectedContainer + }); + } + + createWorkloadTab({ workload }: WorkloadLogsTabData): void { + const pods = podsStore.getPodsByOwnerId(workload.getId()); + + if (!pods.length) return; + + const selectedPod = pods[0]; + const selectedContainer = selectedPod.getAllContainers()[0]; + const title = `${workload.kind} ${selectedPod.getName()}`; + + this.createLogsTab(title, { + pods, + selectedPod, + selectedContainer + }); + } + + renameTab(tabId: string) { + const { selectedPod } = this.getData(tabId); + + dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`); + } + + private createDockTab(tabParams: Partial) { + dockStore.createTab({ + kind: TabKind.POD_LOGS, + ...tabParams + }, false); + } + + private createLogsTab(title: string, data: LogTabData) { + const id = uniqueId("log-tab-"); + + this.createDockTab({ id, title }); + this.setData(id, { + ...data, + showTimestamps: false, + previous: false + }); + } + + private updateTabsData() { + this.data.forEach((tabData, tabId) => { + const pod = new Pod(tabData.selectedPod); + const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid); + const isSelectedPodInList = pods.find(item => item.getId() == pod.getId()); + const selectedPod = isSelectedPodInList ? pod : pods[0]; + const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0]; + + if (pods.length) { + this.setData(tabId, { + ...tabData, + selectedPod, + selectedContainer, + pods + }); + + this.renameTab(tabId); + } else { + this.closeTab(tabId); + } + }); + } + + private closeTab(tabId: string) { + this.clearData(tabId); + dockStore.closeTab(tabId); + } +} + +export const logTabStore = new LogTabStore(); + +export function isLogsTab(tab: IDockTab) { + return tab && tab.kind === TabKind.POD_LOGS; +} diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/log.store.ts similarity index 57% rename from src/renderer/components/dock/pod-logs.store.ts rename to src/renderer/components/dock/log.store.ts index 9e69a15b7f..ec80cf4d58 100644 --- a/src/renderer/components/dock/pod-logs.store.ts +++ b/src/renderer/components/dock/log.store.ts @@ -1,27 +1,16 @@ -import { autorun, computed, observable, reaction } from "mobx"; -import { Pod, IPodContainer, podsApi, IPodLogsQuery } from "../../api/endpoints"; +import { autorun, computed, observable } from "mobx"; + +import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints"; import { autobind, interval } from "../../utils"; -import { DockTabStore } from "./dock-tab.store"; -import { dockStore, IDockTab, TabKind } from "./dock.store"; -import { searchStore } from "../../../common/search-store"; +import { dockStore, TabId } from "./dock.store"; +import { isLogsTab, logTabStore } from "./log-tab.store"; -export interface IPodLogsData { - pod: Pod; - selectedContainer: IPodContainer - containers: IPodContainer[] - initContainers: IPodContainer[] - showTimestamps: boolean - previous: boolean -} - -type TabId = string; type PodLogLine = string; -// Number for log lines to load -export const logRange = 500; +const logLinesToLoad = 500; @autobind() -export class PodLogsStore extends DockTabStore { +export class LogStore { private refresher = interval(10, () => { const id = dockStore.selectedTabId; @@ -30,30 +19,17 @@ export class PodLogsStore extends DockTabStore { }); @observable podLogs = observable.map(); - @observable newLogSince = observable.map(); // Timestamp after which all logs are considered to be new constructor() { - super({ - storageName: "pod_logs" - }); autorun(() => { const { selectedTab, isOpen } = dockStore; - if (isPodLogsTab(selectedTab) && isOpen) { + if (isLogsTab(selectedTab) && isOpen) { this.refresher.start(); } else { this.refresher.stop(); } }, { delay: 500 }); - - reaction(() => this.podLogs.get(dockStore.selectedTabId), () => { - this.setNewLogSince(dockStore.selectedTabId); - }); - - reaction(() => dockStore.selectedTabId, () => { - // Clear search query on tab change - searchStore.reset(); - }); } /** @@ -66,7 +42,7 @@ export class PodLogsStore extends DockTabStore { load = async (tabId: TabId) => { try { const logs = await this.loadLogs(tabId, { - tailLines: this.lines + logRange + tailLines: this.lines + logLinesToLoad }); this.refresher.start(); @@ -83,9 +59,9 @@ export class PodLogsStore extends DockTabStore { }; /** - * Function is used to refreser/stream-like requests. + * Function is used to refresher/stream-like requests. * It changes 'sinceTime' param each time allowing to fetch logs - * starting from last line recieved. + * starting from last line received. * @param tabId */ loadMore = async (tabId: TabId) => { @@ -107,15 +83,15 @@ export class PodLogsStore extends DockTabStore { * @returns {Promise} A fetch request promise */ loadLogs = async (tabId: TabId, params: Partial) => { - const data = this.getData(tabId); + const data = logTabStore.getData(tabId); const { selectedContainer, previous } = data; - const pod = new Pod(data.pod); + const pod = new Pod(data.selectedPod); const namespace = pod.getNs(); const name = pod.getName(); return podsApi.getLogs({ namespace, name }, { ...params, - timestamps: true, // Always setting timestampt to separate old logs from new ones + timestamps: true, // Always setting timestamp to separate old logs from new ones container: selectedContainer.name, previous }).then(result => { @@ -127,17 +103,6 @@ export class PodLogsStore extends DockTabStore { }); }; - /** - * Sets newLogSince separator timestamp to split old logs from new ones - * @param tabId - */ - setNewLogSince(tabId: TabId) { - if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return; - const timestamp = this.getLastSinceTime(tabId); - - this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string - } - /** * Converts logs into a string array * @returns {number} Length of log lines @@ -155,11 +120,7 @@ export class PodLogsStore extends DockTabStore { * Returns logs with timestamps for selected tab */ get logs() { - const id = dockStore.selectedTabId; - - if (!this.podLogs.has(id)) return []; - - return this.podLogs.get(id); + return this.podLogs.get(dockStore.selectedTabId) ?? []; } /** @@ -196,37 +157,6 @@ export class PodLogsStore extends DockTabStore { clearLogs(tabId: TabId) { this.podLogs.delete(tabId); } - - clearData(tabId: TabId) { - this.data.delete(tabId); - this.clearLogs(tabId); - } } -export const podLogsStore = new PodLogsStore(); - -export function createPodLogsTab(data: IPodLogsData, tabParams: Partial = {}) { - const podId = data.pod.getId(); - let tab = dockStore.getTabById(podId); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - - return; - } - // If no existent tab found - tab = dockStore.createTab({ - id: podId, - kind: TabKind.POD_LOGS, - title: data.pod.getName(), - ...tabParams - }, false); - podLogsStore.setData(tab.id, data); - - return tab; -} - -export function isPodLogsTab(tab: IDockTab) { - return tab && tab.kind === TabKind.POD_LOGS; -} +export const logStore = new LogStore(); diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/logs.tsx similarity index 53% rename from src/renderer/components/dock/pod-logs.tsx rename to src/renderer/components/dock/logs.tsx index 696c2bf0ab..0aa31f95fb 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/logs.tsx @@ -6,9 +6,12 @@ import { searchStore } from "../../../common/search-store"; import { autobind } from "../../utils"; import { IDockTab } from "./dock.store"; import { InfoPanel } from "./info-panel"; -import { PodLogControls } from "./pod-log-controls"; -import { PodLogList } from "./pod-log-list"; -import { IPodLogsData, podLogsStore } from "./pod-logs.store"; +import { LogResourceSelector } from "./log-resource-selector"; +import { LogList } from "./log-list"; +import { logStore } from "./log.store"; +import { LogSearch } from "./log-search"; +import { LogControls } from "./log-controls"; +import { LogTabData, logTabStore } from "./log-tab.store"; interface Props { className?: string @@ -16,10 +19,10 @@ interface Props { } @observer -export class PodLogs extends React.Component { +export class Logs extends React.Component { @observable isLoading = true; - private logListElement = React.createRef(); // A reference for VirtualList component + private logListElement = React.createRef(); // A reference for VirtualList component componentDidMount() { disposeOnUnmount(this, @@ -28,7 +31,7 @@ export class PodLogs extends React.Component { } get tabData() { - return podLogsStore.getData(this.tabId); + return logTabStore.getData(this.tabId); } get tabId() { @@ -36,18 +39,18 @@ export class PodLogs extends React.Component { } @autobind() - save(data: Partial) { - podLogsStore.setData(this.tabId, { ...this.tabData, ...data }); + save(data: Partial) { + logTabStore.setData(this.tabId, { ...this.tabData, ...data }); } load = async () => { this.isLoading = true; - await podLogsStore.load(this.tabId); + await logStore.load(this.tabId); this.isLoading = false; }; reload = async () => { - podLogsStore.clearLogs(this.tabId); + logStore.clearLogs(this.tabId); await this.load(); }; @@ -79,39 +82,56 @@ export class PodLogs extends React.Component { }, 100); } - render() { - const logs = podLogsStore.logs; - + renderResourceSelector() { + const logs = logStore.logs; + const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps; const controls = ( - +
+ + +
); + return ( + + ); + } + + render() { + const logs = logStore.logs; + return (
- - +
); } diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx deleted file mode 100644 index bd6cb9db15..0000000000 --- a/src/renderer/components/dock/pod-log-controls.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import "./pod-log-controls.scss"; -import React from "react"; -import { observer } from "mobx-react"; -import { IPodLogsData, podLogsStore } from "./pod-logs.store"; -import { Select, SelectOption } from "../select"; -import { Badge } from "../badge"; -import { Icon } from "../icon"; -import { cssNames, saveFileDialog } from "../../utils"; -import { Pod } from "../../api/endpoints"; -import { PodLogSearch, PodLogSearchProps } from "./pod-log-search"; - -interface Props extends PodLogSearchProps { - ready: boolean - tabId: string - tabData: IPodLogsData - logs: string[] - save: (data: Partial) => void - reload: () => void - onSearch: (query: string) => void -} - -export const PodLogControls = observer((props: Props) => { - const { tabData, save, reload, logs } = props; - const { selectedContainer, showTimestamps, previous } = tabData; - const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null; - const pod = new Pod(tabData.pod); - - const toggleTimestamps = () => { - save({ showTimestamps: !showTimestamps }); - }; - - const togglePrevious = () => { - save({ previous: !previous }); - reload(); - }; - - const downloadLogs = () => { - const fileName = selectedContainer ? selectedContainer.name : pod.getName(); - const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps; - - saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain"); - }; - - const onContainerChange = (option: SelectOption) => { - const { containers, initContainers } = tabData; - - save({ - selectedContainer: containers - .concat(initContainers) - .find(container => container.name === option.value) - }); - reload(); - }; - - const containerSelectOptions = () => { - const { containers, initContainers } = tabData; - - return [ - { - label: `Containers`, - options: containers.map(container => { - return { value: container.name }; - }), - }, - { - label: `Init Containers`, - options: initContainers.map(container => { - return { value: container.name }; - }), - } - ]; - }; - - const formatOptionLabel = (option: SelectOption) => { - const { value, label } = option; - - return label || <> {value}; - }; - - return ( -
- Pod: - Namespace: - Container -