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

Merge branch 'master' into ui-table-empty-state-lines

This commit is contained in:
Alex Andreev 2022-02-21 10:45:06 +03:00
commit 1d99ea36c4
214 changed files with 19492 additions and 6072 deletions

View File

@ -129,6 +129,16 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/member-delimiter-style": ["error", {
"multiline": {
"delimiter": "semi",
"requireLast": true,
},
"singleline": {
"delimiter": "semi",
"requireLast": false,
},
}],
"react/display-name": "off",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", {

View File

@ -42,7 +42,7 @@ jobs:
restore-keys: |
${{ runner.os }}-yarn-
- uses: nick-invision/retry@v2
- uses: nick-fields/retry@v2
name: Install dependencies
with:
timeout_minutes: 10
@ -53,7 +53,7 @@ jobs:
- run: make build-npm
name: Generate npm package
- uses: nick-invision/retry@v2
- uses: nick-fields/retry@v2
name: Build bundled extensions
with:
timeout_minutes: 15

View File

@ -3,4 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export default ""; // mostly path to bundled file or data-url (webpack)
export default {};

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,8 @@
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "latest",
"typescript": "latest",
"webpack": "latest"
"ts-loader": "^8.0.4",
"typescript": "^4.3.2",
"webpack": "^4.46.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,10 @@
],
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"semver": "^7.3.2",
"jest": "latest",
"ts-loader": "latest",
"typescript": "latest",
"webpack": "latest"
"ts-loader": "^8.0.4",
"typescript": "^4.3.2",
"webpack": "^4.44.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -18,9 +18,9 @@
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "latest",
"ts-loader": "latest",
"typescript": "latest",
"webpack": "latest"
"jest": "^26.6.3",
"ts-loader": "^8.0.4",
"typescript": "^4.3.2",
"webpack": "^4.46.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -18,9 +18,9 @@
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "latest",
"ts-loader": "latest",
"typescript": "latest",
"webpack": "latest"
"jest": "^26.6.3",
"ts-loader": "^8.0.4",
"typescript": "^4.3.2",
"webpack": "^4.46.0"
}
}

View File

@ -60,9 +60,7 @@ describe("preferences page tests", () => {
// Skipping, but will turn it on again in the follow up PR
it.skip("ensures helm repos", async () => {
await window.click("[data-testid=kubernetes-tab]");
await window.waitForSelector("[data-testid=repository-name]", {
timeout: 140_000,
});
await window.waitForSelector("[data-testid=repository-name]");
await window.click("#HelmRepoSelect");
await window.waitForSelector("div.Select__option");
}, 10*60*1000);

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "5.4.0-beta.1",
"version": "5.4.0-beta.2",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@ -16,9 +16,9 @@
"dev-build": "concurrently yarn:compile:*",
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
"dev:main": "yarn run compile:main --watch --progress",
"dev:renderer": "yarn run compile:renderer --watch --progress",
"dev:extension-types": "yarn run compile:extension-types --watch --progress",
"dev:main": "yarn run compile:main --watch",
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
"dev:extension-types": "yarn run compile:extension-types --watch",
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "yarn run webpack --config webpack.main.ts",
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
@ -62,7 +62,7 @@
},
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
"\\.(svg|png|jpg|eot|woff2?|ttf)$": "<rootDir>/__mocks__/assetMock.ts"
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
},
"modulePathIgnorePatterns": [
"<rootDir>/dist",
@ -268,7 +268,7 @@
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@sentry/types": "^6.14.1",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^11.2.7",
@ -291,11 +291,12 @@
"@types/lodash": "^4.14.177",
"@types/marked": "^4.0.1",
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^2.4.0",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/mock-fs": "^4.13.1",
"@types/node": "14.17.33",
"@types/node-fetch": "^2.5.12",
"@types/npm": "^2.0.32",
"@types/progress-bar-webpack-plugin": "^2.1.2",
"@types/proper-lockfile": "^4.1.2",
"@types/randomcolor": "^0.5.6",
"@types/react": "^17.0.34",
@ -318,10 +319,10 @@
"@types/triple-beam": "^1.3.2",
"@types/url-parse": "^1.4.5",
"@types/uuid": "^8.3.3",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"@types/webpack": "^4.41.32",
"@types/webpack-dev-server": "^3.11.6",
"@types/webpack-env": "^1.16.3",
"@types/webpack-node-externals": "^2.5.3",
"@types/webpack-node-externals": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"ansi_up": "^5.1.0",
@ -329,24 +330,25 @@
"circular-dependency-plugin": "^5.2.2",
"color": "^3.2.1",
"concurrently": "^5.3.0",
"css-loader": "^6.5.1",
"css-loader": "^5.2.7",
"deepdash": "^5.3.9",
"dompurify": "^2.3.4",
"electron": "^14.2.4",
"electron-builder": "^22.14.5",
"electron-notarize": "^0.3.0",
"esbuild": "^0.13.15",
"esbuild-loader": "^2.18.0",
"esbuild-loader": "^2.16.0",
"eslint": "^8.7.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unused-imports": "^2.0.0",
"file-loader": "^6.2.0",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^6.5.0",
"fork-ts-checker-webpack-plugin": "^5.2.1",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^5.5.0",
"html-webpack-plugin": "^4.5.2",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "26.6.3",
@ -354,41 +356,43 @@
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^1.0.18",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.5.2",
"mini-css-extract-plugin": "^1.6.2",
"node-gyp": "7.1.2",
"node-loader": "^2.0.0",
"node-loader": "^1.0.3",
"nodemon": "^2.0.15",
"playwright": "^1.17.1",
"postcss": "^8.4.5",
"postcss-loader": "^6.2.1",
"postcss-loader": "^4.3.0",
"progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2",
"raw-loader": "^4.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-refresh": "^0.11.0",
"react-refresh-typescript": "^2.0.3",
"react-refresh": "^0.9.0",
"react-router-dom": "^5.3.0",
"react-select": "3.2.0",
"react-select-event": "^5.1.0",
"react-table": "^7.7.0",
"react-window": "^1.8.6",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"sass-loader": "^10.2.0",
"sharp": "^0.29.3",
"style-loader": "^3.3.1",
"style-loader": "^2.0.0",
"tailwindcss": "^3.0.7",
"ts-jest": "26.5.6",
"ts-loader": "^9.2.6",
"ts-loader": "^7.0.5",
"ts-node": "^10.4.0",
"type-fest": "^1.0.2",
"typed-emitter": "^1.4.0",
"typedoc": "0.22.10",
"typedoc-plugin-markdown": "^3.11.12",
"typedoc-plugin-markdown": "^3.11.3",
"typeface-roboto": "^1.1.13",
"typescript": "^4.5.2",
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.67.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.4",
"webpack-node-externals": "^3.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3",
"webpack-node-externals": "^1.7.2",
"xterm": "^4.15.0",
"xterm-addon-fit": "^0.5.0"
}

View File

@ -44,26 +44,26 @@ users:
`;
interface kubeconfig {
apiVersion: string,
apiVersion: string;
clusters: [{
name: string,
name: string;
cluster: {
server: string
}
}],
server: string;
};
}];
contexts: [{
context: {
cluster: string,
user: string,
},
name: string
}],
cluster: string;
user: string;
};
name: string;
}];
users: [{
name: string
}],
kind: string,
"current-context": string,
preferences: {}
name: string;
}];
kind: string;
"current-context": string;
preferences: {};
}
let mockKubeConfig: kubeconfig;

View File

@ -11,7 +11,6 @@ import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
export interface KubernetesClusterPrometheusMetrics {
address?: {
@ -139,7 +138,7 @@ class KubernetesClusterCategory extends CatalogCategory {
public readonly kind = "CatalogCategory";
public metadata = {
name: "Clusters",
icon: KubeClusterCategoryIcon,
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
};
public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev",

View File

@ -3,16 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, computed, observable, makeObservable } from "mobx";
import type { Disposer } from "../utils";
import { strictSet, iter, getOrInsertMap } from "../utils";
import { once } from "lodash";
import { action, computed, makeObservable, observable } from "mobx";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { ExtendedMap, iter, type Disposer } from "../utils";
export type CategoryFilter = (category: CatalogCategory) => any;
export class CatalogCategoryRegistry {
protected categories = observable.set<CatalogCategory>();
protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
protected groupKinds = new Map<string, Map<string, CatalogCategory>>();
protected filters = observable.set<CategoryFilter>([], {
deep: false,
});
@ -22,14 +23,14 @@ export class CatalogCategoryRegistry {
}
@action add(category: CatalogCategory): Disposer {
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
this.categories.add(category);
this.groupKinds
.getOrInsert(category.spec.group, ExtendedMap.new)
.strictSet(category.spec.names.kind, category);
strictSet(byGroup, category.spec.names.kind, category);
return () => {
this.categories.delete(category);
this.groupKinds.get(category.spec.group).delete(category.spec.names.kind);
byGroup.delete(category.spec.names.kind);
};
}

View File

@ -165,7 +165,7 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
* @param id The id of a category is parse
* @returns The group and kind parts of the ID
*/
public static parseId(id: string): { group?: string, kind?: string } {
public static parseId(id: string): { group?: string; kind?: string } {
const [group, kind] = id.split("/") ?? [];
return { group, kind };
@ -250,7 +250,7 @@ export interface CatalogEntityContextMenu {
*/
confirm?: {
message: string;
}
};
}
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
@ -262,7 +262,7 @@ export interface CatalogEntitySettingsMenu {
group?: string;
title: string;
components: {
View: React.ComponentType<any>
View: React.ComponentType<any>;
};
}

View File

@ -7,7 +7,7 @@ import { observable } from "mobx";
export interface ClusterFrameInfo {
frameId: number;
processId: number
processId: number;
}
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();

View File

@ -22,7 +22,7 @@ export interface ClusterStoreModel {
}
interface Dependencies {
createCluster: (model: ClusterModel) => Cluster
createCluster: (model: ClusterModel) => Cluster;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {

View File

@ -157,7 +157,7 @@ export const initialNodeShellImage = "docker.io/alpine:3.13";
* The arguments for requesting to refresh a cluster's metadata
*/
export interface ClusterRefreshOptions {
refreshMetadata?: boolean
refreshMetadata?: boolean;
}
/**
@ -170,7 +170,7 @@ export interface ClusterState {
accessible: boolean;
ready: boolean;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
allowedNamespaces: string[];
allowedResources: string[];
isGlobalWatchEnabled: boolean;
}

View File

@ -4,17 +4,18 @@
*/
import esbuild from "esbuild";
import type { Options as TSLoaderOptions } from "ts-loader";
/**
* A function returning webpack ts/tsx loader
*
* depends on env LENS_DEV_USE_ESBUILD_LOADER to use esbuild-loader (faster) or good-old ts-loader
*
* @param testRegExp - the regex for webpack to conditional find the files
* @returns ts/tsx webpack loader configuration object
*/
const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp) => {
testRegExp ??= /\.tsx?$/; // by default covers react/jsx-stuff
options.transpileOnly ??= true;
const getTSLoader = (
testRegExp: RegExp, transpileOnly = true,
) => {
if (process.env.LENS_DEV_USE_ESBUILD_LOADER === "true") {
console.info(`\n🚀 using esbuild-loader for ts(x)`);
@ -34,7 +35,9 @@ const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp
exclude: /node_modules/,
use: {
loader: "ts-loader",
options,
options: {
transpileOnly,
},
},
};
};

View File

@ -14,7 +14,7 @@ export interface HotbarItem {
};
params?: {
[key: string]: string;
}
};
}
export type Hotbar = Required<CreateHotbarData>;

View File

@ -29,10 +29,10 @@ export function onceCorrect<
listener,
verifier,
}: {
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
source: IPC;
channel: string;
listener: Listener;
verifier: ListVerifier<Rest<Parameters<Listener>>>;
}): void {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]): void {
if (verifier(args)) {
@ -63,10 +63,10 @@ export function onCorrect<
listener,
verifier,
}: {
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
source: IPC;
channel: string;
listener: Listener;
verifier: ListVerifier<Rest<Parameters<Listener>>>;
}): Disposer {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]) {
if (verifier(args)) {
@ -89,9 +89,9 @@ export function handleCorrect<
handler,
verifier,
}: {
channel: string,
handler: Handler,
verifier: ListVerifier<Rest<Parameters<Handler>>>,
channel: string;
handler: Handler;
verifier: ListVerifier<Rest<Parameters<Handler>>>;
}): Disposer {
function wrappedHandler(event: Electron.IpcMainInvokeEvent, ...args: unknown[]): ReturnType<Handler> {
if (verifier(args)) {

View File

@ -90,7 +90,7 @@ export interface RawHelmChart {
urls?: string[];
maintainers?: HelmChartMaintainer[];
dependencies?: RawHelmChartDependency[];
annotations?: Record<string, string>,
annotations?: Record<string, string>;
}
const helmChartMaintainerValidator = Joi.object<HelmChartMaintainer>({

View File

@ -70,8 +70,8 @@ export interface IReleaseRevision {
type EndpointParams = {}
| { namespace: string }
| { namespace: string, name: string }
| { namespace: string, name: string, route: string };
| { namespace: string; name: string }
| { namespace: string; name: string; route: string };
interface EndpointQuery {
all?: boolean;
@ -163,13 +163,13 @@ interface HelmReleaseDto {
}
export interface HelmRelease extends HelmReleaseDto, ItemObject {
getNs: () => string
getChart: (withVersion?: boolean) => string
getRevision: () => number
getStatus: () => string
getVersion: () => string
getUpdated: (humanize?: boolean, compact?: boolean) => string | number
getRepo: () => Promise<string>
getNs: () => string;
getChart: (withVersion?: boolean) => string;
getRevision: () => number;
getStatus: () => string;
getVersion: () => string;
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
getRepo: () => Promise<string>;
}
const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({

View File

@ -58,7 +58,7 @@ export interface IIngressService {
port: {
name?: string;
number?: number;
}
};
}
export const getBackendServiceNamePort = (backend: IIngressBackend) => {
@ -96,8 +96,8 @@ export interface Ingress {
apiGroup: string;
kind: string;
name: string;
}
}>
};
}>;
};
status: {
loadBalancer: {

View File

@ -33,7 +33,7 @@ export enum LimitPart {
type LimitRangeParts = Partial<Record<LimitPart, Record<string, string>>>;
export interface LimitRangeItem extends LimitRangeParts {
type: string
type: string;
}
export interface LimitRange {

View File

@ -101,8 +101,8 @@ export interface Node {
daemonEndpoints?: {
kubeletEndpoint: {
Port: number; //it must be uppercase for backwards compatibility
}
}
};
};
nodeInfo?: {
machineID: string;
systemUUID: string;

View File

@ -16,10 +16,10 @@ export interface PodDisruptionBudget {
selector: LabelSelector;
};
status: {
currentHealthy: number
desiredHealthy: number
disruptionsAllowed: number
expectedPods: number
currentHealthy: number;
desiredHealthy: number;
disruptionsAllowed: number;
expectedPods: number;
};
}

View File

@ -43,9 +43,9 @@ export interface IPodMetrics<T = IMetrics> {
[metric: string]: T;
cpuUsage: T;
memoryUsage: T;
fsUsage: T,
fsWrites: T,
fsReads: T,
fsUsage: T;
fsWrites: T;
fsReads: T;
networkReceive: T;
networkTransmit: T;
cpuRequests?: T;
@ -117,7 +117,7 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
};
secretRef?: {
name: string;
}
};
}[];
volumeMounts?: {
name: string;
@ -273,7 +273,7 @@ export class Pod extends WorkloadKubeObject {
hostIP: string;
podIP: string;
podIPs?: {
ip: string
ip: string;
}[];
startTime: string;
initContainerStatuses?: IPodContainerStatus[];

View File

@ -88,7 +88,7 @@ export interface KubeApiListOptions {
export interface IKubePreferredVersion {
preferredVersion?: {
version: string;
}
};
}
export interface IKubeResourceList {
@ -105,7 +105,7 @@ export interface IKubeResourceList {
export interface ILocalKubeApiConfig {
metadata: {
uid: string;
}
};
}
export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background";
@ -116,7 +116,7 @@ export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Backgroun
export interface IKubeApiCluster extends ILocalKubeApiConfig { }
export type PartialKubeObject<T extends KubeObject> = Partial<Omit<T, "metadata">> & {
metadata?: Partial<T["metadata"]>,
metadata?: Partial<T["metadata"]>;
};
export interface IRemoteKubeApiConfig {
@ -124,12 +124,12 @@ export interface IRemoteKubeApiConfig {
server: string;
caData?: string;
skipTLSVerify?: boolean;
}
};
user: {
token?: string | (() => Promise<string>);
clientCertificateData?: string;
clientKeyData?: string;
}
};
}
export function forCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): KubeApi<T> {
@ -219,7 +219,7 @@ export type KubeApiWatchCallback = (data: IKubeWatchEvent<KubeJsonApiData>, erro
export type KubeApiWatchOptions = {
namespace: string;
callback?: KubeApiWatchCallback;
abortController?: AbortController
abortController?: AbortController;
watchId?: string;
retry?: boolean;

View File

@ -247,7 +247,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
@action
async reloadAll(opts: { force?: boolean, namespaces?: string[], merge?: boolean } = {}) {
async reloadAll(opts: { force?: boolean; namespaces?: string[]; merge?: boolean } = {}) {
const { force = false, ...loadingOptions } = opts;
if (this.isLoading || (this.isLoaded && !force)) {

View File

@ -147,7 +147,7 @@ export function loadConfigFromString(content: string): ConfigResult {
}
export interface SplitConfigEntry {
config: KubeConfig,
config: KubeConfig;
error?: string;
}

View File

@ -63,8 +63,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
}
interface Dependencies {
extensionLoader: ExtensionLoader
extensionsStore: ExtensionsStore
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
}
export abstract class LensProtocolRouter {

View File

@ -327,7 +327,7 @@ export type ExtensionRegistry = {
location: ExtensionRegistryLocation.DEFAULT | ExtensionRegistryLocation.NPMRC;
customUrl?: undefined;
} | {
location: ExtensionRegistryLocation.CUSTOM,
location: ExtensionRegistryLocation.CUSTOM;
customUrl: string;
};

View File

@ -5,13 +5,13 @@
import { app, ipcMain } from "electron";
import semver, { SemVer } from "semver";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store";
import migrations, { fileNameMigration } from "../../migrations/user-store";
import { getAppVersion } from "../utils/app-version";
import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../app-event-bus/event-bus";
import { getOrInsertSet, toggle, toJS } from "../../renderer/utils";
import { getOrInsertSet, toggle, toJS, entries, fromEntries } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers";
import logger from "../../main/logger";
@ -165,55 +165,31 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this.lastSeenAppVersion = lastSeenAppVersion;
}
this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy);
this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell);
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme);
this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(preferences?.terminalTheme);
this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone);
this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs);
this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry);
this.allowErrorReporting = DESCRIPTORS.allowErrorReporting.fromStore(preferences?.allowErrorReporting);
this.downloadMirror = DESCRIPTORS.downloadMirror.fromStore(preferences?.downloadMirror);
this.downloadKubectlBinaries = DESCRIPTORS.downloadKubectlBinaries.fromStore(preferences?.downloadKubectlBinaries);
this.downloadBinariesPath = DESCRIPTORS.downloadBinariesPath.fromStore(preferences?.downloadBinariesPath);
this.kubectlBinariesPath = DESCRIPTORS.kubectlBinariesPath.fromStore(preferences?.kubectlBinariesPath);
this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin);
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
this.terminalConfig = DESCRIPTORS.terminalConfig.fromStore(preferences?.terminalConfig);
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
for (const [key, { fromStore }] of entries(DESCRIPTORS)) {
const curVal = this[key];
const newVal = fromStore((preferences)?.[key] as never) as never;
if (
isObservableArray(curVal)
|| isObservableSet(curVal)
|| isObservableMap(curVal)
) {
curVal.replace(newVal);
} else {
this[key] = newVal;
}
}
}
toJSON(): UserStoreModel {
const model: UserStoreModel = {
lastSeenAppVersion: this.lastSeenAppVersion,
preferences: {
httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy),
shell: DESCRIPTORS.shell.toStore(this.shell),
colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme),
terminalTheme: DESCRIPTORS.terminalTheme.toStore(this.terminalTheme),
localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone),
allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs),
allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry),
allowErrorReporting: DESCRIPTORS.allowErrorReporting.toStore(this.allowErrorReporting),
downloadMirror: DESCRIPTORS.downloadMirror.toStore(this.downloadMirror),
downloadKubectlBinaries: DESCRIPTORS.downloadKubectlBinaries.toStore(this.downloadKubectlBinaries),
downloadBinariesPath: DESCRIPTORS.downloadBinariesPath.toStore(this.downloadBinariesPath),
kubectlBinariesPath: DESCRIPTORS.kubectlBinariesPath.toStore(this.kubectlBinariesPath),
openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin),
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
terminalConfig: DESCRIPTORS.terminalConfig.toStore(this.terminalConfig),
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
},
};
const preferences = fromEntries(
entries(DESCRIPTORS)
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
) as UserPreferencesModel;
return toJS(model);
return toJS({
lastSeenAppVersion: this.lastSeenAppVersion,
preferences,
});
}
}

View File

@ -21,8 +21,8 @@ export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
}
/**
* Like `getOrInsert` but specifically for when `V` is `Map<any, any>` so that
* the typings are inferred.
* Like `getOrInsert` but specifically for when `V` is `Map<MK, MV>` so that
* the typings are inferred correctly.
*/
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
return getOrInsert(map, key, new Map<MK, MV>());
@ -37,11 +37,39 @@ export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
}
/**
* Like `getOrInsert` but with delayed creation of the item
* Like `getOrInsert` but with delayed creation of the item. Which is useful
* if it is very expensive to create the initial value.
*/
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, value: () => V): V {
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V {
if (!map.has(key)) {
map.set(key, value());
map.set(key, builder());
}
return map.get(key);
}
/**
* Set the value associated with `key` iff there was not a previous value
* @param map The map to interact with
* @throws if `key` already in map
* @returns `this` so that `strictSet` can be chained
*/
export function strictSet<K, V>(map: Map<K, V>, key: K, val: V): typeof map {
if (map.has(key)) {
throw new TypeError("Duplicate key in map");
}
return map.set(key, val);
}
/**
* Get the value associated with `key`
* @param map The map to interact with
* @throws if `key` did not a value associated with it
*/
export function strictGet<K, V>(map: Map<K, V>, key: K): V {
if (!map.has(key)) {
throw new TypeError("key not in map");
}
return map.get(key);

View File

@ -1,70 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, ObservableMap, runInAction } from "mobx";
export function multiSet<T, V>(map: Map<T, V>, newEntries: [T, V][]): void {
runInAction(() => {
for (const [key, val] of newEntries) {
map.set(key, val);
}
});
}
export class ExtendedMap<K, V> extends Map<K, V> {
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
return new ExtendedMap<K, V>(entries);
}
/**
* Get the value behind `key`. If it was not present, first insert the value returned by `getVal`
* @param key The key to insert into the map with
* @param getVal A function that returns a new instance of `V`.
* @returns The value in the map
*/
getOrInsert(key: K, getVal: () => V): V {
if (this.has(key)) {
return this.get(key);
}
return this.set(key, getVal()).get(key);
}
/**
* Set the value associated with `key` iff there was not a previous value
* @throws if `key` already in map
* @returns `this` so that `strictSet` can be chained
*/
strictSet(key: K, val: V): this {
if (this.has(key)) {
throw new TypeError("Duplicate key in map");
}
return this.set(key, val);
}
/**
* Get the value associated with `key`
* @throws if `key` did not a value associated with it
*/
strictGet(key: K): V {
if (!this.has(key)) {
throw new TypeError("key not in map");
}
return this.get(key);
}
}
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
@action
getOrInsert(key: K, getVal: () => V): V {
if (this.has(key)) {
return this.get(key);
}
return this.set(key, getVal()).get(key);
}
}

View File

@ -25,13 +25,12 @@ export * from "./delay";
export * from "./disposer";
export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./extended-map";
export * from "./formatDuration";
export * from "./getRandId";
export * from "./hash-set";
export * from "./n-fircate";
export * from "./objects";
export * from "./openExternal";
export * from "./openBrowser";
export * from "./paths";
export * from "./promise-exec";
export * from "./reject-promise";

View File

@ -7,6 +7,10 @@
* A better typed version of `Object.fromEntries` where the keys are known to
* be a specific subset
*/
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): { [k in Key]: T } {
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T> {
return Object.fromEntries(entries) as { [k in Key]: T };
}
export function entries<T extends Record<string, any>>(obj: T): [keyof T, T[keyof T]][] {
return Object.entries(obj);
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { shell } from "electron";
const allowedProtocols = new Set(["http:", "https:"]);
/**
* Opens a link using the program configured as the default browser
* on the target platform. Will reject URLs with a scheme other than
* http or https to prevent programs other than the default browser
* running.
*
* @param url The URL to open
*/
export function openBrowser(url: string): Promise<void> {
if (allowedProtocols.has(new URL(url).protocol)) {
return shell.openExternal(url);
}
return Promise.reject(new TypeError("not an http(s) URL"));
}
/**
* @deprecated use openBrowser
*/
export const openExternal = openBrowser;

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Opens a link in external browser
import { shell } from "electron";
export function openExternal(url: string) {
return shell.openExternal(url);
}

View File

@ -3,6 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export { Singleton, openExternal } from "../../common/utils";
export { Singleton, openExternal, openBrowser } from "../../common/utils";
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
export { cssNames } from "../../renderer/utils/cssNames";

View File

@ -32,7 +32,7 @@ interface Dependencies {
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
installExtension: (name: string) => Promise<void>;
installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void>
installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void>;
extensionPackageRootDirectory: string;
}

View File

@ -13,7 +13,7 @@ import type { PackageJson } from "type-fest";
const logModule = "[EXTENSION-INSTALLER]";
interface Dependencies {
extensionPackageRootDirectory: string
extensionPackageRootDirectory: string;
}
/**

View File

@ -17,7 +17,7 @@ interface FSProvisionModel {
}
interface Dependencies {
directoryForExtensionData: string
directoryForExtensionData: string;
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {

View File

@ -23,13 +23,13 @@ import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
const logModule = "[EXTENSIONS-LOADER]";
interface Dependencies {
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void
createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension,
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension;
}
export interface ExtensionLoading {
isBundled: boolean,
loaded: Promise<void>
isBundled: boolean;
loaded: Promise<void>;
}
/**

View File

@ -36,7 +36,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
protected state = observable.map<LensExtensionId, LensExtensionState>();
isEnabled({ id, isBundled }: { id: string, isBundled: boolean }): boolean {
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 isBundled || Boolean(this.state.get(id)?.enabled);

View File

@ -10,5 +10,5 @@ import type { FileSystemProvisionerStore } from "./extension-loader/create-exten
export const setLensExtensionDependencies = Symbol("set-lens-extension-dependencies");
export interface LensExtensionDependencies {
fileSystemProvisionerStore: FileSystemProvisionerStore
fileSystemProvisionerStore: FileSystemProvisionerStore;
}

View File

@ -38,7 +38,7 @@ export interface PageParams<V = any> {
export interface PageComponentProps<P extends PageParams = {}> {
params?: {
[N in keyof P]: PageParam<P[N]>;
}
};
}
export interface RegisteredPage {

View File

@ -7,7 +7,7 @@ import { pathNames, PathName } from "../../common/app-paths/app-path-names";
import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
interface Dependencies {
getAppPath: (name: PathName) => string
getAppPath: (name: PathName) => string;
}
export const getAppPaths = ({ getAppPath }: Dependencies) =>

View File

@ -10,7 +10,7 @@ import { FSWatcher, watch } from "chokidar";
import fs from "fs";
import path from "path";
import type stream from "stream";
import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop } from "../../../common/utils";
import { bytesToUnits, Disposer, getOrInsertWith, iter, noop } from "../../../common/utils";
import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
@ -48,8 +48,8 @@ const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
interface Dependencies {
directoryForKubeConfigs: string
createCluster: (model: ClusterModel) => Cluster
directoryForKubeConfigs: string;
createCluster: (model: ClusterModel) => Cluster;
}
const kubeConfigSyncName = "lens:kube-sync";
@ -294,7 +294,7 @@ const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source
};
const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
const rootSource = observable.map<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: FSWatcher;
@ -335,7 +335,7 @@ const watchFileChanges = (filePath: string, dependencies: Dependencies): [ICompu
cleanup();
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: rootSource.getOrInsert(childFilePath, observable.map),
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
@ -353,7 +353,7 @@ const watchFileChanges = (filePath: string, dependencies: Dependencies): [ICompu
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: rootSource.getOrInsert(childFilePath, observable.map),
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, computed, type IComputedValue, type IObservableArray, makeObservable, observable } from "mobx";
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor } from "../../common/catalog";
import { iter } from "../../common/utils";

View File

@ -8,8 +8,8 @@ import type { Cluster } from "../../common/cluster/cluster";
import { k8sRequest } from "../k8s-request";
export type ClusterDetectionResult = {
value: string | number | boolean
accuracy: number
value: string | number | boolean;
accuracy: number;
};
export class BaseClusterDetector {

View File

@ -26,7 +26,7 @@ interface PrometheusServicePreferences {
}
interface Dependencies {
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
}
export class ContextHandler {

View File

@ -14,8 +14,8 @@ import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-char
import { iter, sortCharts } from "../../common/utils";
interface ChartCacheEntry {
data: Buffer,
mtimeMs: number,
data: Buffer;
mtimeMs: number;
}
export interface HelmCacheFile {

View File

@ -104,10 +104,8 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
];
try {
const output = await execHelm(args);
return {
log: output,
log: await execHelm(args),
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
};
} finally {

View File

@ -19,19 +19,19 @@ export type HelmEnv = Record<string, string> & {
};
export interface HelmRepoConfig {
repositories: HelmRepo[]
repositories: HelmRepo[];
}
export interface HelmRepo {
name: string;
url: string;
cacheFilePath?: string
caFile?: string,
certFile?: string,
insecureSkipTlsVerify?: boolean,
keyFile?: string,
username?: string,
password?: string,
cacheFilePath?: string;
caFile?: string;
certFile?: string;
insecureSkipTlsVerify?: boolean;
keyFile?: string;
username?: string;
password?: string;
}
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
@ -49,9 +49,9 @@ async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFile
export class HelmRepoManager extends Singleton {
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected initialized: boolean;
protected didUpdateOnce: boolean;
public static async loadAvailableRepos(): Promise<HelmRepo[]> {
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
@ -59,21 +59,31 @@ export class HelmRepoManager extends Singleton {
timeout: 10000,
});
return orderBy<HelmRepo>(res.body, repo => repo.name);
return orderBy(res.body as HelmRepo[], repo => repo.name);
}
private async init() {
private async ensureInitialized() {
helmCli.setLogger(logger);
await helmCli.ensureBinary();
if (!this.initialized) {
this.helmEnv = await HelmRepoManager.parseHelmEnv();
await HelmRepoManager.update();
this.initialized = true;
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list();
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
}
protected static async parseHelmEnv() {
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
@ -95,38 +105,28 @@ export class HelmRepoManager extends Singleton {
return repos.find(repo => repo.name === name);
}
private async readConfig(): Promise<HelmRepoConfig> {
private async list(): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(this.helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig);
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig as HelmRepoConfig;
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return {
repositories: [],
};
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
if (!this.initialized) {
await this.init();
}
await this.ensureInitialized();
const { repositories } = await this.readConfig();
const repos = await this.list();
if (!repositories.length) {
await HelmRepoManager.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
return await this.repositories();
}
return repositories.map(repo => ({
return repos.map(repo => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
@ -137,25 +137,14 @@ export class HelmRepoManager extends Singleton {
}
}
public static async update() {
public async update() {
return execHelm([
"repo",
"update",
]);
}
public static async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
return execHelm([
"repo",
"add",
name,
url,
]);
}
public static async addCustomRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
public async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",
@ -191,7 +180,7 @@ export class HelmRepoManager extends Singleton {
return execHelm(args);
}
public static async removeRepo({ name, url }: HelmRepo): Promise<string> {
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm([

View File

@ -10,7 +10,7 @@ import * as Mobx from "mobx";
import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName, isIntegrationTesting, isMac, isWindows, productName, isDevelopment } from "../common/vars";
import { appName, isIntegrationTesting, isMac, isWindows, productName } from "../common/vars";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
@ -176,7 +176,7 @@ di.runSetups().then(() => {
try {
logger.info("🔌 Starting LensProxy");
await lensProxy.listen(); // lensProxy.port available
await lensProxy.listen();
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
@ -228,16 +228,6 @@ di.runSetups().then(() => {
logger.info("🖥️ Starting WindowManager");
const windowManager = WindowManager.createInstance();
// Override main content view url to local webpack-dev-server to support HMR / live-reload
if (isDevelopment) {
const { createDevServer } = await import("../../webpack.dev-server");
const devServer = createDevServer(lensProxy.port);
windowManager.mainContentUrl = `http://localhost:${devServer.options.port}`;
await devServer.start();
}
const menuItems = di.inject(electronMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);

View File

@ -26,7 +26,7 @@ import { showOpenDialog } from "../../ipc/dialog";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window";
interface Dependencies {
electronMenuItems: IComputedValue<MenuRegistration[]>,
electronMenuItems: IComputedValue<MenuRegistration[]>;
directoryForLensLocalStorage: string;
}

View File

@ -5,7 +5,7 @@
import { BrowserWindow, dialog, OpenDialogOptions } from "electron";
export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[]; }> {
export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[] }> {
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOptions);
return { canceled, filePaths };

View File

@ -13,7 +13,7 @@ import logger from "../logger";
import { LensProxy } from "../lens-proxy";
interface Dependencies {
directoryForTemp: string
directoryForTemp: string;
}
export class KubeconfigManager {

View File

@ -64,10 +64,10 @@ interface Dependencies {
directoryForKubectlBinaries: string;
userStore: {
kubectlBinariesPath?: string
downloadBinariesPath?: string
downloadKubectlBinaries: boolean
downloadMirror: string
kubectlBinariesPath?: string;
downloadBinariesPath?: string;
downloadKubectlBinaries: boolean;
downloadMirror: string;
};
}

View File

@ -20,7 +20,7 @@ import { getBoolean } from "./utils/parse-query";
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
export interface LensProxyFunctions {
getClusterForRequest: GetClusterForRequest,
getClusterForRequest: GetClusterForRequest;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
kubeApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
}

View File

@ -2,14 +2,15 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron";
import { autorun, IComputedValue } from "mobx";
import type { WindowManager } from "../window-manager";
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../../common/vars";
import logger from "../logger";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc";
import packageJson from "../../../package.json";
import { openBrowser } from "../../common/utils";
import * as packageJson from "../../../package.json";
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../../common/routes";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { MenuRegistration } from "./menu-registration";
@ -261,14 +262,18 @@ export function getAppMenu(
label: "Documentation",
id: "documentation",
click: async () => {
shell.openExternal(docsUrl);
openBrowser(docsUrl).catch(error => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
{
label: "Support",
id: "support",
click: async () => {
shell.openExternal(supportUrl);
openBrowser(supportUrl).catch(error => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
...ignoreIf(isMac, [

View File

@ -37,8 +37,8 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
}
interface Dependencies {
extensionLoader: ExtensionLoader
extensionsStore: ExtensionsStore
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
}
export class LensProtocolRouterMain extends proto.LensProtocolRouter {

View File

@ -12,7 +12,7 @@ import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean,
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean;
createShellSession: (args: {
webSocket: WebSocket;

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ExtendedMap } from "../../../../common/utils";
import { getOrInsertMap } from "../../../../common/utils";
import type { ClusterId } from "../../../../common/cluster-types";
import { ipcMainHandle } from "../../../../common/ipc";
import crypto from "crypto";
@ -11,15 +11,14 @@ import { promisify } from "util";
const randomBytes = promisify(crypto.randomBytes);
export class ShellRequestAuthenticator {
private tokens = new ExtendedMap<ClusterId, Map<string, Uint8Array>>();
private tokens = new Map<ClusterId, Map<string, Uint8Array>>();
init() {
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
const authToken = Uint8Array.from(await randomBytes(128));
const forCluster = getOrInsertMap(this.tokens, clusterId);
this.tokens
.getOrInsert(clusterId, () => new Map())
.set(tabId, authToken);
forCluster.set(tabId, authToken);
return authToken;
});

View File

@ -8,8 +8,8 @@ import type net from "net";
import type { Cluster } from "../../common/cluster/cluster";
export interface ProxyApiRequestArgs {
req: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
cluster: Cluster,
req: http.IncomingMessage;
socket: net.Socket;
head: Buffer;
cluster: Cluster;
}

View File

@ -9,7 +9,7 @@ import type http from "http";
import path from "path";
import { readFile } from "fs-extra";
import type { Cluster } from "../common/cluster/cluster";
import { apiPrefix, appName, publicPath } from "../common/vars";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
import { HelmApiRoute, KubeconfigRoute, MetricsRoute, PortForwardRoute, ResourceApplierApiRoute, VersionRoute } from "./routes";
import logger from "./logger";
@ -40,7 +40,7 @@ export interface LensApiRequest<P = any> {
query: URLSearchParams;
raw: {
req: http.IncomingMessage;
}
};
}
function getMimeType(filename: string) {
@ -61,7 +61,7 @@ function getMimeType(filename: string) {
}
interface Dependencies {
routePortForward: (request: LensApiRequest) => Promise<void>
routePortForward: (request: LensApiRequest) => Promise<void>;
}
export class Router {
@ -110,7 +110,7 @@ export class Router {
};
}
protected static async handleStaticFile({ params, response }: LensApiRequest): Promise<void> {
protected static async handleStaticFile({ params, response, raw: { req }}: LensApiRequest): Promise<void> {
let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
@ -124,6 +124,19 @@ export class Router {
}
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
const toWebpackDevServer = filename.includes(appName) || filename.includes("hot-update") || req.url.includes("sockjs-node");
if (isDevelopment && toWebpackDevServer) {
const redirectLocation = `http://localhost:${webpackDevServerPort}${req.url}`;
response.statusCode = 307;
response.setHeader("Location", redirectLocation);
return response.end();
}
const data = await readFile(asset);
response.setHeader("Content-Type", getMimeType(asset));

View File

@ -19,7 +19,7 @@ export interface PortForwardArgs {
}
interface Dependencies {
getKubectlBinPath: (bundled: boolean) => Promise<string>
getKubectlBinPath: (bundled: boolean) => Promise<string>;
}
export class PortForward {

View File

@ -133,7 +133,7 @@ export abstract class ShellSession {
protected abstract get cwd(): string | undefined;
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string>, cwd: string): { shellProcess: pty.IPty, resume: boolean } {
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
const resume = ShellSession.processes.has(this.terminalId);
if (!resume) {

View File

@ -7,8 +7,8 @@ export interface TrayMenuRegistration {
label?: string;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
type?: "normal" | "separator" | "submenu"
type?: "normal" | "separator" | "submenu";
toolTip?: string;
enabled?: boolean;
submenu?: TrayMenuRegistration[]
submenu?: TrayMenuRegistration[];
}

View File

@ -5,11 +5,11 @@
import type { ClusterId } from "../common/cluster-types";
import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import { app, BrowserWindow, dialog, ipcMain, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { ipcMainOn } from "../common/ipc";
import { delay, iter, Singleton } from "../common/utils";
import { delay, iter, Singleton, openBrowser } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger";
@ -28,8 +28,6 @@ export interface SendToViewArgs {
}
export class WindowManager extends Singleton {
public mainContentUrl = `http://localhost:${LensProxy.getInstance().port}`;
protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State;
@ -43,6 +41,10 @@ export class WindowManager extends Singleton {
this.bindEvents();
}
get mainUrl() {
return `http://localhost:${LensProxy.getInstance().port}`;
}
private async initMainWindow(showSplash: boolean) {
// Manage main window size and position with state persistence
if (!this.windowState) {
@ -132,7 +134,9 @@ export class WindowManager extends Singleton {
webPreferences.nodeIntegration = false;
})
.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
openBrowser(details.url).catch(error => {
logger.error("[WINDOW-MANAGER]: failed to open browser", { error });
});
return { action: "deny" };
});
@ -140,8 +144,8 @@ export class WindowManager extends Singleton {
try {
if (showSplash) await this.showSplash();
logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainContentUrl} ...`);
await this.mainWindow.loadURL(this.mainContentUrl);
logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainUrl} ...`);
await this.mainWindow.loadURL(this.mainUrl);
} catch (error) {
logger.error("Loading main window failed", { error });
dialog.showErrorBox("ERROR!", error.toString());

View File

@ -75,7 +75,7 @@ function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">):
};
}
function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: string, oldId: string }): void {
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`);

View File

@ -5,7 +5,7 @@
import type Conf from "conf";
import type { Migrations } from "conf/dist/source/types";
import { ExtendedMap, iter } from "../common/utils";
import { getOrInsert, iter } from "../common/utils";
import { isTestEnv } from "../common/vars";
export function migrationLog(...args: any[]) {
@ -15,15 +15,15 @@ export function migrationLog(...args: any[]) {
}
export interface MigrationDeclaration {
version: string,
version: string;
run(store: Conf<any>): void;
}
export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {
const migrations = new ExtendedMap<string, ((store: Conf<any>) => void)[]>();
const migrations = new Map<string, ((store: Conf<any>) => void)[]>();
for (const decl of declarations) {
migrations.getOrInsert(decl.version, () => []).push(decl.run);
getOrInsert(migrations, decl.version, []).push(decl.run);
}
return Object.fromEntries(

View File

@ -5,6 +5,6 @@
import { createHash } from "crypto";
export function generateNewIdFor(cluster: { kubeConfigPath: string, contextName: string }): string {
export function generateNewIdFor(cluster: { kubeConfigPath: string; contextName: string }): string {
return createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex");
}

View File

@ -9,7 +9,7 @@ import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegis
import "../../common/catalog-entities";
import type { Cluster } from "../../common/cluster/cluster";
import { ClusterStore } from "../../common/cluster-store/cluster-store";
import { type Disposer, iter } from "../utils";
import { Disposer, iter } from "../utils";
import { once } from "lodash";
import logger from "../../common/logger";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";

View File

@ -21,19 +21,19 @@ export enum TerminalChannels {
}
export type TerminalMessage = {
type: TerminalChannels.STDIN,
data: string,
type: TerminalChannels.STDIN;
data: string;
} | {
type: TerminalChannels.STDOUT,
data: string,
type: TerminalChannels.STDOUT;
data: string;
} | {
type: TerminalChannels.CONNECTED
type: TerminalChannels.CONNECTED;
} | {
type: TerminalChannels.RESIZE,
type: TerminalChannels.RESIZE;
data: {
width: number,
height: number,
},
width: number;
height: number;
};
};
enum TerminalColor {

View File

@ -57,7 +57,7 @@ export enum WebSocketApiState {
}
export interface WebSocketEvents {
open: () => void,
open: () => void;
data: (message: string) => void;
close: () => void;
}
@ -65,7 +65,7 @@ export interface WebSocketEvents {
type Defaulted<Params, DefaultParams extends keyof Params> = Required<Pick<Params, DefaultParams>> & Omit<Params, DefaultParams>;
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
protected socket: WebSocket;
protected socket?: WebSocket | null;
protected pendingCommands: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = [];
protected reconnectTimer?: any;
protected pingTimer?: any;
@ -181,7 +181,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
if (error) {
const { reconnectDelay } = this.params;
if (reconnectDelay) {
if (reconnectDelay && this.socket) {
const url = this.socket.url;
this.writeLog("will reconnect in", `${reconnectDelay}s`);

View File

@ -136,14 +136,7 @@ export async function bootstrap(di: DependencyInjectionContainer) {
App = (await import("./frames/cluster-frame/cluster-frame")).ClusterFrame;
}
try {
await initializeApp(rootElem);
} catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href,
isTopFrameView: process.isMainFrame,
});
}
await initializeApp(rootElem);
render(
<DiContextProvider value={{ di }}>

View File

@ -9,7 +9,7 @@ import type { ClusterContext } from "../../common/k8s-api/cluster-context";
import { computed, makeObservable } from "mobx";
interface Dependencies {
namespaceStore: NamespaceStore
namespaceStore: NamespaceStore;
}
export class ClusterFrameContext implements ClusterContext {

View File

@ -34,7 +34,7 @@ interface Option {
}
interface Dependencies {
getCustomKubeConfigDirectory: (directoryName: string) => string
getCustomKubeConfigDirectory: (directoryName: string) => string;
}
function getContexts(config: KubeConfig): Map<string, Option> {

View File

@ -16,7 +16,7 @@ import { navigate } from "../../navigation";
import { catalogCategoryRegistry } from "../../api/catalog-category-registry";
export type CatalogAddButtonProps = {
category: CatalogCategory
category: CatalogCategory;
};
type CategoryId = string;

View File

@ -25,7 +25,7 @@ interface Props<T extends CatalogEntity> {
@observer
export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Props<T>> {
categoryIcon(category: CatalogCategory) {
if (Icon.isSvg(category.metadata.icon)) {
if (category.metadata.icon.includes("<svg")) {
return <Icon svg={category.metadata.icon} smallest />;
} else {
return <Icon material={category.metadata.icon} smallest />;

View File

@ -65,7 +65,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
continue;
}
const key = Icon.isSvg(menuItem.icon) ? "svg" : "material";
const key = menuItem.icon.includes("<svg") ? "svg" : "material";
items.push(
<MenuItem key={menuItem.title} onClick={() => this.onMenuItemClick(menuItem)}>

View File

@ -11,7 +11,7 @@ import { CatalogCategory, catalogCategoryRegistry } from "../../../../common/cat
import { autoBind, disposer } from "../../../../common/utils";
interface Dependencies {
registry: CatalogEntityRegistry
registry: CatalogEntityRegistry;
}
export class CatalogEntityStore extends ItemStore<CatalogEntity> {

View File

@ -28,7 +28,7 @@ function getCategoryIcon(category: CatalogCategory) {
const { icon } = category.metadata ?? {};
if (typeof icon === "string") {
return Icon.isSvg(icon)
return icon.includes("<svg")
? <Icon small svg={icon}/>
: <Icon small material={icon}/>;
}

View File

@ -9,7 +9,7 @@ import { MenuItem } from "../menu";
import type { CatalogEntity } from "../../api/catalog-entity";
export function HotbarToggleMenuItem(props: { entity: CatalogEntity, addContent: ReactNode, removeContent: ReactNode }) {
export function HotbarToggleMenuItem(props: { entity: CatalogEntity; addContent: ReactNode; removeContent: ReactNode }) {
const store = HotbarStore.getInstance();
const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity));

View File

@ -13,7 +13,7 @@ import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-ove
import { withInjectables } from "@ogre-tools/injectable-react";
interface Dependencies {
clusterOverviewStore: ClusterOverviewStore
clusterOverviewStore: ClusterOverviewStore;
}
const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore }: Dependencies) => {

View File

@ -21,7 +21,7 @@ import clusterOverviewStoreInjectable
from "./cluster-overview-store/cluster-overview-store.injectable";
interface Dependencies {
clusterOverviewStore: ClusterOverviewStore
clusterOverviewStore: ClusterOverviewStore;
}
const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => {

View File

@ -5,9 +5,9 @@
import { action, observable, reaction, when, makeObservable } from "mobx";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { Cluster, clusterApi, getMetricsByNodeNames, type IClusterMetrics } from "../../../../common/k8s-api/endpoints";
import { Cluster, clusterApi, getMetricsByNodeNames, IClusterMetrics } from "../../../../common/k8s-api/endpoints";
import { autoBind, StorageHelper } from "../../../utils";
import { type IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
import { IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
import { nodesStore } from "../../+nodes/nodes.store";
export enum MetricType {
@ -22,11 +22,11 @@ export enum MetricNodeRole {
export interface ClusterOverviewStorageState {
metricType: MetricType;
metricNodeRole: MetricNodeRole,
metricNodeRole: MetricNodeRole;
}
interface Dependencies {
storage: StorageHelper<ClusterOverviewStorageState>
storage: StorageHelper<ClusterOverviewStorageState>;
}
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState {

View File

@ -28,8 +28,8 @@ import type { KubeObject } from "../../../common/k8s-api/kube-object";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
interface Dependencies {
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer,
clusterOverviewStore: ClusterOverviewStore
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer;
clusterOverviewStore: ClusterOverviewStore;
}
@observer

View File

@ -24,7 +24,7 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
}
interface Dependencies {
clusterOverviewStore: ClusterOverviewStore
clusterOverviewStore: ClusterOverviewStore;
}
const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependencies) => {

View File

@ -27,7 +27,7 @@ interface Props extends KubeObjectDetailsProps<Secret> {
export class SecretDetails extends React.Component<Props> {
@observable isSaving = false;
@observable data: { [name: string]: string } = {};
revealSecret = new Set<string>();
@observable revealSecret = observable.set<string>();
constructor(props: Props) {
super(props);

View File

@ -23,7 +23,7 @@ export interface KubeEventDetailsProps {
}
interface Dependencies {
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer;
}
@observer

View File

@ -22,7 +22,7 @@ export interface ExtensionInfo {
interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>;
extensionInstallationStateStore: ExtensionInstallationStateStore
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({

View File

@ -34,9 +34,9 @@ interface Dependencies {
installRequest: InstallRequest,
) => Promise<InstallRequestValidated | null>;
getExtensionDestFolder: (name: string) => string
getExtensionDestFolder: (name: string) => string;
extensionInstallationStateStore: ExtensionInstallationStateStore
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
export const attemptInstall =

View File

@ -26,7 +26,7 @@ export interface InstallRequestValidated {
}
interface Dependencies {
extensionDiscovery: ExtensionDiscovery
extensionDiscovery: ExtensionDiscovery;
}
export const createTempFilesAndValidate =

View File

@ -16,9 +16,9 @@ import React from "react";
import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies {
extensionLoader: ExtensionLoader
getExtensionDestFolder: (name: string) => string
extensionInstallationStateStore: ExtensionInstallationStateStore
extensionLoader: ExtensionLoader;
getExtensionDestFolder: (name: string) => string;
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
export const unpackExtension =

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