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

Merge remote-tracking branch 'origin/master' into extension_install_1277

# Conflicts:
#	src/renderer/components/+extensions/extensions.tsx
#	src/renderer/utils/downloadFile.ts
This commit is contained in:
Roman 2020-11-24 15:40:12 +02:00
commit e460b3f532
55 changed files with 1029 additions and 266 deletions

View File

@ -30,10 +30,9 @@ jobs:
displayName: Install Node.js displayName: Install Node.js
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: yarn | $(Agent.OS) | yarn.lock key: 'yarn | "$(Agent.OS)"" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
yarn
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- script: make node_modules - script: make node_modules
@ -70,10 +69,9 @@ jobs:
displayName: Install Node.js displayName: Install Node.js
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: yarn | $(Agent.OS) | yarn.lock key: 'yarn | "$(Agent.OS)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
yarn
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- script: make node_modules - script: make node_modules
@ -116,10 +114,9 @@ jobs:
displayName: Install Node.js displayName: Install Node.js
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: yarn | $(Agent.OS) | yarn.lock key: 'yarn | "$(Agent.OS)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
yarn
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- script: make node_modules - script: make node_modules

View File

@ -26,6 +26,7 @@ module.exports = {
}], }],
"no-unused-vars": "off", "no-unused-vars": "off",
"semi": ["error", "always"], "semi": ["error", "always"],
"object-shorthand": "error",
} }
}, },
{ {
@ -58,6 +59,7 @@ module.exports = {
}], }],
"semi": "off", "semi": "off",
"@typescript-eslint/semi": ["error"], "@typescript-eslint/semi": ["error"],
"object-shorthand": "error",
}, },
}, },
{ {
@ -90,6 +92,7 @@ module.exports = {
}], }],
"semi": "off", "semi": "off",
"@typescript-eslint/semi": ["error"], "@typescript-eslint/semi": ["error"],
"object-shorthand": "error",
}, },
} }
] ]

View File

@ -20,7 +20,7 @@ export default class ExampleExtensionMain extends LensMainExtension {
} }
``` ```
There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By overriding `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled. Note that to see standard output from the main process there must be a console connected to it. This is typically achieved by starting Lens from the command prompt. There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By implementing `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled. Note that to see standard output from the main process there must be a console connected to it. This is typically achieved by starting Lens from the command prompt.
The following example is a little more interesting in that it accesses some Lens state data and periodically logs the name of the currently active cluster in Lens. The following example is a little more interesting in that it accesses some Lens state data and periodically logs the name of the currently active cluster in Lens.

View File

@ -1 +1,425 @@
# Renderer Extension # Renderer Extension
The renderer extension api is the interface to Lens' renderer process (Lens runs in main and renderer processes). It allows you to access, configure, and customize Lens data, add custom Lens UI elements, and generally run custom code in Lens' renderer process. The custom Lens UI elements that can be added include global pages, cluster pages, cluster page menus, cluster features, app preferences, status bar items, KubeObject menu items, and KubeObject details items. These UI elements are based on React components.
## `LensRendererExtension` Class
To create a renderer extension simply extend the `LensRendererExtension` class:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
export default class ExampleExtensionMain extends LensRendererExtension {
onActivate() {
console.log('custom renderer process extension code started');
}
onDeactivate() {
console.log('custom renderer process extension de-activated');
}
}
```
There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By implementing `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled.
### `clusterPages`
Cluster pages appear as part of the cluster dashboard. They are accessible from the side bar, and are shown in the menu list after *Custom Resources*. It is conventional to use a cluster page to show information or provide functionality pertaining to the active cluster, along with custom data and functionality your extension may have. However, it is not limited to the active cluster. Also, your extension can gain access to the Kubernetes resources in the active cluster in a straightforward manner using the [`clusterStore`](../stores#clusterstore).
The following example adds a cluster page definition to a `LensRendererExtension` subclass:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
}
];
}
```
Cluster pages are objects matching the `PageRegistration` interface. The `id` field identiifies the page, and at its simplest is just a string identifier, as shown in the example above. The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()). The `components` field matches the `PageComponents` interface for wich there is one field, `Page`. `Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page. For the example above `ExamplePage` can be defined in `page.tsx`:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import React from "react"
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
render() {
return (
<div>
<p>Hello world!</p>
</div>
)
}
}
```
Note that the `ExamplePage` class defines a property named `extension`. This allows the `ExampleExtension` object to be passed in React-style in the cluster page definition, so that `ExamplePage` can access any `ExampleExtension` subclass data.
### `clusterPageMenus`
The above example code shows how to create a cluster page but not how to make it available to the Lens user. Cluster pages are typically made available through a menu item in the cluster dashboard sidebar. Expanding on the above example a cluster page menu is added to the `ExampleExtension` definition:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
}
];
clusterPageMenus = [
{
target: { pageId: "hello" },
title: "Hello World",
components: {
Icon: ExampleIcon,
}
},
];
}
```
Cluster page menus are objects matching the `ClusterPageMenuRegistration` interface. They define the appearance of the cluster page menu item in the cluster dashboard sidebar and the behaviour when the cluster page menu item is activated (typically by a mouse click). The example above uses the `target` field to set the behaviour as a link to the cluster page with `id` of `"hello"`. This is done by setting `target`'s `pageId` field to `"hello"`. The cluster page menu item's appearance is defined by setting the `title` field to the text that is to be displayed in the cluster dashboard sidebar. The `components` field is used to set an icon that appears to the left of the `title` text in the sidebar. Thus when the `"Hello World"` menu item is activated the cluster dashboard will show the contents of `ExamplePage`. This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`:
``` typescript
import { LensRendererExtension, Component } from "@k8slens/extensions";
import React from "react"
export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={"Hi!"}/>
}
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
render() {
return (
<div>
<p>Hello world!</p>
</div>
)
}
}
```
`ExampleIcon` introduces one of Lens' built-in components available to extension developers, the `Component.Icon`. Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/). One can be selected by name via the `material` field. `ExampleIcon` also sets a tooltip, shown when the Lens user hovers over the icon with a mouse, by setting the `tooltip` field.
A cluster page menu can also be used to define a foldout submenu in the cluster dashboard sidebar. This enables the grouping of cluster pages. The following example shows how to specify a submenu having two menu items:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
},
{
id: "bonjour",
components: {
Page: () => <ExemplePage extension={this}/>,
}
}
];
clusterPageMenus = [
{
id: "example",
title: "Greetings",
components: {
Icon: ExampleIcon,
}
},
{
parentId: "example",
target: { pageId: "hello" },
title: "Hello World",
components: {
Icon: ExampleIcon,
}
},
{
parentId: "example",
target: { pageId: "bonjour" },
title: "Bonjour le monde",
components: {
Icon: ExempleIcon,
}
}
];
}
```
The above defines two cluster pages and three cluster page menu objects. The cluster page definitons are straightforward. The first cluster page menu object defines the parent of a foldout submenu. Setting the `id` field in a cluster page menu definition implies that it is defining a foldout submenu. Also note that the `target` field is not specified (it is ignored if the `id` field is specified). This cluster page menu object specifies the `title` and `components` fields, which are used in displaying the menu item in the cluster dashboard sidebar. Initially the submenu is hidden. Activating this menu item toggles on and off the appearance of the submenu below it. The remaining two cluster page menu objects define the contents of the submenu. A cluster page menu object is defined to be a submenu item by setting the `parentId` field to the id of the parent of a foldout submenu, `"example"` in this case
### `globalPages`
Global pages appear independently of the cluster dashboard and they fill the Lens UI space. A global page is typically triggered from the cluster menu using a [global page menu](#globalpagemenus). They can also be triggered by a [custom app menu selection](../main-extension#appmenus) from a Main Extension or a [custom status bar item](#statusbaritems). Global pages can appear even when there is no active cluster, unlike cluster pages. It is conventional to use a global page to show information and provide functionality relevant across clusters, along with custom data and functionality that your extension may have.
The following example defines a `LensRendererExtension` subclass with a single global page definition:
``` typescript
import { LensRendererExtension } from '@k8slens/extensions';
import { HelpPage } from './page';
import React from 'react';
export default class HelpExtension extends LensRendererExtension {
globalPages = [
{
id: "help",
components: {
Page: () => <HelpPage extension={this}/>,
}
}
];
}
```
Global pages are objects matching the `PageRegistration` interface. The `id` field identiifies the page, and at its simplest is just a string identifier, as shown in the example above. The 'id' field can also convey route path details, such as variable parameters provided to a page ([See example below]()). The `components` field matches the `PageComponents` interface for which there is one field, `Page`. `Page` is of type ` React.ComponentType<any>`, which gives you great flexibility in defining the appearance and behaviour of your page. For the example above `HelpPage` can be defined in `page.tsx`:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import React from "react"
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
render() {
return (
<div>
<p>Help yourself</p>
</div>
)
}
}
```
Note that the `HelpPage` class defines a property named `extension`. This allows the `HelpExtension` object to be passed in React-style in the global page definition, so that `HelpPage` can access any `HelpExtension` subclass data.
This example code shows how to create a global page but not how to make it available to the Lens user. Global pages are typically made available through a number of ways. Menu items can be added to the Lens app menu system and set to open a global page when activated (See [`appMenus` in the Main Extension guide](../main-extension#appmenus)). Interactive elements can be placed on the status bar (the blue strip along the bottom of the Lens UI) and can be configured to link to a global page when activated (See [`statusBarItems`](#statusbaritems)). As well, global pages can be made accessible from the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon. Global page menu icons that are defined using [`globalPageMenus`](#globalpagemenus) appear below the Add Cluster icon.
### `globalPageMenus`
Global page menus connect a global page to the cluster menu, which is the vertical strip along the left side of the Lens UI showing the available cluster icons, and the Add Cluster icon. Expanding on the example from [`globalPages`](#globalPages) a global page menu is added to the `HelpExtension` definition:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { HelpIcon, HelpPage } from "./page"
import React from "react"
export default class HelpExtension extends LensRendererExtension {
clusterPages = [
{
id: "help",
components: {
Page: () => <HelpPage extension={this}/>,
}
}
];
globalPageMenus = [
{
target: { pageId: "help" },
title: "Help",
components: {
Icon: HelpIcon,
}
},
];
}
```
Global page menus are objects matching the `PageMenuRegistration` interface. They define the appearance of the global page menu item in the cluster menu and the behaviour when the global page menu item is activated (typically by a mouse click). The example above uses the `target` field to set the behaviour as a link to the global page with `id` of `"help"`. This is done by setting `target`'s `pageId` field to `"help"`. The global page menu item's appearance is defined by setting the `title` field to the text that is to be displayed as a tooltip in the cluster menu. The `components` field is used to set an icon that appears in the cluster menu. Thus when the `"Help"` icon is activated the contents of `ExamplePage` will be shown. This example requires the definition of another React-based component, `HelpIcon`, which has been added to `page.tsx`:
``` typescript
import { LensRendererExtension, Component } from "@k8slens/extensions";
import React from "react"
export function HelpIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="help"/>
}
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
render() {
return (
<div>
<p>Help</p>
</div>
)
}
}
```
`HelpIcon` introduces one of Lens' built-in components available to extension developers, the `Component.Icon`. Built in are the [Material Design](https://material.io) [icons](https://material.io/resources/icons/). One can be selected by name via the `material` field.
*********************************************************************
WIP below!
*********************************************************************
### `clusterFeatures`
Cluster features are Kubernetes resources that can applied and managed to the active cluster. They can be installed/uninstalled from the [cluster settings page]().
The following example shows how to add a cluster feature:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions"
import { MetricsFeature } from "./src/metrics-feature"
import React from "react"
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
clusterFeatures = [
{
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span>
)
}
},
feature: new MetricsFeature()
}
];
}
```
The `title` and `components.Description` fields appear on the cluster settings page. The cluster feature must extend the abstract class `ClusterFeature.Feature`, and specifically implement the following methods:
``` typescript
abstract install(cluster: Cluster): Promise<void>;
abstract upgrade(cluster: Cluster): Promise<void>;
abstract uninstall(cluster: Cluster): Promise<void>;
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
```
### `appPreferences`
The Preferences page is essentially a global page. Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global, for Lens and extensions alike.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions"
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
export default class ExampleRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "My Custom Preference",
components: {
Hint: () => <MyCustomPreferenceHint/>,
Input: () => <MyCustomPreferenceInput store={myCustomPreferencesStore}/>
}
}
];
}
```
### `statusBarItems`
The Status bar is the blue strip along the bottom of the Lens UI. Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page.
The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and configures the status bar item to navigate to the global upon a mouse click:
``` typescript
import { LensRendererExtension, Navigation } from '@k8slens/extensions';
import { MyStatusBarIcon, MyPage } from './page';
import React from 'react';
export default class ExtensionRenderer extends LensRendererExtension {
globalPages = [
{
path: "/my-extension-path",
hideInMenu: true,
components: {
Page: () => <MyPage extension={this} />,
},
},
];
statusBarItems = [
{
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(this.globalPages[0].path)}
>
<MyStatusBarIcon />
<span>My Status Bar Item</span>
</div>
),
},
];
}
```
### `kubeObjectMenuItems`
An extension can add custom menu items (including actions) for specified Kubernetes resource kinds/apiVersions. These menu items appear under the `...` for each listed resource, and on the title bar of the details page for a specific resource.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions";
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
export default class ExampleExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props: CustomMenuItemProps) => <CustomMenuItem {...props} />
}
}
];
}
```
### `kubeObjectDetailItems`
An extension can add custom details (content) for specified Kubernetes resource kinds/apiVersions. These custom details appear on the details page for a specific resource.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions";
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
export default class ExampleExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "CustomKind",
apiVersions: ["custom.acme.org/v1"],
components: {
Details: (props: CustomKindDetailsProps) => <CustomKindDetails {...props} />
}
}
];
}
```

View File

@ -6,7 +6,7 @@ export default class LicenseLensMainExtension extends LensMainExtension {
parentId: "help", parentId: "help",
label: "License", label: "License",
async click() { async click() {
Util.openExternal("https://k8slens.dev/licenses/eula.md"); Util.openExternal("https://k8slens.dev/licenses/eula");
} }
} }
]; ];

View File

@ -216,10 +216,13 @@
"@types/react-beautiful-dnd": "^13.0.0", "@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.4", "@types/tar": "^4.0.4",
"array-move": "^3.0.0", "array-move": "^3.0.0",
"await-lock": "^2.1.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"chokidar": "^3.4.3",
"command-exists": "1.2.9", "command-exists": "1.2.9",
"conf": "^7.0.1", "conf": "^7.0.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"electron-devtools-installer": "^3.1.1",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"file-type": "^14.7.1", "file-type": "^14.7.1",
@ -279,6 +282,7 @@
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/crypto-js": "^3.1.47", "@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.0.2", "@types/dompurify": "^2.0.2",
"@types/electron-devtools-installer": "^2.2.0",
"@types/electron-window-state": "^2.0.34", "@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/hapi": "^18.0.3", "@types/hapi": "^18.0.3",

View File

@ -84,7 +84,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: migrations, migrations,
}); });
this.pushStateToViewsAutomatically(); this.pushStateToViewsAutomatically();

View File

@ -36,7 +36,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
private constructor() { private constructor() {
super({ super({
// configName: "lens-user-store", // todo: migrate from default "config.json" // configName: "lens-user-store", // todo: migrate from default "config.json"
migrations: migrations, migrations,
}); });
this.handleOnLoad(); this.handleOnLoad();

View File

@ -0,0 +1,329 @@
import chokidar from "chokidar";
import { EventEmitter } from "events";
import fs from "fs-extra";
import os from "os";
import path from "path";
import { getBundledExtensions } from "../common/utils/app-version";
import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
export interface InstalledExtension {
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
const logModule = "[EXTENSION-DISCOVERY]";
const manifestFilename = "package.json";
/**
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
* @param lstat the stats to compare
*/
const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
/**
* Discovers installed bundled and local extensions from the filesystem.
* Also watches for added and removed local extensions by watching the directory.
* Uses ExtensionInstaller to install dependencies for all of the extensions.
* This is also done when a new extension is copied to the local extensions directory.
* .init() must be called to start the directory watching.
* The class emits events for added and removed extensions:
* - "add": When extension is added. The event is of type InstalledExtension
* - "remove": When extension is removed. The event is of type LensExtensionId
*/
export class ExtensionDiscovery {
protected bundledFolderPath: string;
private loadStarted = false;
// This promise is resolved when .load() is finished.
// This allows operations to be added after .load() success.
private loaded: Promise<void>;
// These are called to either resolve or reject this.loaded promise
private resolveLoaded: () => void;
private rejectLoaded: (error: any) => void;
public events: EventEmitter;
constructor() {
this.loaded = new Promise((resolve, reject) => {
this.resolveLoaded = resolve;
this.rejectLoaded = reject;
});
this.events = new EventEmitter();
}
// Each extension is added as a single dependency to this object, which is written as package.json.
// Each dependency key is the name of the dependency, and
// each dependency value is the non-symlinked path to the dependency (folder).
protected packagesJson: PackageJson = {
dependencies: {}
};
get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions");
}
get packageJsonPath() {
return path.join(extensionInstaller.extensionPackagesRoot, manifestFilename);
}
get inTreeTargetPath() {
return path.join(extensionInstaller.extensionPackagesRoot, "extensions");
}
get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions");
}
get nodeModulesPath(): string {
return path.join(extensionInstaller.extensionPackagesRoot, "node_modules");
}
/**
* Initializes the class and setups the file watcher for added/removed local extensions.
*/
init() {
this.watchExtensions();
}
/**
* Watches for added/removed local extensions.
* Dependencies are installed automatically after an extension folder is copied.
*/
async watchExtensions() {
logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`);
// Wait until .load() has been called and has been resolved
await this.loaded;
// chokidar works better than fs.watch
chokidar.watch(this.localFolderPath, {
// Dont watch recursively into subdirectories
depth: 0,
// Try to wait until the file has been completely copied.
// The OS might emit an event for added file even it's not completely written to the filesysten.
awaitWriteFinish: {
// Wait 300ms until the file size doesn't change to consider the file written.
// For a small file like package.json this should be plenty of time.
stabilityThreshold: 300
}
})
// Extension add is detected by watching "<extensionDir>package.json" add
.on("add", this.handleWatchFileAdd)
// Extension remove is detected by watching <extensionDir>" unlink
.on("unlinkDir", this.handleWatchUnlinkDir);
}
handleWatchFileAdd = async (filePath: string) => {
if (path.basename(filePath) === manifestFilename) {
try {
const absPath = path.dirname(filePath);
// this.loadExtensionFromPath updates this.packagesJson
const extension = await this.loadExtensionFromPath(absPath);
if (extension) {
// Install dependencies for the new extension
await this.installPackages();
logger.info(`${logModule} Added extension ${extension.manifest.name}`);
this.events.emit("add", extension);
}
} catch (error) {
console.error(error);
}
}
};
handleWatchUnlinkDir = async (filePath: string) => {
// filePath is the non-symlinked path to the extension folder
// this.packagesJson.dependencies value is the non-symlinked path to the extension folder
// LensExtensionId in extension-loader is the symlinked path to the extension folder manifest file
// Check that the removed path is directly under this.localFolderPath
// Note that the watcher can create unlink events for subdirectories of the extension
const extensionFolderName = path.basename(filePath);
if (path.relative(this.localFolderPath, filePath) === extensionFolderName) {
const extensionName: string | undefined = Object
.entries(this.packagesJson.dependencies)
.find(([_name, extensionFolder]) => filePath === extensionFolder)?.[0];
if (extensionName !== undefined) {
delete this.packagesJson.dependencies[extensionName];
// Reinstall dependencies to remove the extension from package.json
await this.installPackages();
// The path to the manifest file is the lens extension id
// Note that we need to use the symlinked path
const lensExtensionId = path.join(this.nodeModulesPath, extensionName, "package.json");
logger.info(`${logModule} removed extension ${extensionName}`);
this.events.emit("remove", lensExtensionId as LensExtensionId);
} else {
logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`);
}
}
};
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
if (this.loadStarted) {
// The class is simplified by only supporting .load() to be called once
throw new Error("ExtensionDiscovery.load() can be only be called once");
}
this.loadStarted = true;
try {
logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`);
if (fs.existsSync(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
}
try {
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
this.bundledFolderPath = this.inTreeFolderPath;
} catch {
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
await fs.remove(this.inTreeTargetPath);
await fs.ensureDir(this.inTreeTargetPath);
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
this.bundledFolderPath = this.inTreeTargetPath;
}
await fs.ensureDir(this.nodeModulesPath);
await fs.ensureDir(this.localFolderPath);
const extensions = await this.loadExtensions();
// resolve the loaded promise
this.resolveLoaded();
return extensions;
} catch (error) {
this.rejectLoaded(error);
}
}
protected async getByManifest(manifestPath: string, { isBundled = false, isEnabled = isBundled }: {
isBundled?: boolean;
isEnabled?: boolean;
} = {}): Promise<InstalledExtension | null> {
let manifestJson: LensExtensionManifest;
try {
// check manifest file for existence
fs.accessSync(manifestPath, fs.constants.F_OK);
manifestJson = __non_webpack_require__(manifestPath);
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson,
isBundled,
isEnabled,
};
} catch (error) {
logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
return null;
}
}
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
const bundledExtensions = await this.loadBundledExtensions();
const localExtensions = await this.loadFromFolder(this.localFolderPath);
await this.installPackages();
const extensions = bundledExtensions.concat(localExtensions);
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
}
/**
* Write package.json to file system and install dependencies.
*/
installPackages() {
return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson);
}
async loadBundledExtensions() {
const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) {
continue;
}
const absPath = path.resolve(folderPath, fileName);
const extension = await this.loadExtensionFromPath(absPath, { isBundled: true });
if (extension) {
extensions.push(extension);
}
}
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions();
const extensions: InstalledExtension[] = [];
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
// do not allow to override bundled extensions
if (bundledExtensions.includes(fileName)) {
continue;
}
const absPath = path.resolve(folderPath, fileName);
if (!fs.existsSync(absPath)) {
continue;
}
const lstat = await fs.lstat(absPath);
// skip non-directories
if (!isDirectoryLike(lstat)) {
continue;
}
const extension = await this.loadExtensionFromPath(absPath);
if (extension) {
extensions.push(extension);
}
}
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
/**
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
*/
async loadExtensionFromPath(absPath: string, { isBundled = false, isEnabled = isBundled }: {
isBundled?: boolean;
isEnabled?: boolean;
} = {}): Promise<InstalledExtension | null> {
const manifestPath = path.resolve(absPath, manifestFilename);
return this.getByManifest(manifestPath, { isBundled, isEnabled });
}
}
export const extensionDiscovery = new ExtensionDiscovery();

View File

@ -0,0 +1,69 @@
import AwaitLock from 'await-lock';
import child_process from "child_process";
import fs from "fs-extra";
import path from "path";
import logger from "../main/logger";
import { extensionPackagesRoot } from "./extension-loader";
const logModule = "[EXTENSION-INSTALLER]";
type Dependencies = {
[name: string]: string;
};
// Type for the package.json file that is written by ExtensionInstaller
export type PackageJson = {
dependencies: Dependencies;
};
/**
* Installs dependencies for extensions
*/
export class ExtensionInstaller {
private installLock = new AwaitLock();
get extensionPackagesRoot() {
return extensionPackagesRoot();
}
get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli');
}
installDependencies(): Promise<void> {
return new Promise((resolve, reject) => {
logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`);
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
cwd: extensionPackagesRoot(),
silent: true
});
child.on("close", () => {
resolve();
});
child.on("error", (err) => {
reject(err);
});
});
}
/**
* Write package.json to the file system and execute npm install for it.
*/
async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise<void> {
// Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync();
try {
// Write the package.json which will be installed in .installDependencies()
await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), {
mode: 0o600
});
await this.installDependencies();
} finally {
this.installLock.release();
}
}
}
export const extensionInstaller = new ExtensionInstaller();

View File

@ -1,20 +1,25 @@
import { app, ipcRenderer, remote } from "electron";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery";
import { extensionsStore } from "./extensions-store";
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
import type { LensMainExtension } from "./lens-main-extension"; import type { LensMainExtension } from "./lens-main-extension";
import type { LensRendererExtension } from "./lens-renderer-extension"; import type { LensRendererExtension } from "./lens-renderer-extension";
import type { InstalledExtension } from "./extension-manager";
import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import logger from "../main/logger";
import { app, ipcRenderer, remote } from "electron";
import * as registries from "./registries"; import * as registries from "./registries";
import { extensionsStore } from "./extensions-store";
// lazy load so that we get correct userData // lazy load so that we get correct userData
export function extensionPackagesRoot() { export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData")); return path.join((app || remote.app).getPath("userData"));
} }
const logModule = "[EXTENSIONS-LOADER]";
/**
* Loads installed extensions to the Lens application
*/
export class ExtensionLoader { export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>(); protected instances = observable.map<LensExtensionId, LensExtension>();
@ -47,6 +52,17 @@ export class ExtensionLoader {
this.extensions.replace(extensions); this.extensions.replace(extensions);
} }
addExtension(extension: InstalledExtension) {
this.extensions.set(extension.manifestPath as LensExtensionId, extension);
}
removeExtension(lensExtensionId: LensExtensionId) {
// TODO: Remove the extension properly (from menus etc.)
if (!this.extensions.delete(lensExtensionId)) {
throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
}
}
protected async initMain() { protected async initMain() {
this.isLoaded = true; this.isLoaded = true;
this.loadOnMain(); this.loadOnMain();
@ -77,14 +93,14 @@ export class ExtensionLoader {
} }
loadOnMain() { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main'); logger.info(`${logModule}: load on main`);
this.autoInitExtensions((ext: LensMainExtension) => [ this.autoInitExtensions((ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus) registries.menuRegistry.add(ext.appMenus)
]); ]);
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)'); logger.info(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions((ext: LensRendererExtension) => [ this.autoInitExtensions((ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, ext), registries.globalPageRegistry.add(ext.globalPages, ext),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext), registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
@ -95,7 +111,7 @@ export class ExtensionLoader {
} }
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)'); logger.info(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions((ext: LensRendererExtension) => [ this.autoInitExtensions((ext: LensRendererExtension) => [
registries.clusterPageRegistry.add(ext.clusterPages, ext), registries.clusterPageRegistry.add(ext.clusterPages, ext),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext), registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
@ -118,14 +134,15 @@ export class ExtensionLoader {
instance.enable(); instance.enable();
this.instances.set(extId, instance); this.instances.set(extId, instance);
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err }); logger.error(`${logModule}: activation extension error`, { ext, err });
} }
} else if (!ext.isEnabled && instance) { } else if (!ext.isEnabled && instance) {
logger.info(`${logModule} deleting extension ${extId}`);
try { try {
instance.disable(); instance.disable();
this.instances.delete(extId); this.instances.delete(extId);
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err }); logger.error(`${logModule}: deactivation extension error`, { ext, err });
} }
} }
} }
@ -146,7 +163,7 @@ export class ExtensionLoader {
return __non_webpack_require__(extEntrypoint).default; return __non_webpack_require__(extEntrypoint).default;
} }
} catch (err) { } catch (err) {
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension }); console.error(`${logModule}: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
console.trace(err); console.trace(err);
} }
} }

View File

@ -1,172 +0,0 @@
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
import path from "path";
import os from "os";
import fs from "fs-extra";
import child_process from "child_process";
import logger from "../main/logger";
import { extensionPackagesRoot } from "./extension-loader";
import { getBundledExtensions } from "../common/utils/app-version";
export interface InstalledExtension {
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
type Dependencies = {
[name: string]: string;
};
type PackageJson = {
dependencies: Dependencies;
};
export class ExtensionManager {
protected bundledFolderPath: string;
protected packagesJson: PackageJson = {
dependencies: {}
};
get extensionPackagesRoot() {
return extensionPackagesRoot();
}
get inTreeTargetPath() {
return path.join(this.extensionPackagesRoot, "extensions");
}
get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions");
}
get nodeModulesPath(): string {
return path.join(this.extensionPackagesRoot, "node_modules");
}
get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions");
}
get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli');
}
get packageJsonPath() {
return path.join(this.extensionPackagesRoot, "package.json");
}
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot);
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"));
}
try {
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
this.bundledFolderPath = this.inTreeFolderPath;
} catch {
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
await fs.remove(this.inTreeTargetPath);
await fs.ensureDir(this.inTreeTargetPath);
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
this.bundledFolderPath = this.inTreeTargetPath;
}
await fs.ensureDir(this.nodeModulesPath);
await fs.ensureDir(this.localFolderPath);
return await this.loadExtensions();
}
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest;
try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
manifestJson = __non_webpack_require__(manifestPath);
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name);
return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson,
isBundled: isBundled,
isEnabled: isBundled,
};
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
}
}
protected installPackages(): Promise<void> {
return new Promise((resolve, reject) => {
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
cwd: extensionPackagesRoot(),
silent: true
});
child.on("close", () => {
resolve();
});
child.on("error", (err) => {
reject(err);
});
});
}
async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions();
const localExtensions = await this.loadFromFolder(this.localFolderPath);
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 });
await this.installPackages();
const extensions = bundledExtensions.concat(localExtensions);
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
}
async loadBundledExtensions() {
const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) {
continue;
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null);
if (ext) {
extensions.push(ext);
}
}
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions();
const extensions: InstalledExtension[] = [];
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
continue;
}
const absPath = path.resolve(folderPath, fileName);
if (!fs.existsSync(absPath)) {
continue;
}
const lstat = await fs.lstat(absPath);
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
continue;
}
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath).catch(() => null);
if (ext) {
extensions.push(ext);
}
}
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
}
export const extensionManager = new ExtensionManager();

View File

@ -1,4 +1,4 @@
import type { InstalledExtension } from "./extension-manager"; import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx"; import { action, observable, reaction } from "mobx";
import logger from "../main/logger"; import logger from "../main/logger";
@ -28,6 +28,7 @@ export class LensExtension {
} }
get id(): LensExtensionId { get id(): LensExtensionId {
// This is the symlinked path under node_modules
return this.manifestPath; return this.manifestPath;
} }

View File

@ -11,7 +11,7 @@ export class LensMainExtension extends LensExtension {
const windowManager = WindowManager.getInstance<WindowManager>(); const windowManager = WindowManager.getInstance<WindowManager>();
const pageUrl = getExtensionPageUrl({ const pageUrl = getExtensionPageUrl({
extensionId: this.name, extensionId: this.name,
pageId: pageId, pageId,
params: params ?? {}, // compile to url with params params: params ?? {}, // compile to url with params
}); });
await windowManager.navigate(pageUrl, frameId); await windowManager.navigate(pageUrl, frameId);

View File

@ -19,7 +19,7 @@ export class LensRendererExtension extends LensExtension {
const { navigate } = await import("../renderer/navigation"); const { navigate } = await import("../renderer/navigation");
const pageUrl = getExtensionPageUrl({ const pageUrl = getExtensionPageUrl({
extensionId: this.name, extensionId: this.name,
pageId: pageId, pageId,
params: params ?? {}, // compile to url with params params: params ?? {}, // compile to url with params
}); });
navigate(pageUrl); navigate(pageUrl);

View File

@ -13,7 +13,7 @@ export class ClusterIdDetector extends BaseClusterDetector {
id = this.cluster.apiUrl; id = this.cluster.apiUrl;
} }
const value = createHash("sha256").update(id).digest("hex"); const value = createHash("sha256").update(id).digest("hex");
return { value: value, accuracy: 100 }; return { value, accuracy: 100 };
} }
protected async getDefaultNamespaceId() { protected async getDefaultNamespaceId() {

View File

@ -91,7 +91,7 @@ export class ContextHandler {
return { return {
target: proxyUrl, target: proxyUrl,
changeOrigin: true, changeOrigin: true,
timeout: timeout, timeout,
headers: { headers: {
"Host": this.clusterUrl.hostname, "Host": this.clusterUrl.hostname,
}, },

View File

@ -0,0 +1,11 @@
/**
* Installs Electron developer tools in the development build.
* The dependency is not bundled to the production build.
*/
export const installDeveloperTools = async () => {
if (process.env.NODE_ENV === 'development') {
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
}
};

View File

@ -8,7 +8,7 @@ export class HelmCli extends LensBinary {
public constructor(baseDir: string, version: string) { public constructor(baseDir: string, version: string) {
const opts: LensBinaryOpts = { const opts: LensBinaryOpts = {
version, version,
baseDir: baseDir, baseDir,
originalBinaryName: "helm", originalBinaryName: "helm",
newBinaryName: "helm3" newBinaryName: "helm3"
}; };

View File

@ -40,7 +40,7 @@ export class HelmReleaseManager {
log: stdout, log: stdout,
release: { release: {
name: releaseName, name: releaseName,
namespace: namespace namespace
} }
}; };
} finally { } finally {

View File

@ -21,8 +21,10 @@ import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store"; import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { extensionManager } from "../extensions/extension-manager";
import { extensionsStore } from "../extensions/extensions-store"; import { extensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number; let proxyPort: number;
@ -49,6 +51,8 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
await installDeveloperTools();
// preload // preload
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
@ -79,8 +83,22 @@ app.on("ready", async () => {
} }
extensionLoader.init(); extensionLoader.init();
extensionDiscovery.init();
windowManager = WindowManager.getInstance<WindowManager>(proxyPort); windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
extensionLoader.initExtensions(await extensionManager.load()); // call after windowManager to see splash earlier
// call after windowManager to see splash earlier
const extensions = await extensionDiscovery.load();
// Subscribe to extensions that are copied or deleted to/from the extensions folder
extensionDiscovery.events.on("add", (extension: InstalledExtension) => {
extensionLoader.addExtension(extension);
});
extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => {
extensionLoader.removeExtension(lensExtensionId);
});
extensionLoader.initExtensions(extensions);
setTimeout(() => { setTimeout(() => {
appEventBus.emit({ name: "service", action: "start" }); appEventBus.emit({ name: "service", action: "start" });

View File

@ -67,15 +67,15 @@ export class Router {
output: "data", output: "data",
}); });
return { return {
cluster: cluster, cluster,
path: url.pathname, path: url.pathname,
raw: { raw: {
req: req, req,
}, },
response: res, response: res,
query: url.searchParams, query: url.searchParams,
payload: payload, payload,
params: params params
}; };
} }

View File

@ -78,16 +78,16 @@ class PortForwardRoute extends LensApi {
let portForward = PortForward.getPortforward({ let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName, clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace: namespace, port: port namespace, port
}); });
if (!portForward) { if (!portForward) {
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`); logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
portForward = new PortForward({ portForward = new PortForward({
clusterId: cluster.id, clusterId: cluster.id,
kind: resourceType, kind: resourceType,
namespace: namespace, namespace,
name: resourceName, name: resourceName,
port: port, port,
kubeConfig: cluster.getProxyKubeconfigPath() kubeConfig: cluster.getProxyKubeconfigPath()
}); });
const started = await portForward.start(); const started = await portForward.start();

View File

@ -47,7 +47,7 @@ export class ShellSession extends EventEmitter {
this.shellProcess = pty.spawn(shell, args, { this.shellProcess = pty.spawn(shell, args, {
cols: 80, cols: 80,
cwd: this.cwd() || env.HOME, cwd: this.cwd() || env.HOME,
env: env, env,
name: "xterm-256color", name: "xterm-256color",
rows: 30, rows: 30,
}); });

View File

@ -124,7 +124,7 @@ export class WindowManager extends Singleton {
await this.ensureMainWindow(); await this.ensureMainWindow();
this.sendToView({ this.sendToView({
channel: "renderer:navigate", channel: "renderer:navigate",
frameId: frameId, frameId,
data: [url], data: [url],
}); });
} }

View File

@ -26,7 +26,7 @@ export default migration({
user["auth-provider"].config = authConfig; user["auth-provider"].config = authConfig;
kubeConfig.users = [{ kubeConfig.users = [{
name: userObj.name, name: userObj.name,
user: user user
}]; }];
cluster.kubeConfig = yaml.safeDump(kubeConfig); cluster.kubeConfig = yaml.safeDump(kubeConfig);
store.set(clusterKey, cluster); store.set(clusterKey, cluster);

View File

@ -8,7 +8,7 @@ export class ClusterApi extends KubeApi<Cluster> {
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> { async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
const nodes = nodeNames.join("|"); const nodes = nodeNames.join("|");
const opts = { category: "cluster", nodes: nodes }; const opts = { category: "cluster", nodes };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
memoryUsage: opts, memoryUsage: opts,

View File

@ -20,7 +20,7 @@ export class DeploymentApi extends KubeApi<Deployment> {
data: { data: {
metadata: params, metadata: params,
spec: { spec: {
replicas: replicas replicas
} }
} }
}); });

View File

@ -119,7 +119,7 @@ export const helmReleasesApi = {
const path = endpoint({ name, namespace }) + "/rollback"; const path = endpoint({ name, namespace }) + "/rollback";
return apiBase.put(path, { return apiBase.put(path, {
data: { data: {
revision: revision revision
} }
}); });
} }

View File

@ -84,8 +84,8 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
} }
const infoLog: JsonApiLog = { const infoLog: JsonApiLog = {
method: reqInit.method.toUpperCase(), method: reqInit.method.toUpperCase(),
reqUrl: reqUrl, reqUrl,
reqInit: reqInit, reqInit,
}; };
return cancelableFetch(reqUrl, reqInit).then(res => { return cancelableFetch(reqUrl, reqInit).then(res => {
return this.parseResponse<D>(res, infoLog); return this.parseResponse<D>(res, infoLog);

View File

@ -73,7 +73,7 @@ export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeC
}); });
return new KubeApi({ return new KubeApi({
objectConstructor: kubeClass, objectConstructor: kubeClass,
request: request request
}); });
} }
@ -228,7 +228,7 @@ export class KubeApi<T extends KubeObject = any> {
apiVersion: this.apiVersionWithGroup, apiVersion: this.apiVersionWithGroup,
resource: this.apiResource, resource: this.apiResource,
namespace: this.isNamespaced ? namespace : undefined, namespace: this.isNamespaced ? namespace : undefined,
name: name, name,
}); });
return resourcePath + (query ? `?` + stringify(this.normalizeQuery(query)) : ""); return resourcePath + (query ? `?` + stringify(this.normalizeQuery(query)) : "");
} }
@ -256,7 +256,7 @@ export class KubeApi<T extends KubeObject = any> {
this.setResourceVersion("", metadata.resourceVersion); this.setResourceVersion("", metadata.resourceVersion);
return items.map(item => new KubeObjectConstructor({ return items.map(item => new KubeObjectConstructor({
kind: this.kind, kind: this.kind,
apiVersion: apiVersion, apiVersion,
...item, ...item,
})); }));
} }

View File

@ -24,6 +24,7 @@ import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
import { ExecValidationNotFoundError } from "../../../common/custom-errors"; import { ExecValidationNotFoundError } from "../../../common/custom-errors";
import { appEventBus } from "../../../common/event-bus";
enum KubeConfigSourceTab { enum KubeConfigSourceTab {
FILE = "file", FILE = "file",
@ -47,6 +48,7 @@ export class AddCluster extends React.Component {
componentDidMount() { componentDidMount() {
clusterStore.setActive(null); clusterStore.setActive(null);
this.setKubeConfig(userStore.kubeConfigPath); this.setKubeConfig(userStore.kubeConfigPath);
appEventBus.emit({ name: "cluster-add", action: "start" });
} }
componentWillUnmount() { componentWillUnmount() {
@ -133,7 +135,7 @@ export class AddCluster extends React.Component {
} }
this.error = ""; this.error = "";
this.isWaiting = true; this.isWaiting = true;
appEventBus.emit({ name: "cluster-add", action: "click" });
newClusters = this.selectedContexts.filter(context => { newClusters = this.selectedContexts.filter(context => {
try { try {
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
@ -156,7 +158,7 @@ export class AddCluster extends React.Component {
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder : ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return { return {
id: clusterId, id: clusterId,
kubeConfigPath: kubeConfigPath, kubeConfigPath,
workspace: workspaceStore.currentWorkspaceId, workspace: workspaceStore.currentWorkspaceId,
contextName: kubeConfig.currentContext, contextName: kubeConfig.currentContext,
preferences: { preferences: {

View File

@ -36,7 +36,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
const loadVersions = (repo: string) => { const loadVersions = (repo: string) => {
return helmChartsApi.get(repo, chartName).then(({ versions }) => { return helmChartsApi.get(repo, chartName).then(({ versions }) => {
return versions.map(chart => ({ return versions.map(chart => ({
repo: repo, repo,
version: chart.getVersion() version: chart.getVersion()
})); }));
}); });

View File

@ -27,7 +27,7 @@ export const ClusterMetrics = observer(() => {
id: metricType + metricNodeRole, id: metricType + metricNodeRole,
label: metricType.toUpperCase() + " usage", label: metricType.toUpperCase() + " usage",
borderColor: colors[metricType], borderColor: colors[metricType],
data: data data
}]; }];
const cpuOptions: ChartOptions = { const cpuOptions: ChartOptions = {
scales: { scales: {

View File

@ -92,11 +92,11 @@ export class AddSecretDialog extends React.Component<Props> {
const { name, namespace, type } = this; const { name, namespace, type } = this;
const { data = [], labels = [], annotations = [] } = this.secret[type]; const { data = [], labels = [], annotations = [] } = this.secret[type];
const secret: Partial<Secret> = { const secret: Partial<Secret> = {
type: type, type,
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""), data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
metadata: { metadata: {
name: name, name,
namespace: namespace, namespace,
annotations: this.getDataFromFields(annotations), annotations: this.getDataFromFields(annotations),
labels: this.getDataFromFields(labels), labels: this.getDataFromFields(labels),
} as IKubeObjectMetadata } as IKubeObjectMetadata

View File

@ -17,7 +17,7 @@ import { PageLayout } from "../layout/page-layout";
import { Clipboard } from "../clipboard"; import { Clipboard } from "../clipboard";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
import { extensionLoader } from "../../../extensions/extension-loader"; import { extensionLoader } from "../../../extensions/extension-loader";
import { extensionManager } from "../../../extensions/extension-manager"; import { extensionDiscovery } from "../../../extensions/extension-discovery";
import { LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension"; import { LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { downloadFile } from "../../../common/utils"; import { downloadFile } from "../../../common/utils";
@ -57,7 +57,7 @@ export class Extensions extends React.Component {
} }
get extensionsPath() { get extensionsPath() {
return extensionManager.localFolderPath; return extensionDiscovery.localFolderPath;
} }
getExtensionPackageTemp(fileName = "") { getExtensionPackageTemp(fileName = "") {

View File

@ -60,7 +60,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
kind: "Namespace", kind: "Namespace",
apiVersion: "v1", apiVersion: "v1",
metadata: { metadata: {
name: name, name,
uid: "", uid: "",
resourceVersion: "", resourceVersion: "",
selfLink: `/api/v1/namespaces/${name}` selfLink: `/api/v1/namespaces/${name}`

View File

@ -137,7 +137,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
else { else {
const name = useRoleForBindingName ? selectedRole.getName() : bindingName; const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
roleBinding = await roleBindingsStore.create({ name, namespace }, { roleBinding = await roleBindingsStore.create({ name, namespace }, {
subjects: subjects, subjects,
roleRef: { roleRef: {
name: selectedRole.getName(), name: selectedRole.getName(),
kind: selectedRole.kind, kind: selectedRole.kind,

View File

@ -88,7 +88,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
apiVersion: "v1", apiVersion: "v1",
kind: "Secret", kind: "Secret",
metadata: { metadata: {
name: name, name,
uid: null, uid: null,
selfLink: null, selfLink: null,
resourceVersion: null resourceVersion: null

View File

@ -59,7 +59,7 @@ export class App extends React.Component {
name: "cluster", name: "cluster",
action: "open", action: "open",
params: { params: {
clusterId: clusterId clusterId
} }
}); });
window.addEventListener("online", () => { window.addEventListener("online", () => {

View File

@ -30,7 +30,7 @@ export class Checkbox extends React.PureComponent<CheckboxProps> {
render() { render() {
const { label, inline, className, value, theme, children, ...inputProps } = this.props; const { label, inline, className, value, theme, children, ...inputProps } = this.props;
const componentClass = cssNames('Checkbox flex', className, { const componentClass = cssNames('Checkbox flex', className, {
inline: inline, inline,
checked: value, checked: value,
disabled: this.props.disabled, disabled: this.props.disabled,
["theme-" + theme]: theme, ["theme-" + theme]: theme,

View File

@ -55,7 +55,7 @@ export class EditResource extends React.Component<Props> {
} }
editResourceStore.setData(this.tabId, { editResourceStore.setData(this.tabId, {
...this.tabData, ...this.tabData,
draft: draft, draft,
}); });
} }

View File

@ -38,7 +38,7 @@ export class FileInput extends React.Component<Props> {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
resolve({ resolve({
file: file, file,
data: reader.result, data: reader.result,
error: reader.error ? String(reader.error) : null, error: reader.error ? String(reader.error) : null,
}); });

View File

@ -151,7 +151,7 @@ export class Input extends React.Component<InputProps, State> {
this.setState({ this.setState({
validating: false, validating: false,
valid: !errors.length, valid: !errors.length,
errors: errors, errors,
}); });
} }
@ -275,11 +275,11 @@ export class Input extends React.Component<InputProps, State> {
const className = cssNames("Input", this.props.className, { const className = cssNames("Input", this.props.className, {
[`theme ${theme}`]: theme, [`theme ${theme}`]: theme,
focused: focused, focused,
disabled: disabled, disabled,
invalid: !valid, invalid: !valid,
dirty: dirty, dirty,
validating: validating, validating,
validatingLine: validating && showValidationLine, validatingLine: validating && showValidationLine,
}); });

View File

@ -81,7 +81,7 @@ export class MenuActions extends React.Component<MenuActionsProps> {
...menuProps ...menuProps
} = this.props; } = this.props;
const menuClassName = cssNames("MenuActions flex", className, { const menuClassName = cssNames("MenuActions flex", className, {
toolbar: toolbar, toolbar,
gaps: toolbar, // add spacing for .flex gaps: toolbar, // add spacing for .flex
}); });
const autoClose = !toolbar; const autoClose = !toolbar;

View File

@ -15,7 +15,7 @@ export class Notifications extends React.Component {
static ok(message: NotificationMessage) { static ok(message: NotificationMessage) {
notificationsStore.add({ notificationsStore.add({
message: message, message,
timeout: 2500, timeout: 2500,
status: NotificationStatus.OK status: NotificationStatus.OK
}); });
@ -23,7 +23,7 @@ export class Notifications extends React.Component {
static error(message: NotificationMessage) { static error(message: NotificationMessage) {
notificationsStore.add({ notificationsStore.add({
message: message, message,
timeout: 10000, timeout: 10000,
status: NotificationStatus.ERROR status: NotificationStatus.ERROR
}); });
@ -33,7 +33,7 @@ export class Notifications extends React.Component {
return notificationsStore.add({ return notificationsStore.add({
status: NotificationStatus.INFO, status: NotificationStatus.INFO,
timeout: 0, timeout: 0,
message: message, message,
...customOpts, ...customOpts,
}); });
} }

View File

@ -24,10 +24,10 @@ export class RadioGroup extends React.Component<RadioGroupProps, {}> {
<div className={className}> <div className={className}>
{radios.map(radio => { {radios.map(radio => {
return React.cloneElement(radio, { return React.cloneElement(radio, {
name: name, name,
disabled: disabled !== undefined ? disabled : radio.props.disabled, disabled: disabled !== undefined ? disabled : radio.props.disabled,
checked: radio.props.value === value, checked: radio.props.value === value,
onChange: onChange onChange
} as any); } as any);
})} })}
</div> </div>
@ -66,7 +66,7 @@ export class Radio extends React.Component<RadioProps> {
render() { render() {
const { className, label, checked, children, ...inputProps } = this.props; const { className, label, checked, children, ...inputProps } = this.props;
const componentClass = cssNames('Radio flex align-center', className, { const componentClass = cssNames('Radio flex align-center', className, {
checked: checked, checked,
disabled: this.props.disabled, disabled: this.props.disabled,
}); });
return ( return (

View File

@ -60,7 +60,7 @@ export class TableCell extends React.Component<TableCellProps> {
render() { render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props; const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, { const classNames = cssNames("TableCell", className, {
checkbox: checkbox, checkbox,
nowrap: _nowrap, nowrap: _nowrap,
sorting: this.isSortable, sorting: this.isSortable,
}); });

View File

@ -82,7 +82,7 @@ export class Table extends React.Component<TableProps> {
typeof elem.props.children === "string" ? elem.props.children : undefined typeof elem.props.children === "string" ? elem.props.children : undefined
); );
return React.cloneElement(elem, { return React.cloneElement(elem, {
title: title, title,
_sort: this.sort, _sort: this.sort,
_sorting: this.sortParams, _sorting: this.sortParams,
_nowrap: headElem.props.nowrap, _nowrap: headElem.props.nowrap,

View File

@ -32,10 +32,10 @@ export class Tabs extends React.PureComponent<TabsProps> {
render() { render() {
const { center, wrap, onChange, value, autoFocus, scrollable = true, withBorder, ...elemProps } = this.props; const { center, wrap, onChange, value, autoFocus, scrollable = true, withBorder, ...elemProps } = this.props;
const className = cssNames("Tabs", this.props.className, { const className = cssNames("Tabs", this.props.className, {
center: center, center,
wrap: wrap, wrap,
scrollable: scrollable, scrollable,
withBorder: withBorder, withBorder,
}); });
return ( return (
<TabsContext.Provider value={{ autoFocus, value, onChange }}> <TabsContext.Provider value={{ autoFocus, value, onChange }}>
@ -120,7 +120,7 @@ export class Tab extends React.PureComponent<TabProps> {
let { className } = this.props; let { className } = this.props;
className = cssNames("Tab flex gaps align-center", className, { className = cssNames("Tab flex gaps align-center", className, {
"active": this.isActive, "active": this.isActive,
"disabled": disabled, disabled,
}); });
return ( return (
<div <div

View File

@ -180,8 +180,8 @@ export class Tooltip extends React.Component<TooltipProps> {
break; break;
} }
return { return {
left: left, left,
top: top, top,
right: left + tooltipBounds.width, right: left + tooltipBounds.width,
bottom: top + tooltipBounds.height, bottom: top + tooltipBounds.height,
}; };

View File

@ -146,7 +146,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
protected bindWatchEventsUpdater(delay = 1000) { protected bindWatchEventsUpdater(delay = 1000) {
return reaction(() => this.eventsBuffer.toJS()[0], this.updateFromEventsBuffer, { return reaction(() => this.eventsBuffer.toJS()[0], this.updateFromEventsBuffer, {
delay: delay delay
}); });
} }

View File

@ -67,7 +67,7 @@ export function getSelectedDetails() {
export function getDetailsUrl(details: string) { export function getDetailsUrl(details: string) {
if (!details) return ""; if (!details) return "";
return getQueryString({ return getQueryString({
details: details, details,
selected: getSelectedDetails(), selected: getSelectedDetails(),
}); });
} }

View File

@ -7,19 +7,19 @@ export function interval(timeSec = 1, callback: IntervalCallback, autoRun = fals
let timer = -1; let timer = -1;
let isRunning = false; let isRunning = false;
const intervalManager = { const intervalManager = {
start: function (runImmediately = false) { start (runImmediately = false) {
if (isRunning) return; if (isRunning) return;
const tick = () => callback(++count); const tick = () => callback(++count);
isRunning = true; isRunning = true;
timer = window.setInterval(tick, 1000 * timeSec); timer = window.setInterval(tick, 1000 * timeSec);
if (runImmediately) tick(); if (runImmediately) tick();
}, },
stop: function () { stop () {
count = 0; count = 0;
isRunning = false; isRunning = false;
clearInterval(timer); clearInterval(timer);
}, },
restart: function (runImmediately = false) { restart (runImmediately = false) {
this.stop(); this.stop();
this.start(runImmediately); this.start(runImmediately);
}, },

View File

@ -1867,6 +1867,11 @@
dependencies: dependencies:
"@types/trusted-types" "*" "@types/trusted-types" "*"
"@types/electron-devtools-installer@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz#32ee4ebbe99b3daf9847a6d2097dc00b5de94f10"
integrity sha512-HJNxpaOXuykCK4rQ6FOMxAA0NLFYsf7FiPFGmab0iQmtVBHSAfxzy3MRFpLTTDDWbV0yD2YsHOQvdu8yCqtCfw==
"@types/electron-window-state@^2.0.34": "@types/electron-window-state@^2.0.34":
version "2.0.34" version "2.0.34"
resolved "https://registry.yarnpkg.com/@types/electron-window-state/-/electron-window-state-2.0.34.tgz#794b9bc62eb4485837947c543c9d2757ee4c74c0" resolved "https://registry.yarnpkg.com/@types/electron-window-state/-/electron-window-state-2.0.34.tgz#794b9bc62eb4485837947c543c9d2757ee4c74c0"
@ -3262,6 +3267,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
await-lock@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.1.0.tgz#bc78c51d229a34d5d90965a1c94770e772c6145e"
integrity sha512-t7Zm5YGgEEc/3eYAicF32m/TNvL+XOeYZy9CvBUeJY/szM7frLolFylhrlZNWV/ohWhcUXygrBGjYmoQdxF4CQ==
aws-sign2@~0.7.0: aws-sign2@~0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -4116,7 +4126,7 @@ chokidar@^3.2.2:
optionalDependencies: optionalDependencies:
fsevents "~2.1.2" fsevents "~2.1.2"
chokidar@^3.4.1: chokidar@^3.4.1, chokidar@^3.4.3:
version "3.4.3" version "3.4.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b"
integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==
@ -5493,6 +5503,15 @@ electron-chromedriver@^9.0.0:
"@electron/get" "^1.12.2" "@electron/get" "^1.12.2"
extract-zip "^2.0.0" extract-zip "^2.0.0"
electron-devtools-installer@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.1.1.tgz#7b56c8c86475c5e4e10de6917d150c53c9ceb55e"
integrity sha512-g2D4J6APbpsiIcnLkFMyKZ6bOpEJ0Ltcc2m66F7oKUymyGAt628OWeU9nRZoh1cNmUs/a6Cls2UfOmsZtE496Q==
dependencies:
rimraf "^3.0.2"
semver "^7.2.1"
unzip-crx-3 "^0.2.0"
electron-notarize@^0.3.0: electron-notarize@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.3.0.tgz#b93c606306eac558b250c78ff95273ddb9fedf0a" resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.3.0.tgz#b93c606306eac558b250c78ff95273ddb9fedf0a"
@ -7427,6 +7446,11 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
import-fresh@^3.0.0, import-fresh@^3.1.0: import-fresh@^3.0.0, import-fresh@^3.1.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@ -8853,6 +8877,16 @@ jss@10.2.0, jss@^10.0.3:
is-in-browser "^1.1.3" is-in-browser "^1.1.3"
tiny-warning "^1.0.2" tiny-warning "^1.0.2"
jszip@^3.1.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6"
integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
set-immediate-shim "~1.0.1"
keyv@3.0.0: keyv@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
@ -9122,6 +9156,13 @@ libnpx@^10.2.4:
y18n "^4.0.0" y18n "^4.0.0"
yargs "^14.2.3" yargs "^14.2.3"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lines-and-columns@^1.1.6: lines-and-columns@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@ -11013,7 +11054,7 @@ pacote@^9.1.0, pacote@^9.5.12, pacote@^9.5.3:
unique-filename "^1.1.1" unique-filename "^1.1.1"
which "^1.3.1" which "^1.3.1"
pako@~1.0.5: pako@~1.0.2, pako@~1.0.5:
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@ -12773,6 +12814,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
set-value@^2.0.0, set-value@^2.0.1: set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@ -14327,6 +14373,15 @@ unset-value@^1.0.0:
has-value "^0.3.1" has-value "^0.3.1"
isobject "^3.0.0" isobject "^3.0.0"
unzip-crx-3@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz#d5324147b104a8aed9ae8639c95521f6f7cda292"
integrity sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==
dependencies:
jszip "^3.1.0"
mkdirp "^0.5.1"
yaku "^0.16.6"
unzip-response@^2.0.1: unzip-response@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
@ -15052,6 +15107,11 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
yaku@^0.16.6:
version "0.16.7"
resolved "https://registry.yarnpkg.com/yaku/-/yaku-0.16.7.tgz#1d195c78aa9b5bf8479c895b9504fd4f0847984e"
integrity sha1-HRlceKqbW/hHnIlblQT9TwhHmE4=
yallist@^2.1.2: yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"