diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml
index 73fe5f9ee3..afec611177 100644
--- a/.azure-pipelines.yml
+++ b/.azure-pipelines.yml
@@ -19,27 +19,26 @@ jobs:
node_12.x:
node_version: 12.x
steps:
- - displayName: Set the tag name as an environment variable
- powershell: |
+ - powershell: |
$CI_BUILD_TAG = git describe --tags
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
+ displayName: Set the tag name as an environment variable
- - displayName: Install Node.js
- task: NodeTool@0
+ - task: NodeTool@0
inputs:
versionSpec: $(node_version)
+ displayName: Install Node.js
- - displayName: Cache Yarn packages
- task: Cache@2
+ - task: Cache@2
inputs:
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
restoreKeys: |
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
+ displayName: Cache Yarn packages
- - displayName: Customize config
- bash: |
+ - bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
@@ -47,15 +46,15 @@ jobs:
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
+ displayName: Customize config
- - displayName: Install dependencies
- script: make node_modules
+ - script: make node_modules
+ displayName: Install dependencies
- - displayName: Generate npm package
- script: make build-npm
+ - script: make build-npm
+ displayName: Generate npm package
- - displayName: Build
- script: make build
+ - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
WIN_CSC_LINK: $(WIN_CSC_LINK)
@@ -63,6 +62,7 @@ jobs:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
BUILD_NUMBER: $(Build.BuildNumber)
+ displayName: Build
- job: macOS
pool:
@@ -72,25 +72,24 @@ jobs:
node_12.x:
node_version: 12.x
steps:
- - displayName: Set the tag name as an environment variable
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
+ - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
+ displayName: Set the tag name as an environment variable
- - displayName: Install Node.js
- task: NodeTool@0
+ - task: NodeTool@0
inputs:
versionSpec: $(node_version)
+ displayName: Install Node.js
- - displayName: Cache Yarn packages
- task: Cache@2
+ - task: Cache@2
inputs:
key: 'yarn | "$(Agent.OS)" | yarn.lock'
restoreKeys: |
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
+ displayName: Cache Yarn packages
- - displayName: Customize config
- bash: |
+ - bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
@@ -98,15 +97,15 @@ jobs:
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
+ displayName: Customize config
- - displayName: Install dependencies
- script: make node_modules
+ - script: make node_modules
+ displayName: Install dependencies
- - displayName: Generate npm package
- script: make build-npm
+ - script: make build-npm
+ displayName: Generate npm package
- - displayName: Build
- script: make build
+ - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
APPLEID: $(APPLEID)
@@ -116,6 +115,7 @@ jobs:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
BUILD_NUMBER: $(Build.BuildNumber)
+ displayName: Build
- job: Linux
pool:
@@ -125,25 +125,24 @@ jobs:
node_12.x:
node_version: 12.x
steps:
- - displayName: Set the tag name as an environment variable
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
+ - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
+ displayName: Set the tag name as an environment variable
- - displayName: Install Node.js
- task: NodeTool@0
+ - task: NodeTool@0
inputs:
versionSpec: $(node_version)
+ displayName: Install Node.js
- - displayName: Cache Yarn packages
- task: Cache@2
+ - task: Cache@2
inputs:
key: 'yarn | "$(Agent.OS)" | yarn.lock'
restoreKeys: |
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
+ displayName: Cache Yarn packages
- - displayName: Customize config
- bash: |
+ - bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
@@ -151,15 +150,15 @@ jobs:
jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
+ displayName: Customize config
- - displayName: Install dependencies
- script: make node_modules
+ - script: make node_modules
+ displayName: Install dependencies
- - displayName: Generate npm package
- script: make build-npm
+ - script: make build-npm
+ displayName: Generate npm package
- - displayName: Setup snapcraft
- bash: |
+ - bash: |
sudo chown root:root /
sudo apt-get update && sudo apt-get install -y snapd
sudo snap install snapcraft --classic
@@ -168,11 +167,12 @@ jobs:
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
SNAP_LOGIN: $(SNAP_LOGIN)
+ displayName: Setup snapcraft
- - displayName: Build
- script: make build
+ - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
BUILD_NUMBER: $(Build.BuildNumber)
+ displayName: Build
diff --git a/.dependabot/config.yml b/.dependabot/config.yml
deleted file mode 100644
index 10842da386..0000000000
--- a/.dependabot/config.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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
- ignore:
- - dependency-name: "*"
- update-types:
- - version-update:semver-major
diff --git a/.eslintrc.js b/.eslintrc.js
index 09e6bd3819..7ef30d22ff 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -129,6 +129,7 @@ module.exports = {
"avoidEscape": true,
"allowTemplateLiterals": true,
}],
+ "react/prop-types": "off",
"semi": "off",
"@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"],
@@ -196,6 +197,7 @@ module.exports = {
"avoidEscape": true,
"allowTemplateLiterals": true,
}],
+ "react/prop-types": "off",
"semi": "off",
"@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"],
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9770f52991..822722ed6c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,9 +1,19 @@
-# Please see the documentation for all configuration options:
-# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+# 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" # See documentation for possible values
- directory: "/" # Location of package manifests
+ - package-ecosystem: npm
+ directory: /
schedule:
- interval: "daily"
+ interval: daily
+ open-pull-requests-limit: 4
+ reviewers:
+ - lensapp/lens-ide
+ labels:
+ - dependencies
+ versioning-strategy: increase
+ ignore:
+ - dependency-name: "*"
+ update-types:
+ - version-update:semver-major
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index f7b3529b74..93536874e2 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -10,6 +10,7 @@ categories:
- title: '🧰 Maintenance'
labels:
- 'chore'
+ - 'area/documentation'
- 'area/ci'
- 'area/tests'
- 'dependencies'
diff --git a/Makefile b/Makefile
index f2c9d0eb13..d9a5fe1a83 100644
--- a/Makefile
+++ b/Makefile
@@ -44,7 +44,7 @@ release-version:
.PHONY: tag-release
tag-release:
- scripts/tag-release.sh
+ scripts/tag-release.sh $(CMD_ARGS)
.PHONY: test
test: binaries/client
@@ -70,7 +70,7 @@ integration-win: binaries/client build-extension-types build-extensions
.PHONY: build
build: node_modules binaries/client
yarn run npm:fix-build-version
- $(MAKE) build-extensions
+ $(MAKE) build-extensions -B
yarn run compile
ifeq "$(DETECTED_OS)" "Windows"
yarn run electron-builder --publish onTag --x64 --ia32
diff --git a/build/set_build_version.ts b/build/set_build_version.ts
index 80fd385c59..a4c45dd167 100644
--- a/build/set_build_version.ts
+++ b/build/set_build_version.ts
@@ -33,6 +33,7 @@ function getBuildChannel(): string {
* Note: it is by design that we don't use `rc` as a build channel for these versions
*/
switch (versionInfo.prerelease?.[0]) {
+ case "rc":
case "beta":
return "beta";
case undefined:
diff --git a/docs/README.md b/docs/README.md
index f98403eb75..67369f86fc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -35,6 +35,14 @@ Just like Lens itself, the extension API updates on a monthly cadence, rolling o
Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases).
+## Important changes since Lens v4
+
+Lens has undergone major design improvements in v5, which have resulted in several large changes to the extension API.
+Workspaces are gone, and the catalog is introduced for containing clusters, as well as other items, including custom entities.
+Lens has migrated from using mobx 5 to mobx 6 for internal state management, and this may have ramifications for extension implementations.
+Although the API retains many components from v4, given these changes, extensions written for Lens v4 are not compatible with the Lens v5 extension API.
+See the [Lens v4 to v5 extension migration notes](extensions/extension-migration.md) on getting old extensions working in Lens v5.
+
## Looking for Help
If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time.
@@ -43,4 +51,4 @@ To provide feedback on the documentation or issues with the Lens Extension API,
## Downloading Lens
-[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.
+[Download Lens](https://k8slens.dev/) for macOS, Windows, or Linux.
diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md
index a399a7bc03..4993e94181 100644
--- a/docs/extensions/capabilities/common-capabilities.md
+++ b/docs/extensions/capabilities/common-capabilities.md
@@ -189,36 +189,6 @@ export default class ExampleExtension extends Renderer.LensExtension {
```
-### Cluster Features
-
-This extension can register installable features for a cluster.
-These features are visible in the "Cluster Settings" page.
-
-```typescript
-import React from "react"
-import { Renderer } from "@k8slens/extensions"
-import { MyCustomFeature } from "./src/my-custom-feature"
-
-export default class ExampleExtension extends Renderer.LensExtension {
- clusterFeatures = [
- {
- title: "My Custom Feature",
- components: {
- Description: () => {
- return (
-
- Just an example.
-
- )
- }
- },
- feature: new MyCustomFeature()
- }
- ]
-}
-
-```
-
### Top Bar Items
This extension can register custom components to a top bar area.
diff --git a/docs/extensions/extension-migration.md b/docs/extensions/extension-migration.md
new file mode 100644
index 0000000000..93ccd0d923
--- /dev/null
+++ b/docs/extensions/extension-migration.md
@@ -0,0 +1,24 @@
+# Lens v4 to v5 Extension Migration Notes
+
+* Lens v5 inspects the version of the extension to ensure it is compatible.
+The `package.json` for your extension must have an `"engines"` field specifying the lens version that your extension is targeted for, e.g:
+```
+ "engines": {
+ "lens": "^5.0.0-beta.7"
+ },
+```
+Note that Lens v5 supports all the range semantics that [semver](https://www.npmjs.com/package/semver) provides.
+* Types and components have been reorganized, many have been grouped by process (`Main` and `Renderer`) plus those not specific to a process (`Common`).
+For example the `LensMainExtension` class is now referred to by `Main.LensExtension`.
+See the [API Reference](api/README.md) for the new organization.
+* The `globalPageMenus` field of the Renderer extension class (now `Renderer.LensExtension`) is removed.
+Global pages can still be made accessible via the application menus and the status bar, as well as from the newly added Welcome menu.
+* The `clusterFeatures` field of the Renderer extension class (now `Renderer.LensExtension`) is removed.
+Cluster features can still be implemented but Lens no longer dictates how a feature's lifecycle (install/upgrade/uninstall) is managed.
+`Renderer.K8sApi.ResourceStack` provides the functionality to input and apply kubernetes resources to a cluster.
+It is up to the extension developer to manage the lifecycle.
+It could be applied automatically to a cluster by the extension or the end-user could be expected to install it, etc. from the cluster **Settings** page.
+* Lens v5 now relies on mobx 6 for state management. Extensions that use mobx will need to be modified to work with mobx 6.
+See [Migrating from Mobx 4/5](https://mobx.js.org/migrating-from-4-or-5.html) for specific details.
+
+For an example of an existing extension that is compatible with Lens v5 see the [Lens Resource Map Extension](https://github.com/nevalla/lens-resource-map-extension)
diff --git a/docs/extensions/get-started/your-first-extension.md b/docs/extensions/get-started/your-first-extension.md
index fa570e6932..c8c1167943 100644
--- a/docs/extensions/get-started/your-first-extension.md
+++ b/docs/extensions/get-started/your-first-extension.md
@@ -78,7 +78,7 @@ npm run dev
You must restart Lens for the extension to load.
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
-With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/latest/clusters/adding-clusters) for details on how to add a cluster in Lens IDE.
+With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE.
You will see the "Hello World" page in the left-side cluster menu.
## Develop the Extension
diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md
index c1c2521f5a..434aec7444 100644
--- a/docs/extensions/guides/README.md
+++ b/docs/extensions/guides/README.md
@@ -17,8 +17,9 @@ Each guide or code sample includes the following:
| Guide | APIs |
| ----- | ----- |
| [Generate new extension project](generator.md) ||
-| [Main process extension](main-extension.md) | LensMainExtension |
-| [Renderer process extension](renderer-extension.md) | LensRendererExtension |
+| [Main process extension](main-extension.md) | Main.LensExtension |
+| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
+| [Resource stack (cluster feature)](resource-stack.md) | |
| [Stores](stores.md) | |
| [Components](components.md) | |
| [KubeObjectListLayout](kube-object-list-layout.md) | |
diff --git a/docs/extensions/guides/catalog.md b/docs/extensions/guides/catalog.md
new file mode 100644
index 0000000000..5425382638
--- /dev/null
+++ b/docs/extensions/guides/catalog.md
@@ -0,0 +1,5 @@
+# Catalog (WIP)
+
+## CatalogCategoryRegistry
+
+## CatalogEntityRegistry
\ No newline at end of file
diff --git a/docs/extensions/guides/images/clusterfeature.png b/docs/extensions/guides/images/clusterfeature.png
new file mode 100644
index 0000000000..ada8beba6f
Binary files /dev/null and b/docs/extensions/guides/images/clusterfeature.png differ
diff --git a/docs/extensions/guides/images/clusterpagemenus.png b/docs/extensions/guides/images/clusterpagemenus.png
index 3ed1c79e5b..77a3ec118a 100644
Binary files a/docs/extensions/guides/images/clusterpagemenus.png and b/docs/extensions/guides/images/clusterpagemenus.png differ
diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md
index 32d24acdef..8a501ca533 100644
--- a/docs/extensions/guides/main-extension.md
+++ b/docs/extensions/guides/main-extension.md
@@ -2,13 +2,14 @@
The Main Extension API is the interface to Lens's main process.
Lens runs in both main and renderer processes.
-The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items, and run custom code in Lens's main process.
+The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
+It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
-## `LensMainExtension` Class
+## `Main.LensExtension` Class
### `onActivate()` and `onDeactivate()` Methods
-To create a main extension simply extend the `LensMainExtension` class:
+To create a main extension simply extend the `Main.LensExtension` class:
```typescript
import { Main } from "@k8slens/extensions";
@@ -75,3 +76,27 @@ Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
In this example, we simply log a message.
However, you would typically have this navigate to a specific page or perform another operation.
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
+
+The following example demonstrates how an application menu can be used to navigate to such a page:
+
+``` typescript
+import { Main } from "@k8slens/extensions";
+
+export default class SamplePageMainExtension extends Main.LensExtension {
+ appMenus = [
+ {
+ parentId: "help",
+ label: "Sample",
+ click: () => this.navigate("myGlobalPage")
+ }
+ ]
+}
+```
+
+When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
+This page would be defined in your extension's `Renderer.LensExtension` implmentation (See [`Renderer.LensExtension`](renderer-extension.md)).
+
+### `addCatalogSource()` and `removeCatalogSource()` Methods
+
+The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
+See the [`Catalog`](catalog.md) documentation for full details about the catalog.
\ No newline at end of file
diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md
index c96391fde0..527dc0c623 100644
--- a/docs/extensions/guides/renderer-extension.md
+++ b/docs/extensions/guides/renderer-extension.md
@@ -1,27 +1,40 @@
-# Renderer Extension
+# Renderer Extension (WIP)
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 Renderer Extension API allows you to access, configure, and customize Lens data, add custom Lens UI elements, protocol handlers, and command palette commands, as well as 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)
+* [Welcome menus](#welcomemenus)
* [App preferences](#apppreferences)
+* [Top bar items](#topbaritems)
* [Status bar items](#statusbaritems)
* [KubeObject menu items](#kubeobjectmenuitems)
* [KubeObject detail items](#kubeobjectdetailitems)
+* [KubeObject status texts](#kubeobjectstatustexts)
+* [Kube workloads overview items](#kubeworkloadsoverviewitems)
+
+as well as catalog-related UI elements:
+
+* [Entity settings](#entitysettings)
+* [Catalog entity detail items](#catalogentitydetailitems)
All UI elements are based on React components.
-## `LensRendererExtension` Class
+Finally, you can also add commands and protocol handlers:
+
+* [Command palette commands](#commandpalettecommands)
+* [protocol handlers](protocol-handlers.md)
+
+## `Renderer.LensExtension` Class
### `onActivate()` and `onDeactivate()` Methods
-To create a renderer extension, extend the `LensRendererExtension` class:
+To create a renderer extension, extend the `Renderer.LensExtension` class:
```typescript
import { Renderer } from "@k8slens/extensions";
@@ -47,7 +60,7 @@ Implementing `onDeactivate()` gives you the opportunity to clean up after your e
1. Navigate to **File** > **Extensions** in the top menu bar.
(On Mac, it is **Lens** > **Extensions**.)
- 2. Click **Disable** on the extension you want to disable.
+ 2. For the extension you want to disable, open the context menu (click on the three vertical dots) and choose **Disable**.
The example above logs messages when the extension is enabled and disabled.
@@ -58,7 +71,7 @@ Use cluster pages to display information about or add functionality to the activ
It is also possible to include custom details from other clusters.
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
-Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
+Add a cluster page definition to a `Renderer.LensExtension` subclass with the following example:
```typescript
import { Renderer } from "@k8slens/extensions";
@@ -111,7 +124,7 @@ Use `clusterPageMenus`, covered in the next section, to add cluster pages to the
### `clusterPageMenus`
-`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav.
+`clusterPageMenus` allows you to add cluster page menu items to the secondary left nav, below Lens's standard cluster menus like **Workloads**, **Custom Resources**, etc.
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
@@ -253,18 +266,18 @@ Activating this menu item toggles on and off the appearance of the submenu below
The remaining two cluster page menu objects define the contents of the submenu.
A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case.
-This is what the example will look like, including how the menu item will appear in the secondary left nav:
+This is what the example could look like, including how the menu item will appear in the secondary left nav:
+
+
### `globalPages`
Global pages are independent of the cluster dashboard and can fill the entire Lens UI.
-Their primary use is to display information and provide functionality across clusters, including customized data and functionality unique to your extension.
+Their primary use is to display information and provide functionality across clusters (or catalog entities), including customized data and functionality unique to your extension.
-Typically, you would use a [global page menu](#globalpagemenus) located in the left nav to trigger a global page.
-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.
+Unlike cluster pages, users can trigger global pages even when there is no active cluster (or catalog entity).
-The following example defines a `LensRendererExtension` subclass with a single global page definition:
+The following example defines a `Renderer.LensExtension` subclass with a single global page definition:
```typescript
import { Renderer } from '@k8slens/extensions';
@@ -313,260 +326,13 @@ This allows the `HelpExtension` object to be passed in the global page definitio
This way, `HelpPage` can access all `HelpExtension` subclass data.
This example code shows how to create a global page, but not how to make that page available to the Lens user.
-Global pages can be made available in the following ways:
+Global pages are typically made available in the following ways:
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
* To add global pages 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`
-
-`globalPageMenus` allows you to add global page menu items to the left nav.
-
-By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
-
-```typescript
-import { Renderer } from "@k8slens/extensions";
-import { HelpIcon, HelpPage } from "./page"
-import React from "react"
-
-export default class HelpExtension extends Renderer.LensExtension {
- globalPages = [
- {
- id: "help",
- components: {
- Page: () => ,
- }
- }
- ];
-
- globalPageMenus = [
- {
- target: { pageId: "help" },
- title: "Help",
- components: {
- Icon: HelpIcon,
- }
- },
- ];
-}
-```
-
-`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:
-
-* `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 { Renderer } from "@k8slens/extensions";
-import React from "react"
-
-type IconProps = Renderer.Component.IconProps;
-
-const {
- Component: { Icon },
-} = Renderer;
-
-export function HelpIcon(props: IconProps) {
- return
-}
-
-export class HelpPage extends React.Component<{ extension: Renderer.LensExtension }> {
- render() {
- return (
-
- )
- }
-}
-```
-
-Lens includes various built-in components available for extension developers to use.
-One of these is the `Renderer.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 `Renderer.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:
-
-
-
-### `clusterFeatures`
-
-Cluster features are Kubernetes resources that can be applied to and managed within the active cluster.
-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
-import { Renderer } from "@k8slens/extensions"
-import { ExampleFeature } from "./src/example-feature"
-import React from "react"
-
-export default class ExampleFeatureExtension extends Renderer.LensExtension {
- clusterFeatures = [
- {
- title: "Example Feature",
- components: {
- Description: () => {
- return (
-
- Enable an example feature.
-
- )
- }
- },
- feature: new ExampleFeature()
- }
- ];
-}
-```
-
-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 four methods listed above are defined as follows:
-
-* 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 `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 `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
-import { Renderer, Common } from "@k8slens/extensions";
-import * as path from "path";
-
-const {
- K8sApi: {
- ResourceStack,
- forCluster,
- StorageClass,
- Namespace,
- }
-} = Renderer;
-
-type ResourceStack = Renderer.K8sApi.ResourceStack;
-type Pod = Renderer.K8sApi.Pod;
-type KubernetesCluster = Common.Catalog.KubernetesCluster;
-
-export interface MetricsStatus {
- installed: boolean;
- canUpgrade: boolean;
-}
-
-export class ExampleFeature {
- protected stack: ResourceStack;
-
- constructor(protected cluster: KubernetesCluster) {
- this.stack = new ResourceStack(cluster, this.name);
- }
-
- install(): Promise {
- return this.stack.kubectlApplyFolder(path.join(__dirname, "../resources/"));
- }
-
- upgrade(): Promise {
- return this.install(config);
- }
-
- async getStatus(): Promise {
- const status: MetricsStatus = { installed: false, canUpgrade: false};
-
- try {
- const pod = forCluster(cluster, Pod);
- const examplePod = await pod.get({name: "example-pod", namespace: "default"});
-
- if (examplePod?.kind) {
- status.installed = true;
- status.currentVersion = examplePod.spec.containers[0].image.split(":")[1];
- status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature
- } else {
- status.installed = false;
- status.canUpgrade = false;
- }
- } catch(e) {
- if (e?.error?.code === 404) {
- status.installed = false;
- status.canUpgrade = false;
- }
- }
-
- return status;
- }
-
- async uninstall(): Promise {
- return this.stack.kubectlDeleteFolder(this.resourceFolder);
- }
-}
-```
-
-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 the folder path is the `../resources` subfolder relative to the current source code's folder.
-The file `../resources/example-pod.yml` could contain:
-
-``` yaml
-apiVersion: v1
-kind: Pod
-metadata:
- name: example-pod
-spec:
- containers:
- - name: example-pod
- image: nginx
-```
-
-The example above implements the four methods as follows:
-
-* 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.
-
-* 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.
+* To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
+### `welcomeMenus`
### `appPreferences`
The Lens **Preferences** page is a built-in global page.
@@ -615,6 +381,7 @@ In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExampleP
```typescript
import { Renderer } from "@k8slens/extensions";
+import { makeObservable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
@@ -633,6 +400,11 @@ export class ExamplePreferenceProps {
@observer
export class ExamplePreferenceInput extends React.Component {
+ public constructor() {
+ super({preference: { enabled: false}});
+ makeObservable(this);
+ }
+
render() {
const { preference } = this.props;
return (
@@ -666,7 +438,7 @@ It is used to indicate the state of the preference, and it is bound to the check
`ExamplePreferenceHint` is a simple text span.
-The above example introduces the decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
+The above example introduces the decorators `makeObservable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages.
`mobx` simplifies state management.
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.
@@ -677,6 +449,8 @@ Note that you can manage an extension's state data using an `ExtensionStore` obj
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).
+### `topBarItems`
+
### `statusBarItems`
The status bar is the blue strip along the bottom of the Lens UI.
@@ -998,3 +772,17 @@ Construct the table using the `Renderer.Component.Table` and related elements.
For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods.
The table is constructed using the `Renderer.Component.Table` and related elements.
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
+
+### `kubeObjectStatusTexts`
+
+### `kubeWorkloadsOverviewItems`
+
+### `entitySettings`
+
+### `catalogEntityDetailItems`
+
+### `commandPaletteCommands`
+
+### `protocolHandlers`
+
+See the [Protocol Handlers Guide](protocol-handlers.md)
diff --git a/docs/extensions/guides/resource-stack.md b/docs/extensions/guides/resource-stack.md
new file mode 100644
index 0000000000..8d36bb6495
--- /dev/null
+++ b/docs/extensions/guides/resource-stack.md
@@ -0,0 +1,238 @@
+# Resource Stack (Cluster Feature)
+
+A cluster feature is a set of Kubernetes resources that can be applied to and managed within the active cluster.
+The `Renderer.K8sApi.ResourceStack` class provides the functionality to input and apply kubernetes resources to a cluster.
+It is up to the extension developer to manage the life cycle of the resource stack.
+It could be applied automatically to a cluster by the extension, or the end-user could be required to install it.
+
+The code examples in this section show how to create a resource stack, and define a cluster feature that is configurable 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 resource stack in this example consists of a single kubernetes resource:
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: example-pod
+spec:
+ containers:
+ - name: example-pod
+ image: nginx
+```
+
+It is simply a pod named `example-pod`, running nginx. Assume this content is in the file `../resources/example-pod.yml`.
+
+The following code sample shows how to use the `Renderer.K8sApi.ResourceStack` to manage installing and uninstalling this resource stack:
+
+```typescript
+import { Renderer, Common } from "@k8slens/extensions";
+import * as path from "path";
+
+const {
+ K8sApi: {
+ ResourceStack,
+ forCluster,
+ Pod,
+ }
+} = Renderer;
+
+type ResourceStack = Renderer.K8sApi.ResourceStack;
+type Pod = Renderer.K8sApi.Pod;
+type KubernetesCluster = Common.Catalog.KubernetesCluster;
+
+export class ExampleClusterFeature {
+ protected stack: ResourceStack;
+
+ constructor(protected cluster: KubernetesCluster) {
+ this.stack = new ResourceStack(cluster, "example-resource-stack");
+ }
+
+ get resourceFolder() {
+ return path.join(__dirname, "../resources/");
+ }
+
+ install(): Promise {
+ console.log("installing example-pod");
+ return this.stack.kubectlApplyFolder(this.resourceFolder);
+ }
+
+ async isInstalled(): Promise {
+ try {
+ const podApi = forCluster(this.cluster, Pod);
+ const examplePod = await podApi.get({name: "example-pod", namespace: "default"});
+
+ if (examplePod?.kind) {
+ console.log("found example-pod");
+ return true;
+ }
+ } catch(e) {
+ console.log("Error getting example-pod:", e);
+ }
+ console.log("didn't find example-pod");
+
+ return false;
+ }
+
+ async uninstall(): Promise {
+ console.log("uninstalling example-pod");
+ return this.stack.kubectlDeleteFolder(this.resourceFolder);
+ }
+}
+```
+
+The `ExampleClusterFeature` class constructor takes a `Common.Catalog.KubernetesCluster` argument.
+This is the cluster that the resource stack will be applied to, and the constructor instantiates a `Renderer.K8sApi.ResourceStack` as such.
+`ExampleClusterFeature` implements an `install()` method which simply invokes the `kubectlApplyFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
+`kubectlApplyFolder()` applies to the cluster all kubernetes resources found in the folder passed to it, in this case `../resources`.
+Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
+`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
+
+`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utiliize the kubernetes api to inspect the resource stack status.
+`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
+This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
+
+To allow the end-user to control the life cycle of this cluster feature the following code sample shows how to implement a user interface based on React and custom Lens UI components:
+
+```typescript
+ import React from "react";
+ import { Common, Renderer } from "@k8slens/extensions";
+ import { observer } from "mobx-react";
+ import { computed, observable, makeObservable } from "mobx";
+ import { ExampleClusterFeature } from "./example-cluster-feature";
+
+ const {
+ Component: {
+ SubTitle, Button,
+ }
+ } = Renderer;
+
+ interface Props {
+ cluster: Common.Catalog.KubernetesCluster;
+ }
+
+ @observer
+ export class ExampleClusterFeatureSettings extends React.Component {
+ constructor(props: Props) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @observable installed = false;
+ @observable inProgress = false;
+
+ feature: ExampleClusterFeature;
+
+ async componentDidMount() {
+ this.feature = new ExampleClusterFeature(this.props.cluster);
+
+ await this.updateFeatureState();
+ }
+
+ async updateFeatureState() {
+ this.installed = await this.feature.isInstalled();
+ }
+
+ async save() {
+ this.inProgress = true;
+
+ try {
+ if (this.installed) {
+ await this.feature.uninstall();
+ } else {
+ await this.feature.install();
+ }
+ } finally {
+ this.inProgress = false;
+
+ await this.updateFeatureState();
+ }
+ }
+
+ @computed get buttonLabel()Â {
+ if (this.inProgress && this.installed) return "Uninstalling ...";
+ if (this.inProgress) return "Applying ...";
+
+ if (this.installed) {
+ return "Uninstall";
+ }
+
+ return "Apply";
+ }
+
+ render() {
+ return (
+ <>
+
+
+ this.save()}
+ primary />
+
+ >
+ );
+ }
+}
+```
+
+The `ExampleClusterFeatureSettings` class extends `React.Component` and simply renders a subtitle and a button.
+`ExampleClusterFeatureSettings` takes the cluster as a prop and when the React component has mounted the `ExampleClusterFeature` is instantiated using this cluster (in `componentDidMount()`).
+The rest of the logic concerns the button appearance and action, based on the `ExampleClusterFeatureSettings` fields `installed` and `inProgress`.
+The `installed` value is of course determined using the aforementioned `ExampleClusterFeature` method `isInstalled()`.
+The `inProgress` value is true while waiting for the feature to be installed (or uninstalled).
+
+Note that the button is a `Renderer.Component.Button` element and this example takes advantage of its `waiting` prop to show a "waiting" animation while the install (or uninstall) is in progress.
+Using elements from `Renderer.Component` is encouraged, to take advantage of their built-in properties, and to ensure a common look and feel.
+
+Also note that [MobX 6](https://mobx.js.org/README.html) is used for state management, ensuring that the UI is rerendered when state has changed.
+The `ExampleClusterFeatureSettings` class is marked as an `@observer`, and its constructor must call `makeObservable()`.
+As well, the `installed` and `inProgress` fields are marked as `@observable`, ensuring that the button gets rerendered any time these fields change.
+
+Finally, `ExampleClusterFeatureSettings` needs to be connected to the extension, and would typically appear on the cluster **Settings** page via a `Renderer.LensExtension.entitySettings` entry.
+The `ExampleExtension` would look like this:
+
+```typescript
+import { Common, Renderer } from "@k8slens/extensions";
+import { ExampleClusterFeatureSettings } from "./src/example-cluster-feature-settings"
+import React from "react"
+
+export default class ExampleExtension extends Renderer.LensExtension {
+ entitySettings = [
+ {
+ apiVersions: ["entity.k8slens.dev/v1alpha1"],
+ kind: "KubernetesCluster",
+ title: "Example Cluster Feature",
+ priority: 5,
+ components: {
+ View: ({ entity = null }: { entity: Common.Catalog.KubernetesCluster}) => {
+ return (
+
+ );
+ }
+ }
+ }
+ ];
+
+}
+```
+
+An entity setting is added to the `entitySettings` array field of the `Renderer.LensExtension` class.
+Because Lens's catalog can contain different kinds of entities, the kind must be identified.
+For more details about the catalog see the [Catalog Guide](catalog.md).
+Clusters are a built-in kind, so the `apiVersions` and `kind` fields should be set as above.
+The `title` is shown as a navigation item on the cluster **Settings** page and the `components.View` is displayed when the navigation item is clicked on.
+The `components.View` definition above shows how the `ExampleClusterFeatureSettings` element is included, and how its `cluster` prop is set.
+`priority` determines the order of the entity settings, the higher the number the higher in the navigation panel the setting is placed. The default value is 50.
+
+The final result looks like this:
+
+
+
+`ExampleClusterFeature` and `ExampleClusterFeatureSettings` demonstrate a cluster feature for a simple resource stack.
+In practice a resource stack can include many resources, and require more sophisticated life cycle management (upgrades, partial installations, etc.)
+Using `Renderer.K8sApi.ResourceStack` and `entitySettings` it is possible to implement solutions for more complex cluster features.
+The **Lens Metrics** setting (on the cluster **Settings** page) is a good example of an advanced solution.
\ No newline at end of file
diff --git a/extensions/kube-object-event-status/package-lock.json b/extensions/kube-object-event-status/package-lock.json
index e1234ebd73..f3b0fe6669 100644
--- a/extensions/kube-object-event-status/package-lock.json
+++ b/extensions/kube-object-event-status/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "kube-object-event-status",
- "version": "0.1.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json
index f6a43b3f72..15feb302b8 100644
--- a/extensions/metrics-cluster-feature/package-lock.json
+++ b/extensions/metrics-cluster-feature/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "lens-metrics-cluster-feature",
- "version": "0.1.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/extensions/metrics-cluster-feature/src/metrics-settings.tsx b/extensions/metrics-cluster-feature/src/metrics-settings.tsx
index b5a78eda1a..f05854b0f2 100644
--- a/extensions/metrics-cluster-feature/src/metrics-settings.tsx
+++ b/extensions/metrics-cluster-feature/src/metrics-settings.tsx
@@ -81,7 +81,7 @@ export class MetricsSettings extends React.Component {
@computed get isTogglable() {
if (this.inProgress) return false;
- if (!this.props.cluster.status.active) return false;
+ if (this.props.cluster.status.phase !== "connected") return false;
if (this.canUpgrade) return false;
if (!this.isActiveMetricsProvider) return false;
@@ -205,7 +205,7 @@ export class MetricsSettings extends React.Component {
render() {
return (
<>
- { !this.props.cluster.status.active && (
+ { this.props.cluster.status.phase !== "connected" && (
Lens Metrics settings requires established connection to the cluster.
@@ -225,7 +225,7 @@ export class MetricsSettings extends React.Component {
control={
this.togglePrometheus(v.target.checked)}
name="prometheus"
/>
@@ -243,7 +243,7 @@ export class MetricsSettings extends React.Component {
control={
this.toggleKubeStateMetrics(v.target.checked)}
name="node-exporter"
/>
@@ -262,7 +262,7 @@ export class MetricsSettings extends React.Component {
control={
this.toggleNodeExporter(v.target.checked)}
name="node-exporter"
/>
@@ -281,7 +281,9 @@ export class MetricsSettings extends React.Component {
waiting={this.inProgress}
onClick={() => this.save()}
primary
- disabled={!this.changed} />
+ disabled={!this.changed}
+ className="w-60 h-14"
+ />
{this.canUpgrade && (
An update is available for enabled metrics components.
diff --git a/extensions/node-menu/package-lock.json b/extensions/node-menu/package-lock.json
index 245b548fdc..3ca2677ed4 100644
--- a/extensions/node-menu/package-lock.json
+++ b/extensions/node-menu/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "lens-node-menu",
- "version": "0.1.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/extensions/node-menu/src/node-menu.tsx b/extensions/node-menu/src/node-menu.tsx
index fd8186b18e..12ded04899 100644
--- a/extensions/node-menu/src/node-menu.tsx
+++ b/extensions/node-menu/src/node-menu.tsx
@@ -86,23 +86,26 @@ export function NodeMenu(props: NodeMenuProps) {
return (
<>
-
+
Shell
- {!node.isUnschedulable() && (
-
-
- Cordon
-
- )}
- {node.isUnschedulable() && (
-
-
- Uncordon
-
- )}
+ {
+ node.isUnschedulable()
+ ? (
+
+
+ Uncordon
+
+ )
+ : (
+
+
+ Cordon
+
+ )
+ }
-
+
Drain
>
diff --git a/extensions/pod-menu/package-lock.json b/extensions/pod-menu/package-lock.json
index e0cc8c93c0..70154d1055 100644
--- a/extensions/pod-menu/package-lock.json
+++ b/extensions/pod-menu/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "lens-pod-menu",
- "version": "0.1.0",
+ "version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/extensions/pod-menu/src/attach-menu.tsx b/extensions/pod-menu/src/attach-menu.tsx
index bb43d151bc..ad02782641 100644
--- a/extensions/pod-menu/src/attach-menu.tsx
+++ b/extensions/pod-menu/src/attach-menu.tsx
@@ -74,7 +74,7 @@ export class PodAttachMenu extends React.Component {
return (
this.attachToPod(containers[0].name))}>
-
+
Attach Pod
{containers.length > 1 && (
<>
diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx
index 042490f568..41deb92e1a 100644
--- a/extensions/pod-menu/src/logs-menu.tsx
+++ b/extensions/pod-menu/src/logs-menu.tsx
@@ -62,7 +62,7 @@ export class PodLogsMenu extends React.Component {
return (
this.showLogs(containers[0]))}>
-
+
Logs
{containers.length > 1 && (
<>
diff --git a/extensions/pod-menu/src/shell-menu.tsx b/extensions/pod-menu/src/shell-menu.tsx
index e1c72391aa..4c52379525 100644
--- a/extensions/pod-menu/src/shell-menu.tsx
+++ b/extensions/pod-menu/src/shell-menu.tsx
@@ -79,7 +79,7 @@ export class PodShellMenu extends React.Component {
return (
this.execShell(containers[0].name))}>
-
+
Shell
{containers.length > 1 && (
<>
diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts
index 24ad38dcfc..27a0b508a8 100644
--- a/integration/__tests__/cluster-pages.tests.ts
+++ b/integration/__tests__/cluster-pages.tests.ts
@@ -41,40 +41,38 @@ describe("Lens cluster pages", () => {
const BACKSPACE = "\uE003";
let app: Application;
const ready = minikubeReady(TEST_NAMESPACE);
+ let clusterAdded = false;
utils.describeIf(ready)("test common pages", () => {
- let clusterAdded = false;
const addCluster = async () => {
- await app.client.waitUntilTextExists("div", "Catalog");
await waitForMinikubeDashboard(app);
await app.client.click('a[href="/nodes"]');
await app.client.waitUntilTextExists("div.TableCell", "Ready");
};
+ const appStartAddCluster = async () => {
+ app = await utils.appStart();
+ await addCluster();
+ clusterAdded = true;
+ };
+
+ const tearDown = async () => {
+ await utils.tearDown(app);
+ clusterAdded = false;
+ };
+
describe("cluster add", () => {
utils.beforeAllWrapped(async () => {
app = await utils.appStart();
});
- utils.afterAllWrapped(async () => {
- if (app?.isRunning()) {
- return utils.tearDown(app);
- }
- });
+ utils.afterAllWrapped(tearDown);
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}"]`;
@@ -86,12 +84,7 @@ describe("Lens cluster pages", () => {
describe("cluster pages", () => {
utils.beforeAllWrapped(appStartAddCluster);
-
- utils.afterAllWrapped(async () => {
- if (app?.isRunning()) {
- return utils.tearDown(app);
- }
- });
+ utils.afterAllWrapped(tearDown);
const tests: {
drawer?: string
@@ -376,12 +369,7 @@ describe("Lens cluster pages", () => {
describe("viewing pod logs", () => {
utils.beforeEachWrapped(appStartAddCluster);
-
- utils.afterEachWrapped(async () => {
- if (app?.isRunning()) {
- return utils.tearDown(app);
- }
- });
+ utils.afterEachWrapped(tearDown);
it(`shows a log for a pod`, async () => {
expect(clusterAdded).toBe(true);
@@ -409,8 +397,12 @@ describe("Lens cluster pages", () => {
// Open logs tab in dock
await app.client.click(".list .TableRow:first-child");
await app.client.waitForVisible(".Drawer");
- await app.client.waitForVisible(`ul.KubeObjectMenu li.MenuItem i[title="Logs"]`);
- await app.client.click("ul.KubeObjectMenu li.MenuItem i[title='Logs']");
+
+ const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']";
+
+ await app.client.waitForVisible(logsButton);
+ await app.client.click(logsButton);
+
// Check if controls are available
await app.client.waitForVisible(".LogList .VirtualList");
await app.client.waitForVisible(".LogResourceSelector");
@@ -427,12 +419,7 @@ describe("Lens cluster pages", () => {
describe("cluster operations", () => {
utils.beforeEachWrapped(appStartAddCluster);
-
- utils.afterEachWrapped(async () => {
- if (app?.isRunning()) {
- return utils.tearDown(app);
- }
- });
+ utils.afterEachWrapped(tearDown);
it("shows default namespace", async () => {
expect(clusterAdded).toBe(true);
diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts
index bbe411047b..8698bd6be2 100644
--- a/integration/helpers/minikube.ts
+++ b/integration/helpers/minikube.ts
@@ -65,6 +65,7 @@ export async function waitForMinikubeDashboard(app: Application) {
await app.client.waitUntilTextExists("div.TableCell", "minikube");
await app.client.click("div.TableRow");
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
+ await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
await app.client.waitForExist(`iframe[name="minikube"]`);
diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts
index 9ce33b21c3..d6f633e416 100644
--- a/integration/helpers/utils.ts
+++ b/integration/helpers/utils.ts
@@ -37,27 +37,27 @@ interface DoneCallback {
* This is necessary because Jest doesn't do this correctly.
* @param fn The function to call
*/
-export function wrapJestLifecycle(fn: () => Promise): (done: DoneCallback) => void {
+export function wrapJestLifecycle(fn: () => Promise | void): (done: DoneCallback) => void {
return function (done: DoneCallback) {
- fn()
+ (async () => fn())()
.then(() => done())
.catch(error => done.fail(error));
};
}
-export function beforeAllWrapped(fn: () => Promise): void {
+export function beforeAllWrapped(fn: () => Promise | void): void {
beforeAll(wrapJestLifecycle(fn));
}
-export function beforeEachWrapped(fn: () => Promise): void {
+export function beforeEachWrapped(fn: () => Promise | void): void {
beforeEach(wrapJestLifecycle(fn));
}
-export function afterAllWrapped(fn: () => Promise): void {
+export function afterAllWrapped(fn: () => Promise | void): void {
afterAll(wrapJestLifecycle(fn));
}
-export function afterEachWrapped(fn: () => Promise): void {
+export function afterEachWrapped(fn: () => Promise | void): void {
afterEach(wrapJestLifecycle(fn));
}
@@ -99,13 +99,17 @@ export async function appStart() {
}
export async function showCatalog(app: Application) {
- await app.client.waitUntilTextExists("[data-test-id=catalog-link]", "Catalog");
- await app.client.click("[data-test-id=catalog-link]");
+ await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon");
+ await app.client.click("#hotbarIcon-catalog-entity .Icon");
}
type AsyncPidGetter = () => Promise;
-export async function tearDown(app: Application) {
+export async function tearDown(app?: Application) {
+ if (!app?.isRunning()) {
+ return;
+ }
+
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
await app.stop();
diff --git a/mkdocs.yml b/mkdocs.yml
index 123506a585..4ed763e6d3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -22,6 +22,8 @@ nav:
- Generator: extensions/guides/generator.md
- Main Extension: extensions/guides/main-extension.md
- Renderer Extension: extensions/guides/renderer-extension.md
+ - Catalog: extensions/guides/catalog.md
+ - Resource Stack: extensions/guides/resource-stack.md
- Stores: extensions/guides/stores.md
- Working with MobX: extensions/guides/working-with-mobx.md
- Protocol Handlers: extensions/guides/protocol-handlers.md
@@ -83,7 +85,7 @@ extra:
link: https://k8slens.dev/
name: Lens Website
version:
- method: mike
+ provider: mike
analytics:
provider: google
property: UA-159377374-2
diff --git a/package.json b/package.json
index a9e013bddd..1ad83a1e35 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
- "version": "5.0.0-beta.8",
+ "version": "5.0.0-beta.13",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@@ -222,7 +222,7 @@
"openid-client": "^3.15.2",
"p-limit": "^3.1.0",
"path-to-regexp": "^6.1.0",
- "proper-lockfile": "^4.1.1",
+ "proper-lockfile": "^4.1.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router": "^5.2.0",
@@ -321,7 +321,7 @@
"electron-notarize": "^0.3.0",
"eslint": "^7.7.0",
"eslint-plugin-header": "^3.1.1",
- "eslint-plugin-react": "^7.21.5",
+ "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-unused-imports": "^1.0.1",
"file-loader": "^6.2.0",
@@ -367,7 +367,7 @@
"typed-emitter": "^1.3.1",
"typedoc": "0.21.0-beta.2",
"typedoc-plugin-markdown": "^3.9.0",
- "typeface-roboto": "^0.0.75",
+ "typeface-roboto": "^1.1.13",
"typescript": "^4.3.2",
"typescript-plugin-css-modules": "^3.2.0",
"url-loader": "^4.1.0",
diff --git a/scripts/tag-release.sh b/scripts/tag-release.sh
index 82b7f14e6c..ce2ddffb2b 100755
--- a/scripts/tag-release.sh
+++ b/scripts/tag-release.sh
@@ -1,10 +1,21 @@
#!/bin/bash
+while [[ $# -gt 0 ]]; do
+ key="$1"
+
+ case $key in
+ -f|--force)
+ FORCE="--force"
+ shift # past argument
+ ;;
+ esac
+done
+
if [[ `git branch --show-current` =~ ^release/v ]]
then
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
- git tag ${VERSION_STRING}
- git push ${GIT_REMOTE:-origin} ${VERSION_STRING}
+ git tag ${VERSION_STRING} ${FORCE}
+ git push ${GIT_REMOTE:-origin} ${VERSION_STRING} ${FORCE}
else
echo "You must be in a release branch"
fi
diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts
index 3b87f53670..6d4198d8d7 100644
--- a/src/common/__tests__/cluster-store.test.ts
+++ b/src/common/__tests__/cluster-store.test.ts
@@ -95,7 +95,7 @@ describe("empty config", () => {
mockFs(mockOpts);
- await ClusterStore.createInstance().load();
+ ClusterStore.createInstance();
});
afterEach(() => {
@@ -125,33 +125,28 @@ describe("empty config", () => {
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
});
-
- it("removes cluster from store", async () => {
- await ClusterStore.getInstance().removeById("foo");
- expect(ClusterStore.getInstance().getById("foo")).toBeNull();
- });
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
- ClusterStore.getInstance().addClusters(
- new Cluster({
- id: "prod",
- contextName: "foo",
- preferences: {
- clusterName: "prod"
- },
- kubeConfigPath: embed("prod", kubeconfig)
- }),
- new Cluster({
- id: "dev",
- contextName: "foo2",
- preferences: {
- clusterName: "dev"
- },
- kubeConfigPath: embed("dev", kubeconfig)
- })
- );
+ const store = ClusterStore.getInstance();
+
+ store.addCluster({
+ id: "prod",
+ contextName: "foo",
+ preferences: {
+ clusterName: "prod"
+ },
+ kubeConfigPath: embed("prod", kubeconfig)
+ });
+ store.addCluster({
+ id: "dev",
+ contextName: "foo2",
+ preferences: {
+ clusterName: "dev"
+ },
+ kubeConfigPath: embed("dev", kubeconfig)
+ });
});
it("check if store can contain multiple clusters", () => {
@@ -208,7 +203,7 @@ describe("config with existing clusters", () => {
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -222,16 +217,6 @@ describe("config with existing clusters", () => {
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
- it("allows to delete a cluster", () => {
- ClusterStore.getInstance().removeById("cluster2");
- const storedCluster = ClusterStore.getInstance().getById("cluster1");
-
- expect(storedCluster).toBeTruthy();
- const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
-
- expect(storedCluster2).toBeNull();
- });
-
it("allows getting all of the clusters", async () => {
const storedClusters = ClusterStore.getInstance().clustersList;
@@ -300,7 +285,7 @@ users:
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -359,7 +344,7 @@ describe("pre 2.0 config with an existing cluster", () => {
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -429,7 +414,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -471,7 +456,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -510,7 +495,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
@@ -546,7 +531,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
mockFs(mockOpts);
- return ClusterStore.createInstance().load();
+ return ClusterStore.createInstance();
});
afterEach(() => {
diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts
index 51002fee23..267fbe3e8e 100644
--- a/src/common/__tests__/hotbar-store.test.ts
+++ b/src/common/__tests__/hotbar-store.test.ts
@@ -23,7 +23,7 @@ import mockFs from "mock-fs";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
-jest.mock("../../renderer/api/catalog-entity-registry", () => ({
+jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: {
items: [
{
@@ -39,7 +39,14 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({
name: "my_shiny_cluster",
source: "remote"
}
- }
+ },
+ {
+ metadata: {
+ uid: "catalog-entity",
+ name: "Catalog",
+ source: "app"
+ },
+ },
]
}
}));
@@ -120,29 +127,31 @@ jest.mock("electron", () => {
describe("HotbarStore", () => {
beforeEach(() => {
- ClusterStore.resetInstance();
+ mockFs({
+ "tmp": {
+ "lens-hotbar-store.json": JSON.stringify({})
+ }
+ });
ClusterStore.createInstance();
-
- HotbarStore.resetInstance();
- mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
+ HotbarStore.createInstance();
});
afterEach(() => {
+ ClusterStore.resetInstance();
+ HotbarStore.resetInstance();
mockFs.restore();
});
describe("load", () => {
it("loads one hotbar by default", () => {
- HotbarStore.createInstance().load();
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2);
});
@@ -150,106 +159,104 @@ describe("HotbarStore", () => {
describe("hotbar items", () => {
it("initially creates 12 empty cells", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
expect(hotbarStore.getActive().items.length).toEqual(12);
});
- it("adds items", () => {
- const hotbarStore = HotbarStore.createInstance();
+ it("initially adds catalog entity as first item", () => {
+ const hotbarStore = HotbarStore.getInstance();
+
+ expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
+ });
+
+ it("adds items", () => {
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
- expect(items.length).toEqual(1);
+ expect(items.length).toEqual(2);
});
it("removes items", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test");
+ hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(0);
});
it("does nothing if removing with invalid uid", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean);
- expect(items.length).toEqual(1);
+ expect(items.length).toEqual(2);
});
it("moves item to empty cell", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
- expect(hotbarStore.getActive().items[5]).toBeNull();
+ expect(hotbarStore.getActive().items[6]).toBeNull();
hotbarStore.restackItems(1, 5);
expect(hotbarStore.getActive().items[5]).toBeTruthy();
- expect(hotbarStore.getActive().items[5].entity.uid).toEqual("minikube");
+ expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
});
it("moves items down", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
- // aws -> test
- hotbarStore.restackItems(2, 0);
+ // aws -> catalog
+ hotbarStore.restackItems(3, 0);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
- expect(items.slice(0, 4)).toEqual(["aws", "test", "minikube", null]);
+ expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
});
it("moves items up", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// test -> aws
- hotbarStore.restackItems(0, 2);
+ hotbarStore.restackItems(1, 3);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
- expect(items.slice(0, 4)).toEqual(["minikube", "aws", "test", null]);
+ expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
});
it("does nothing when item moved to same cell", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
- hotbarStore.restackItems(0, 0);
+ hotbarStore.restackItems(1, 1);
- expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
+ expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
});
it("new items takes first empty cell", () => {
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
@@ -260,13 +267,13 @@ describe("HotbarStore", () => {
it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render.
- const err = console.error;
+ const { error, warn } = console;
console.error = jest.fn();
+ console.warn = jest.fn();
- const hotbarStore = HotbarStore.createInstance();
+ const hotbarStore = HotbarStore.getInstance();
- hotbarStore.load();
hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
@@ -275,7 +282,8 @@ describe("HotbarStore", () => {
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
// Restore writing to stderr.
- console.error = err;
+ console.error = error;
+ console.warn = warn;
});
});
@@ -346,7 +354,7 @@ describe("HotbarStore", () => {
mockFs(mockOpts);
- return HotbarStore.createInstance().load();
+ HotbarStore.createInstance();
});
afterEach(() => {
diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts
index 465c598bb0..b1b3f60ce6 100644
--- a/src/common/__tests__/user-store.test.ts
+++ b/src/common/__tests__/user-store.test.ts
@@ -52,7 +52,7 @@ describe("user store tests", () => {
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
- return UserStore.getInstance().load();
+ UserStore.getInstance();
});
afterEach(() => {
@@ -81,10 +81,8 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance();
- us.isLoaded = true;
-
us.colorTheme = "some other theme";
- await us.resetTheme();
+ us.resetTheme();
expect(us.colorTheme).toBe(UserStore.defaultTheme);
});
@@ -111,7 +109,7 @@ describe("user store tests", () => {
}
});
- return UserStore.createInstance().load();
+ UserStore.createInstance();
});
afterEach(() => {
diff --git a/src/common/base-store.ts b/src/common/base-store.ts
index 842e0304b4..d30725d6bb 100644
--- a/src/common/base-store.ts
+++ b/src/common/base-store.ts
@@ -23,41 +23,42 @@ import path from "path";
import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron";
-import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx";
+import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx";
import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
-export interface BaseStoreParams extends ConfOptions {
- autoLoad?: boolean;
- syncEnabled?: boolean;
+export interface BaseStoreParams extends ConfOptions {
syncOptions?: IReactionOptions;
}
/**
* Note: T should only contain base JSON serializable types.
*/
-export abstract class BaseStore extends Singleton {
+export abstract class BaseStore extends Singleton {
protected storeConfig?: Config;
protected syncDisposers: Disposer[] = [];
- @observable isLoaded = false;
-
- get whenLoaded() {
- return when(() => this.isLoaded);
- }
-
- protected constructor(protected params: BaseStoreParams) {
+ protected constructor(protected params: BaseStoreParams) {
super();
makeObservable(this);
+ }
- this.params = {
- autoLoad: false,
- syncEnabled: true,
- ...params,
- };
- this.init();
+ /**
+ * This must be called after the last child's constructor is finished (or just before it finishes)
+ */
+ load() {
+ this.storeConfig = new Config({
+ ...this.params,
+ projectName: "lens",
+ projectVersion: getAppVersion(),
+ cwd: this.cwd(),
+ });
+
+ logger.info(`[STORE]: LOADED from ${this.path}`);
+ this.fromStore(this.storeConfig.store);
+ this.enableSync();
}
get name() {
@@ -76,31 +77,6 @@ export abstract class BaseStore extends Singleton {
return this.storeConfig?.path || "";
}
- protected async init() {
- if (this.params.autoLoad) {
- await this.load();
- }
-
- if (this.params.syncEnabled) {
- await this.whenLoaded;
- this.enableSync();
- }
- }
-
- async load() {
- const { autoLoad, syncEnabled, ...confOptions } = this.params;
-
- this.storeConfig = new Config({
- ...confOptions,
- projectName: "lens",
- projectVersion: getAppVersion(),
- cwd: this.cwd(),
- });
- logger.info(`[STORE]: LOADED from ${this.path}`);
- this.fromStore(this.storeConfig.store);
- this.isLoaded = true;
- }
-
protected cwd() {
return (app || remote.app).getPath("userData");
}
@@ -159,10 +135,7 @@ export abstract class BaseStore extends Singleton {
protected applyWithoutSync(callback: () => void) {
this.disableSync();
runInAction(callback);
-
- if (this.params.syncEnabled) {
- this.enableSync();
- }
+ this.enableSync();
}
protected onSync(model: T) {
diff --git a/src/common/catalog-entities/general.ts b/src/common/catalog-entities/general.ts
new file mode 100644
index 0000000000..5d13eeabee
--- /dev/null
+++ b/src/common/catalog-entities/general.ts
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { navigate } from "../../renderer/navigation";
+import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
+import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
+
+interface GeneralEntitySpec extends CatalogEntitySpec {
+ path: string;
+ icon?: {
+ material?: string;
+ background?: string;
+ };
+}
+
+export class GeneralEntity extends CatalogEntity {
+ public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
+ public readonly kind = "General";
+
+ async onRun() {
+ navigate(this.spec.path);
+ }
+
+ public onSettingsOpen(): void {
+ return;
+ }
+
+ public onDetailsOpen(): void {
+ return;
+ }
+
+ public onContextMenuOpen(): void {
+ return;
+ }
+}
+
+export class GeneralCategory extends CatalogCategory {
+ public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
+ public readonly kind = "CatalogCategory";
+ public metadata = {
+ name: "General",
+ icon: "settings"
+ };
+ public spec = {
+ group: "entity.k8slens.dev",
+ versions: [
+ {
+ name: "v1alpha1",
+ entityClass: GeneralEntity
+ }
+ ],
+ names: {
+ kind: "General"
+ }
+ };
+}
+
+catalogCategoryRegistry.add(new GeneralCategory());
diff --git a/src/common/catalog-entities/index.ts b/src/common/catalog-entities/index.ts
index 621383c6ec..ee8a41b552 100644
--- a/src/common/catalog-entities/index.ts
+++ b/src/common/catalog-entities/index.ts
@@ -19,5 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+export * from "./general";
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
index 82582d2bc5..e1766c1bb0 100644
--- a/src/common/catalog-entities/kubernetes-cluster.ts
+++ b/src/common/catalog-entities/kubernetes-cluster.ts
@@ -21,15 +21,16 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
-import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
+import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
-import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { addClusterURL } from "../routes";
import { app } from "electron";
+import type { CatalogEntitySpec } from "../catalog/catalog-entity";
+import { HotbarStore } from "../hotbar-store";
-export type KubernetesClusterPrometheusMetrics = {
+export interface KubernetesClusterPrometheusMetrics {
address?: {
namespace: string;
service: string;
@@ -37,20 +38,25 @@ export type KubernetesClusterPrometheusMetrics = {
prefix: string;
};
type?: string;
-};
+}
-export type KubernetesClusterSpec = {
+export interface KubernetesClusterSpec extends CatalogEntitySpec {
kubeconfigPath: string;
kubeconfigContext: string;
- iconData?: string;
metrics?: {
source: string;
prometheus?: KubernetesClusterPrometheusMetrics;
- }
-};
+ };
+ icon?: {
+ // TODO: move to CatalogEntitySpec once any-entity icons are supported
+ src?: string;
+ material?: string;
+ background?: string;
+ };
+}
export interface KubernetesClusterStatus extends CatalogEntityStatus {
- phase: "connected" | "disconnected";
+ phase: "connected" | "disconnected" | "deleting";
}
export class KubernetesCluster extends CatalogEntity {
@@ -103,39 +109,38 @@ export class KubernetesCluster extends CatalogEntity context.navigate(`/entity/${this.metadata.uid}/settings`)
- });
- }
-
- if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
- context.menuItems.push({
- title: "Delete",
- icon: "delete",
- onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
- confirm: {
- message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
- }
- });
+ context.menuItems.push(
+ {
+ title: "Settings",
+ icon: "edit",
+ onClick: () => context.navigate(`/entity/${this.metadata.uid}/settings`)
+ },
+ {
+ title: "Delete",
+ icon: "delete",
+ onClick: () => {
+ HotbarStore.getInstance().removeAllHotbarItems(this.getId());
+ requestMain(clusterDeleteHandler, this.metadata.uid);
+ },
+ confirm: {
+ // TODO: change this to be a tag with better formatting once this code can accept it.
+ message: `Delete the "${this.metadata.name}" context from "${this.metadata.labels.file}"?`
+ }
+ },
+ );
}
if (this.status.phase == "connected") {
context.menuItems.push({
title: "Disconnect",
icon: "link_off",
- onClick: async () => {
- requestMain(clusterDisconnectHandler, this.metadata.uid);
- }
+ onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
});
} else {
context.menuItems.push({
title: "Connect",
icon: "link",
- onClick: async () => {
- context.navigate(`/cluster/${this.metadata.uid}`);
- }
+ onClick: () => context.navigate(`/cluster/${this.metadata.uid}`)
});
}
@@ -149,7 +154,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
- name: "Kubernetes Clusters",
+ name: "Clusters",
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
};
public spec: CatalogCategorySpec = {
diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts
index 7e0f024421..e18b3ac931 100644
--- a/src/common/catalog-entities/web-link.ts
+++ b/src/common/catalog-entities/web-link.ts
@@ -19,11 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
+import { CatalogCategory, CatalogEntity, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
+import { productName } from "../vars";
+import { WeblinkStore } from "../weblink-store";
export interface WebLinkStatus extends CatalogEntityStatus {
- phase: "valid" | "invalid";
+ phase: "available" | "unavailable";
}
export type WebLinkSpec = {
@@ -42,12 +44,17 @@ export class WebLink extends CatalogEntity WeblinkStore.getInstance().removeById(this.metadata.uid),
+ confirm: {
+ message: `Remove Web Link "${this.metadata.name}" from ${productName}?`
+ }
+ });
+ }
}
}
@@ -56,7 +63,7 @@ export class WebLinkCategory extends CatalogCategory {
public readonly kind = "CatalogCategory";
public metadata = {
name: "Web Links",
- icon: "link"
+ icon: "public"
};
public spec = {
group: "entity.k8slens.dev",
@@ -70,6 +77,21 @@ export class WebLinkCategory extends CatalogCategory {
kind: "WebLink"
}
};
+ public static onAdd?: () => void;
+
+ constructor() {
+ super();
+
+ this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
+ ctx.menuItems.push({
+ icon: "public",
+ title: "Add web link",
+ onClick: () => {
+ WebLinkCategory.onAdd();
+ }
+ });
+ });
+ }
}
catalogCategoryRegistry.add(new WebLinkCategory());
diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts
index 9acc2a47c9..bc3e360a7b 100644
--- a/src/common/catalog/catalog-entity.ts
+++ b/src/common/catalog/catalog-entity.ts
@@ -86,6 +86,11 @@ export interface CatalogEntityMetadata {
export interface CatalogEntityStatus {
phase: string;
reason?: string;
+
+ /**
+ * @default true
+ */
+ enabled?: boolean;
message?: string;
active?: boolean;
}
@@ -144,6 +149,7 @@ export interface CatalogEntityAddMenuContext {
export type CatalogEntitySpec = Record;
+
export interface CatalogEntityData<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Status extends CatalogEntityStatus = CatalogEntityStatus,
diff --git a/src/common/cluster-frames.ts b/src/common/cluster-frames.ts
index 794a09d827..88dc2d3163 100644
--- a/src/common/cluster-frames.ts
+++ b/src/common/cluster-frames.ts
@@ -21,9 +21,9 @@
import { observable } from "mobx";
-export type ClusterFrameInfo = {
+export interface ClusterFrameInfo {
frameId: number;
processId: number
-};
+}
export const clusterFrameMap = observable.map();
diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts
index 13725b37dc..9191d93048 100644
--- a/src/common/cluster-ipc.ts
+++ b/src/common/cluster-ipc.ts
@@ -24,5 +24,6 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterVisibilityHandler = "cluster:visibility";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
+export const clusterDeleteHandler = "cluster:delete";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts
index d2fcd616ed..b637bddac3 100644
--- a/src/common/cluster-store.ts
+++ b/src/common/cluster-store.ts
@@ -21,7 +21,6 @@
import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
-import { unlink } from "fs-extra";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
@@ -30,7 +29,7 @@ import * as uuid from "uuid";
import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { ipcMainHandle, ipcMainOn, ipcRendererOn, requestMain } from "./ipc";
-import { disposer, noop, toJS } from "./utils";
+import { disposer, toJS } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
@@ -73,7 +72,7 @@ export interface ClusterModel {
workspace?: string;
/** User context in kubeconfig */
- contextName?: string;
+ contextName: string;
/** Preferences */
preferences?: ClusterPreferences;
@@ -81,11 +80,13 @@ export interface ClusterModel {
/** Metadata */
metadata?: ClusterMetadata;
+ /**
+ * Labels for the catalog entity
+ */
+ labels?: Record;
+
/** List of accessible namespaces */
accessibleNamespaces?: string[];
-
- /** @deprecated */
- kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
@@ -109,21 +110,22 @@ export interface ClusterPrometheusPreferences {
};
}
+const initialStates = "cluster:states";
+
export class ClusterStore extends BaseStore {
private static StateChannel = "cluster:state";
static get storedKubeConfigFolder(): string {
- return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
+ return path.resolve((app ?? remote.app).getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
}
- @observable clusters = observable.map();
- @observable removedClusters = observable.map();
+ clusters = observable.map();
+ removedClusters = observable.map();
- private static stateRequestChannel = "cluster:states";
protected disposer = disposer();
constructor() {
@@ -137,50 +139,31 @@ export class ClusterStore extends BaseStore {
});
makeObservable(this);
-
+ this.load();
this.pushStateToViewsAutomatically();
}
- async load() {
- await super.load();
- type clusterStateSync = {
- id: string;
- state: ClusterState;
- };
+ async loadInitialOnRenderer() {
+ logger.info("[CLUSTER-STORE] requesting initial state sync");
- if (ipcRenderer) {
- logger.info("[CLUSTER-STORE] requesting initial state sync");
- const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
-
- clusterStates.forEach((clusterState) => {
- const cluster = this.getById(clusterState.id);
-
- if (cluster) {
- cluster.setState(clusterState.state);
- }
- });
- } else if (ipcMain) {
- ipcMainHandle(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
- const clusterStates: clusterStateSync[] = [];
-
- this.clustersList.forEach((cluster) => {
- clusterStates.push({
- state: cluster.getState(),
- id: cluster.id
- });
- });
-
- return clusterStates;
- });
+ for (const { id, state } of await requestMain(initialStates)) {
+ this.getById(id)?.setState(state);
}
}
+ provideInitialFromMain() {
+ ipcMainHandle(initialStates, () => {
+ return this.clustersList.map(cluster => ({
+ id: cluster.id,
+ state: cluster.getState(),
+ }));
+ });
+ }
+
protected pushStateToViewsAutomatically() {
if (ipcMain) {
this.disposer.push(
- reaction(() => this.connectedClustersList, () => {
- this.pushState();
- }),
+ reaction(() => this.connectedClustersList, () => this.pushState()),
);
}
}
@@ -229,18 +212,6 @@ export class ClusterStore extends BaseStore {
return this.clusters.get(id) ?? null;
}
- @action
- addClusters(...models: ClusterModel[]): Cluster[] {
- const clusters: Cluster[] = [];
-
- models.forEach(model => {
- clusters.push(this.addCluster(model));
- });
-
- return clusters;
- }
-
- @action
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });
@@ -253,25 +224,6 @@ export class ClusterStore extends BaseStore {
return cluster;
}
- async removeCluster(model: ClusterModel) {
- await this.removeById(model.id);
- }
-
- @action
- async removeById(clusterId: ClusterId) {
- appEventBus.emit({ name: "cluster", action: "remove" });
- const cluster = this.getById(clusterId);
-
- if (cluster) {
- this.clusters.delete(clusterId);
-
- // remove only custom kubeconfigs (pasted as text)
- if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
- await unlink(cluster.kubeConfigPath).catch(noop);
- }
- }
- }
-
@action
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = new Map(this.clusters);
diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts
index 2d91ec0cc5..b90a007b34 100644
--- a/src/common/hotbar-store.ts
+++ b/src/common/hotbar-store.ts
@@ -38,16 +38,12 @@ export interface HotbarItem {
}
}
-export interface Hotbar {
- id: string;
- name: string;
- items: HotbarItem[];
-}
+export type Hotbar = Required;
export interface HotbarCreateOptions {
id?: string;
name: string;
- items?: HotbarItem[];
+ items?: (HotbarItem | null)[];
}
export interface HotbarStoreModel {
@@ -71,6 +67,7 @@ export class HotbarStore extends BaseStore {
migrations,
});
makeObservable(this);
+ this.load();
}
get activeHotbarId() {
@@ -91,20 +88,13 @@ export class HotbarStore extends BaseStore {
return this.hotbarIndex(this.activeHotbarId);
}
- get initialItems() {
+ static getInitialItems() {
return [...Array.from(Array(defaultHotbarCells).fill(null))];
}
- @action protected async fromStore(data: Partial = {}) {
- if (data.hotbars?.length === 0) {
- this.hotbars = [{
- id: uuid.v4(),
- name: "Default",
- items: this.initialItems,
- }];
- } else {
- this.hotbars = data.hotbars;
- }
+ @action
+ protected async fromStore(data: Partial = {}) {
+ this.hotbars = data.hotbars;
if (data.activeHotbarId) {
if (this.getById(data.activeHotbarId)) {
@@ -129,18 +119,19 @@ export class HotbarStore extends BaseStore {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
- add(data: HotbarCreateOptions) {
+ @action
+ add(data: HotbarCreateOptions, { setActive = false } = {}) {
const {
id = uuid.v4(),
- items = this.initialItems,
+ items = HotbarStore.getInitialItems(),
name,
} = data;
- const hotbar = { id, name, items };
+ this.hotbars.push({ id, name, items });
- this.hotbars.push(hotbar as Hotbar);
-
- return hotbar as Hotbar;
+ if (setActive) {
+ this._activeHotbarId = id;
+ }
}
@action
@@ -181,7 +172,7 @@ export class HotbarStore extends BaseStore {
}
@action
- removeFromHotbar(uid: string) {
+ removeFromHotbar(uid: string): void {
const hotbar = this.getActive();
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
@@ -192,6 +183,25 @@ export class HotbarStore extends BaseStore {
hotbar.items[index] = null;
}
+ /**
+ * Remvove all hotbar items that reference the `uid`.
+ * @param uid The `EntityId` that each hotbar item refers to
+ * @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
+ */
+ @action
+ removeAllHotbarItems(uid: string) {
+ const undoItems: [Hotbar, number, HotbarItem][] = [];
+
+ for (const hotbar of this.hotbars) {
+ const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
+
+ if (index >= 0) {
+ undoItems.push([hotbar, index, hotbar.items[index]]);
+ hotbar.items[index] = null;
+ }
+ }
+ }
+
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts
index 7967d0bda3..c387b18a55 100644
--- a/src/common/protocol-handler/router.ts
+++ b/src/common/protocol-handler/router.ts
@@ -30,6 +30,8 @@ import { ExtensionsStore } from "../../extensions/extensions-store";
import { ExtensionLoader } from "../../extensions/extension-loader";
import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
+import { when } from "mobx";
+import { ipcRenderer } from "electron";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler";
@@ -178,16 +180,28 @@ export abstract class LensProtocolRouter extends Singleton {
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = [publisher, partialName].filter(Boolean).join("/");
+ const extensionLoader = ExtensionLoader.getInstance();
- const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
-
- if (!extension) {
- logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
+ try {
+ /**
+ * Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
+ */
+ await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
+ } catch(error) {
+ logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
return name;
}
- if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
+ const extension = extensionLoader.getInstanceByName(name);
+
+ if (!extension) {
+ logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
+
+ return name;
+ }
+
+ if (!ExtensionsStore.getInstance().isEnabled(extension)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name;
diff --git a/src/common/user-store.ts b/src/common/user-store.ts
index f7ab221bb8..c1e956b331 100644
--- a/src/common/user-store.ts
+++ b/src/common/user-store.ts
@@ -68,7 +68,10 @@ export class UserStore extends BaseStore {
configName: "lens-user-store",
migrations,
});
+
makeObservable(this);
+ fileNameMigration();
+ this.load();
}
@observable lastSeenAppVersion = "0.0.0";
@@ -101,33 +104,6 @@ export class UserStore extends BaseStore {
[path.join(os.homedir(), ".kube"), {}]
]);
- 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();
- await super.load();
-
- if (app) {
- // track telemetry availability
- reaction(() => this.allowTelemetry, allowed => {
- appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
- });
-
- // open at system start-up
- reaction(() => this.openAtLogin, openAtLogin => {
- app.setLoginItemSettings({
- openAtLogin,
- openAsHidden: true,
- args: ["--hidden"]
- });
- }, {
- fireImmediately: true,
- });
- }
- }
-
@computed get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}
@@ -136,6 +112,24 @@ export class UserStore extends BaseStore {
return this.shell || process.env.SHELL || process.env.PTYSHELL;
}
+ startMainReactions() {
+ // track telemetry availability
+ reaction(() => this.allowTelemetry, allowed => {
+ appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" });
+ });
+
+ // open at system start-up
+ reaction(() => this.openAtLogin, openAtLogin => {
+ app.setLoginItemSettings({
+ openAtLogin,
+ openAsHidden: true,
+ args: ["--hidden"]
+ });
+ }, {
+ fireImmediately: true,
+ });
+ }
+
/**
* Checks if a column (by ID) for a table (by ID) is configured to be hidden
* @param tableId The ID of the table to be checked against
@@ -165,8 +159,7 @@ export class UserStore extends BaseStore {
}
@action
- async resetTheme() {
- await this.whenLoaded;
+ resetTheme() {
this.colorTheme = UserStore.defaultTheme;
}
diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts
index 34bd65755b..d652933341 100644
--- a/src/common/utils/index.ts
+++ b/src/common/utils/index.ts
@@ -19,8 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-// Common utils (main OR renderer)
-
+/**
+ * A function that does nothing
+ */
export function noop(...args: T): void {
return void args;
}
diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts
index 277c58213e..59fce41829 100644
--- a/src/common/utils/iter.ts
+++ b/src/common/utils/iter.ts
@@ -140,3 +140,19 @@ export function* filterMapStrict(src: Iterable, fn: (from: T) => U | nu
}
}
}
+
+/**
+ * Iterate through `src` until `match` returns a truthy value
+ * @param src A type that can be iterated over
+ * @param match A function that should return a truthy value for the item that you want to find
+ * @returns The first entry that `match` returns a truthy value for, or `undefined`
+ */
+export function find(src: Iterable, match: (i: T) => any): T | undefined {
+ for (const from of src) {
+ if (match(from)) {
+ return from;
+ }
+ }
+
+ return void 0;
+}
diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts
new file mode 100644
index 0000000000..7b3a62f946
--- /dev/null
+++ b/src/common/weblink-store.ts
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { action, comparer, observable, makeObservable } from "mobx";
+import { BaseStore } from "./base-store";
+import migrations from "../migrations/hotbar-store";
+import * as uuid from "uuid";
+import { toJS } from "./utils";
+
+export interface WeblinkData {
+ id: string;
+ name: string;
+ url: string;
+}
+
+export interface WeblinkCreateOptions {
+ id?: string;
+ name: string;
+ url: string;
+}
+
+
+export interface WeblinkStoreModel {
+ weblinks: WeblinkData[];
+}
+
+export class WeblinkStore extends BaseStore {
+ @observable weblinks: WeblinkData[] = [];
+
+ constructor() {
+ super({
+ configName: "lens-weblink-store",
+ accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
+ syncOptions: {
+ equals: comparer.structural,
+ },
+ migrations,
+ });
+ makeObservable(this);
+ this.load();
+ }
+
+ @action protected async fromStore(data: Partial = {}) {
+ this.weblinks = data.weblinks || [];
+ }
+
+ add(data: WeblinkCreateOptions) {
+ const {
+ id = uuid.v4(),
+ name,
+ url
+ } = data;
+
+ const weblink = { id, name, url };
+
+ this.weblinks.push(weblink as WeblinkData);
+
+ return weblink as WeblinkData;
+ }
+
+ @action
+ removeById(id: string) {
+ this.weblinks = this.weblinks.filter((w) => w.id !== id);
+ }
+
+ toJSON(): WeblinkStoreModel {
+ const model: WeblinkStoreModel = {
+ weblinks: this.weblinks
+ };
+
+ return toJS(model);
+ }
+}
diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts
index 347487e556..6db33acf19 100644
--- a/src/extensions/__tests__/extension-discovery.test.ts
+++ b/src/extensions/__tests__/extension-discovery.test.ts
@@ -39,6 +39,12 @@ jest.mock("../extension-installer", () => ({
installPackage: jest.fn()
}
}));
+jest.mock("electron", () => ({
+ app: {
+ getPath: () => "tmp",
+ setLoginItemSettings: jest.fn(),
+ },
+}));
console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction;
diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts
index dadde70ceb..7b7688e85f 100644
--- a/src/extensions/common-api/registrations.ts
+++ b/src/extensions/common-api/registrations.ts
@@ -24,6 +24,6 @@ export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from ".
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 { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
export type { StatusBarRegistration } from "../registries/status-bar-registry";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts
index ea34798271..9f2fe15067 100644
--- a/src/extensions/extension-discovery.ts
+++ b/src/extensions/extension-discovery.ts
@@ -357,8 +357,8 @@ export class ExtensionDiscovery extends Singleton {
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise {
try {
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
- const installedManifestPath = this.getInstalledManifestPath(manifest.name);
- const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
+ const id = this.getInstalledManifestPath(manifest.name);
+ const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled });
const extensionDir = path.dirname(manifestPath);
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
@@ -369,9 +369,9 @@ export class ExtensionDiscovery extends Singleton {
}
return {
- id: installedManifestPath,
+ id,
absolutePath,
- manifestPath: installedManifestPath,
+ manifestPath: id,
manifest,
isBundled,
isEnabled,
diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts
index 4073ad1e79..af69a6bef8 100644
--- a/src/extensions/extension-loader.ts
+++ b/src/extensions/extension-loader.ts
@@ -22,7 +22,7 @@
import { app, ipcRenderer, remote } from "electron";
import { EventEmitter } from "events";
import { isEqual } from "lodash";
-import { action, computed, makeObservable, observable, reaction, when } from "mobx";
+import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path";
import { getHostedCluster } from "../common/cluster-store";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
@@ -48,6 +48,18 @@ export class ExtensionLoader extends Singleton {
protected extensions = observable.map();
protected instances = observable.map();
+ /**
+ * This is the set of extensions that don't come with either
+ * - Main.LensExtension when running in the main process
+ * - Renderer.LensExtension when running in the renderer process
+ */
+ protected nonInstancesByName = observable.set();
+
+ /**
+ * This is updated by the `observe` in the constructor. DO NOT write directly to it
+ */
+ protected instancesByName = observable.map();
+
// IPC channel to broadcast changes to extensions from main
protected static readonly extensionsMainChannel = "extensions:main";
@@ -65,8 +77,23 @@ export class ExtensionLoader extends Singleton {
constructor() {
super();
-
makeObservable(this);
+ observe(this.instances, change => {
+ switch (change.type) {
+ case "add":
+ if (this.instancesByName.has(change.newValue.name)) {
+ throw new TypeError("Extension names must be unique");
+ }
+
+ this.instancesByName.set(change.newValue.name, change.newValue);
+ break;
+ case "delete":
+ this.instancesByName.delete(change.oldValue.name);
+ break;
+ case "update":
+ throw new Error("Extension instances shouldn't be updated");
+ }
+ });
}
@computed get userExtensions(): Map {
@@ -81,28 +108,21 @@ export class ExtensionLoader extends Singleton {
return extensions;
}
- @computed get userExtensionsByName(): Map {
- const extensions = new Map();
-
- for (const [, val] of this.instances.toJSON()) {
- if (val.isBundled) {
- continue;
- }
-
- extensions.set(val.manifest.name, val);
+ /**
+ * Get the extension instance by its manifest name
+ * @param name The name of the extension
+ * @returns one of the following:
+ * - the instance of `Main.LensExtension` on the main process if created
+ * - the instance of `Renderer.LensExtension` on the renderer process if created
+ * - `null` if no class definition is provided for the current process
+ * - `undefined` if the name is not known about
+ */
+ getInstanceByName(name: string): LensExtension | null | undefined {
+ if (this.nonInstancesByName.has(name)) {
+ return null;
}
- return extensions;
- }
-
- getExtensionByName(name: string): LensExtension | null {
- for (const [, val] of this.instances) {
- if (val.name === name) {
- return val;
- }
- }
-
- return null;
+ return this.instancesByName.get(name);
}
// Transform userExtensions to a state object for storing into ExtensionsStore
@@ -124,7 +144,7 @@ export class ExtensionLoader extends Singleton {
await this.initMain();
}
- await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
+ await Promise.all([this.whenLoaded]);
// broadcasting extensions between main/renderer processes
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
@@ -145,6 +165,7 @@ export class ExtensionLoader extends Singleton {
this.extensions.set(extension.id, extension);
}
+ @action
removeInstance(lensExtensionId: LensExtensionId) {
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
const instance = this.instances.get(lensExtensionId);
@@ -157,6 +178,7 @@ export class ExtensionLoader extends Singleton {
instance.disable();
this.events.emit("remove", instance);
this.instances.delete(lensExtensionId);
+ this.nonInstancesByName.delete(instance.name);
} catch (error) {
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
}
@@ -170,6 +192,10 @@ export class ExtensionLoader extends Singleton {
}
}
+ setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
+ this.extensions.get(lensExtensionId).isEnabled = isEnabled;
+ }
+
protected async initMain() {
this.isLoaded = true;
this.loadOnMain();
@@ -302,13 +328,14 @@ export class ExtensionLoader extends Singleton {
protected autoInitExtensions(register: (ext: LensExtension) => Promise) {
return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, extension] of installedExtensions) {
- const alreadyInit = this.instances.has(extId);
+ const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
try {
const LensExtensionClass = this.requireExtension(extension);
if (!LensExtensionClass) {
+ this.nonInstancesByName.add(extension.manifest.name);
continue;
}
@@ -355,6 +382,10 @@ export class ExtensionLoader extends Singleton {
return this.extensions.get(extId);
}
+ getInstanceById(extId: LensExtensionId): E {
+ return this.instances.get(extId) as E;
+ }
+
toJSON(): Map {
return toJS(this.extensions);
}
diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts
index 2c5eb44262..6b4e9c5489 100644
--- a/src/extensions/extension-store.ts
+++ b/src/extensions/extension-store.ts
@@ -26,13 +26,13 @@ import type { LensExtension } from "./lens-extension";
export abstract class ExtensionStore extends BaseStore {
protected extension: LensExtension;
- async loadExtension(extension: LensExtension) {
+ loadExtension(extension: LensExtension) {
this.extension = extension;
return super.load();
}
- async load() {
+ load() {
if (!this.extension) { return; }
return super.load();
diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts
index 86a90e0238..be40181242 100644
--- a/src/extensions/extensions-store.ts
+++ b/src/extensions/extensions-store.ts
@@ -39,6 +39,7 @@ export class ExtensionsStore extends BaseStore {
configName: "lens-extensions",
});
makeObservable(this);
+ this.load();
}
@computed
@@ -50,12 +51,10 @@ export class ExtensionsStore extends BaseStore {
protected state = observable.map();
- isEnabled(extId: LensExtensionId): boolean {
- const state = this.state.get(extId);
-
+ isEnabled({ id, isBundled }: { id: string, isBundled: boolean }): boolean {
// By default false, so that copied extensions are disabled by default.
// If user installs the extension from the UI, the Extensions component will specifically enable it.
- return Boolean(state?.enabled);
+ return isBundled || Boolean(this.state.get(id)?.enabled);
}
@action
diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts
index 023a8b92f9..cbdba9e07c 100644
--- a/src/extensions/lens-main-extension.ts
+++ b/src/extensions/lens-main-extension.ts
@@ -21,7 +21,6 @@
import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
-import { getExtensionPageUrl } from "./registries/page-registry";
import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
@@ -30,15 +29,8 @@ import type { MenuRegistration } from "./registries";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];
- async navigate(pageId?: string, params?: P, frameId?: number) {
- const windowManager = WindowManager.getInstance();
- const pageUrl = getExtensionPageUrl({
- extensionId: this.name,
- pageId,
- params: params ?? {}, // compile to url with params
- });
-
- await windowManager.navigate(pageUrl, frameId);
+ async navigate(pageId?: string, params?: Record, frameId?: number) {
+ return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
}
addCatalogSource(id: string, source: IObservableArray) {
diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts
index 7119b0f7ac..a3026d99a5 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -19,33 +19,26 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import type {
- AppPreferenceRegistration, CatalogEntityDetailRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
- KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
-} from "./registries";
+import type * as registries from "./registries";
import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry";
-import type { CommandRegistration } from "./registries/command-registry";
-import type { EntitySettingRegistration } from "./registries/entity-setting-registry";
-import type { TopBarRegistration } from "./registries/topbar-registry";
export class LensRendererExtension extends LensExtension {
- globalPages: PageRegistration[] = [];
- clusterPages: PageRegistration[] = [];
- globalPageMenus: PageMenuRegistration[] = [];
- clusterPageMenus: ClusterPageMenuRegistration[] = [];
- kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
- appPreferences: AppPreferenceRegistration[] = [];
- entitySettings: EntitySettingRegistration[] = [];
- statusBarItems: StatusBarRegistration[] = [];
- kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
- kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
- kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
- commands: CommandRegistration[] = [];
- welcomeMenus: WelcomeMenuRegistration[] = [];
- catalogEntityDetailItems: CatalogEntityDetailRegistration[] = [];
- topBarItems: TopBarRegistration[] = [];
+ globalPages: registries.PageRegistration[] = [];
+ clusterPages: registries.PageRegistration[] = [];
+ clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
+ kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
+ appPreferences: registries.AppPreferenceRegistration[] = [];
+ entitySettings: registries.EntitySettingRegistration[] = [];
+ statusBarItems: registries.StatusBarRegistration[] = [];
+ kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
+ kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
+ kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
+ commands: registries.CommandRegistration[] = [];
+ welcomeMenus: registries.WelcomeMenuRegistration[] = [];
+ catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
+ topBarItems: registries.TopBarRegistration[] = [];
async navigate(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json
index 6857b12dda..594f26f56e 100644
--- a/src/extensions/npm/extensions/package.json
+++ b/src/extensions/npm/extensions/package.json
@@ -19,6 +19,7 @@
"@types/node": "*",
"@types/react-select": "*",
"@material-ui/core": "*",
- "conf": "^7.0.1"
+ "conf": "^7.0.1",
+ "typed-emitter": "^1.3.1"
}
}
diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts
index 73997fb461..17e5f10491 100644
--- a/src/extensions/registries/page-menu-registry.ts
+++ b/src/extensions/registries/page-menu-registry.ts
@@ -26,23 +26,20 @@ import type { PageTarget, RegisteredPage } from "./page-registry";
import type { LensExtension } from "../lens-extension";
import { BaseRegistry } from "./base-registry";
-export interface PageMenuRegistration {
- target?: PageTarget;
- title: React.ReactNode;
- components: PageMenuComponents;
-}
-
-export interface ClusterPageMenuRegistration extends PageMenuRegistration {
+export interface ClusterPageMenuRegistration {
id?: string;
parentId?: string;
+ target?: PageTarget;
+ title: React.ReactNode;
+ components: ClusterPageMenuComponents;
}
-export interface PageMenuComponents {
+export interface ClusterPageMenuComponents {
Icon: React.ComponentType;
}
-export class PageMenuRegistry extends BaseRegistry {
- add(items: T[], ext: LensExtension) {
+export class ClusterPageMenuRegistry extends BaseRegistry {
+ add(items: ClusterPageMenuRegistration[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => {
menuItem.target = {
extensionId: ext.name,
@@ -54,9 +51,7 @@ export class PageMenuRegistry extends BaseRegist
return super.add(normalizedItems);
}
-}
-export class ClusterPageMenuRegistry extends PageMenuRegistry {
getRootItems() {
return this.getItems().filter((item) => !item.parentId);
}
diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts
index 49b3ef933c..f19ef18f7a 100644
--- a/src/extensions/renderer-api/components.ts
+++ b/src/extensions/renderer-api/components.ts
@@ -20,6 +20,8 @@
*/
// layouts
+export * from "../../renderer/components/layout/main-layout";
+export * from "../../renderer/components/layout/setting-layout";
export * from "../../renderer/components/layout/page-layout";
export * from "../../renderer/components/layout/wizard-layout";
export * from "../../renderer/components/layout/tab-layout";
diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts
index 9210739195..8c50518c85 100644
--- a/src/main/__test__/cluster.test.ts
+++ b/src/main/__test__/cluster.test.ts
@@ -120,8 +120,8 @@ describe("create clusters", () => {
protected bindEvents() {
return;
}
- protected async ensureKubectl() {
- return Promise.resolve(true);
+ async ensureKubectl() {
+ return Promise.resolve(null);
}
}({
id: "foo",
diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts
index ad3b42f5d2..0ba3a2d028 100644
--- a/src/main/__test__/context-handler.test.ts
+++ b/src/main/__test__/context-handler.test.ts
@@ -22,6 +22,14 @@
import { UserStore } from "../../common/user-store";
import { ContextHandler } from "../context-handler";
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
+import mockFs from "mock-fs";
+
+jest.mock("electron", () => ({
+ app: {
+ getPath: () => "tmp",
+ setLoginItemSettings: jest.fn(),
+ },
+}));
enum ServiceResult {
Success,
@@ -70,6 +78,10 @@ function getHandler() {
describe("ContextHandler", () => {
beforeEach(() => {
+ mockFs({
+ "tmp": {}
+ });
+
PrometheusProviderRegistry.createInstance();
UserStore.createInstance();
});
@@ -77,6 +89,7 @@ describe("ContextHandler", () => {
afterEach(() => {
PrometheusProviderRegistry.resetInstance();
UserStore.resetInstance();
+ mockFs.restore();
});
describe("getPrometheusService", () => {
diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts
index 5006859637..e0dd7f73c6 100644
--- a/src/main/__test__/kube-auth-proxy.test.ts
+++ b/src/main/__test__/kube-auth-proxy.test.ts
@@ -46,7 +46,8 @@ jest.mock("winston", () => ({
jest.mock("electron", () => ({
app: {
- getPath: () => "/foo",
+ getPath: () => "tmp",
+ setLoginItemSettings: jest.fn(),
},
}));
@@ -77,8 +78,6 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction {
beforeEach(() => {
jest.clearAllMocks();
- UserStore.resetInstance();
- UserStore.createInstance();
const mockMinikubeConfig = {
"minikube-config.yml": JSON.stringify({
@@ -102,13 +101,16 @@ describe("kube auth proxy tests", () => {
}],
kind: "Config",
preferences: {},
- })
+ }),
+ "tmp": {},
};
mockFs(mockMinikubeConfig);
+ UserStore.createInstance();
});
afterEach(() => {
+ UserStore.resetInstance();
mockFs.restore();
});
diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts
index 5c9a41b702..b662c36f69 100644
--- a/src/main/catalog-pusher.ts
+++ b/src/main/catalog-pusher.ts
@@ -30,7 +30,7 @@ import type { CatalogEntity } from "../common/catalog";
const broadcaster = debounce((items: CatalogEntity[]) => {
broadcastMessage("catalog:items", items);
-}, 1_000, { trailing: true });
+}, 1_000, { leading: true, trailing: true });
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return reaction(() => toJS(catalog.items), (items) => {
diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts
index 7fcd3c1db6..ac0eb3b33b 100644
--- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts
+++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts
@@ -27,6 +27,7 @@ import { computeDiff, configToModels } from "../kubeconfig-sync";
import mockFs from "mock-fs";
import fs from "fs";
import { ClusterStore } from "../../../common/cluster-store";
+import { ClusterManager } from "../../cluster-manager";
jest.mock("electron", () => ({
app: {
@@ -38,10 +39,13 @@ describe("kubeconfig-sync.source tests", () => {
beforeEach(() => {
mockFs();
ClusterStore.createInstance();
+ ClusterManager.createInstance();
});
afterEach(() => {
mockFs.restore();
+ ClusterStore.resetInstance();
+ ClusterManager.resetInstance();
});
describe("configsToModels", () => {
diff --git a/src/main/catalog-sources/general.ts b/src/main/catalog-sources/general.ts
new file mode 100644
index 0000000000..6b16984d1d
--- /dev/null
+++ b/src/main/catalog-sources/general.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { observable } from "mobx";
+import { GeneralEntity } from "../../common/catalog-entities/general";
+import { catalogURL, preferencesURL } from "../../common/routes";
+import { catalogEntityRegistry } from "../catalog";
+
+export const catalogEntity = new GeneralEntity({
+ metadata: {
+ uid: "catalog-entity",
+ name: "Catalog",
+ source: "app",
+ labels: {}
+ },
+ spec: {
+ path: catalogURL(),
+ icon: {
+ material: "view_list",
+ background: "#3d90ce"
+ }
+ },
+ status: {
+ phase: "active",
+ }
+});
+
+const preferencesEntity = new GeneralEntity({
+ metadata: {
+ uid: "preferences-entity",
+ name: "Preferences",
+ source: "app",
+ labels: {}
+ },
+ spec: {
+ path: preferencesURL(),
+ icon: {
+ material: "settings",
+ background: "#3d90ce"
+ }
+ },
+ status: {
+ phase: "active",
+ }
+});
+
+const generalEntities = observable([
+ catalogEntity,
+ preferencesEntity
+]);
+
+export function syncGeneralEntities() {
+ catalogEntityRegistry.addObservableSource("lens:general", generalEntities);
+}
diff --git a/src/main/catalog-sources/index.ts b/src/main/catalog-sources/index.ts
index 3f1a76790f..70c2d073fb 100644
--- a/src/main/catalog-sources/index.ts
+++ b/src/main/catalog-sources/index.ts
@@ -19,4 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+export { syncWeblinks } from "./weblinks";
export { KubeconfigSyncManager } from "./kubeconfig-sync";
+export { syncGeneralEntities } from "./general";
diff --git a/src/main/catalog-sources/kubeconfig-sync.ts b/src/main/catalog-sources/kubeconfig-sync.ts
index af6d7e150d..6a6f4120f9 100644
--- a/src/main/catalog-sources/kubeconfig-sync.ts
+++ b/src/main/catalog-sources/kubeconfig-sync.ts
@@ -31,7 +31,7 @@ import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster";
-import { catalogEntityFromCluster } from "../cluster-manager";
+import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager";
import { UserStore } from "../../common/user-store";
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
import { createHash } from "crypto";
@@ -170,6 +170,9 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
// remove and disconnect clusters that were removed from the config
if (!model) {
+ // remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
+ ClusterManager.getInstance().deleting.delete(value[0].id);
+
value[0].disconnect();
source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
diff --git a/src/main/catalog-sources/weblinks.ts b/src/main/catalog-sources/weblinks.ts
new file mode 100644
index 0000000000..2593fedc7c
--- /dev/null
+++ b/src/main/catalog-sources/weblinks.ts
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { observable, reaction } from "mobx";
+import { WeblinkStore } from "../../common/weblink-store";
+import { WebLink } from "../../common/catalog-entities";
+import { catalogEntityRegistry } from "../catalog";
+import got from "got";
+import logger from "../logger";
+import { docsUrl, slackUrl } from "../../common/vars";
+
+const defaultLinks = [
+ { title: "Lens Website", url: "https://k8slens.dev" },
+ { title: "Lens Documentation", url: docsUrl },
+ { title: "Lens Community Slack", url: slackUrl },
+ { title: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" },
+ { title: "Lens on Twitter", url: "https://twitter.com/k8slens" },
+ { title: "Lens Official Blog", url: "https://medium.com/k8slens" }
+].map((link) => (
+ new WebLink({
+ metadata: {
+ uid: link.url,
+ name: link.title,
+ source: "app",
+ labels: {}
+ },
+ spec: {
+ url: link.url
+ },
+ status: {
+ phase: "available",
+ active: true
+ }
+ })
+));
+
+async function validateLink(link: WebLink) {
+ try {
+ const response = await got.get(link.spec.url, {
+ throwHttpErrors: false
+ });
+
+ if (response.statusCode >= 200 && response.statusCode < 500) {
+ link.status.phase = "available";
+ } else {
+ link.status.phase = "unavailable";
+ }
+ } catch(error) {
+ link.status.phase = "unavailable";
+ }
+}
+
+
+export function syncWeblinks() {
+ const weblinkStore = WeblinkStore.getInstance();
+ const weblinks = observable.array(defaultLinks);
+
+ reaction(() => weblinkStore.weblinks, (links) => {
+ weblinks.replace(links.map((data) => new WebLink({
+ metadata: {
+ uid: data.id,
+ name: data.name,
+ source: "local",
+ labels: {}
+ },
+ spec: {
+ url: data.url
+ },
+ status: {
+ phase: "available",
+ active: true
+ }
+ })));
+ weblinks.push(...defaultLinks);
+
+ for (const link of weblinks) {
+ validateLink(link).catch((error) => {
+ logger.error(`failed to validate link ${link.spec.url}: %s`, error);
+ });
+ }
+ }, {fireImmediately: true});
+
+ catalogEntityRegistry.addObservableSource("weblinks", weblinks);
+}
diff --git a/src/main/catalog/__tests__/catalog-entity-registry.test.ts b/src/main/catalog/__tests__/catalog-entity-registry.test.ts
index fff3faa3d9..ee8450ae26 100644
--- a/src/main/catalog/__tests__/catalog-entity-registry.test.ts
+++ b/src/main/catalog/__tests__/catalog-entity-registry.test.ts
@@ -58,7 +58,7 @@ describe("CatalogEntityRegistry", () => {
url: "https://k8slens.dev"
},
status: {
- phase: "valid"
+ phase: "available"
}
});
const invalidEntity = new InvalidEntity({
@@ -72,7 +72,7 @@ describe("CatalogEntityRegistry", () => {
url: "https://k8slens.dev"
},
status: {
- phase: "valid"
+ phase: "available"
}
});
diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts
index 4075c81e36..fd7ef779dd 100644
--- a/src/main/cluster-manager.ts
+++ b/src/main/cluster-manager.ts
@@ -21,8 +21,8 @@
import "../common/cluster-ipc";
import type http from "http";
-import { action, autorun, makeObservable, reaction, toJS } from "mobx";
-import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
+import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
+import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import type { Cluster } from "./cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
@@ -30,17 +30,18 @@ import { Singleton } from "../common/utils";
import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc";
+import { once } from "lodash";
export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance();
+ deleting = observable.set();
constructor() {
super();
makeObservable(this);
- this.bindEvents();
}
- private bindEvents() {
+ init = once(() => {
// reacting to every cluster's state change and total amount of items
reaction(
() => this.store.clustersList.map(c => c.getState()),
@@ -59,6 +60,12 @@ export class ClusterManager extends Singleton {
this.syncClustersFromCatalog(entities);
});
+ observe(this.deleting, change => {
+ if (change.type === "add") {
+ this.updateEntityStatus(catalogEntityRegistry.getById(change.newValue));
+ }
+ });
+
// auto-stop removed clusters
autorun(() => {
const removedClusters = Array.from(this.store.removedClusters.values());
@@ -76,7 +83,7 @@ export class ClusterManager extends Singleton {
ipcMainOn("network:offline", this.onNetworkOffline);
ipcMainOn("network:online", this.onNetworkOnline);
- }
+ });
@action
protected updateCatalog(clusters: Cluster[]) {
@@ -96,6 +103,9 @@ export class ClusterManager extends Singleton {
this.updateEntityStatus(entity, cluster);
+ entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
+ entity.metadata.labels.distro = cluster.distribution;
+
if (cluster.preferences?.clusterName) {
entity.metadata.name = cluster.preferences.clusterName;
}
@@ -110,13 +120,25 @@ export class ClusterManager extends Singleton {
entity.spec.metrics.prometheus = prometheus;
}
- entity.spec.iconData = cluster.preferences.icon;
+ if (cluster.preferences.icon) {
+ entity.spec.icon ??= {};
+ entity.spec.icon.src = cluster.preferences.icon;
+ } else {
+ entity.spec.icon = null;
+ }
catalogEntityRegistry.items.splice(index, 1, entity);
}
- protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
- entity.status.phase = cluster.accessible ? "connected" : "disconnected";
+ @action
+ protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
+ if (this.deleting.has(entity.getId())) {
+ entity.status.phase = "deleting";
+ entity.status.enabled = false;
+ } else {
+ entity.status.phase = cluster?.accessible ? "connected" : "disconnected";
+ entity.status.enabled = true;
+ }
}
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
@@ -204,7 +226,8 @@ export function catalogEntityFromCluster(cluster: Cluster) {
},
spec: {
kubeconfigPath: cluster.kubeConfigPath,
- kubeconfigContext: cluster.contextName
+ kubeconfigContext: cluster.contextName,
+ icon: {}
},
status: {
phase: cluster.disconnected ? "disconnected" : "connected",
diff --git a/src/main/cluster.ts b/src/main/cluster.ts
index df0da0a552..6675bd0151 100644
--- a/src/main/cluster.ts
+++ b/src/main/cluster.ts
@@ -88,12 +88,7 @@ export interface ClusterState {
export class Cluster implements ClusterModel, ClusterState {
/** Unique id for a cluster */
public readonly id: ClusterId;
- /**
- * Kubectl
- *
- * @internal
- */
- public kubeCtl: Kubectl;
+ private kubeCtl: Kubectl;
/**
* Context handler
*
@@ -212,6 +207,11 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable accessibleNamespaces: string[] = [];
+ /**
+ * Labels for the catalog entity
+ */
+ @observable labels: Record = {};
+
/**
* Is cluster available
*
@@ -309,6 +309,10 @@ export class Cluster implements ClusterModel, ClusterState {
if (model.accessibleNamespaces) {
this.accessibleNamespaces = model.accessibleNamespaces;
}
+
+ if (model.labels) {
+ this.labels = model.labels;
+ }
}
/**
@@ -363,7 +367,7 @@ export class Cluster implements ClusterModel, ClusterState {
if (this.accessible) {
await this.refreshAccessibility();
- this.ensureKubectl();
+ this.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
}
this.activated = true;
@@ -373,10 +377,12 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
- protected async ensureKubectl() {
- this.kubeCtl = new Kubectl(this.version);
+ async ensureKubectl() {
+ this.kubeCtl ??= new Kubectl(this.version);
- return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
+ await this.kubeCtl.ensureKubectl();
+
+ return this.kubeCtl;
}
/**
@@ -586,6 +592,7 @@ export class Cluster implements ClusterModel, ClusterState {
preferences: this.preferences,
metadata: this.metadata,
accessibleNamespaces: this.accessibleNamespaces,
+ labels: this.labels,
};
return toJS(model);
@@ -650,7 +657,7 @@ export class Cluster implements ClusterModel, ClusterState {
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
try {
- const { body: { items }} = await api.listNamespace();
+ const { body: { items } } = await api.listNamespace();
const namespaces = items.map(ns => ns.metadata.name);
this.getAllowedNamespacesErrorCount = 0; // reset on success
diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts
index 402619bdc9..46afe45d6e 100644
--- a/src/main/extension-filesystem.ts
+++ b/src/main/extension-filesystem.ts
@@ -42,6 +42,7 @@ export class FilesystemProvisionerStore extends BaseStore {
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
});
makeObservable(this);
+ this.load();
}
/**
diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts
index 9ec1237c07..b8a18a1021 100644
--- a/src/main/helm/helm-release-manager.ts
+++ b/src/main/helm/helm-release-manager.ts
@@ -169,9 +169,10 @@ export async function rollback(name: string, namespace: string, revision: number
async function getResources(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath();
- const kubectl = await cluster.kubeCtl.getPath();
+ const kubectl = await cluster.ensureKubectl();
+ const kubectlPath = await kubectl.getPath();
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`);
+ const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
return JSON.parse(stdout).items;
} catch {
diff --git a/src/main/index.ts b/src/main/index.ts
index e04746e1e2..0599f820cb 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -35,14 +35,10 @@ import { shellSync } from "./shell-sync";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
-import { ClusterStore } from "../common/cluster-store";
-import { UserStore } from "../common/user-store";
import { appEventBus } from "../common/event-bus";
import { ExtensionLoader } from "../extensions/extension-loader";
-import { ExtensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
-import { FilesystemProvisionerStore } from "./extension-filesystem";
import { installDeveloperTools } from "./developer-tools";
import { LensProtocolRouterMain } from "./protocol-handler";
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
@@ -51,13 +47,18 @@ import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { pushCatalogToRenderer } from "./catalog-pusher";
import { catalogEntityRegistry } from "./catalog";
-import { HotbarStore } from "../common/hotbar-store";
import { HelmRepoManager } from "./helm/helm-repo-manager";
-import { KubeconfigSyncManager } from "./catalog-sources";
+import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry } from "./prometheus";
import * as initializers from "./initializers";
+import { ClusterStore } from "../common/cluster-store";
+import { HotbarStore } from "../common/hotbar-store";
+import { UserStore } from "../common/user-store";
+import { WeblinkStore } from "../common/weblink-store";
+import { ExtensionsStore } from "../extensions/extensions-store";
+import { FilesystemProvisionerStore } from "./extension-filesystem";
const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
@@ -128,27 +129,32 @@ app.on("ready", async () => {
PrometheusProviderRegistry.createInstance();
initializers.initPrometheusProviderRegistry();
- const userStore = UserStore.createInstance();
- const clusterStore = ClusterStore.createInstance();
- const hotbarStore = HotbarStore.createInstance();
- const extensionsStore = ExtensionsStore.createInstance();
- const filesystemStore = FilesystemProvisionerStore.createInstance();
+ /**
+ * The following sync MUST be done before HotbarStore creation, because that
+ * store has migrations that will remove items that previous migrations add
+ * if this is not presant
+ */
+ syncGeneralEntities();
+
+ logger.info("💾 Loading stores");
+
+ UserStore.createInstance().startMainReactions();
+ ClusterStore.createInstance().provideInitialFromMain();
+ HotbarStore.createInstance();
+ ExtensionsStore.createInstance();
+ FilesystemProvisionerStore.createInstance();
+ WeblinkStore.createInstance();
+
+ syncWeblinks();
HelmRepoManager.createInstance(); // create the instance
- logger.info("💾 Loading stores");
- // preload
- await Promise.all([
- userStore.load(),
- clusterStore.load(),
- hotbarStore.load(),
- extensionsStore.load(),
- filesystemStore.load(),
- ]);
+ const lensProxy = LensProxy.createInstance(
+ handleWsUpgrade,
+ req => ClusterManager.getInstance().getClusterForRequest(req),
+ );
- const lensProxy = LensProxy.createInstance(handleWsUpgrade);
-
- ClusterManager.createInstance();
+ ClusterManager.createInstance().init();
KubeconfigSyncManager.createInstance();
try {
@@ -201,10 +207,6 @@ app.on("ready", async () => {
LensProtocolRouterMain.getInstance().rendererLoaded = true;
});
- ExtensionLoader.getInstance().whenLoaded.then(() => {
- LensProtocolRouterMain.getInstance().extensionsLoaded = true;
- });
-
logger.info("🧩 Initializing extensions");
// call after windowManager to see splash earlier
@@ -254,13 +256,13 @@ app.on("will-quit", (event) => {
// This is called when the close button of the main window is clicked
const lprm = LensProtocolRouterMain.getInstance(false);
-
+
logger.info("APP:QUIT");
appEventBus.emit({ name: "app", action: "close" });
ClusterManager.getInstance(false)?.stop(); // close cluster connections
KubeconfigSyncManager.getInstance(false)?.stopSync();
cleanup();
-
+
if (lprm) {
// This is set to false here so that LPRM can wait to send future lens://
// requests until after it loads again
@@ -269,7 +271,7 @@ app.on("will-quit", (event) => {
if (blockQuit) {
// Quit app on Cmd+Q (MacOS)
-
+
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
diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/ipc.ts
index e3bb4f5ff7..95c4571a41 100644
--- a/src/main/initializers/ipc.ts
+++ b/src/main/initializers/ipc.ts
@@ -22,11 +22,15 @@
import type { IpcMainInvokeEvent } from "electron";
import type { KubernetesCluster } from "../../common/catalog-entities";
import { clusterFrameMap } from "../../common/cluster-frames";
-import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../common/cluster-ipc";
+import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
import { ClusterId, ClusterStore } from "../../common/cluster-store";
import { appEventBus } from "../../common/event-bus";
import { ipcMainHandle } from "../../common/ipc";
import { catalogEntityRegistry } from "../catalog";
+import { ClusterManager } from "../cluster-manager";
+import { bundledKubectlPath } from "../kubectl";
+import logger from "../logger";
+import { promiseExecFile } from "../promise-exec";
import { ResourceApplier } from "../resource-applier";
export function initIpcMainHandlers() {
@@ -73,6 +77,29 @@ export function initIpcMainHandlers() {
}
});
+ ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
+ appEventBus.emit({ name: "cluster", action: "remove" });
+ const cluster = ClusterStore.getInstance().getById(clusterId);
+
+ if (!cluster) {
+ return;
+ }
+
+ ClusterManager.getInstance().deleting.add(clusterId);
+ cluster.disconnect();
+ clusterFrameMap.delete(cluster.id);
+ const kubectlPath = bundledKubectlPath();
+ const args = ["config", "delete-context", cluster.contextName, "--kubeconfig", cluster.kubeConfigPath];
+
+ try {
+ await promiseExecFile(kubectlPath, args);
+ } catch ({ stderr }) {
+ logger.error(`[CLUSTER-REMOVE]: failed to remove cluster: ${stderr}`, { clusterId, context: cluster.contextName });
+
+ throw `Failed to remove cluster: ${stderr}`;
+ }
+ });
+
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts
index 1a00d2919d..01b38c67fe 100644
--- a/src/main/kubectl.ts
+++ b/src/main/kubectl.ts
@@ -31,6 +31,7 @@ import { UserStore } from "../common/user-store";
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
+import { SemVer } from "semver";
const bundledVersion = getBundledKubectlVersion();
const kubectlMap: Map = new Map([
@@ -92,14 +93,19 @@ export class Kubectl {
// Returns the single bundled Kubectl instance
public static bundled() {
- if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
-
- return Kubectl.bundledInstance;
+ return Kubectl.bundledInstance ??= new Kubectl(Kubectl.bundledKubectlVersion);
}
constructor(clusterVersion: string) {
- const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
- const minorVersion = versionParts[1];
+ let version: SemVer;
+
+ try {
+ version = new SemVer(clusterVersion, { includePrerelease: false });
+ } catch {
+ version = new SemVer(Kubectl.bundledKubectlVersion);
+ }
+
+ const minorVersion = `${version.major}.${version.minor}`;
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
@@ -107,7 +113,7 @@ export class Kubectl {
this.kubectlVersion = kubectlMap.get(minorVersion);
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
} else {
- this.kubectlVersion = versionParts[1] + versionParts[2];
+ this.kubectlVersion = version.format();
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
}
diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts
index 3056b77ce1..63341fe30e 100644
--- a/src/main/prometheus/lens.ts
+++ b/src/main/prometheus/lens.ts
@@ -54,6 +54,8 @@ export class PrometheusLens extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`);
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
case "memoryRequests":
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
case "memoryLimits":
@@ -88,6 +90,8 @@ export class PrometheusLens extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (instance)`;
case "memoryCapacity":
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
case "memoryAllocatableCapacity":
diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts
index 10c8fdbc33..58d41210b5 100644
--- a/src/main/prometheus/operator.ts
+++ b/src/main/prometheus/operator.ts
@@ -39,6 +39,8 @@ export class PrometheusOperator extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
case "memoryRequests":
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
case "memoryLimits":
@@ -73,6 +75,8 @@ export class PrometheusOperator extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (node)`;
case "memoryCapacity":
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
case "memoryAllocatableCapacity":
diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts
index 673c5069d8..7960e666a8 100644
--- a/src/main/prometheus/stacklight.ts
+++ b/src/main/prometheus/stacklight.ts
@@ -54,6 +54,8 @@ export class PrometheusStacklight extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
case "memoryRequests":
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
case "memoryLimits":
@@ -88,6 +90,8 @@ export class PrometheusStacklight extends PrometheusProvider {
switch (queryName) {
case "memoryUsage":
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
+ case "workloadMemoryUsage":
+ return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (instance)`;
case "memoryCapacity":
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
case "memoryAllocatableCapacity":
diff --git a/src/main/promise-exec.ts b/src/main/promise-exec.ts
index 8c55272c60..8b9d5b72cc 100644
--- a/src/main/promise-exec.ts
+++ b/src/main/promise-exec.ts
@@ -20,6 +20,7 @@
*/
import * as util from "util";
-import { exec } from "child_process";
+import { exec, execFile } from "child_process";
export const promiseExec = util.promisify(exec);
+export const promiseExecFile = util.promisify(execFile);
diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts
index 6c1d9e777a..1216403838 100644
--- a/src/main/protocol-handler/__test__/router.test.ts
+++ b/src/main/protocol-handler/__test__/router.test.ts
@@ -28,9 +28,17 @@ import { LensExtension } from "../../../extensions/main-api";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import { ExtensionsStore } from "../../../extensions/extensions-store";
import { LensProtocolRouterMain } from "../router";
+import mockFs from "mock-fs";
jest.mock("../../../common/ipc");
+jest.mock("electron", () => ({
+ app: {
+ getPath: () => "tmp",
+ setLoginItemSettings: jest.fn(),
+ },
+}));
+
function throwIfDefined(val: any): void {
if (val != null) {
throw val;
@@ -39,12 +47,14 @@ function throwIfDefined(val: any): void {
describe("protocol router tests", () => {
beforeEach(() => {
+ mockFs({
+ "tmp": {}
+ });
ExtensionsStore.createInstance();
ExtensionLoader.createInstance();
const lpr = LensProtocolRouterMain.createInstance();
- lpr.extensionsLoaded = true;
lpr.rendererLoaded = true;
});
@@ -54,23 +64,24 @@ describe("protocol router tests", () => {
ExtensionsStore.resetInstance();
ExtensionLoader.resetInstance();
LensProtocolRouterMain.resetInstance();
+ mockFs.restore();
});
- it("should throw on non-lens URLS", () => {
+ it("should throw on non-lens URLS", async () => {
try {
const lpr = LensProtocolRouterMain.getInstance();
- expect(lpr.route("https://google.ca")).toBeUndefined();
+ expect(await lpr.route("https://google.ca")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
- it("should throw when host not internal or extension", () => {
+ it("should throw when host not internal or extension", async () => {
try {
const lpr = LensProtocolRouterMain.getInstance();
- expect(lpr.route("lens://foobar")).toBeUndefined();
+ expect(await lpr.route("lens://foobar")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
@@ -103,13 +114,13 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/", noop);
try {
- expect(lpr.route("lens://app")).toBeUndefined();
+ expect(await lpr.route("lens://app")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
try {
- expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
+ expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -119,14 +130,14 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
});
- it("should call handler if matches", () => {
+ it("should call handler if matches", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called = false;
lpr.addInternalHandler("/page", () => { called = true; });
try {
- expect(lpr.route("lens://app/page")).toBeUndefined();
+ expect(await lpr.route("lens://app/page")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -135,7 +146,7 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
});
- it("should call most exact handler", () => {
+ it("should call most exact handler", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
@@ -143,7 +154,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
try {
- expect(lpr.route("lens://app/page/foo")).toBeUndefined();
+ expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -183,7 +194,7 @@ describe("protocol router tests", () => {
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
try {
- expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
+ expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -251,7 +262,7 @@ describe("protocol router tests", () => {
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
try {
- expect(lpr.route("lens://extension/icecream/page")).toBeUndefined();
+ expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -268,7 +279,7 @@ describe("protocol router tests", () => {
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
});
- it("should call most exact handler with 3 found handlers", () => {
+ it("should call most exact handler with 3 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
@@ -278,7 +289,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/bar", () => { called = 4; });
try {
- expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
+ expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
@@ -287,7 +298,7 @@ describe("protocol router tests", () => {
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
});
- it("should call most exact handler with 2 found handlers", () => {
+ it("should call most exact handler with 2 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
@@ -296,7 +307,7 @@ describe("protocol router tests", () => {
lpr.addInternalHandler("/page/bar", () => { called = 4; });
try {
- expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
+ expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
diff --git a/src/main/protocol-handler/router.ts b/src/main/protocol-handler/router.ts
index 6a8090617b..fe178705e7 100644
--- a/src/main/protocol-handler/router.ts
+++ b/src/main/protocol-handler/router.ts
@@ -54,7 +54,6 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
private missingExtensionHandlers: FallbackHandler[] = [];
@observable rendererLoaded = false;
- @observable extensionsLoaded = false;
protected disposers = disposer();
@@ -74,7 +73,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
* This will send an IPC message to the renderer router to do the same
* in the renderer.
*/
- public route(rawUrl: string) {
+ public async route(rawUrl: string) {
try {
const url = new URLParse(rawUrl, true);
@@ -82,15 +81,15 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
}
+ WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
const routeInternally = checkHost(url);
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
- WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
if (routeInternally) {
this._routeToInternal(url);
} else {
- this.disposers.push(when(() => this.extensionsLoaded, () => this._routeToExtension(url)));
+ await this._routeToExtension(url);
}
} catch (error) {
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
diff --git a/src/main/proxy/lens-proxy.ts b/src/main/proxy/lens-proxy.ts
index 199d48398b..00ef5fbb48 100644
--- a/src/main/proxy/lens-proxy.ts
+++ b/src/main/proxy/lens-proxy.ts
@@ -29,7 +29,7 @@ import { Router } from "../router";
import type { ContextHandler } from "../context-handler";
import logger from "../logger";
import { Singleton } from "../../common/utils";
-import { ClusterManager } from "../cluster-manager";
+import type { Cluster } from "../cluster";
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
@@ -42,7 +42,7 @@ export class LensProxy extends Singleton {
public port: number;
- constructor(handleWsUpgrade: WSUpgradeHandler) {
+ constructor(handleWsUpgrade: WSUpgradeHandler, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster | undefined) {
super();
const proxy = this.createProxy();
@@ -104,7 +104,7 @@ export class LensProxy extends Singleton {
}
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
- const cluster = ClusterManager.getInstance().getClusterForRequest(req);
+ const cluster = this.getClusterForRequest(req);
if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
@@ -220,7 +220,7 @@ export class LensProxy extends Singleton {
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
- const cluster = ClusterManager.getInstance().getClusterForRequest(req);
+ const cluster = this.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts
index 5dd02cc617..f4772c5275 100644
--- a/src/main/resource-applier.ts
+++ b/src/main/resource-applier.ts
@@ -36,15 +36,15 @@ export class ResourceApplier {
async apply(resource: KubernetesObject | any): Promise {
resource = this.sanitizeObject(resource);
- appEventBus.emit({name: "resource", action: "apply"});
+ appEventBus.emit({ name: "resource", action: "apply" });
return await this.kubectlApply(yaml.safeDump(resource));
}
protected async kubectlApply(content: string): Promise {
- const { kubeCtl } = this.cluster;
- const kubectlPath = await kubeCtl.getPath();
- const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
+ const kubectl = await this.cluster.ensureKubectl();
+ const kubectlPath = await kubectl.getPath();
+ const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
return new Promise((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" });
@@ -82,9 +82,9 @@ export class ResourceApplier {
}
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise {
- const { kubeCtl } = this.cluster;
- const kubectlPath = await kubeCtl.getPath();
- const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
+ const kubectl = await this.cluster.ensureKubectl();
+ const kubectlPath = await kubectl.getPath();
+ const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
return new Promise((resolve, reject) => {
const tmpDir = tempy.directory();
diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts
index b30d26d519..9a56aaafb8 100644
--- a/src/main/window-manager.ts
+++ b/src/main/window-manager.ts
@@ -27,7 +27,7 @@ import { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc";
import { initMenu } from "./menu";
import { initTray } from "./tray";
-import { delay, Singleton } from "../common/utils";
+import { delay, iter, Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger";
@@ -38,6 +38,12 @@ function isHideable(window: BrowserWindow | null): boolean {
return Boolean(window && !window.isDestroyed());
}
+export interface SendToViewArgs {
+ channel: string;
+ frameInfo?: ClusterFrameInfo;
+ data?: any[];
+}
+
export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow;
@@ -175,7 +181,7 @@ export class WindowManager extends Singleton {
return this.mainWindow;
}
- sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
+ private sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) {
if (frameInfo) {
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
} else {
@@ -183,10 +189,22 @@ export class WindowManager extends Singleton {
}
}
+ async navigateExtension(extId: string, pageId?: string, params?: Record, frameId?: number) {
+ await this.ensureMainWindow();
+
+ const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
+
+ this.sendToView({
+ channel: "extension:navigate",
+ frameInfo,
+ data: [extId, pageId, params],
+ });
+ }
+
async navigate(url: string, frameId?: number) {
await this.ensureMainWindow();
- const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId);
+ const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
const channel = frameInfo
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
diff --git a/src/migrations/cluster-store/2.0.0-beta.2.ts b/src/migrations/cluster-store/2.0.0-beta.2.ts
index 0d54588194..db2fd0bff0 100644
--- a/src/migrations/cluster-store/2.0.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.0.0-beta.2.ts
@@ -19,12 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-/* Early store format had the kubeconfig directly under context name, this moves
- it under the kubeConfig key */
+import type { MigrationDeclaration } from "../helpers";
-import { migration } from "../migration-wrapper";
+/**
+ * Early store format had the kubeconfig directly under context name, this moves
+ * it under the kubeConfig key
+ */
-export default migration({
+export default {
version: "2.0.0-beta.2",
run(store) {
for (const value of store) {
@@ -35,4 +37,4 @@ export default migration({
store.set(contextName, { kubeConfig: value[1] });
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/2.4.1.ts b/src/migrations/cluster-store/2.4.1.ts
index 59d4ef21dc..192f7f3dc8 100644
--- a/src/migrations/cluster-store/2.4.1.ts
+++ b/src/migrations/cluster-store/2.4.1.ts
@@ -19,10 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-// Cleans up a store that had the state related data stored
-import { migration } from "../migration-wrapper";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+// Cleans up a store that had the state related data stored
+
+export default {
version: "2.4.1",
run(store) {
for (const value of store) {
@@ -34,4 +35,4 @@ export default migration({
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/2.6.0-beta.2.ts b/src/migrations/cluster-store/2.6.0-beta.2.ts
index ffe303a28d..a1d7df0c1b 100644
--- a/src/migrations/cluster-store/2.6.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.2.ts
@@ -20,9 +20,9 @@
*/
// Move cluster icon from root to preferences
-import { migration } from "../migration-wrapper";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "2.6.0-beta.2",
run(store) {
for (const value of store) {
@@ -40,4 +40,4 @@ export default migration({
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/2.6.0-beta.3.ts b/src/migrations/cluster-store/2.6.0-beta.3.ts
index 9ec844fe65..75662582e5 100644
--- a/src/migrations/cluster-store/2.6.0-beta.3.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.3.ts
@@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import { migration } from "../migration-wrapper";
import yaml from "js-yaml";
+import { MigrationDeclaration, migrationLog } from "../helpers";
-export default migration({
+export default {
version: "2.6.0-beta.3",
- run(store, log) {
+ run(store) {
for (const value of store) {
const clusterKey = value[0];
@@ -50,7 +50,7 @@ export default migration({
if (authConfig.expiry) {
authConfig.expiry = `${authConfig.expiry}`;
}
- log(authConfig);
+ migrationLog(authConfig);
user["auth-provider"].config = authConfig;
kubeConfig.users = [{
name: userObj.name,
@@ -62,4 +62,4 @@ export default migration({
}
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/2.7.0-beta.0.ts b/src/migrations/cluster-store/2.7.0-beta.0.ts
index a23d3be991..150cb3f8ce 100644
--- a/src/migrations/cluster-store/2.7.0-beta.0.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.0.ts
@@ -20,9 +20,9 @@
*/
// Add existing clusters to "default" workspace
-import { migration } from "../migration-wrapper";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "2.7.0-beta.0",
run(store) {
for (const value of store) {
@@ -35,4 +35,4 @@ export default migration({
store.set(clusterKey, cluster);
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/2.7.0-beta.1.ts b/src/migrations/cluster-store/2.7.0-beta.1.ts
index 06439063c6..b7f9c8b0a0 100644
--- a/src/migrations/cluster-store/2.7.0-beta.1.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.1.ts
@@ -20,10 +20,10 @@
*/
// Add id for clusters and store them to array
-import { migration } from "../migration-wrapper";
import { v4 as uuid } from "uuid";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "2.7.0-beta.1",
run(store) {
const clusters: any[] = [];
@@ -48,4 +48,4 @@ export default migration({
store.set("clusters", clusters);
}
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts
index 5339d5232b..2599005332 100644
--- a/src/migrations/cluster-store/3.6.0-beta.1.ts
+++ b/src/migrations/cluster-store/3.6.0-beta.1.ts
@@ -23,69 +23,68 @@
// convert file path cluster icons to their base64 encoded versions
import path from "path";
-import { app, remote } from "electron";
-import { migration } from "../migration-wrapper";
+import { app } from "electron";
import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfigFromFileSync } from "../../common/kube-helpers";
+import { MigrationDeclaration, migrationLog } from "../helpers";
-export default migration({
+interface Pre360ClusterModel extends ClusterModel {
+ kubeConfig: string;
+}
+
+export default {
version: "3.6.0-beta.1",
- run(store, printLog) {
- const userDataPath = (app || remote.app).getPath("userData");
- const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
- const storedClusters: ClusterModel[] = store.get("clusters") || [];
+ run(store) {
+ const userDataPath = app.getPath("userData");
+ const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? [];
+ const migratedClusters: ClusterModel[] = [];
- if (!storedClusters.length) return;
- fse.ensureDirSync(kubeConfigBase);
+ fse.ensureDirSync(ClusterStore.storedKubeConfigFolder);
- printLog("Number of clusters to migrate: ", storedClusters.length);
- const migratedClusters = storedClusters
- .map(cluster => {
- /**
- * migrate kubeconfig
- */
- try {
- const absPath = ClusterStore.getCustomKubeConfigPath(cluster.id);
+ migrationLog("Number of clusters to migrate: ", storedClusters.length);
- fse.ensureDirSync(path.dirname(absPath));
- fse.writeFileSync(absPath, cluster.kubeConfig, { encoding: "utf-8", mode: 0o600 });
- // take the embedded kubeconfig and dump it into a file
- cluster.kubeConfigPath = absPath;
- cluster.contextName = loadConfigFromFileSync(cluster.kubeConfigPath).config.getCurrentContext();
- delete cluster.kubeConfig;
+ for (const clusterModel of storedClusters) {
+ /**
+ * migrate kubeconfig
+ */
+ try {
+ const absPath = ClusterStore.getCustomKubeConfigPath(clusterModel.id);
- } catch (error) {
- printLog(`Failed to migrate Kubeconfig for cluster "${cluster.id}", removing cluster...`, error);
+ // take the embedded kubeconfig and dump it into a file
+ fse.writeFileSync(absPath, clusterModel.kubeConfig, { encoding: "utf-8", mode: 0o600 });
- return undefined;
+ clusterModel.kubeConfigPath = absPath;
+ clusterModel.contextName = loadConfigFromFileSync(clusterModel.kubeConfigPath).config.getCurrentContext();
+ delete clusterModel.kubeConfig;
+
+ } catch (error) {
+ migrationLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error);
+
+ continue;
+ }
+
+ /**
+ * migrate cluster icon
+ */
+ try {
+ if (clusterModel.preferences?.icon) {
+ migrationLog(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`);
+ const iconPath = clusterModel.preferences.icon.replace("store://", "");
+ const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
+
+ clusterModel.preferences.icon = `data:;base64,${fileData.toString("base64")}`;
+ } else {
+ delete clusterModel.preferences?.icon;
}
+ } catch (error) {
+ migrationLog(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error);
+ delete clusterModel.preferences.icon;
+ }
- /**
- * migrate cluster icon
- */
- try {
- if (cluster.preferences?.icon) {
- printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`);
- const iconPath = cluster.preferences.icon.replace("store://", "");
- const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
-
- cluster.preferences.icon = `data:;base64,${fileData.toString("base64")}`;
- } else {
- delete cluster.preferences?.icon;
- }
- } catch (error) {
- printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
- delete cluster.preferences.icon;
- }
-
- return cluster;
- })
- .filter(c => c);
-
- // "overwrite" the cluster configs
- if (migratedClusters.length > 0) {
- store.set("clusters", migratedClusters);
+ migratedClusters.push(clusterModel);
}
+
+ store.set("clusters", migratedClusters);
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts
new file mode 100644
index 0000000000..3a2b5bdfe3
--- /dev/null
+++ b/src/migrations/cluster-store/5.0.0-beta.10.ts
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import path from "path";
+import { app } from "electron";
+import fse from "fs-extra";
+import type { ClusterModel } from "../../common/cluster-store";
+import type { MigrationDeclaration } from "../helpers";
+
+interface Pre500WorkspaceStoreModel {
+ workspaces: {
+ id: string;
+ name: string;
+ }[];
+}
+
+export default {
+ version: "5.0.0-beta.10",
+ run(store) {
+ const userDataPath = app.getPath("userData");
+
+ try {
+ const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
+ const workspaces = new Map(); // mapping from WorkspaceId to name
+
+ for (const { id, name } of workspaceData.workspaces) {
+ workspaces.set(id, name);
+ }
+
+ const clusters: ClusterModel[] = store.get("clusters");
+
+ for (const cluster of clusters) {
+ if (cluster.workspace && workspaces.has(cluster.workspace)) {
+ cluster.labels ??= {};
+ cluster.labels.workspace = workspaces.get(cluster.workspace);
+ }
+ }
+
+ store.set("clusters", clusters);
+ } catch (error) {
+ if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
+ // ignore lens-workspace-store.json being missing
+ throw error;
+ }
+ }
+ },
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/migrations/cluster-store/5.0.0-beta.13.ts
new file mode 100644
index 0000000000..09628ed234
--- /dev/null
+++ b/src/migrations/cluster-store/5.0.0-beta.13.ts
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-store";
+import { MigrationDeclaration, migrationLog } from "../helpers";
+import { generateNewIdFor } from "../utils";
+import path from "path";
+import { app } from "electron";
+import { moveSync, removeSync } from "fs-extra";
+
+function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
+ if (left.prometheus && left.prometheusProvider) {
+ return {
+ prometheus: left.prometheus,
+ prometheusProvider: left.prometheusProvider,
+ };
+ }
+
+ if (right.prometheus && right.prometheusProvider) {
+ return {
+ prometheus: right.prometheus,
+ prometheusProvider: right.prometheusProvider,
+ };
+ }
+
+ return {};
+}
+
+function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
+ return {
+ terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
+ clusterName: left.clusterName || right.clusterName || undefined,
+ iconOrder: left.iconOrder || right.iconOrder || undefined,
+ icon: left.icon || right.icon || undefined,
+ httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
+ hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
+ ...mergePrometheusPreferences(left, right),
+ };
+}
+
+function mergeLabels(left: Record, right: Record): Record {
+ return {
+ ...right,
+ ...left
+ };
+}
+
+function mergeSet(left: Iterable, right: Iterable): string[] {
+ return [...new Set([...left, ...right])];
+}
+
+function mergeClusterModel(prev: ClusterModel, right: Omit): ClusterModel {
+ return {
+ id: prev.id,
+ kubeConfigPath: prev.id,
+ contextName: prev.contextName,
+ preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
+ metadata: prev.metadata,
+ labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
+ accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
+ };
+}
+
+function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: string, oldId: string }): void {
+ const oldPath = path.resolve(folder, `${oldId}.json`);
+ const newPath = path.resolve(folder, `${newId}.json`);
+
+ try {
+ moveSync(oldPath, newPath);
+ } catch (error) {
+ if (String(error).includes("dest already exists")) {
+ migrationLog(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`);
+ removeSync(oldPath);
+ }
+ }
+}
+
+export default {
+ version: "5.0.0-beta.13",
+ run(store) {
+ const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
+
+ const oldClusters: ClusterModel[] = store.get("clusters");
+ const clusters = new Map();
+
+ for (const { id: oldId, ...cluster } of oldClusters) {
+ const newId = generateNewIdFor(cluster);
+
+ if (clusters.has(newId)) {
+ clusters.set(newId, mergeClusterModel(clusters.get(newId), cluster));
+ } else {
+ clusters.set(newId, {
+ ...cluster,
+ id: newId,
+ });
+ moveStorageFolder({ folder, newId, oldId });
+ }
+ }
+
+ store.set("clusters", [...clusters.values()]);
+ }
+} as MigrationDeclaration;
diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts
index b80911bbfe..b68e098a16 100644
--- a/src/migrations/cluster-store/index.ts
+++ b/src/migrations/cluster-store/index.ts
@@ -21,6 +21,8 @@
// Cluster store migrations
+import { joinMigrations } from "../helpers";
+
import version200Beta2 from "./2.0.0-beta.2";
import version241 from "./2.4.1";
import version260Beta2 from "./2.6.0-beta.2";
@@ -28,15 +30,19 @@ import version260Beta3 from "./2.6.0-beta.3";
import version270Beta0 from "./2.7.0-beta.0";
import version270Beta1 from "./2.7.0-beta.1";
import version360Beta1 from "./3.6.0-beta.1";
+import version500Beta10 from "./5.0.0-beta.10";
+import version500Beta13 from "./5.0.0-beta.13";
import snap from "./snap";
-export default {
- ...version200Beta2,
- ...version241,
- ...version260Beta2,
- ...version260Beta3,
- ...version270Beta0,
- ...version270Beta1,
- ...version360Beta1,
- ...snap
-};
+export default joinMigrations(
+ version200Beta2,
+ version241,
+ version260Beta2,
+ version260Beta3,
+ version270Beta0,
+ version270Beta1,
+ version360Beta1,
+ version500Beta10,
+ version500Beta13,
+ snap,
+);
diff --git a/src/migrations/cluster-store/snap.ts b/src/migrations/cluster-store/snap.ts
index 3dcf998f1c..c1c5eb2dc5 100644
--- a/src/migrations/cluster-store/snap.ts
+++ b/src/migrations/cluster-store/snap.ts
@@ -21,22 +21,22 @@
// Fix embedded kubeconfig paths under snap config
-import { migration } from "../migration-wrapper";
import type { ClusterModel } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version";
import fs from "fs";
+import { MigrationDeclaration, migrationLog } from "../helpers";
-export default migration({
+export default {
version: getAppVersion(), // Run always after upgrade
- run(store, printLog) {
+ run(store) {
if (!process.env["SNAP"]) return;
- printLog("Migrating embedded kubeconfig paths");
+ migrationLog("Migrating embedded kubeconfig paths");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
- printLog("Number of clusters to migrate: ", storedClusters.length);
+ migrationLog("Number of clusters to migrate: ", storedClusters.length);
const migratedClusters = storedClusters
.map(cluster => {
/**
@@ -54,4 +54,4 @@ export default migration({
store.set("clusters", migratedClusters);
}
-});
+} as MigrationDeclaration;
diff --git a/src/renderer/components/layout/login-layout.tsx b/src/migrations/helpers.ts
old mode 100755
new mode 100644
similarity index 56%
rename from src/renderer/components/layout/login-layout.tsx
rename to src/migrations/helpers.ts
index 49e8099dd1..623d46a63e
--- a/src/renderer/components/layout/login-layout.tsx
+++ b/src/migrations/helpers.ts
@@ -19,40 +19,37 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import "./login-layout.scss";
+import type Conf from "conf";
+import type { Migrations } from "conf/dist/source/types";
+import { ExtendedMap, iter } from "../common/utils";
+import { isTestEnv } from "../common/vars";
-import React from "react";
-import { Link } from "react-router-dom";
-import { cssNames } from "../../utils";
-import { Icon } from "../icon";
-
-interface Props {
- className?: any;
- header?: any;
- title?: any;
- footer?: any;
-}
-
-export class LoginLayout extends React.Component {
- render() {
- const { className, header, title, footer, children } = this.props;
-
- return (
-
- {header}
-
-
-
-
-
- {title}
-
-
- {children}
-
-
- {footer}
-
- );
+export function migrationLog(...args: any[]) {
+ if (!isTestEnv) {
+ console.log(...args);
}
}
+
+export interface MigrationDeclaration {
+ version: string,
+ run(store: Conf): void;
+}
+
+export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations {
+ const migrations = new ExtendedMap) => void)[]>();
+
+ for (const decl of declarations) {
+ migrations.getOrInsert(decl.version, () => []).push(decl.run);
+ }
+
+ return Object.fromEntries(
+ iter.map(
+ migrations,
+ ([v, fns]) => [v, (store: Conf) => {
+ for (const fn of fns) {
+ fn(store);
+ }
+ }]
+ )
+ );
+}
diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts
index 461d6e1886..1a61ce8ef3 100644
--- a/src/migrations/hotbar-store/5.0.0-alpha.0.ts
+++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts
@@ -20,34 +20,24 @@
*/
// Cleans up a store that had the state related data stored
-import type { Hotbar } from "../../common/hotbar-store";
-import { ClusterStore } from "../../common/cluster-store";
-import { migration } from "../migration-wrapper";
-import { v4 as uuid } from "uuid";
+import { Hotbar, HotbarStore } from "../../common/hotbar-store";
+import * as uuid from "uuid";
+import type { MigrationDeclaration } from "../helpers";
+import { catalogEntity } from "../../main/catalog-sources/general";
-export default migration({
+export default {
version: "5.0.0-alpha.0",
run(store) {
- const hotbars: Hotbar[] = [];
+ const hotbar: Hotbar = {
+ id: uuid.v4(),
+ name: "default",
+ items: HotbarStore.getInitialItems(),
+ };
- ClusterStore.getInstance().clustersList.forEach((cluster: any) => {
- const name = cluster.workspace;
+ const { metadata: { uid, name, source } } = catalogEntity;
- if (!name) return;
+ hotbar.items[0] = { entity: { uid, name, source } };
- let hotbar = hotbars.find((h) => h.name === name);
-
- if (!hotbar) {
- hotbar = { id: uuid(), name, items: [] };
- hotbars.push(hotbar);
- }
-
- hotbar.items.push({
- entity: { uid: cluster.id },
- params: {}
- });
- });
-
- store.set("hotbars", hotbars);
+ store.set("hotbars", [hotbar]);
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/hotbar-store/5.0.0-alpha.2.ts b/src/migrations/hotbar-store/5.0.0-alpha.2.ts
index 83696d01b0..e049a6d731 100644
--- a/src/migrations/hotbar-store/5.0.0-alpha.2.ts
+++ b/src/migrations/hotbar-store/5.0.0-alpha.2.ts
@@ -21,10 +21,10 @@
// Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbar-store";
-import { migration } from "../migration-wrapper";
import * as uuid from "uuid";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "5.0.0-alpha.2",
run(store) {
const hotbars = (store.get("hotbars") || []) as Hotbar[];
@@ -34,4 +34,4 @@ export default migration({
...hotbar
})));
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts
new file mode 100644
index 0000000000..41b4aeb081
--- /dev/null
+++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { app } from "electron";
+import fse from "fs-extra";
+import { isNull, uniqBy } from "lodash";
+import path from "path";
+import * as uuid from "uuid";
+import type { ClusterStoreModel } from "../../common/cluster-store";
+import { defaultHotbarCells, Hotbar, HotbarStore } from "../../common/hotbar-store";
+import { catalogEntity } from "../../main/catalog-sources/general";
+import type { MigrationDeclaration } from "../helpers";
+import { generateNewIdFor } from "../utils";
+
+interface Pre500WorkspaceStoreModel {
+ workspaces: {
+ id: string;
+ name: string;
+ }[];
+}
+
+export default {
+ version: "5.0.0-beta.10",
+ run(store) {
+ const hotbars: Hotbar[] = store.get("hotbars");
+ const userDataPath = app.getPath("userData");
+
+ try {
+ const workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
+ const { clusters }: ClusterStoreModel = fse.readJSONSync(path.join(userDataPath, "lens-cluster-store.json"));
+ const workspaceHotbars = new Map(); // mapping from WorkspaceId to HotBar
+ const uniqueClusters = uniqBy(clusters, "metadata.id"); // Filtering out duplicated clusters
+
+ for (const { id, name } of workspaceStoreData.workspaces) {
+ workspaceHotbars.set(id, {
+ id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs
+ items: [],
+ name: `Workspace: ${name}`,
+ });
+ }
+
+ {
+ // grab the default named hotbar or the first.
+ const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
+ const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
+
+ workspaceHotbars.set("default", {
+ name,
+ id,
+ items: items.filter(Boolean),
+ });
+ }
+
+ for (const cluster of uniqueClusters) {
+ const workspaceHotbar = workspaceHotbars.get(cluster.workspace);
+
+ if (workspaceHotbar?.items.length < defaultHotbarCells) {
+ workspaceHotbar.items.push({
+ entity: {
+ uid: generateNewIdFor(cluster),
+ name: cluster.preferences.clusterName || cluster.contextName,
+ }
+ });
+ }
+ }
+
+ for (const hotbar of workspaceHotbars.values()) {
+ while (hotbar.items.length < defaultHotbarCells) {
+ hotbar.items.push(null);
+ }
+
+ hotbars.push(hotbar);
+ }
+
+ /**
+ * Finally, make sure that the catalog entity hotbar item is in place.
+ * Just in case something else removed it.
+ *
+ * if every hotbar has elements that all not the `catalog-entity` item
+ */
+ if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
+ // note, we will add a new whole hotbar here called "default" if that was previously removed
+ const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default");
+ const { metadata: { uid, name, source } } = catalogEntity;
+
+ if (defaultHotbar) {
+ const freeIndex = defaultHotbar.items.findIndex(isNull);
+
+ if (freeIndex === -1) {
+ // making a new hotbar is less destructive if the first hotbar
+ // called "default" is full than overriding a hotbar item
+ const hotbar = {
+ id: uuid.v4(),
+ name: "initial",
+ items: HotbarStore.getInitialItems(),
+ };
+
+ hotbar.items[0] = { entity: { uid, name, source } };
+ hotbars.unshift(hotbar);
+ } else {
+ defaultHotbar.items[freeIndex] = { entity: { uid, name, source } };
+ }
+ } else {
+ const hotbar = {
+ id: uuid.v4(),
+ name: "default",
+ items: HotbarStore.getInitialItems(),
+ };
+
+ hotbar.items[0] = { entity: { uid, name, source } };
+ hotbars.unshift(hotbar);
+ }
+ }
+
+ store.set("hotbars", hotbars);
+ } catch (error) {
+ if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
+ // ignore lens-workspace-store.json being missing
+ throw error;
+ }
+ }
+ }
+} as MigrationDeclaration;
diff --git a/src/migrations/hotbar-store/5.0.0-beta.5.ts b/src/migrations/hotbar-store/5.0.0-beta.5.ts
index 33e39f2a13..8d589e9543 100644
--- a/src/migrations/hotbar-store/5.0.0-beta.5.ts
+++ b/src/migrations/hotbar-store/5.0.0-beta.5.ts
@@ -20,10 +20,10 @@
*/
import type { Hotbar } from "../../common/hotbar-store";
-import { migration } from "../migration-wrapper";
-import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry";
+import { catalogEntityRegistry } from "../../main/catalog";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "5.0.0-beta.5",
run(store) {
const hotbars: Hotbar[] = store.get("hotbars");
@@ -48,4 +48,4 @@ export default migration({
store.set("hotbars", hotbars);
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/hotbar-store/index.ts b/src/migrations/hotbar-store/index.ts
index fabedee4b7..44c5ebcdf4 100644
--- a/src/migrations/hotbar-store/index.ts
+++ b/src/migrations/hotbar-store/index.ts
@@ -21,12 +21,16 @@
// Hotbar store migrations
+import { joinMigrations } from "../helpers";
+
import version500alpha0 from "./5.0.0-alpha.0";
import version500alpha2 from "./5.0.0-alpha.2";
import version500beta5 from "./5.0.0-beta.5";
+import version500beta10 from "./5.0.0-beta.10";
-export default {
- ...version500alpha0,
- ...version500alpha2,
- ...version500beta5,
-};
+export default joinMigrations(
+ version500alpha0,
+ version500alpha2,
+ version500beta5,
+ version500beta10,
+);
diff --git a/src/migrations/user-store/2.1.0-beta.4.ts b/src/migrations/user-store/2.1.0-beta.4.ts
index eace0dd367..12aa3d1cf0 100644
--- a/src/migrations/user-store/2.1.0-beta.4.ts
+++ b/src/migrations/user-store/2.1.0-beta.4.ts
@@ -20,11 +20,11 @@
*/
// Add / reset "lastSeenAppVersion"
-import { migration } from "../migration-wrapper";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "2.1.0-beta.4",
run(store) {
store.set("lastSeenAppVersion", "0.0.0");
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/user-store/5.0.0-alpha.3.ts b/src/migrations/user-store/5.0.0-alpha.3.ts
index 57692c0bc0..3f70240164 100644
--- a/src/migrations/user-store/5.0.0-alpha.3.ts
+++ b/src/migrations/user-store/5.0.0-alpha.3.ts
@@ -20,9 +20,9 @@
*/
// Switch representation of hiddenTableColumns in store
-import { migration } from "../migration-wrapper";
+import type { MigrationDeclaration } from "../helpers";
-export default migration({
+export default {
version: "5.0.0-alpha.3",
run(store) {
const preferences = store.get("preferences");
@@ -36,4 +36,4 @@ export default migration({
store.set("preferences", preferences);
}
-});
+} as MigrationDeclaration;
diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts
index e0c848d3a8..122cd78102 100644
--- a/src/migrations/user-store/file-name-migration.ts
+++ b/src/migrations/user-store/file-name-migration.ts
@@ -23,18 +23,18 @@ import fse from "fs-extra";
import { app, remote } from "electron";
import path from "path";
-export async function fileNameMigration() {
+export 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);
+ fse.moveSync(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);
+ fse.removeSync(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 cf345b1bfb..c08ef2c5c5 100644
--- a/src/migrations/user-store/index.ts
+++ b/src/migrations/user-store/index.ts
@@ -21,6 +21,8 @@
// User store migrations
+import { joinMigrations } from "../helpers";
+
import version210Beta4 from "./2.1.0-beta.4";
import version500Alpha3 from "./5.0.0-alpha.3";
import { fileNameMigration } from "./file-name-migration";
@@ -29,7 +31,7 @@ export {
fileNameMigration
};
-export default {
- ...version210Beta4,
- ...version500Alpha3,
-};
+export default joinMigrations(
+ version210Beta4,
+ version500Alpha3,
+);
diff --git a/src/renderer/components/layout/login-layout.scss b/src/migrations/utils.ts
old mode 100755
new mode 100644
similarity index 61%
rename from src/renderer/components/layout/login-layout.scss
rename to src/migrations/utils.ts
index 9996d07ed0..f9f9985e38
--- a/src/renderer/components/layout/login-layout.scss
+++ b/src/migrations/utils.ts
@@ -19,53 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+import { createHash } from "crypto";
-.LoginLayout {
- $logo-size: 6 * $unit;
- height: 100vh;
- padding: $padding * 2;
-
- .logo {
- width: $logo-size;
- height: $logo-size;
- margin: auto;
-
- svg * {
- fill: $lensBlue;
- }
- }
-
- .header {
- font-size: $font-size-small;
- }
-
- .main {
- $spacing: $padding * 3;
- width: 100%;
- min-width: 34 * $unit;
- max-width: 42 * $unit;
- margin: $padding * 2 0;
- overflow: hidden;
-
- > * {
- padding: $spacing;
- }
-
- .title {
- background: $layoutBackground;
- text-align: center;
-
- h5 {
- color: $textColorAccent;
- }
- }
-
- .content {
- background: $contentColor;
- }
- }
-
- .footer {
- font-size: $font-size-small;
- }
-}
\ No newline at end of file
+export function generateNewIdFor(cluster: { kubeConfigPath: string, contextName: string }): string {
+ return createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex");
+}
diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx
index 8c7fe38d4c..7768a9e0eb 100644
--- a/src/renderer/bootstrap.tsx
+++ b/src/renderer/bootstrap.tsx
@@ -31,21 +31,22 @@ import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { render, unmountComponentAtNode } from "react-dom";
import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars";
-import { HotbarStore } from "../common/hotbar-store";
import { ClusterStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store";
import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader";
-import { ExtensionsStore } from "../extensions/extensions-store";
-import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { App } from "./components/app";
import { LensApp } from "./lens-app";
-import { ThemeStore } from "./theme.store";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers";
+import { HotbarStore } from "../common/hotbar-store";
+import { WeblinkStore } from "../common/weblink-store";
+import { ExtensionsStore } from "../extensions/extensions-store";
+import { FilesystemProvisionerStore } from "../main/extension-filesystem";
+import { ThemeStore } from "./theme.store";
configurePackages();
@@ -77,32 +78,25 @@ export async function bootstrap(App: AppComponent) {
initializers.intiKubeObjectDetailRegistry();
initializers.initWelcomeMenuRegistry();
initializers.initWorkloadsOverviewDetailRegistry();
+ initializers.initCatalog();
+ initializers.initIpcRendererListeners();
ExtensionLoader.createInstance().init();
ExtensionDiscovery.createInstance().init();
- const userStore = UserStore.createInstance();
- const clusterStore = ClusterStore.createInstance();
- const extensionsStore = ExtensionsStore.createInstance();
- const filesystemStore = FilesystemProvisionerStore.createInstance();
- const themeStore = ThemeStore.createInstance();
- const hotbarStore = HotbarStore.createInstance();
+ UserStore.createInstance();
+ await ClusterStore.createInstance().loadInitialOnRenderer();
+ HotbarStore.createInstance();
+ ExtensionsStore.createInstance();
+ FilesystemProvisionerStore.createInstance();
+ ThemeStore.createInstance();
+ WeblinkStore.createInstance();
ExtensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager
- // preload common stores
- await Promise.all([
- userStore.load(),
- hotbarStore.load(),
- clusterStore.load(),
- extensionsStore.load(),
- filesystemStore.load(),
- themeStore.init(),
- ]);
-
// Register additional store listeners
- clusterStore.registerIpcListener();
+ ClusterStore.getInstance().registerIpcListener();
// init app's dependencies if any
if (App.init) {
diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx
index b04c9a2989..7ee8529492 100644
--- a/src/renderer/components/+add-cluster/add-cluster.tsx
+++ b/src/renderer/components/+add-cluster/add-cluster.tsx
@@ -38,8 +38,8 @@ import { navigate } from "../../navigation";
import { iter } from "../../utils";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
-import { PageLayout } from "../layout/page-layout";
import { Notifications } from "../notifications";
+import { SettingLayout } from "../layout/setting-layout";
interface Option {
config: KubeConfig;
@@ -108,11 +108,11 @@ export class AddCluster extends React.Component {
render() {
return (
-
+
Add Clusters from Kubeconfig
Clusters added here are not merged into the ~/.kube/config file.
- Read more about adding clusters here .
+ Read more about adding clusters here .
-
+
);
}
}
diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx
index d7b6b76279..e5bda1f873 100644
--- a/src/renderer/components/+apps-releases/release-details.tsx
+++ b/src/renderer/components/+apps-releases/release-details.tsx
@@ -208,7 +208,7 @@ export class ReleaseDetails extends Component {
{items.map(item => {
const name = item.getName();
const namespace = item.getNs();
- const api = apiManager.getApi(item.metadata.selfLink);
+ const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return (
diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx
index 2e85c40c05..30f048c8cb 100644
--- a/src/renderer/components/+apps-releases/release-menu.tsx
+++ b/src/renderer/components/+apps-releases/release-menu.tsx
@@ -63,7 +63,7 @@ export class HelmReleaseMenu extends React.Component {
<>
{hasRollback && (
-
+
Rollback
)}
diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx
index 639d7c4843..fd1b874c82 100644
--- a/src/renderer/components/+catalog/catalog-entity-details.tsx
+++ b/src/renderer/components/+catalog/catalog-entity-details.tsx
@@ -22,32 +22,23 @@
import "./catalog-entity-details.scss";
import React, { Component } from "react";
import { observer } from "mobx-react";
-import { Drawer, DrawerItem, DrawerItemLabels } from "../drawer";
-import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
-import type { CatalogCategory } from "../../../common/catalog";
+import { Drawer, DrawerItem } from "../drawer";
+import { catalogEntityRunContext } from "../../api/catalog-entity";
+import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon";
-import { KubeObject } from "../../api/kube-object";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { HotbarIcon } from "../hotbar/hotbar-icon";
+import type { CatalogEntityItem } from "./catalog-entity.store";
+import { isDevelopment } from "../../../common/vars";
-interface Props {
- entity: CatalogEntity;
+interface Props {
+ item: CatalogEntityItem | null | undefined;
hideDetails(): void;
}
@observer
-export class CatalogEntityDetails extends Component {
- private abortController?: AbortController;
-
- constructor(props: Props) {
- super(props);
- }
-
- componentWillUnmount() {
- this.abortController?.abort();
- }
-
+export class CatalogEntityDetails extends Component> {
categoryIcon(category: CatalogCategory) {
if (category.metadata.icon.includes(" ;
@@ -56,16 +47,10 @@ export class CatalogEntityDetails extends Component {
}
}
- openEntity() {
- this.props.entity.onRun(catalogEntityRunContext);
- }
-
- renderContent() {
- const { entity } = this.props;
- const labels = KubeObject.stringifyLabels(entity.metadata.labels);
- const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion);
- const details = detailItems.map((item, index) => {
- return ;
+ renderContent(item: CatalogEntityItem) {
+ const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(item.kind, item.apiVersion);
+ const details = detailItems.map(({ components }, index) => {
+ return ;
});
const showDetails = detailItems.find((item) => item.priority > 999) === undefined;
@@ -76,30 +61,42 @@ export class CatalogEntityDetails extends Component {
this.openEntity()}
+ uid={item.id}
+ title={item.name}
+ source={item.source}
+ src={item.entity.spec.icon?.src}
+ material={item.entity.spec.icon?.material}
+ background={item.entity.spec.icon?.background}
+ disabled={!item?.enabled}
+ onClick={() => item.onRun(catalogEntityRunContext)}
size={128} />
-
- Click to open
-
+ {item?.enabled && (
+
+ Click to open
+
+ )}
- {entity.metadata.name}
+ {item.name}
- {entity.kind}
+ {item.kind}
- {entity.metadata.source}
+ {item.source}
-
+
+ {item.phase}
+
+
+ {...item.getLabelBadges(this.props.hideDetails)}
+
+ {isDevelopment && (
+
+ {item.getId()}
+
+ )}
)}
@@ -111,8 +108,8 @@ export class CatalogEntityDetails extends Component {
}
render() {
- const { entity, hideDetails } = this.props;
- const title = `${entity.kind}: ${entity.metadata.name}`;
+ const { item, hideDetails } = this.props;
+ const title = `${item.kind}: ${item.name}`;
return (
{
usePortal={true}
open={true}
title={title}
- toolbar={ }
+ toolbar={ }
onClose={hideDetails}
>
- {this.renderContent()}
+ {item && this.renderContent(item)}
);
}
diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
index d4f75d7434..af3104afe2 100644
--- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
+++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
@@ -30,9 +30,10 @@ import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog";
import { HotbarStore } from "../../../common/hotbar-store";
import { Icon } from "../icon";
+import type { CatalogEntityItem } from "./catalog-entity.store";
export interface CatalogEntityDrawerMenuProps extends MenuActionsProps {
- entity: T | null | undefined;
+ item: CatalogEntityItem | null | undefined;
}
@observer
@@ -47,9 +48,9 @@ export class CatalogEntityDrawerMenu extends React.Comp
componentDidMount() {
this.contextMenu = {
menuItems: [],
- navigate: (url: string) => navigate(url)
+ navigate: (url: string) => navigate(url),
};
- this.props.entity?.onContextMenuOpen(this.contextMenu);
+ this.props.item?.onContextMenuOpen(this.contextMenu);
}
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
@@ -90,7 +91,7 @@ export class CatalogEntityDrawerMenu extends React.Comp
items.push(
this.onMenuItemClick(menuItem)}>
@@ -99,7 +100,7 @@ export class CatalogEntityDrawerMenu extends React.Comp
items.push(
this.addToHotbar(entity) }>
-
+
);
@@ -107,19 +108,19 @@ export class CatalogEntityDrawerMenu extends React.Comp
}
render() {
- if (!this.contextMenu) {
+ const { className, item: entity, ...menuProps } = this.props;
+
+ if (!this.contextMenu || !entity.enabled) {
return null;
}
- const { className, entity, ...menuProps } = this.props;
-
return (
- {this.getMenuItems(entity)}
+ {this.getMenuItems(entity.entity)}
);
}
diff --git a/src/renderer/components/+catalog/catalog-entity.store.ts b/src/renderer/components/+catalog/catalog-entity.store.tsx
similarity index 71%
rename from src/renderer/components/+catalog/catalog-entity.store.ts
rename to src/renderer/components/+catalog/catalog-entity.store.tsx
index 7c63bcb0f0..fea9e5e6ba 100644
--- a/src/renderer/components/+catalog/catalog-entity.store.ts
+++ b/src/renderer/components/+catalog/catalog-entity.store.tsx
@@ -19,19 +19,34 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+import styles from "./catalog.module.css";
+
+import React from "react";
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { ItemObject, ItemStore } from "../../item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind } from "../../../common/utils";
-export class CatalogEntityItem implements ItemObject {
- constructor(public entity: CatalogEntity) {}
+import { Badge } from "../badge";
+import { navigation } from "../../navigation";
+import { searchUrlParam } from "../input";
+import { makeCss } from "../../../common/utils/makeCss";
+import { KubeObject } from "../../api/kube-object";
+
+const css = makeCss(styles);
+
+export class CatalogEntityItem implements ItemObject {
+ constructor(public entity: T) {}
get kind() {
return this.entity.kind;
}
+ get apiVersion() {
+ return this.entity.apiVersion;
+ }
+
get name() {
return this.entity.metadata.name;
}
@@ -52,16 +67,29 @@ export class CatalogEntityItem implements ItemObject {
return this.entity.status.phase;
}
+ get enabled() {
+ return this.entity.status.enabled ?? true;
+ }
+
get labels() {
- const labels: string[] = [];
+ return KubeObject.stringifyLabels(this.entity.metadata.labels);
+ }
- Object.keys(this.entity.metadata.labels).forEach((key) => {
- const value = this.entity.metadata.labels[key];
-
- labels.push(`${key}=${value}`);
- });
-
- return labels;
+ getLabelBadges(onClick?: React.MouseEventHandler) {
+ return this.labels
+ .map(label => (
+ {
+ navigation.searchParams.set(searchUrlParam.name, label);
+ onClick?.(event);
+ event.stopPropagation();
+ }}
+ />
+ ));
}
get source() {
@@ -73,7 +101,8 @@ export class CatalogEntityItem implements ItemObject {
this.name,
this.id,
this.phase,
- ...this.labels.map((value, key) => `${key}=${value}`)
+ `source=${this.source}`,
+ ...this.labels,
];
}
@@ -87,7 +116,7 @@ export class CatalogEntityItem implements ItemObject {
}
}
-export class CatalogEntityStore extends ItemStore {
+export class CatalogEntityStore extends ItemStore> {
constructor() {
super();
makeObservable(this);
@@ -95,6 +124,7 @@ export class CatalogEntityStore extends ItemStore {
}
@observable activeCategory?: CatalogCategory;
+ @observable selectedItemId?: string;
@computed get entities() {
if (!this.activeCategory) {
@@ -104,6 +134,10 @@ export class CatalogEntityStore extends ItemStore {
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
}
+ @computed get selectedItem() {
+ return this.entities.find(e => e.getId() === this.selectedItemId);
+ }
+
watch() {
const disposers: IReactionDisposer[] = [
reaction(() => this.entities, () => this.loadAll()),
diff --git a/src/renderer/components/+catalog/catalog.module.css b/src/renderer/components/+catalog/catalog.module.css
index cc1e7ce411..4c529a0934 100644
--- a/src/renderer/components/+catalog/catalog.module.css
+++ b/src/renderer/components/+catalog/catalog.module.css
@@ -23,10 +23,10 @@
max-width: 40px;
display: flex;
align-items: center;
+}
- .MuiAvatar-root {
- font-size: var(--unit);
- }
+.iconCell > div * {
+ font-size: var(--unit);
}
.nameCell {
@@ -40,11 +40,11 @@
max-width: 100px;
}
-.connected {
+.connected, .available {
color: var(--colorSuccess);
}
-.disconnected {
+.disconnected, .deleting, .unavailable {
color: var(--halfGray);
}
@@ -63,6 +63,10 @@
max-width: unset;
}
+.badge:hover {
+ color: var(--textColorAccent);
+}
+
.badge:not(:first-child) {
margin-left: 0.5em;
}
diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx
index 067dab9009..9192364e91 100644
--- a/src/renderer/components/+catalog/catalog.tsx
+++ b/src/renderer/components/+catalog/catalog.tsx
@@ -24,26 +24,27 @@ import styles from "./catalog.module.css";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
-import { action, makeObservable, observable, reaction, when } from "mobx";
+import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
-import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog";
-import { catalogCategoryRegistry } from "../../../common/catalog";
+import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
import { CatalogAddButton } from "./catalog-add-button";
import type { RouteComponentProps } from "react-router";
import { Notifications } from "../notifications";
import { MainLayout } from "../layout/main-layout";
-import { cssNames } from "../../utils";
+import { createAppStorage, cssNames } from "../../utils";
import { makeCss } from "../../../common/utils/makeCss";
import { CatalogEntityDetails } from "./catalog-entity-details";
-import type { CatalogViewRouteParam } from "../../../common/routes";
+import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
import { CatalogMenu } from "./catalog-menu";
import { HotbarIcon } from "../hotbar/hotbar-icon";
+export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
+
enum sortBy {
name = "name",
kind = "kind",
@@ -59,57 +60,56 @@ export class Catalog extends React.Component {
@observable private catalogEntityStore?: CatalogEntityStore;
@observable private contextMenu: CatalogEntityContextMenuContext;
@observable activeTab?: string;
- @observable selectedItem?: CatalogEntityItem;
constructor(props: Props) {
super(props);
makeObservable(this);
+ this.catalogEntityStore = new CatalogEntityStore();
}
- get routeActiveTab(): string | undefined {
+ get routeActiveTab(): string {
const { group, kind } = this.props.match.params ?? {};
if (group && kind) {
return `${group}/${kind}`;
}
- return undefined;
+ return "";
}
async componentDidMount() {
this.contextMenu = {
menuItems: observable.array([]),
- navigate: (url: string) => navigate(url)
+ navigate: (url: string) => navigate(url),
};
- this.catalogEntityStore = new CatalogEntityStore();
disposeOnUnmount(this, [
this.catalogEntityStore.watch(),
- when(() => catalogCategoryRegistry.items.length > 0, () => {
- const item = catalogCategoryRegistry.items.find(i => i.getId() === this.routeActiveTab);
+ reaction(() => this.routeActiveTab, async (routeTab) => {
+ previousActiveTab.set(this.routeActiveTab);
- if (item || this.routeActiveTab === undefined) {
- this.activeTab = this.routeActiveTab;
- this.catalogEntityStore.activeCategory = item;
- } else {
- Notifications.error(Unknown category: {this.routeActiveTab}
);
+ try {
+ await when(() => (routeTab === "" || !!catalogCategoryRegistry.items.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load
+ const item = catalogCategoryRegistry.items.find(i => i.getId() === routeTab);
+
+ runInAction(() => {
+ this.activeTab = routeTab;
+ this.catalogEntityStore.activeCategory = item;
+ });
+ } catch(error) {
+ console.error(error);
+ Notifications.error(Unknown category: {routeTab}
);
}
- }),
- reaction(() => catalogCategoryRegistry.items, (items) => {
- if (!this.activeTab && items.length > 0) {
- this.activeTab = items[0].getId();
- this.catalogEntityStore.activeCategory = items[0];
- }
- }),
+ }, {fireImmediately: true}),
]);
}
- addToHotbar(item: CatalogEntityItem): void {
+ addToHotbar(item: CatalogEntityItem): void {
HotbarStore.getInstance().addToHotbar(item.entity);
}
- onDetails(item: CatalogEntityItem) {
- this.selectedItem = item;
- }
+ onDetails = (item: CatalogEntityItem) => {
+ this.catalogEntityStore.selectedItemId = item.getId();
+ };
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
if (menuItem.confirm) {
@@ -136,15 +136,18 @@ export class Catalog extends React.Component {
onTabChange = (tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId);
- this.catalogEntityStore.activeCategory = activeCategory;
- this.activeTab = activeCategory?.getId();
+ if (activeCategory) {
+ navigate(catalogURL({ params: {group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }}));
+ } else {
+ navigate(catalogURL());
+ }
};
renderNavigation() {
return ;
}
- renderItemMenu = (item: CatalogEntityItem) => {
+ renderItemMenu = (item: CatalogEntityItem) => {
const onOpen = () => {
this.contextMenu.menuItems = [];
@@ -167,49 +170,57 @@ export class Catalog extends React.Component {
);
};
- renderIcon(item: CatalogEntityItem) {
+ renderIcon(item: CatalogEntityItem) {
return (
this.onDetails(item)}
- size={24} />
+ size={24}
+ />
);
}
renderSingleCategoryList() {
return (
item.name,
- [sortBy.source]: (item: CatalogEntityItem) => item.source,
- [sortBy.status]: (item: CatalogEntityItem) => item.phase,
+ [sortBy.name]: (item: CatalogEntityItem) => item.name,
+ [sortBy.source]: (item: CatalogEntityItem) => item.source,
+ [sortBy.status]: (item: CatalogEntityItem) => item.phase,
}}
searchFilters={[
- (entity: CatalogEntityItem) => entity.searchFields,
+ (entity: CatalogEntityItem) => entity.searchFields,
]}
renderTableHeader={[
- { title: "", className: css.iconCell },
- { title: "Name", className: css.nameCell, sortBy: sortBy.name },
- { title: "Source", className: css.sourceCell, sortBy: sortBy.source },
- { title: "Labels", className: css.labelsCell },
- { title: "Status", className: css.statusCell, sortBy: sortBy.status },
+ { title: "", className: css.iconCell, id: "icon" },
+ { title: "Name", className: css.nameCell, sortBy: sortBy.name, id: "name" },
+ { title: "Source", className: css.sourceCell, sortBy: sortBy.source, id: "source" },
+ { title: "Labels", className: css.labelsCell, id: "labels" },
+ { title: "Status", className: css.statusCell, sortBy: sortBy.status, id: "status" },
]}
- renderTableContents={(item: CatalogEntityItem) => [
+ customizeTableRowProps={(item: CatalogEntityItem) => ({
+ disabled: !item.enabled,
+ })}
+ renderTableContents={(item: CatalogEntityItem) => [
this.renderIcon(item),
item.name,
item.source,
- item.labels.map((label) => ),
+ item.getLabelBadges(),
{ title: item.phase, className: cssNames(css[item.phase]) }
]}
- onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
+ onDetails={this.onDetails}
renderItemMenu={this.renderItemMenu}
/>
);
@@ -218,43 +229,56 @@ export class Catalog extends React.Component {
renderAllCategoriesList() {
return (
item.name,
- [sortBy.kind]: (item: CatalogEntityItem) => item.kind,
- [sortBy.source]: (item: CatalogEntityItem) => item.source,
- [sortBy.status]: (item: CatalogEntityItem) => item.phase,
+ [sortBy.name]: (item: CatalogEntityItem) => item.name,
+ [sortBy.kind]: (item: CatalogEntityItem) => item.kind,
+ [sortBy.source]: (item: CatalogEntityItem) => item.source,
+ [sortBy.status]: (item: CatalogEntityItem) => item.phase,
}}
searchFilters={[
- (entity: CatalogEntityItem) => entity.searchFields,
+ (entity: CatalogEntityItem) => entity.searchFields,
]}
renderTableHeader={[
- { title: "", className: css.iconCell },
- { title: "Name", className: css.nameCell, sortBy: sortBy.name },
- { title: "Kind", className: css.kindCell, sortBy: sortBy.kind },
- { title: "Source", className: css.sourceCell, sortBy: sortBy.source },
- { title: "Labels", className: css.labelsCell },
- { title: "Status", className: css.statusCell, sortBy: sortBy.status },
+ { title: "", className: css.iconCell, id: "icon" },
+ { title: "Name", className: css.nameCell, sortBy: sortBy.name, id: "name" },
+ { title: "Kind", className: css.kindCell, sortBy: sortBy.kind, id: "kind" },
+ { title: "Source", className: css.sourceCell, sortBy: sortBy.source, id: "source" },
+ { title: "Labels", className: css.labelsCell, id: "labels" },
+ { title: "Status", className: css.statusCell, sortBy: sortBy.status, id: "status" },
]}
- renderTableContents={(item: CatalogEntityItem) => [
+ customizeTableRowProps={(item: CatalogEntityItem) => ({
+ disabled: !item.enabled,
+ })}
+ renderTableContents={(item: CatalogEntityItem) => [
this.renderIcon(item),
item.name,
item.kind,
item.source,
- item.labels.map((label) => ),
+ item.getLabelBadges(),
{ title: item.phase, className: cssNames(css[item.phase]) }
]}
- detailsItem={this.selectedItem}
- onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
+ detailsItem={this.catalogEntityStore.selectedItem}
+ onDetails={this.onDetails}
renderItemMenu={this.renderItemMenu}
/>
);
}
+ renderCategoryList() {
+ if (this.activeTab === undefined) {
+ return null;
+ }
+
+ return this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList();
+ }
+
render() {
if (!this.catalogEntityStore) {
return null;
@@ -263,17 +287,18 @@ export class Catalog extends React.Component {
return (
- { this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
+ { this.renderCategoryList() }
- { !this.selectedItem && (
-
- )}
- { this.selectedItem && (
- this.selectedItem = null}
- />
- )}
+ {
+ this.catalogEntityStore.selectedItem
+ ? this.catalogEntityStore.selectedItemId = null}
+ />
+ :
+ }
);
}
diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx
index b6ec3a7076..609f41d1ad 100644
--- a/src/renderer/components/+cluster/cluster-pie-charts.tsx
+++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx
@@ -174,7 +174,13 @@ export const ClusterPieCharts = observer(() => {
{cpuLimitsOverload && renderLimitWarning()}
@@ -182,7 +188,13 @@ export const ClusterPieCharts = observer(() => {
{memoryLimitsOverload && renderLimitWarning()}
diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx
index d37fdef953..ad0713e342 100644
--- a/src/renderer/components/+entity-settings/entity-settings.tsx
+++ b/src/renderer/components/+entity-settings/entity-settings.tsx
@@ -25,7 +25,6 @@ import React from "react";
import { observable, makeObservable } from "mobx";
import type { RouteComponentProps } from "react-router";
import { observer } from "mobx-react";
-import { PageLayout } from "../layout/page-layout";
import { navigation } from "../../navigation";
import { Tabs, Tab } from "../tabs";
import type { CatalogEntity } from "../../api/catalog-entity";
@@ -33,6 +32,8 @@ import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { EntitySettingRegistry } from "../../../extensions/registries";
import type { EntitySettingsRouteParams } from "../../../common/routes";
import { groupBy } from "lodash";
+import { SettingLayout } from "../layout/setting-layout";
+import { HotbarIcon } from "../hotbar/hotbar-icon";
interface Props extends RouteComponentProps {
}
@@ -83,10 +84,19 @@ export class EntitySettings extends React.Component {
return (
<>
- {this.entity.metadata.name}
+
+
+
{this.entity.metadata.name}
+
- { groups.map((group) => (
- <>
+ { groups.map((group, groupIndex) => (
+
+
{group[0]}
{ group[1].map((setting, index) => (
{
data-testid={`${setting.id}-tab`}
/>
))}
- >
+
))}
>
@@ -120,10 +130,9 @@ export class EntitySettings extends React.Component {
const activeSetting = this.menuItems.find((setting) => setting.id === this.activeTab);
return (
-
@@ -132,7 +141,7 @@ export class EntitySettings extends React.Component {
-
+
);
}
}
diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
index 3361362b4b..3c2484b92e 100644
--- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx
+++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
@@ -61,7 +61,7 @@ describe("Extensions", () => {
ExtensionInstallationStateStore.reset();
UserStore.resetInstance();
- await UserStore.createInstance().load();
+ UserStore.createInstance();
ExtensionDiscovery.resetInstance();
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx
index 831ec6c1cd..b135487343 100644
--- a/src/renderer/components/+extensions/extensions.tsx
+++ b/src/renderer/components/+extensions/extensions.tsx
@@ -39,12 +39,13 @@ import logger from "../../../main/logger";
import { Button } from "../button";
import { ConfirmDialog } from "../confirm-dialog";
import { DropFileInput, InputValidators } from "../input";
-import { PageLayout } from "../layout/page-layout";
import { Notifications } from "../notifications";
import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store";
import { Install } from "./install";
import { InstalledExtensions } from "./installed-extensions";
import { Notice } from "./notice";
+import { SettingLayout } from "../layout/setting-layout";
+import { docsUrl } from "../../../common/vars";
function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") {
@@ -277,7 +278,7 @@ async function unpackExtension(request: InstallRequestValidated, disposeDownload
await when(() => ExtensionLoader.getInstance().userExtensions.has(id));
// Enable installed extensions by default.
- ExtensionLoader.getInstance().userExtensions.get(id).isEnabled = true;
+ ExtensionLoader.getInstance().setIsEnabled(id, true);
Notifications.ok(
Extension {displayName} successfully installed!
@@ -317,7 +318,7 @@ export async function attemptInstallByInfo({ name, version, requireConfirmation
version = json["dist-tags"][version];
} else {
Notifications.error(The {name} extension does not have a version or tag {version}.
);
-
+
return disposer();
}
}
@@ -510,11 +511,17 @@ export class Extensions extends React.Component {
return (
-
+
Extensions
-
+
+
+ Add new features via Lens Extensions.{" "}
+ Check out docs {" "}
+ and list of available extensions .
+
+
{
uninstall={confirmUninstallExtension}
/>
-
+
);
}
diff --git a/src/renderer/components/+extensions/notice.tsx b/src/renderer/components/+extensions/notice.tsx
index d224b65a7d..e4b844a0ae 100644
--- a/src/renderer/components/+extensions/notice.tsx
+++ b/src/renderer/components/+extensions/notice.tsx
@@ -20,17 +20,14 @@
*/
import styles from "./notice.module.css";
-import React from "react";
-import { docsUrl } from "../../../common/vars";
+import React, { DOMAttributes } from "react";
-export function Notice() {
+interface Props extends DOMAttributes {}
+
+export function Notice(props: Props) {
return (
-
- Add new features via Lens Extensions.{" "}
- Check out docs {" "}
- and list of available extensions .
-
+ {props.children}
);
}
diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts
index f865787d76..c292f0f9fa 100644
--- a/src/renderer/components/+namespaces/namespace.store.ts
+++ b/src/renderer/components/+namespaces/namespace.store.ts
@@ -112,7 +112,7 @@ export class NamespaceStore extends KubeObjectStore {
protected async loadItems(params: KubeObjectStoreLoadingParams) {
const { allowedNamespaces } = this;
- let namespaces = (await super.loadItems(params)) || [];
+ let namespaces = await super.loadItems(params).catch(() => []);
namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName()));
diff --git a/src/renderer/components/+nodes/node-charts.tsx b/src/renderer/components/+nodes/node-charts.tsx
index 3edcb4b12f..60d6e08842 100644
--- a/src/renderer/components/+nodes/node-charts.tsx
+++ b/src/renderer/components/+nodes/node-charts.tsx
@@ -54,6 +54,7 @@ export const NodeCharts = observer(() => {
cpuUsage,
cpuRequests,
cpuCapacity,
+ cpuAllocatableCapacity,
podUsage,
podCapacity,
fsSize,
@@ -77,6 +78,13 @@ export const NodeCharts = observer(() => {
borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y }))
},
+ {
+ id: `${id}-cpuAllocatableCapacity`,
+ label: `Allocatable Capacity`,
+ tooltip: `CPU allocatable capacity`,
+ borderColor: "#032b4d",
+ data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y }))
+ },
{
id: `${id}-cpuCapacity`,
label: `Capacity`,
diff --git a/src/renderer/components/+preferences/kubeconfig-syncs.tsx b/src/renderer/components/+preferences/kubeconfig-syncs.tsx
index 152fad13ba..c1e049cbde 100644
--- a/src/renderer/components/+preferences/kubeconfig-syncs.tsx
+++ b/src/renderer/components/+preferences/kubeconfig-syncs.tsx
@@ -32,6 +32,7 @@ import { SubTitle } from "../layout/sub-title";
import { Spinner } from "../spinner";
import logger from "../../../main/logger";
import { iter } from "../../utils";
+import { isWindows } from "../../../common/vars";
interface SyncInfo {
type: "file" | "folder" | "unknown";
@@ -69,6 +70,8 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[
}
}
+type SelectPathOptions = ("openFile" | "openDirectory")[];
+
@observer
export class KubeconfigSyncs extends React.Component {
syncs = observable.map();
@@ -106,11 +109,11 @@ export class KubeconfigSyncs extends React.Component {
}
@action
- openFileDialog = async () => {
+ async openDialog(message: string, actions: SelectPathOptions) {
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
- properties: ["openFile", "showHiddenFiles", "multiSelections", "openDirectory"],
- message: "Select kubeconfig file(s) and folder(s)",
+ properties: ["showHiddenFiles", "multiSelections", ...actions],
+ message,
buttonLabel: "Sync",
});
@@ -123,7 +126,7 @@ export class KubeconfigSyncs extends React.Component {
for (const [filePath, info] of newEntries) {
this.syncs.set(filePath, info);
}
- };
+ }
renderEntryIcon(entry: Entry) {
switch (entry.info.type) {
@@ -181,16 +184,41 @@ export class KubeconfigSyncs extends React.Component {
);
}
+ renderSyncButtons() {
+ if (isWindows) {
+ return (
+
+ void this.openDialog("Sync file(s)", ["openFile"])}
+ />
+ void this.openDialog("Sync folder(s)", ["openDirectory"])}
+ />
+
+ );
+ }
+
+ return (
+ void this.openDialog("Sync file(s) and folder(s)", ["openFile", "openDirectory"])}
+ />
+ );
+ }
+
render() {
return (
<>
- void this.openFileDialog()}
- />
+ {this.renderSyncButtons()}
Sync an individual file or all files in a folder (non-recursive).
diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx
index 0a85c6369d..9bf82f9e4f 100644
--- a/src/renderer/components/+preferences/preferences.tsx
+++ b/src/renderer/components/+preferences/preferences.tsx
@@ -31,7 +31,6 @@ import { AppPreferenceRegistry, RegisteredAppPreference } from "../../../extensi
import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store";
import { Input } from "../input";
-import { PageLayout } from "../layout/page-layout";
import { SubTitle } from "../layout/sub-title";
import { Select, SelectOption } from "../select";
import { HelmCharts } from "./helm-charts";
@@ -40,6 +39,7 @@ import { navigation } from "../../navigation";
import { Tab, Tabs } from "../tabs";
import { FormSwitch, Switcher } from "../switch";
import { KubeconfigSyncs } from "./kubeconfig-syncs";
+import { SettingLayout } from "../layout/setting-layout";
enum Pages {
Application = "application",
@@ -136,8 +136,7 @@ export class Preferences extends React.Component {
);
return (
- !e.showInPreferencesTab).map(this.renderExtension)}
)}
-
+
);
}
}
diff --git a/src/renderer/components/+user-management/+role-bindings/dialog.tsx b/src/renderer/components/+user-management/+role-bindings/dialog.tsx
index 0bed732381..c163a01b83 100644
--- a/src/renderer/components/+user-management/+role-bindings/dialog.tsx
+++ b/src/renderer/components/+user-management/+role-bindings/dialog.tsx
@@ -91,17 +91,15 @@ export class RoleBindingDialog extends React.Component {
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount" as const,
- namespace: this.bindingNamespace,
+ namespace: sa.getNs(),
}));
const users = Array.from(this.selectedUsers, user => ({
name: user,
- kind: "User" as const,
- namespace: this.bindingNamespace,
+ kind: "User" as const
}));
const groups = Array.from(this.selectedGroups, group => ({
name: group,
- kind: "Group" as const,
- namespace: this.bindingNamespace,
+ kind: "Group" as const
}));
return [
diff --git a/src/renderer/components/+user-management/+service-accounts/view.tsx b/src/renderer/components/+user-management/+service-accounts/view.tsx
index 282ae3ec63..65798af192 100644
--- a/src/renderer/components/+user-management/+service-accounts/view.tsx
+++ b/src/renderer/components/+user-management/+service-accounts/view.tsx
@@ -93,7 +93,7 @@ export function ServiceAccountMenu(props: KubeObjectMenuProps) {
return (
openServiceAccountKubeConfig(object)}>
-
+
Kubeconfig
);
diff --git a/src/renderer/components/+welcome/welcome.scss b/src/renderer/components/+welcome/welcome.scss
index 6141f9f517..bb1318c64e 100644
--- a/src/renderer/components/+welcome/welcome.scss
+++ b/src/renderer/components/+welcome/welcome.scss
@@ -68,4 +68,12 @@
height: 200px;
color: var(--primary);
}
+
+ a.link {
+ color: var(--colorInfo);
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
}
diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx
index 703520fa17..2b06d8d255 100644
--- a/src/renderer/components/+welcome/welcome.tsx
+++ b/src/renderer/components/+welcome/welcome.tsx
@@ -39,7 +39,7 @@ export class Welcome extends React.Component {
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
- If you have any questions or feedback, please join our Lens Community slack channel .
+ If you have any questions or feedback, please join our Lens Community slack channel .
diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
index df976a187b..b1a812cc4f 100644
--- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
+++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx
@@ -107,7 +107,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
return (
<>
CronJobTriggerDialog.open(object)}>
-
+
Trigger
@@ -126,7 +126,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
Resume CronJob {object.getName()} ?
),
})}>
-
+
Resume
@@ -144,7 +144,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) {
Suspend CronJob {object.getName()} ?
),
})}>
-
+
Suspend
}
diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx
index d57d5135b2..c07dc397cc 100644
--- a/src/renderer/components/+workloads-deployments/deployments.tsx
+++ b/src/renderer/components/+workloads-deployments/deployments.tsx
@@ -124,7 +124,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {
return (
<>
DeploymentScaleDialog.open(object)}>
-
+
Scale
ConfirmDialog.open({
@@ -146,7 +146,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {
),
})}>
-
+
Restart
>
diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx
index b14915e85f..51d63fec55 100644
--- a/src/renderer/components/+workloads-replicasets/replicasets.tsx
+++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx
@@ -98,7 +98,7 @@ export function ReplicaSetMenu(props: KubeObjectMenuProps) {
return (
<>
ReplicaSetScaleDialog.open(object)}>
-
+
Scale
>
diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
index 0431191d7d..82b1fff151 100644
--- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
+++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx
@@ -103,7 +103,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps) {
return (
<>
StatefulSetScaleDialog.open(object)}>
-
+
Scale
>
diff --git a/src/renderer/components/avatar/avatar.tsx b/src/renderer/components/avatar/avatar.tsx
index 2cf78866f4..4164b65261 100644
--- a/src/renderer/components/avatar/avatar.tsx
+++ b/src/renderer/components/avatar/avatar.tsx
@@ -32,6 +32,7 @@ interface Props extends DOMAttributes, Partial {
height?: number;
src?: string;
className?: string;
+ background?: string;
}
function getNameParts(name: string): string[] {
@@ -69,11 +70,23 @@ function getIconString(title: string) {
}
export function Avatar(props: Props) {
- const { title, src, width = 32, height = 32, colorHash, ...settings } = props;
+ const { title, width = 32, height = 32, colorHash, children, background, ...settings } = props;
+
+ const getBackgroundColor = () => {
+ if (background) {
+ return background;
+ }
+
+ if (settings.src) {
+ return "transparent";
+ }
+
+ return randomColor({ seed: colorHash, luminosity: "dark" });
+ };
const generateAvatarStyle = (): React.CSSProperties => {
return {
- backgroundColor: randomColor({ seed: colorHash, luminosity: "dark" }),
+ backgroundColor: getBackgroundColor(),
width,
height,
textTransform: "uppercase"
@@ -86,7 +99,7 @@ export function Avatar(props: Props) {
style={generateAvatarStyle()}
{...settings}
>
- {getIconString(title)}
+ {children || getIconString(title)}
);
}
diff --git a/src/renderer/components/badge/badge.scss b/src/renderer/components/badge/badge.scss
index 4e7f9702f6..23f8e99ff0 100644
--- a/src/renderer/components/badge/badge.scss
+++ b/src/renderer/components/badge/badge.scss
@@ -36,4 +36,8 @@
&.small {
font-size: $font-size-small;
}
-}
\ No newline at end of file
+
+ &.clickable {
+ cursor: pointer;
+ }
+}
diff --git a/src/renderer/components/badge/badge.tsx b/src/renderer/components/badge/badge.tsx
index 57d1e8917a..14959b04d1 100644
--- a/src/renderer/components/badge/badge.tsx
+++ b/src/renderer/components/badge/badge.tsx
@@ -35,9 +35,10 @@ export interface BadgeProps extends React.HTMLAttributes, TooltipDecoratorP
export class Badge extends React.Component {
render() {
const { className, label, small, flat, children, ...elemProps } = this.props;
+ const clickable = Boolean(this.props.onClick);
return <>
-
+
{label}
{children}
diff --git a/src/renderer/components/catalog-entities/weblink-add-command.tsx b/src/renderer/components/catalog-entities/weblink-add-command.tsx
new file mode 100644
index 0000000000..04310ac46d
--- /dev/null
+++ b/src/renderer/components/catalog-entities/weblink-add-command.tsx
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import React from "react";
+import { observer } from "mobx-react";
+import { CommandOverlay } from "../command-palette";
+import { Input } from "../input";
+import { isUrl } from "../input/input_validators";
+import { WeblinkStore } from "../../../common/weblink-store";
+import { computed, makeObservable, observable } from "mobx";
+
+@observer
+export class WeblinkAddCommand extends React.Component {
+ @observable url = "";
+ @observable nameHidden = true;
+ @observable dirty = false;
+
+ constructor(props: {}) {
+ super(props);
+
+ makeObservable(this);
+ }
+
+ onChangeUrl(url: string) {
+ this.dirty = true;
+ this.url = url;
+ }
+
+ onSubmitUrl(url: string) {
+ this.dirty = true;
+ this.url = url;
+ this.nameHidden = false;
+ }
+
+ onSubmit(name: string) {
+ WeblinkStore.getInstance().add({
+ name: name || this.url,
+ url: this.url
+ });
+
+ CommandOverlay.close();
+ }
+
+ @computed get showValidation() {
+ return this.url?.length > 0;
+ }
+
+ render() {
+ return (
+ <>
+ this.onChangeUrl(v)}
+ onSubmit={(v) => this.onSubmitUrl(v)}
+ showValidationLine={true} />
+ { this.nameHidden && (
+
+ Please provide a web link URL (Press "Enter" to continue or "Escape" to cancel)
+
+ )}
+ { !this.nameHidden && (
+ <>
+ this.onSubmit(v)}
+ dirty={true}/>
+
+ Please provide a name for the web link (Press "Enter" to confirm or "Escape" to cancel)
+
+ >
+ )}
+ >
+ );
+ }
+}
diff --git a/src/renderer/components/cluster-manager/bottom-bar.scss b/src/renderer/components/cluster-manager/bottom-bar.scss
index d89eefdc3e..c09e75889f 100644
--- a/src/renderer/components/cluster-manager/bottom-bar.scss
+++ b/src/renderer/components/cluster-manager/bottom-bar.scss
@@ -27,18 +27,6 @@
padding: 0 2px;
height: var(--bottom-bar-height);
- #catalog-link {
- font-size: var(--font-size-small);
- color: white;
- padding: $padding / 4 $padding / 2;
- cursor: pointer;
-
- &:hover {
- background-color: #ffffff33;
- cursor: pointer;
- }
- }
-
.extensions {
font-size: var(--font-size-small);
color: white;
diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx
index 8e37128198..0f61a03833 100644
--- a/src/renderer/components/cluster-manager/bottom-bar.tsx
+++ b/src/renderer/components/cluster-manager/bottom-bar.tsx
@@ -24,9 +24,6 @@ import "./bottom-bar.scss";
import React from "react";
import { observer } from "mobx-react";
import { StatusBarRegistration, StatusBarRegistry } from "../../../extensions/registries";
-import { navigate } from "../../navigation";
-import { Icon } from "../icon";
-import { catalogURL } from "../../../common/routes";
@observer
export class BottomBar extends React.Component {
@@ -67,10 +64,6 @@ export class BottomBar extends React.Component {
render() {
return (
-
navigate(catalogURL())}>
-
- Catalog
-
{this.renderRegisteredItems()}
);
diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss
index da31c2a07b..dddbb1a5e5 100644
--- a/src/renderer/components/cluster-manager/cluster-manager.scss
+++ b/src/renderer/components/cluster-manager/cluster-manager.scss
@@ -35,6 +35,10 @@
position: relative;
display: flex;
flex-direction: column;
+
+ > * {
+ z-index: 1;
+ }
}
.HotbarMenu {
diff --git a/src/renderer/components/cluster-manager/cluster-topbar.tsx b/src/renderer/components/cluster-manager/cluster-topbar.tsx
index 7dcc3c7d5a..eb6fbd6e59 100644
--- a/src/renderer/components/cluster-manager/cluster-topbar.tsx
+++ b/src/renderer/components/cluster-manager/cluster-topbar.tsx
@@ -30,6 +30,7 @@ import { MaterialTooltip } from "../material-tooltip/material-tooltip";
import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store";
import type { ClusterViewRouteParams } from "../../../common/routes";
+import { previousActiveTab } from "../+catalog";
interface Props extends RouteComponentProps {
}
@@ -43,7 +44,9 @@ export const ClusterTopbar = observer((props: Props) => {
- navigate(catalogURL())}/>
+ {
+ navigate(`${catalogURL()}/${previousActiveTab.get()}`);
+ }}/>
diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx
index 78626cccb1..db48e051c8 100644
--- a/src/renderer/components/cluster-manager/cluster-view.tsx
+++ b/src/renderer/components/cluster-manager/cluster-view.tsx
@@ -33,6 +33,7 @@ import { clusterActivateHandler } from "../../../common/cluster-ipc";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation";
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
+import { previousActiveTab } from "../+catalog";
interface Props extends RouteComponentProps {
}
@@ -84,7 +85,7 @@ export class ClusterView extends React.Component {
const disconnected = values[1];
if (hasLoadedView(this.clusterId) && disconnected) {
- navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
+ navigate(`${catalogURL()}/${previousActiveTab.get()}`); // redirect to catalog when active cluster get disconnected/not available
}
}),
]);
diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx
index ed01ce4fb7..2f11883fa0 100644
--- a/src/renderer/components/cluster-settings/cluster-settings.tsx
+++ b/src/renderer/components/cluster-settings/cluster-settings.tsx
@@ -40,12 +40,16 @@ export function GeneralSettings({ entity }: EntitySettingViewProps) {
return (
-
-
@@ -106,10 +110,9 @@ export function MetricsSettings({ entity }: EntitySettingViewProps) {
+
-
diff --git a/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx b/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx
index ab2a03f73c..6794646bea 100644
--- a/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx
@@ -54,6 +54,7 @@ export class ClusterAccessibleNamespaces extends React.Component {
this.namespaces.delete(oldNamesapce);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
+ inputTheme="round-black"
/>
This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.
diff --git a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx
index 2800586a11..d0ee10f0ee 100644
--- a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx
@@ -21,15 +21,13 @@
import React from "react";
import type { Cluster } from "../../../../main/cluster";
-//import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
import { boundMethod } from "../../../utils";
-import { Button } from "../../button";
import { observable } from "mobx";
import { observer } from "mobx-react";
-import { SubTitle } from "../../layout/sub-title";
import { HotbarIcon } from "../../hotbar/hotbar-icon";
import type { KubernetesCluster } from "../../../../common/catalog-entities";
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
+import { MenuActions, MenuItem } from "../../menu";
enum GeneralInputStatus {
CLEAN = "clean",
@@ -46,6 +44,8 @@ export class ClusterIconSetting extends React.Component {
@observable status = GeneralInputStatus.CLEAN;
@observable errorText?: string;
+ private element = React.createRef();
+
@boundMethod
async onIconPick([file]: File[]) {
const { cluster } = this.props;
@@ -66,16 +66,11 @@ export class ClusterIconSetting extends React.Component {
}
}
- getClearButton() {
- if (this.props.cluster.preferences.icon) {
- return this.onIconPick([])}
- />;
- }
+ @boundMethod
+ onUploadClick() {
+ const input = this.element.current.querySelector("input[type=file]") as HTMLInputElement;
- return null;
+ input.click();
}
render() {
@@ -86,25 +81,33 @@ export class ClusterIconSetting extends React.Component {
uid={entity.metadata.uid}
title={entity.metadata.name}
source={entity.metadata.source}
- icon={entity.spec.iconData}
+ src={entity.spec.icon?.src}
+ size={53}
/>
- Browse for new icon...
>
);
return (
- <>
-
-
-
- {this.getClearButton()}
+
+
+
+
+
+
+
+ Upload Icon
+
+ this.onIconPick([])} disabled={!this.props.cluster.preferences.icon}>
+ Clear
+
+
- >
+
);
}
}
diff --git a/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx b/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx
index 878f6cab7d..9c95c70c5d 100644
--- a/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx
@@ -25,6 +25,7 @@ import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title";
import { boundMethod } from "../../../../common/utils";
import { shell } from "electron";
+import { Notice } from "../../+extensions/notice";
interface Props {
cluster: Cluster;
@@ -42,14 +43,12 @@ export class ClusterKubeconfig extends React.Component
{
render() {
return (
- <>
+
-
{this.props.cluster.kubeConfigPath}
-
- >
+
);
}
}
diff --git a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx
index 21a6b3242d..8cce59bbe8 100644
--- a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx
@@ -100,6 +100,7 @@ export class ClusterMetricsSetting extends React.Component {
options={Object.values(ClusterMetricsResourceType)}
onChange={this.onChangeSelect}
formatOptionLabel={this.formatOptionLabel}
+ themeName="lens"
/>
{
render() {
return (
-
+
{this.renderMetricsSelect()}
diff --git a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx
index dfecec3573..a74606b1ec 100644
--- a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx
@@ -138,26 +138,30 @@ export class ClusterPrometheusSetting extends React.Component
{
this.onSaveProvider();
}}
options={this.options}
+ themeName="lens"
/>
What query format is used to fetch metrics from Prometheus
>
}
{this.canEditPrometheusPath && (
-
+ <>
+
+
+ >
)}
>
);
diff --git a/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx b/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx
index 9a6d1e084f..6544f2325b 100644
--- a/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx
+++ b/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx
@@ -25,6 +25,7 @@ import type { Cluster } from "../../../../main/cluster";
import { observable, reaction, makeObservable } from "mobx";
import { Badge } from "../../badge/badge";
import { Icon } from "../../icon/icon";
+import { Notice } from "../../+extensions/notice";
interface Props {
cluster: Cluster;
@@ -55,14 +56,20 @@ export class ShowMetricsSetting extends React.Component {
}
renderMetrics() {
+ const metrics = Array.from(this.hiddenMetrics);
+
+ if (!metrics.length) {
+ return (
+ All metrics are visible on the UI
+ );
+ }
return (
-
- Array.from(this.hiddenMetrics).map(name => {
+ metrics.map(name => {
const tooltipId = `${name}`;
return (
-
+
{name}
{
render() {
return (
-
- {this.renderMetrics()}
-
+
+
+ {this.renderMetrics()}
+
+
);
}
}
diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx
index 62566b868a..17ebb92d09 100644
--- a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx
+++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx
@@ -30,10 +30,11 @@ import type { LogTabData } from "../log-tab.store";
import { dockerPod, deploymentPod1 } from "./pod.mock";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
+import mockFs from "mock-fs";
jest.mock("electron", () => ({
app: {
- getPath: () => "/foo",
+ getPath: () => "tmp",
},
}));
@@ -71,6 +72,9 @@ const getFewPodsTabData = (): LogTabData => {
describe(" ", () => {
beforeEach(() => {
+ mockFs({
+ "tmp": {}
+ });
UserStore.createInstance();
ThemeStore.createInstance();
});
@@ -78,6 +82,7 @@ describe(" ", () => {
afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
+ mockFs.restore();
});
it("renders w/o errors", () => {
diff --git a/src/renderer/components/dock/dock-tab.tsx b/src/renderer/components/dock/dock-tab.tsx
index bf3fea3016..dd4ff63824 100644
--- a/src/renderer/components/dock/dock-tab.tsx
+++ b/src/renderer/components/dock/dock-tab.tsx
@@ -94,7 +94,7 @@ export class DockTab extends React.Component {
{!pinned && (
)}
diff --git a/src/renderer/components/editable-list/editable-list.scss b/src/renderer/components/editable-list/editable-list.scss
index b30fdf7f57..0f88d009d9 100644
--- a/src/renderer/components/editable-list/editable-list.scss
+++ b/src/renderer/components/editable-list/editable-list.scss
@@ -23,7 +23,10 @@
.el-contents {
display: flex;
flex-direction: column;
- margin: $padding 0px;
+
+ &:not(:empty) {
+ margin: $padding 0px;
+ }
.el-value-remove {
.Icon {
diff --git a/src/renderer/components/editable-list/editable-list.tsx b/src/renderer/components/editable-list/editable-list.tsx
index ec31a2586e..0b427b6f2a 100644
--- a/src/renderer/components/editable-list/editable-list.tsx
+++ b/src/renderer/components/editable-list/editable-list.tsx
@@ -25,7 +25,7 @@ import { observer } from "mobx-react";
import React from "react";
import { Icon } from "../icon";
-import { Input } from "../input";
+import { Input, InputProps } from "../input";
import { boundMethod } from "../../utils";
export interface Props {
@@ -37,11 +37,13 @@ export interface Props {
// An optional prop used to convert T to a displayable string
// defaults to `String`
renderItem?: (item: T, index: number) => React.ReactNode,
+ inputTheme?: InputProps["theme"];
}
const defaultProps: Partial> = {
placeholder: "Add new item...",
- renderItem: (item: any, index: number) => {item}
+ renderItem: (item: any, index: number) => {item} ,
+ inputTheme: "round"
};
@observer
@@ -59,13 +61,13 @@ export class EditableList extends React.Component> {
}
render() {
- const { items, remove, renderItem, placeholder } = this.props;
+ const { items, remove, renderItem, placeholder, inputTheme } = this.props;
return (
diff --git a/src/renderer/components/file-picker/file-picker.tsx b/src/renderer/components/file-picker/file-picker.tsx
index 565168efac..cc1e850cd2 100644
--- a/src/renderer/components/file-picker/file-picker.tsx
+++ b/src/renderer/components/file-picker/file-picker.tsx
@@ -71,15 +71,15 @@ export interface BaseProps {
// the larger number is upper limit, the lower is lower limit
// the lower limit is capped at 0 and the upper limit is capped at Infinity
limit?: [number, number];
-
+
// default is "Reject"
onOverLimit?: OverLimitStyle;
-
+
// individual files are checked before the total size.
maxSize?: number;
// default is "Reject"
onOverSizeLimit?: OverSizeLimitStyle;
-
+
maxTotalSize?: number;
// default is "Reject"
onOverTotalSizeLimit?: OverTotalSizeLimitStyle;
@@ -158,7 +158,7 @@ export class FilePicker extends React.Component
{
files = _.orderBy(files, ["size"]);
case OverTotalSizeLimitStyle.FILTER_LAST:
let newTotalSize = totalSize;
-
+
for (;files.length > 0;) {
newTotalSize -= files.pop().size;
@@ -180,12 +180,12 @@ export class FilePicker extends React.Component {
const numberLimitedFiles = this.handleFileCount(files);
const sizeLimitedFiles = this.handleIndiviualFileSizes(numberLimitedFiles);
const totalSizeLimitedFiles = this.handleTotalFileSizes(sizeLimitedFiles);
-
+
if ("uploadDir" in this.props) {
const { uploadDir } = this.props;
this.status = FileInputStatus.PROCESSING;
-
+
const paths: string[] = [];
const promises = totalSizeLimitedFiles.map(async file => {
const destinationPath = path.join(uploadDir, file.name);
@@ -214,9 +214,9 @@ export class FilePicker extends React.Component {
return
{label} {this.getIconRight()}
-
{
getIconRight(): React.ReactNode {
switch (this.status) {
- case FileInputStatus.CLEAR:
- return
;
case FileInputStatus.PROCESSING:
return
;
case FileInputStatus.ERROR:
- return
;
+ return
;
+ default:
+ return null;
}
}
}
diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx
new file mode 100644
index 0000000000..119212753d
--- /dev/null
+++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import "@testing-library/jest-dom/extend-expect";
+import { HotbarRemoveCommand } from "../hotbar-remove-command";
+import { render, fireEvent } from "@testing-library/react";
+import React from "react";
+import { ThemeStore } from "../../../theme.store";
+import { UserStore } from "../../../../common/user-store";
+import { Notifications } from "../../notifications";
+import mockFs from "mock-fs";
+
+jest.mock("electron", () => ({
+ app: {
+ getPath: () => "tmp",
+ setLoginItemSettings: jest.fn(),
+ },
+}));
+
+const mockHotbars: {[id: string]: any} = {
+ "1": {
+ id: "1",
+ name: "Default",
+ items: [] as any
+ }
+};
+
+jest.mock("../../../../common/hotbar-store", () => ({
+ HotbarStore: {
+ getInstance: () => ({
+ hotbars: [mockHotbars["1"]],
+ getById: (id: string) => mockHotbars[id],
+ remove: () => {},
+ hotbarIndex: () => 0
+ })
+ }
+}));
+
+describe("
", () => {
+ beforeEach(() => {
+ mockFs({
+ "tmp": {}
+ });
+ UserStore.createInstance();
+ ThemeStore.createInstance();
+ });
+
+ afterEach(() => {
+ UserStore.resetInstance();
+ ThemeStore.resetInstance();
+ mockFs.restore();
+ });
+
+ it("renders w/o errors", () => {
+ const { container } = render(
);
+
+ expect(container).toBeInstanceOf(HTMLElement);
+ });
+
+ it("displays error notification if user tries to remove last hotbar", () => {
+ const spy = jest.spyOn(Notifications, "error");
+ const { getByText } = render(
);
+
+ fireEvent.click(getByText("1: Default"));
+
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx
index 8141594b7c..fde97ef561 100644
--- a/src/renderer/components/hotbar/hotbar-add-command.tsx
+++ b/src/renderer/components/hotbar/hotbar-add-command.tsx
@@ -33,22 +33,14 @@ const uniqueHotbarName: InputValidator = {
@observer
export class HotbarAddCommand extends React.Component {
-
- onSubmit(name: string) {
+ onSubmit = (name: string) => {
if (!name.trim()) {
return;
}
- const hotbarStore = HotbarStore.getInstance();
-
- const hotbar = hotbarStore.add({
- name
- });
-
- hotbarStore.activeHotbarId = hotbar.id;
-
+ HotbarStore.getInstance().add({ name }, { setActive: true });
CommandOverlay.close();
- }
+ };
render() {
return (
@@ -58,10 +50,11 @@ export class HotbarAddCommand extends React.Component {
autoFocus={true}
theme="round-black"
data-test-id="command-palette-hotbar-add-name"
- validators={[uniqueHotbarName]}
- onSubmit={(v) => this.onSubmit(v)}
+ validators={uniqueHotbarName}
+ onSubmit={this.onSubmit}
dirty={true}
- showValidationLine={true} />
+ showValidationLine={true}
+ />
Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel)
diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx
index 5708379a7a..b03bf3fb92 100644
--- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx
+++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx
@@ -54,7 +54,7 @@ export class HotbarEntityIcon extends React.Component
{
componentDidMount() {
this.contextMenu = {
menuItems: [],
- navigate: (url: string) => navigate(url)
+ navigate: (url: string) => navigate(url),
};
}
@@ -74,6 +74,10 @@ export class HotbarEntityIcon extends React.Component {
}
get ledIcon() {
+ if (this.props.entity.kind !== "KubernetesCluster") {
+ return null;
+ }
+
const className = cssNames("led", { online: this.props.entity.status.phase == "connected"}); // TODO: make it more generic
return
;
@@ -129,7 +133,9 @@ export class HotbarEntityIcon extends React.Component {
uid={entity.metadata.uid}
title={entity.metadata.name}
source={entity.metadata.source}
- icon={entity.spec.iconData}
+ src={entity.spec.icon?.src}
+ material={entity.spec.icon?.material}
+ background={entity.spec.icon?.background}
className={className}
active={isActive}
onMenuOpen={onOpen}
diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss
index 9e3451ab94..225a054011 100644
--- a/src/renderer/components/hotbar/hotbar-icon.scss
+++ b/src/renderer/components/hotbar/hotbar-icon.scss
@@ -20,7 +20,6 @@
*/
.HotbarMenu {
-
.HotbarIconMenu {
left: 30px;
min-width: 250px;
@@ -50,6 +49,10 @@
cursor: default;
filter: grayscale(0.7);
+ &.contextMenuAvailable {
+ cursor: context-menu;
+ }
+
&:hover {
&:not(.active) {
box-shadow: none;
@@ -113,7 +116,6 @@
}
img {
- padding: 3px;
border-radius: 6px;
&.active {
@@ -126,4 +128,11 @@
}
}
}
+
+ .materialIcon {
+ margin-left: 1px;
+ margin-top: 1px;
+ text-shadow: none;
+ font-size: 180%;
+ }
}
diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx
index 0ee8f2ddc8..b739fa2b4c 100644
--- a/src/renderer/components/hotbar/hotbar-icon.tsx
+++ b/src/renderer/components/hotbar/hotbar-icon.tsx
@@ -30,18 +30,21 @@ import { Menu, MenuItem } from "../menu";
import { MaterialTooltip } from "../material-tooltip/material-tooltip";
import { observer } from "mobx-react";
import { Avatar } from "../avatar/avatar";
+import { Icon } from "../icon";
export interface HotbarIconProps extends DOMAttributes {
uid: string;
title: string;
source: string;
- icon?: string;
+ src?: string;
+ material?: string;
onMenuOpen?: () => void;
className?: IClassName;
active?: boolean;
menuItems?: CatalogEntityContextMenu[];
disabled?: boolean;
size?: number;
+ background?: string;
}
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
@@ -62,7 +65,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
}
export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => {
- const { uid, title, icon, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
+ const { uid, title, src, material, active, className, source, disabled, onMenuOpen, onClick, children, ...rest } = props;
const id = `hotbarIcon-${uid}`;
const [menuOpen, setMenuOpen] = useState(false);
@@ -71,27 +74,28 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: Hotba
};
const renderIcon = () => {
- if (icon) {
- return ;
- } else {
- return ;
- }
+ src={src}
+ onClick={(event) => {
+ if (!disabled) {
+ onClick?.(event);
+ }
+ }}
+ >
+ {material && }
+
+ );
};
return (
-
+
0 })}>
{renderIcon()}
@@ -110,13 +114,13 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: Hotba
toggleMenu();
}}
close={() => toggleMenu()}>
- { menuItems.map((menuItem) => {
- return (
- onMenuItemClick(menuItem) }>
+ {
+ menuItems.map((menuItem) => (
+ onMenuItemClick(menuItem)}>
{menuItem.title}
- );
- })}
+ ))
+ }
);
diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx
index 51f489a537..dd7267f213 100644
--- a/src/renderer/components/hotbar/hotbar-menu.tsx
+++ b/src/renderer/components/hotbar/hotbar-menu.tsx
@@ -27,7 +27,7 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
-import { CatalogEntity, CatalogEntityContextMenu, catalogEntityRunContext } from "../../api/catalog-entity";
+import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell";
@@ -110,12 +110,6 @@ export class HotbarMenu extends React.Component {
renderGrid() {
return this.items.map((item, index) => {
const entity = this.getEntity(item);
- const disabledMenuItems: CatalogEntityContextMenu[] = [
- {
- title: "Unpin from Hotbar",
- onClick: () => this.removeItem(item.entity.uid)
- }
- ];
return (
@@ -160,10 +154,15 @@ export class HotbarMenu extends React.Component {
/>
) : (
this.removeItem(item.entity.uid)
+ }
+ ]}
disabled
size={40}
/>
diff --git a/src/renderer/components/hotbar/hotbar-remove-command.tsx b/src/renderer/components/hotbar/hotbar-remove-command.tsx
index de75bf0259..58aa1d849d 100644
--- a/src/renderer/components/hotbar/hotbar-remove-command.tsx
+++ b/src/renderer/components/hotbar/hotbar-remove-command.tsx
@@ -27,6 +27,7 @@ import { HotbarStore } from "../../../common/hotbar-store";
import { hotbarDisplayLabel } from "./hotbar-display-label";
import { CommandOverlay } from "../command-palette";
import { ConfirmDialog } from "../confirm-dialog";
+import { Notifications } from "../notifications";
@observer
export class HotbarRemoveCommand extends React.Component {
@@ -45,11 +46,18 @@ export class HotbarRemoveCommand extends React.Component {
const hotbarStore = HotbarStore.getInstance();
const hotbar = hotbarStore.getById(id);
+ CommandOverlay.close();
+
if (!hotbar) {
return;
}
- CommandOverlay.close();
+ if (hotbarStore.hotbars.length === 1) {
+ Notifications.error("Can't remove the last hotbar");
+
+ return;
+ }
+
ConfirmDialog.open({
okButtonProps: {
label: `Remove Hotbar`,
diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx
index ec541f7457..ee24b6e3fa 100644
--- a/src/renderer/components/icon/icon.tsx
+++ b/src/renderer/components/icon/icon.tsx
@@ -122,7 +122,7 @@ export class Icon extends React.PureComponent {
// render as material-icon
if (material) {
- iconContent = {material} ;
+ iconContent = {material} ;
}
// wrap icon's content passed from decorator
diff --git a/src/renderer/components/input/input.scss b/src/renderer/components/input/input.scss
index 97ad5c058a..b683f46d3a 100644
--- a/src/renderer/components/input/input.scss
+++ b/src/renderer/components/input/input.scss
@@ -136,10 +136,15 @@
border-color: var(--inputControlBorder);
color: var(--textColorTertiary);
padding: $padding;
+ transition: border-color 0.1s;
&:hover {
border-color: var(--inputControlHoverBorder);
}
+
+ &:focus-within {
+ border-color: $colorInfo;
+ }
}
}
}
diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx
index 5de07cb856..fa1692e988 100644
--- a/src/renderer/components/input/input.tsx
+++ b/src/renderer/components/input/input.tsx
@@ -261,7 +261,7 @@ export class Input extends React.Component {
switch (evt.key) {
case "Enter":
- if (this.props.onSubmit && !modified && !evt.repeat && this.isValid) {
+ if (this.props.onSubmit && !modified && !evt.repeat && this.isValid()) {
this.props.onSubmit(this.getValue(), evt);
if (this.isUncontrolled) {
diff --git a/src/renderer/components/item-object-list/page-filters-list.tsx b/src/renderer/components/item-object-list/page-filters-list.tsx
index 49d3245bf9..1bee3be8ba 100644
--- a/src/renderer/components/item-object-list/page-filters-list.tsx
+++ b/src/renderer/components/item-object-list/page-filters-list.tsx
@@ -27,6 +27,7 @@ import { cssNames } from "../../utils";
import { Filter, pageFilters } from "./page-filters.store";
import { FilterIcon } from "./filter-icon";
import { Icon } from "../icon";
+import { searchUrlParam } from "../input";
interface Props {
filters?: Filter[];
@@ -41,7 +42,10 @@ export class PageFiltersList extends React.Component {
};
reset = () => pageFilters.reset();
- remove = (filter: Filter) => pageFilters.removeFilter(filter);
+ remove = (filter: Filter) => {
+ pageFilters.removeFilter(filter);
+ searchUrlParam.clear();
+ };
renderContent() {
const { filters } = this.props;
diff --git a/src/renderer/components/item-object-list/page-filters.store.ts b/src/renderer/components/item-object-list/page-filters.store.ts
index f2d36a0da8..f4fe8ac2f4 100644
--- a/src/renderer/components/item-object-list/page-filters.store.ts
+++ b/src/renderer/components/item-object-list/page-filters.store.ts
@@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import { computed, observable, reaction, makeObservable } from "mobx";
+import { computed, observable, reaction, makeObservable, action } from "mobx";
import { autoBind } from "../../utils";
import { searchUrlParam } from "../input/search-input-url";
@@ -68,6 +68,7 @@ export class PageFiltersStore {
return () => disposers.forEach(dispose => dispose());
}
+ @action
addFilter(filter: Filter, begin = false) {
if (begin) this.filters.unshift(filter);
else {
@@ -75,6 +76,7 @@ export class PageFiltersStore {
}
}
+ @action
removeFilter(filter: Filter) {
if (!this.filters.remove(filter)) {
const filterCopy = this.filters.find(f => f.type === filter.type && f.value === filter.value);
@@ -99,19 +101,26 @@ export class PageFiltersStore {
return !this.isDisabled.get(type);
}
+ @action
disable(type: FilterType | FilterType[]) {
[type].flat().forEach(type => this.isDisabled.set(type, true));
return () => this.enable(type);
}
+ @action
enable(type: FilterType | FilterType[]) {
[type].flat().forEach(type => this.isDisabled.delete(type));
return () => this.disable(type);
}
+ @action
reset() {
+ if (this.isEnabled(FilterType.SEARCH)) {
+ searchUrlParam.clear();
+ }
+
this.filters.length = 0;
this.isDisabled.clear();
}
diff --git a/src/renderer/components/kube-object/kube-object-menu.tsx b/src/renderer/components/kube-object/kube-object-menu.tsx
index da576fcba0..f614ffc980 100644
--- a/src/renderer/components/kube-object/kube-object-menu.tsx
+++ b/src/renderer/components/kube-object/kube-object-menu.tsx
@@ -83,7 +83,9 @@ export class KubeObjectMenu extends React.Component extends React.Component extends React.Component
- {this.getMenuItems(object)}
+ {this.getMenuItems()}
);
}
diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx
index a85e90b158..34b0c1ed95 100755
--- a/src/renderer/components/layout/main-layout.tsx
+++ b/src/renderer/components/layout/main-layout.tsx
@@ -34,6 +34,11 @@ interface Props {
footer?: React.ReactNode;
}
+/**
+ * Main layout is commonly used as a wrapper for "global pages"
+ *
+ * @link https://api-docs.k8slens.dev/master/extensions/capabilities/common-capabilities/#global-pages
+ */
@observer
export class MainLayout extends React.Component {
onSidebarResize = (width: number) => {
diff --git a/src/renderer/components/layout/page-layout.tsx b/src/renderer/components/layout/page-layout.tsx
index 6978cdc218..a393603885 100644
--- a/src/renderer/components/layout/page-layout.tsx
+++ b/src/renderer/components/layout/page-layout.tsx
@@ -19,96 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import "./page-layout.scss";
+import { SettingLayout } from "./setting-layout";
-import React from "react";
-import { observer } from "mobx-react";
-import { boundMethod, cssNames, IClassName } from "../../utils";
-import { navigation } from "../../navigation";
-import { Icon } from "../icon";
-
-export interface PageLayoutProps extends React.DOMAttributes {
- className?: IClassName;
- contentClass?: IClassName;
- provideBackButtonNavigation?: boolean;
- contentGaps?: boolean;
- showOnTop?: boolean; // covers whole app view
- navigation?: React.ReactNode;
- back?: (evt: React.MouseEvent | KeyboardEvent) => void;
-}
-
-const defaultProps: Partial = {
- provideBackButtonNavigation: true,
- contentGaps: true,
-};
-
-@observer
-export class PageLayout extends React.Component {
- static defaultProps = defaultProps as object;
-
- @boundMethod
- back(evt?: React.MouseEvent | KeyboardEvent) {
- if (this.props.back) {
- this.props.back(evt);
- } else {
- navigation.goBack();
- }
- }
-
- async componentDidMount() {
- window.addEventListener("keydown", this.onEscapeKey);
- }
-
- componentWillUnmount() {
- window.removeEventListener("keydown", this.onEscapeKey);
- }
-
- onEscapeKey = (evt: KeyboardEvent) => {
- if (!this.props.provideBackButtonNavigation) {
- return;
- }
-
- if (evt.code === "Escape") {
- evt.stopPropagation();
- this.back(evt);
- }
- };
-
- render() {
- const {
- contentClass, provideBackButtonNavigation,
- contentGaps, showOnTop, navigation, children, ...elemProps
- } = this.props;
- const className = cssNames("PageLayout", { showOnTop, showNavigation: navigation }, this.props.className);
-
- return (
-
- { navigation && (
-
-
- {navigation}
-
-
- )}
-
-
- );
- }
-}
+/**
+ * PageLayout is deprecated. See MainLayout & SettingLayout
+ *
+ * @deprecated
+ */
+export class PageLayout extends SettingLayout {}
diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/setting-layout.scss
similarity index 85%
rename from src/renderer/components/layout/page-layout.scss
rename to src/renderer/components/layout/setting-layout.scss
index 8b2c30494e..3e6bfd8726 100644
--- a/src/renderer/components/layout/page-layout.scss
+++ b/src/renderer/components/layout/setting-layout.scss
@@ -19,20 +19,20 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-.PageLayout {
- --width: 75%;
- --nav-width: 180px;
+.SettingLayout {
--nav-column-width: 30vw;
-
- position: relative;
width: 100%;
height: 100%;
- display: grid !important;
+ display: grid;
color: var(--settingsColor);
-
- @include media("<1000px") {
- --width: 85%;
- }
+ position: fixed;
+ z-index: 13!important;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ height: unset;
+ background-color: var(--settingsBackground);
&.showNavigation {
grid-template-columns: var(--nav-column-width) 1fr;
@@ -42,18 +42,6 @@
}
}
- // covers whole app view area
- &.showOnTop {
- position: fixed !important; // allow to cover ClustersMenu
- z-index: 13;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- height: unset;
- background-color: var(--settingsBackground);
- }
-
> .sidebarRegion {
display: flex;
justify-content: flex-end;
@@ -65,12 +53,25 @@
padding: 60px 0 60px 20px;
h2 {
- margin-bottom: 10px;
- font-size: 18px;
- padding: 6px 10px;
+ font-size: 15px;
overflow-wrap: anywhere;
color: var(--textColorAccent);
font-weight: 600;
+ padding-right: 20px;
+ word-break: break-word;
+ }
+
+ hr {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-right: 20px;
+ height: 1px;
+ border-top: thin solid var(--hrColor);
+
+ &:first-child {
+ display: none;
+ }
}
.Tabs {
@@ -80,6 +81,7 @@
font-weight: 800;
line-height: 16px;
text-transform: uppercase;
+ color: var(--textColorPrimary);
&:first-child {
padding-top: 0;
@@ -114,9 +116,14 @@
> .label {
width: 100%;
+ font-weight: 500;
}
}
}
+
+ .HotbarIcon {
+ margin: 0 11px;
+ }
}
}
@@ -126,7 +133,9 @@
justify-content: center;
> .content {
- width: var(--width);
+ width: 100%;
+ max-width: 740px;
+ min-width: 460px;
padding: 60px 40px 80px;
> section {
@@ -159,7 +168,7 @@
}
.Icon {
- color: var(--textColorSecondary);
+ color: var(--textColorTertiary);
}
}
@@ -202,7 +211,7 @@
font-size: 18px;
line-height: 20px;
font-weight: 600;
- margin-bottom: 20px;
+ margin-bottom: 30px;
}
a {
@@ -212,6 +221,7 @@
.hint {
margin-top: 8px;
font-size: 14px;
+ line-height: 20px;
}
.SubTitle {
diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx
new file mode 100644
index 0000000000..53e54e07ac
--- /dev/null
+++ b/src/renderer/components/layout/setting-layout.tsx
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import "./setting-layout.scss";
+
+import React from "react";
+import { observer } from "mobx-react";
+import { boundMethod, cssNames, IClassName } from "../../utils";
+import { navigation } from "../../navigation";
+import { Icon } from "../icon";
+
+export interface SettingLayoutProps extends React.DOMAttributes {
+ className?: IClassName;
+ contentClass?: IClassName;
+ provideBackButtonNavigation?: boolean;
+ contentGaps?: boolean;
+ navigation?: React.ReactNode;
+ back?: (evt: React.MouseEvent | KeyboardEvent) => void;
+}
+
+const defaultProps: Partial = {
+ provideBackButtonNavigation: true,
+ contentGaps: true,
+};
+
+/**
+ * Layout for settings like pages with navigation
+ */
+@observer
+export class SettingLayout extends React.Component {
+ static defaultProps = defaultProps as object;
+
+ @boundMethod
+ back(evt?: React.MouseEvent | KeyboardEvent) {
+ if (this.props.back) {
+ this.props.back(evt);
+ } else {
+ navigation.goBack();
+ }
+ }
+
+ async componentDidMount() {
+ window.addEventListener("keydown", this.onEscapeKey);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("keydown", this.onEscapeKey);
+ }
+
+ onEscapeKey = (evt: KeyboardEvent) => {
+ if (!this.props.provideBackButtonNavigation) {
+ return;
+ }
+
+ if (evt.code === "Escape") {
+ evt.stopPropagation();
+ this.back(evt);
+ }
+ };
+
+ render() {
+ const {
+ contentClass, provideBackButtonNavigation,
+ contentGaps, navigation, children, ...elemProps
+ } = this.props;
+ const className = cssNames("SettingLayout", { showNavigation: navigation }, this.props.className);
+
+ return (
+
+ { navigation && (
+
+
+ {navigation}
+
+
+ )}
+
+
+ );
+ }
+}
diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx
index 6017c699f4..a69f226e5e 100644
--- a/src/renderer/components/menu/menu-actions.tsx
+++ b/src/renderer/components/menu/menu-actions.tsx
@@ -137,13 +137,13 @@ export class MenuActions extends React.Component {
{children}
{updateAction && (
-
+
Edit
)}
{removeAction && (
-
+
Remove
)}
diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss
index 55fddec850..84f348f4c0 100644
--- a/src/renderer/components/select/select.scss
+++ b/src/renderer/components/select/select.scss
@@ -213,6 +213,7 @@ html {
:hover {
&.Select__control {
box-shadow: 0 0 0 1px var(--inputControlHoverBorder);
+ transition: box-shadow 0.1s;
}
}
diff --git a/src/migrations/migration-wrapper.ts b/src/renderer/initializers/catalog.tsx
similarity index 68%
rename from src/migrations/migration-wrapper.ts
rename to src/renderer/initializers/catalog.tsx
index 26b17e0450..b9c56bc0c6 100644
--- a/src/migrations/migration-wrapper.ts
+++ b/src/renderer/initializers/catalog.tsx
@@ -19,24 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import type Config from "conf";
-import { isTestEnv } from "../common/vars";
+import React from "react";
+import { WebLinkCategory } from "../../common/catalog-entities";
+import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
+import { CommandOverlay } from "../components/command-palette";
-export interface MigrationOpts {
- version: string;
- run(storeConfig: Config, log: (...args: any[]) => void): void;
+function initWebLinks() {
+ WebLinkCategory.onAdd = () => CommandOverlay.open( );
}
-function infoLog(...args: any[]) {
- if (isTestEnv) return;
- console.log(...args);
-}
-
-export function migration({ version, run }: MigrationOpts) {
- return {
- [version]: (storeConfig: Config) => {
- infoLog(`STORE MIGRATION (${storeConfig.path}): ${version}`,);
- run(storeConfig, infoLog);
- }
- };
+export function initCatalog() {
+ initWebLinks();
}
diff --git a/src/renderer/initializers/entity-settings-registry.ts b/src/renderer/initializers/entity-settings-registry.ts
index e7203ca97b..10723c8ebf 100644
--- a/src/renderer/initializers/entity-settings-registry.ts
+++ b/src/renderer/initializers/entity-settings-registry.ts
@@ -30,6 +30,7 @@ export function initEntitySettingsRegistry() {
kind: "KubernetesCluster",
source: "local",
title: "General",
+ group: "Settings",
components: {
View: clusterSettings.GeneralSettings,
}
@@ -38,6 +39,7 @@ export function initEntitySettingsRegistry() {
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Proxy",
+ group: "Settings",
components: {
View: clusterSettings.ProxySettings,
}
@@ -46,6 +48,7 @@ export function initEntitySettingsRegistry() {
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Terminal",
+ group: "Settings",
components: {
View: clusterSettings.TerminalSettings,
}
@@ -54,6 +57,7 @@ export function initEntitySettingsRegistry() {
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Namespaces",
+ group: "Settings",
components: {
View: clusterSettings.NamespacesSettings,
}
@@ -62,6 +66,7 @@ export function initEntitySettingsRegistry() {
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Metrics",
+ group: "Settings",
components: {
View: clusterSettings.MetricsSettings,
}
diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts
index a5cbb49257..03f32f1a9d 100644
--- a/src/renderer/initializers/index.ts
+++ b/src/renderer/initializers/index.ts
@@ -26,3 +26,5 @@ export * from "./kube-object-menu-registry";
export * from "./registries";
export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry";
+export * from "./catalog";
+export * from "./ipc";
diff --git a/src/renderer/initializers/ipc.ts b/src/renderer/initializers/ipc.ts
new file mode 100644
index 0000000000..fedb007d9b
--- /dev/null
+++ b/src/renderer/initializers/ipc.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021 OpenLens Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import { ipcRendererOn } from "../../common/ipc";
+import { ExtensionLoader } from "../../extensions/extension-loader";
+import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
+
+export function initIpcRendererListeners() {
+ ipcRendererOn("extension:navigate", (event, extId: string, pageId ?: string, params?: Record) => {
+ ExtensionLoader.getInstance().getInstanceById(extId).navigate(pageId, params);
+ });
+}
diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts
index 08594d41c5..1d455a0bcc 100644
--- a/src/renderer/initializers/registries.ts
+++ b/src/renderer/initializers/registries.ts
@@ -32,7 +32,6 @@ export function initRegistries() {
registries.KubeObjectDetailRegistry.createInstance();
registries.KubeObjectMenuRegistry.createInstance();
registries.KubeObjectStatusRegistry.createInstance();
- registries.PageMenuRegistry.createInstance();
registries.StatusBarRegistry.createInstance();
registries.WelcomeMenuRegistry.createInstance();
registries.WorkloadsOverviewDetailRegistry.createInstance();
diff --git a/src/renderer/initializers/welcome-menu-registry.ts b/src/renderer/initializers/welcome-menu-registry.ts
index 9fce6a8c61..979522b519 100644
--- a/src/renderer/initializers/welcome-menu-registry.ts
+++ b/src/renderer/initializers/welcome-menu-registry.ts
@@ -27,9 +27,9 @@ export function initWelcomeMenuRegistry() {
WelcomeMenuRegistry.getInstance()
.add([
{
- title: "Browse Clusters",
+ title: "Browse Clusters in Catalog",
icon: "view_list",
- click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" } } ))
+ click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" } } )),
}
]);
}
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx
index 4b61205bd7..338f71c38d 100644
--- a/src/renderer/ipc/index.tsx
+++ b/src/renderer/ipc/index.tsx
@@ -25,7 +25,6 @@ import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, Upda
import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
import { isMac } from "../../common/vars";
-import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
import { ClusterStore } from "../../common/cluster-store";
import { navigate } from "../navigation";
import { entitySettingsURL } from "../../common/routes";
@@ -60,8 +59,8 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
(
Update Available
-
Version {updateInfo.version} of Lens IDE is now available. Would you like to update?
-
Manual restart required!
+
Version {updateInfo.version} of Lens IDE is available and ready to be installed. Would you like to update now?
+
Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.
sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
@@ -87,7 +86,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) {
listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now);
- } else {
+ } else {
// don't bother the user too often
return;
}
@@ -100,7 +99,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
Add Accessible Namespaces
Cluster {ClusterStore.getInstance().getById(clusterId).name} does not have permissions to list namespaces. Please add the namespaces you have access to.
-
{
+ {
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));
notificationsStore.remove(notificationId);
}} />
@@ -120,7 +119,6 @@ export function registerIpcHandlers() {
listener: UpdateAvailableHandler,
verifier: areArgsUpdateAvailableFromMain,
});
- onCorrect(invalidKubeconfigHandler);
onCorrect({
source: ipcRenderer,
channel: ClusterListNamespaceForbiddenChannel,
diff --git a/src/renderer/ipc/invalid-kubeconfig-handler.tsx b/src/renderer/ipc/invalid-kubeconfig-handler.tsx
deleted file mode 100644
index ffdd0168d7..0000000000
--- a/src/renderer/ipc/invalid-kubeconfig-handler.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Copyright (c) 2021 OpenLens Authors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- * the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-import React from "react";
-import { ipcRenderer, IpcRendererEvent, shell } from "electron";
-import { ClusterStore } from "../../common/cluster-store";
-import { InvalidKubeConfigArgs, InvalidKubeconfigChannel } from "../../common/ipc/invalid-kubeconfig";
-import { Notifications, notificationsStore } from "../components/notifications";
-import { Button } from "../components/button";
-import { productName } from "../../common/vars";
-
-export const invalidKubeconfigHandler = {
- source: ipcRenderer,
- channel: InvalidKubeconfigChannel,
- listener: InvalidKubeconfigListener,
- verifier: (args: [unknown]): args is InvalidKubeConfigArgs => {
- return args.length === 1 && typeof args[0] === "string" && !!ClusterStore.getInstance().getById(args[0]);
- },
-};
-
-function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: InvalidKubeConfigArgs): void {
- const notificationId = `invalid-kubeconfig:${clusterId}`;
- const cluster = ClusterStore.getInstance().getById(clusterId);
- const contextName = cluster.name !== cluster.contextName ? `(context: ${cluster.contextName})` : "";
-
- Notifications.error(
- (
-
-
Cluster with Invalid Kubeconfig Detected!
-
Cluster {cluster.name} has invalid kubeconfig {contextName} and cannot be displayed.
- Please fix the { e.preventDefault(); shell.showItemInFolder(cluster.kubeConfigPath); }}>kubeconfig manually and restart {productName}
- or remove the cluster.
-
Do you want to remove the cluster now?
-
- {
- ClusterStore.getInstance().removeById(clusterId);
- notificationsStore.remove(notificationId);
- }} />
- notificationsStore.remove(notificationId)} />
-
-
- ),
- {
- id: notificationId,
- timeout: 0
- }
- );
-}
diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts
index 0c9f8688a2..62dd1cb117 100644
--- a/src/renderer/kube-object.store.ts
+++ b/src/renderer/kube-object.store.ts
@@ -280,7 +280,7 @@ export abstract class KubeObjectStore extends ItemSt
async update(item: T, data: Partial): Promise {
const newItem = await item.update(data);
-
+
ensureObjectSelfLink(this.api, newItem);
const index = this.items.findIndex(item => item.getId() === newItem.getId());
@@ -332,6 +332,10 @@ export abstract class KubeObjectStore extends ItemSt
}
private watchNamespace(namespace: string, abortController: AbortController) {
+ if (!this.api.getResourceVersion(namespace)) {
+ return;
+ }
+
let timedRetry: NodeJS.Timeout;
const watch = () => this.api.watch({
namespace,
diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts
index e3c1fd71ea..6caf9cf78a 100644
--- a/src/renderer/theme.store.ts
+++ b/src/renderer/theme.store.ts
@@ -20,9 +20,11 @@
*/
import { computed, observable, reaction, makeObservable } from "mobx";
-import { autoBind, boundMethod, Singleton } from "./utils";
+import { autoBind, iter, Singleton } from "./utils";
import { UserStore } from "../common/user-store";
import logger from "../main/logger";
+import darkTheme from "./themes/lens-dark.json";
+import lightTheme from "./themes/lens-light.json";
export type ThemeId = string;
@@ -32,12 +34,15 @@ export enum ThemeType {
}
export interface Theme {
- id: ThemeId; // filename without .json-extension
type: ThemeType;
- name?: string;
- colors?: Record;
- description?: string;
- author?: string;
+ name: string;
+ colors: Record;
+ description: string;
+ author: string;
+}
+
+export interface ThemeItems extends Theme {
+ id: string;
}
export class ThemeStore extends Singleton {
@@ -45,16 +50,12 @@ export class ThemeStore extends Singleton {
// bundled themes from `themes/${themeId}.json`
private allThemes = observable.map([
- ["lens-dark", { id: "lens-dark", type: ThemeType.DARK }],
- ["lens-light", { id: "lens-light", type: ThemeType.LIGHT }],
+ ["lens-dark", { ...darkTheme, type: ThemeType.DARK }],
+ ["lens-light", { ...lightTheme, type: ThemeType.LIGHT }],
]);
- @computed get themeIds(): string[] {
- return Array.from(this.allThemes.keys());
- }
-
- @computed get themes(): Theme[] {
- return Array.from(this.allThemes.values());
+ @computed get themes(): ThemeItems[] {
+ return Array.from(iter.map(this.allThemes, ([id, theme]) => ({ id, ...theme })));
}
@computed get activeThemeId(): string {
@@ -62,12 +63,7 @@ export class ThemeStore extends Singleton {
}
@computed get activeTheme(): Theme {
- const activeTheme = this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark");
-
- return {
- colors: {},
- ...activeTheme,
- };
+ return this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark");
}
constructor() {
@@ -77,9 +73,9 @@ export class ThemeStore extends Singleton {
autoBind(this);
// auto-apply active theme
- reaction(() => this.activeThemeId, async themeId => {
+ reaction(() => this.activeThemeId, themeId => {
try {
- this.applyTheme(await this.loadTheme(themeId));
+ this.applyTheme(this.getThemeById(themeId));
} catch (err) {
logger.error(err);
UserStore.getInstance().resetTheme();
@@ -89,38 +85,10 @@ export class ThemeStore extends Singleton {
});
}
- async init() {
- // preload all themes
- await Promise.all(this.themeIds.map(this.loadTheme));
- }
-
getThemeById(themeId: ThemeId): Theme {
return this.allThemes.get(themeId);
}
- @boundMethod
- protected async loadTheme(themeId: ThemeId): Promise {
- try {
- const existingTheme = this.getThemeById(themeId);
-
- if (existingTheme) {
- const theme = await import(
- /* webpackChunkName: "themes/[name]" */
- `./themes/${themeId}.json`
- );
-
- existingTheme.author = theme.author;
- existingTheme.colors = theme.colors;
- existingTheme.description = theme.description;
- existingTheme.name = theme.name;
- }
-
- return existingTheme;
- } catch (err) {
- throw new Error(`Can't load theme "${themeId}": ${err}`);
- }
- }
-
protected applyTheme(theme: Theme) {
if (!this.styles) {
this.styles = document.createElement("style");
diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json
index 5084b63c52..8893fe27ac 100644
--- a/src/renderer/themes/lens-light.json
+++ b/src/renderer/themes/lens-light.json
@@ -12,7 +12,7 @@
"textColorPrimary": "#555555",
"textColorSecondary": "#51575d",
"textColorTertiary": "#555555",
- "textColorAccent": "#333333",
+ "textColorAccent": "#222222",
"textColorDimmed": "#5557598c",
"borderColor": "#c9cfd3",
"borderFaintColor": "#dfdfdf",
diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts
index f432cc850e..0e1452ac80 100755
--- a/src/renderer/utils/createStorage.ts
+++ b/src/renderer/utils/createStorage.ts
@@ -41,8 +41,11 @@ const storage = observable({
* @param defaultValue
*/
export function createStorage(key: string, defaultValue: T) {
+ return createAppStorage(key, defaultValue, getHostedClusterId());
+}
+
+export function createAppStorage(key: string, defaultValue: T, clusterId?: string | undefined) {
const { logPrefix } = StorageHelper;
- const clusterId = getHostedClusterId();
const folder = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage");
const fileName = `${clusterId ?? "app"}.json`;
const filePath = path.resolve(folder, fileName);
diff --git a/yarn.lock b/yarn.lock
index 100f25bacf..afe4766f18 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2620,13 +2620,15 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
-array-includes@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
- integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+array-includes@^3.1.1, array-includes@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
+ integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==
dependencies:
+ call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.17.0"
+ es-abstract "^1.18.0-next.2"
+ get-intrinsic "^1.1.1"
is-string "^1.0.5"
array-move@^3.0.1:
@@ -2656,7 +2658,7 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-array.prototype.flatmap@^1.2.3:
+array.prototype.flatmap@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==
@@ -3413,6 +3415,14 @@ call-bind@^1.0.0:
function-bind "^1.1.1"
get-intrinsic "^1.0.0"
+call-bind@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
call-limit@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4"
@@ -5363,23 +5373,6 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.1.1"
-es-abstract@^1.17.0:
- version "1.17.7"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
- integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
- dependencies:
- es-to-primitive "^1.2.1"
- function-bind "^1.1.1"
- has "^1.0.3"
- has-symbols "^1.0.1"
- is-callable "^1.2.2"
- is-regex "^1.1.1"
- object-inspect "^1.8.0"
- object-keys "^1.1.1"
- object.assign "^4.1.1"
- string.prototype.trimend "^1.0.1"
- string.prototype.trimstart "^1.0.1"
-
es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
version "1.17.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
@@ -5397,7 +5390,7 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
-es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
+es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
@@ -5415,6 +5408,28 @@ es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
+es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
+ version "1.18.3"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
+ integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
+ dependencies:
+ call-bind "^1.0.2"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ get-intrinsic "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.2"
+ is-callable "^1.2.3"
+ is-negative-zero "^2.0.1"
+ is-regex "^1.1.3"
+ is-string "^1.0.6"
+ object-inspect "^1.10.3"
+ object-keys "^1.1.1"
+ object.assign "^4.1.2"
+ string.prototype.trimend "^1.0.4"
+ string.prototype.trimstart "^1.0.4"
+ unbox-primitive "^1.0.1"
+
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -5493,22 +5508,23 @@ eslint-plugin-react-hooks@^4.2.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
-eslint-plugin-react@^7.21.5:
- version "7.21.5"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz#50b21a412b9574bfe05b21db176e8b7b3b15bff3"
- integrity sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==
+eslint-plugin-react@^7.24.0:
+ version "7.24.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz#eadedfa351a6f36b490aa17f4fa9b14e842b9eb4"
+ integrity sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==
dependencies:
- array-includes "^3.1.1"
- array.prototype.flatmap "^1.2.3"
+ array-includes "^3.1.3"
+ array.prototype.flatmap "^1.2.4"
doctrine "^2.1.0"
has "^1.0.3"
jsx-ast-utils "^2.4.1 || ^3.0.0"
- object.entries "^1.1.2"
- object.fromentries "^2.0.2"
- object.values "^1.1.1"
+ minimatch "^3.0.4"
+ object.entries "^1.1.4"
+ object.fromentries "^2.0.4"
+ object.values "^1.1.4"
prop-types "^15.7.2"
- resolve "^1.18.1"
- string.prototype.matchall "^4.0.2"
+ resolve "^2.0.0-next.3"
+ string.prototype.matchall "^4.0.5"
eslint-plugin-unused-imports@^1.0.1:
version "1.0.1"
@@ -6518,6 +6534,15 @@ get-intrinsic@^1.0.0:
has "^1.0.3"
has-symbols "^1.0.1"
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+ integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -6788,12 +6813,7 @@ got@^9.6.0:
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
-graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4:
- version "4.2.4"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
- integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
-
-graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2:
+graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
@@ -6855,6 +6875,11 @@ has-ansi@^2.0.0:
dependencies:
ansi-regex "^2.0.0"
+has-bigints@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+ integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+
has-flag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
@@ -6875,6 +6900,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+ integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
has-unicode@^2.0.0, has-unicode@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -7475,14 +7505,14 @@ internal-ip@^4.3.0:
default-gateway "^4.2.0"
ipaddr.js "^1.9.0"
-internal-slot@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3"
- integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==
+internal-slot@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+ integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
dependencies:
- es-abstract "^1.17.0-next.1"
+ get-intrinsic "^1.1.0"
has "^1.0.3"
- side-channel "^1.0.2"
+ side-channel "^1.0.4"
interpret@1.2.0:
version "1.2.0"
@@ -7553,6 +7583,11 @@ is-arrayish@^0.3.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+is-bigint@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
+ integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==
+
is-binary-path@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@@ -7567,6 +7602,13 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
+is-boolean-object@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8"
+ integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==
+ dependencies:
+ call-bind "^1.0.2"
+
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -7582,6 +7624,11 @@ is-callable@^1.2.2:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
+is-callable@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
+ integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
+
is-ci@^1.0.10:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
@@ -7603,13 +7650,6 @@ is-cidr@^3.0.0:
dependencies:
cidr-regex "^2.0.10"
-is-core-module@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946"
- integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==
- dependencies:
- has "^1.0.3"
-
is-core-module@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
@@ -7778,6 +7818,11 @@ is-negative-zero@^2.0.0:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+is-negative-zero@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+ integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+
is-npm@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -7793,6 +7838,11 @@ is-npm@^5.0.0:
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8"
integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==
+is-number-object@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb"
+ integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -7882,6 +7932,14 @@ is-regex@^1.0.5:
dependencies:
has-symbols "^1.0.1"
+is-regex@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
+ integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
+ dependencies:
+ call-bind "^1.0.2"
+ has-symbols "^1.0.2"
+
is-retry-allowed@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
@@ -7902,6 +7960,11 @@ is-string@^1.0.5:
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+is-string@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
+ integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
+
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@@ -7909,6 +7972,13 @@ is-symbol@^1.0.2:
dependencies:
has-symbols "^1.0.1"
+is-symbol@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+ integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+ dependencies:
+ has-symbols "^1.0.2"
+
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -10544,6 +10614,11 @@ object-hash@^2.1.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09"
integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==
+object-inspect@^1.10.3, object-inspect@^1.9.0:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
+ integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
+
object-inspect@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
@@ -10584,7 +10659,7 @@ object.assign@^4.1.0:
has-symbols "^1.0.0"
object-keys "^1.0.11"
-object.assign@^4.1.1:
+object.assign@^4.1.1, object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -10594,23 +10669,23 @@ object.assign@^4.1.1:
has-symbols "^1.0.1"
object-keys "^1.1.1"
-object.entries@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
- integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
+object.entries@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd"
+ integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==
dependencies:
+ call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.17.5"
- has "^1.0.3"
+ es-abstract "^1.18.2"
-object.fromentries@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
- integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
+object.fromentries@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8"
+ integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==
dependencies:
+ call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.17.0-next.1"
- function-bind "^1.1.1"
+ es-abstract "^1.18.0-next.2"
has "^1.0.3"
object.getownpropertydescriptors@^2.0.3:
@@ -10628,15 +10703,14 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
- integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+object.values@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
+ integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
dependencies:
+ call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.17.0-next.1"
- function-bind "^1.1.1"
- has "^1.0.3"
+ es-abstract "^1.18.2"
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
@@ -11640,12 +11714,12 @@ proper-lockfile@^1.2.0:
graceful-fs "^4.1.2"
retry "^0.10.0"
-proper-lockfile@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.1.tgz#284cf9db9e30a90e647afad69deb7cb06881262c"
- integrity sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg==
+proper-lockfile@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
+ integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==
dependencies:
- graceful-fs "^4.1.11"
+ graceful-fs "^4.2.4"
retry "^0.12.0"
signal-exit "^3.0.2"
@@ -12271,7 +12345,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
-regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
+regexp.prototype.flags@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
@@ -12279,6 +12353,14 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
+regexp.prototype.flags@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
+ integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+
regexpp@^3.0.0, regexpp@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
@@ -12475,22 +12557,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.3.2:
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
- integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
- dependencies:
- path-parse "^1.0.6"
-
-resolve@^1.18.1:
- version "1.19.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
- integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
- dependencies:
- is-core-module "^2.1.0"
- path-parse "^1.0.6"
-
-resolve@^1.20.0:
+resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.3.2:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -12498,6 +12565,14 @@ resolve@^1.20.0:
is-core-module "^2.2.0"
path-parse "^1.0.6"
+resolve@^2.0.0-next.3:
+ version "2.0.0-next.3"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
+ integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==
+ dependencies:
+ is-core-module "^2.2.0"
+ path-parse "^1.0.6"
+
responselike@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
@@ -13000,13 +13075,14 @@ shiki@^0.9.3:
onigasm "^2.2.5"
vscode-textmate "^5.2.0"
-side-channel@^1.0.2, side-channel@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
- integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
- es-abstract "^1.18.0-next.0"
- object-inspect "^1.8.0"
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
@@ -13492,18 +13568,19 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
-string.prototype.matchall@^4.0.2:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz#24243399bc31b0a49d19e2b74171a15653ec996a"
- integrity sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==
+string.prototype.matchall@^4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz#59370644e1db7e4c0c045277690cf7b01203c4da"
+ integrity sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==
dependencies:
- call-bind "^1.0.0"
+ call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.0-next.1"
- has-symbols "^1.0.1"
- internal-slot "^1.0.2"
- regexp.prototype.flags "^1.3.0"
- side-channel "^1.0.3"
+ es-abstract "^1.18.2"
+ get-intrinsic "^1.1.1"
+ has-symbols "^1.0.2"
+ internal-slot "^1.0.3"
+ regexp.prototype.flags "^1.3.1"
+ side-channel "^1.0.4"
string.prototype.trimend@^1.0.0:
version "1.0.1"
@@ -13521,6 +13598,14 @@ string.prototype.trimend@^1.0.1:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
+string.prototype.trimend@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
+ integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+
string.prototype.trimleft@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
@@ -13555,6 +13640,14 @@ string.prototype.trimstart@^1.0.1:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
+string.prototype.trimstart@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
+ integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+
string_decoder@^1.0.0, string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -14364,10 +14457,10 @@ typedoc@0.21.0-beta.2:
shiki "^0.9.3"
typedoc-default-themes "^0.12.10"
-typeface-roboto@^0.0.75:
- version "0.0.75"
- resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
- integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
+typeface-roboto@^1.1.13:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5"
+ integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==
typescript-plugin-css-modules@^3.2.0:
version "3.2.0"
@@ -14415,6 +14508,16 @@ umask@^1.1.0, umask@~1.1.0:
resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
+unbox-primitive@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
+ integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+ dependencies:
+ function-bind "^1.1.1"
+ has-bigints "^1.0.1"
+ has-symbols "^1.0.2"
+ which-boxed-primitive "^1.0.2"
+
undefsafe@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
@@ -15049,6 +15152,17 @@ whatwg-url@^8.0.0:
tr46 "^2.0.2"
webidl-conversions "^5.0.0"
+which-boxed-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+ integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+ dependencies:
+ is-bigint "^1.0.1"
+ is-boolean-object "^1.1.0"
+ is-number-object "^1.0.4"
+ is-string "^1.0.5"
+ is-symbol "^1.0.3"
+
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"