mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into mobx-6.2
# Conflicts: # extensions/survey/package-lock.json # extensions/survey/package.json # extensions/survey/src/survey-preferences-store.ts # extensions/telemetry/src/telemetry-preferences-store.ts # src/common/user-store.ts # src/extensions/core-api/catalog.ts # src/extensions/extension-discovery.ts # src/extensions/lens-extension.ts # src/main/cluster-manager.ts # src/renderer/bootstrap.tsx # src/renderer/components/+apps-helm-charts/helm-chart.store.ts # src/renderer/components/+apps-releases/release.store.ts # src/renderer/components/+catalog/catalog.tsx # src/renderer/components/+extensions/extension-install.store.ts # src/renderer/components/+extensions/extensions.tsx # src/renderer/components/+preferences/preferences.tsx # src/renderer/components/dock/create-resource.store.ts # src/renderer/components/kube-object/kube-object-list-layout.tsx # src/renderer/theme.store.ts # src/renderer/utils/storageHelper.ts
This commit is contained in:
commit
a0193a2cda
263 files changed
+3401
-31747
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { watch } from "chokidar";
|
||||
import { join, normalize } from "path";
|
||||
import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
jest.mock("../../common/ipc");
|
||||
jest.mock("fs-extra");
|
||||
@@ -17,6 +18,12 @@ jest.mock("../extension-installer", () => ({
|
||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
beforeEach(() => {
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionsStore.resetInstance();
|
||||
ExtensionsStore.getInstanceOrCreate();
|
||||
});
|
||||
|
||||
it("emits add for added extension", async done => {
|
||||
globalThis.__non_webpack_require__.mockImplementation(() => ({
|
||||
name: "my-extension"
|
||||
@@ -36,7 +43,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
@@ -76,7 +83,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { ExtensionLoader } from "../extension-loader";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const manifestPath = "manifest/path";
|
||||
const manifestPath2 = "manifest/path2";
|
||||
const manifestPath3 = "manifest/path3";
|
||||
|
||||
jest.mock("../extensions-store", () => ({
|
||||
extensionsStore: {
|
||||
whenLoaded: Promise.resolve(true),
|
||||
mergeState: jest.fn()
|
||||
ExtensionsStore: {
|
||||
getInstance: () => ({
|
||||
whenLoaded: Promise.resolve(true),
|
||||
mergeState: jest.fn()
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -99,8 +105,12 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe("ExtensionLoader", () => {
|
||||
it("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
beforeEach(() => {
|
||||
ExtensionLoader.resetInstance();
|
||||
});
|
||||
|
||||
it.only("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = ExtensionLoader.getInstanceOrCreate();
|
||||
|
||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
||||
|
||||
@@ -140,20 +150,20 @@ describe("ExtensionLoader", () => {
|
||||
});
|
||||
|
||||
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||
(extensionsStore.mergeState as any).mockClear();
|
||||
(ExtensionsStore.getInstance().mergeState as any).mockClear();
|
||||
|
||||
// Disable sending events in this test
|
||||
(ipcRenderer.on as any).mockImplementation();
|
||||
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
const extensionLoader = ExtensionLoader.getInstanceOrCreate();
|
||||
|
||||
await extensionLoader.init();
|
||||
|
||||
expect(extensionsStore.mergeState).not.toHaveBeenCalled();
|
||||
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
||||
|
||||
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
||||
|
||||
expect(extensionsStore.mergeState).toHaveBeenCalledWith({
|
||||
expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({
|
||||
"manifest/path": {
|
||||
enabled: false,
|
||||
name: "TestExtension"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import path from "path";
|
||||
import hb from "handlebars";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { ResourceApplier } from "../main/resource-applier";
|
||||
import { KubernetesCluster } from "./core-api/stores";
|
||||
import { KubernetesCluster } from "./core-api/catalog";
|
||||
import logger from "../main/logger";
|
||||
import { app } from "electron";
|
||||
import { requestMain } from "../common/ipc";
|
||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
|
||||
export interface ClusterFeatureStatus {
|
||||
/** feature's current version, as set by the implementation */
|
||||
@@ -89,7 +89,7 @@ export abstract class ClusterFeature {
|
||||
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
|
||||
let resources: string[];
|
||||
|
||||
const clusterModel = clusterStore.getById(cluster.metadata.uid);
|
||||
const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
|
||||
|
||||
if (!clusterModel) {
|
||||
throw new Error(`cluster not found`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
export const version = getAppVersion();
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
|
||||
export function getEnabledExtensions(): string[] {
|
||||
return extensionsStore.enabledExtensions;
|
||||
return ExtensionsStore.getInstance().enabledExtensions;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { CatalogEntity } from "../../common/catalog-entity";
|
||||
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
|
||||
|
||||
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
|
||||
export * from "../../common/catalog-entities";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
|
||||
@@ -9,10 +9,12 @@ import * as Store from "./stores";
|
||||
import * as Util from "./utils";
|
||||
import * as ClusterFeature from "./cluster-feature";
|
||||
import * as Interface from "../interfaces";
|
||||
import * as Catalog from "./catalog";
|
||||
|
||||
export {
|
||||
App,
|
||||
EventBus,
|
||||
Catalog,
|
||||
ClusterFeature,
|
||||
Interface,
|
||||
Store,
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
export { ExtensionStore } from "../extension-store";
|
||||
export { KubernetesCluster, KubernetesClusterCategory } from "../../common/catalog-entities/kubernetes-cluster";
|
||||
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
|
||||
export { catalogEntities } from "./catalog";
|
||||
@@ -6,10 +6,10 @@ import { observable, reaction, toJS, when, makeObservable } from "mobx";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { getBundledExtensions } from "../common/utils/app-version";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export interface InstalledExtension {
|
||||
@@ -51,7 +51,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb
|
||||
* - "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 {
|
||||
export class ExtensionDiscovery extends Singleton {
|
||||
protected bundledFolderPath: string;
|
||||
|
||||
private loadStarted = false;
|
||||
@@ -67,6 +67,7 @@ export class ExtensionDiscovery {
|
||||
public events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this);
|
||||
this.events = new EventEmitter();
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export class ExtensionDiscovery {
|
||||
depth: 1,
|
||||
ignoreInitial: true,
|
||||
// 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.
|
||||
// The OS might emit an event for added file even it's not completely written to the file-system.
|
||||
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.
|
||||
@@ -237,7 +238,7 @@ export class ExtensionDiscovery {
|
||||
/**
|
||||
* Uninstalls extension.
|
||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||
* @param extension Extension to unistall.
|
||||
* @param extension Extension to uninstall.
|
||||
*/
|
||||
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
|
||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||
@@ -327,7 +328,7 @@ export class ExtensionDiscovery {
|
||||
manifestJson = __non_webpack_require__(manifestPath);
|
||||
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
|
||||
|
||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||
|
||||
return {
|
||||
id: installedManifestPath,
|
||||
@@ -349,7 +350,7 @@ export class ExtensionDiscovery {
|
||||
|
||||
await this.installBundledPackages(this.packageJsonPath, bundledExtensions);
|
||||
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
|
||||
|
||||
for (const extension of userExtensions) {
|
||||
if ((await fs.pathExists(extension.manifestPath)) === false) {
|
||||
@@ -383,14 +384,9 @@ export class ExtensionDiscovery {
|
||||
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.loadExtensionFromFolder(absPath, { isBundled: true });
|
||||
|
||||
@@ -403,8 +399,7 @@ export class ExtensionDiscovery {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await fs.readdir(folderPath);
|
||||
|
||||
@@ -461,5 +456,3 @@ export class ExtensionDiscovery {
|
||||
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionDiscovery = new ExtensionDiscovery();
|
||||
@@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when, makeObservable } fr
|
||||
import path from "path";
|
||||
import { getHostedCluster } from "../common/cluster-store";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
|
||||
import type { LensMainExtension } from "./lens-main-extension";
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
@@ -24,7 +25,7 @@ const logModule = "[EXTENSIONS-LOADER]";
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
export class ExtensionLoader extends Singleton {
|
||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||
|
||||
@@ -99,11 +100,11 @@ export class ExtensionLoader {
|
||||
await this.initMain();
|
||||
}
|
||||
|
||||
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
|
||||
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
|
||||
|
||||
// save state on change `extension.isEnabled`
|
||||
reaction(() => this.storeState, extensionsState => {
|
||||
extensionsStore.mergeState(extensionsState);
|
||||
ExtensionsStore.getInstance().mergeState(extensionsState);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,8 +216,8 @@ export class ExtensionLoader {
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
const removeItems = [
|
||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
||||
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
|
||||
registries.appPreferenceRegistry.add(extension.appPreferences),
|
||||
registries.entitySettingRegistry.add(extension.entitySettings),
|
||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
||||
registries.commandRegistry.add(extension.commands),
|
||||
];
|
||||
@@ -330,5 +331,3 @@ export class ExtensionLoader {
|
||||
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionLoader = new ExtensionLoader();
|
||||
@@ -52,5 +52,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionsStore = new ExtensionsStore();
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { action, observable, reaction, makeObservable } from "mobx";
|
||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import logger from "../main/logger";
|
||||
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
|
||||
|
||||
@@ -50,7 +50,7 @@ export class LensExtension {
|
||||
* folder name.
|
||||
*/
|
||||
async getExtensionFileFolder(): Promise<string> {
|
||||
return filesystemProvisionerStore.requestDirectory(this.id);
|
||||
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
|
||||
}
|
||||
|
||||
get description() {
|
||||
|
||||
@@ -10,7 +10,7 @@ export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const windowManager = WindowManager.getInstance();
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
pageId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Cluster } from "../main/cluster";
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import { CommandRegistration } from "./registries/command-registry";
|
||||
import { EntitySettingRegistration } from "./registries/entity-setting-registry";
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
globalPages: PageRegistration[] = [];
|
||||
@@ -11,6 +12,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||
appPreferences: AppPreferenceRegistration[] = [];
|
||||
entitySettings: EntitySettingRegistration[] = [];
|
||||
statusBarItems: StatusBarRegistration[] = [];
|
||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
|
||||
import { LensExtension } from "../../lens-extension";
|
||||
import React from "react";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type React from "react";
|
||||
import { CatalogEntity } from "../../common/catalog-entity";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface EntitySettingViewProps {
|
||||
entity: CatalogEntity;
|
||||
}
|
||||
|
||||
export interface EntitySettingComponents {
|
||||
View: React.ComponentType<EntitySettingViewProps>;
|
||||
}
|
||||
|
||||
export interface EntitySettingRegistration {
|
||||
title: string;
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
source?: string;
|
||||
id?: string;
|
||||
components: EntitySettingComponents;
|
||||
}
|
||||
|
||||
export interface RegisteredEntitySetting extends EntitySettingRegistration {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistration, RegisteredEntitySetting> {
|
||||
getRegisteredItem(item: EntitySettingRegistration): RegisteredEntitySetting {
|
||||
return {
|
||||
id: item.id || item.title.toLowerCase(),
|
||||
...item,
|
||||
};
|
||||
}
|
||||
|
||||
getItemsForKind(kind: string, apiVersion: string, source?: string) {
|
||||
const items = this.getItems().filter((item) => {
|
||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||
});
|
||||
|
||||
if (source) {
|
||||
return items.filter((item) => {
|
||||
return !item.source || item.source === source;
|
||||
});
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const entitySettingRegistry = new EntitySettingRegistry();
|
||||
@@ -9,3 +9,4 @@ export * from "./kube-object-detail-registry";
|
||||
export * from "./kube-object-menu-registry";
|
||||
export * from "./kube-object-status-registry";
|
||||
export * from "./command-registry";
|
||||
export * from "./entity-setting-registry";
|
||||
@@ -55,5 +55,4 @@ export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuReg
|
||||
}
|
||||
}
|
||||
|
||||
export const globalPageMenuRegistry = new PageMenuRegistry();
|
||||
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
|
||||
@@ -1,5 +1,6 @@
|
||||
// Common UI components
|
||||
export * from "../../renderer/components/layout/sub-title";
|
||||
export * from "../../renderer/components/input/search-input";
|
||||
|
||||
// layouts
|
||||
export * from "../../renderer/components/layout/page-layout";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { themeStore } from "../../renderer/theme.store";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
|
||||
export function getActiveTheme() {
|
||||
return themeStore.activeTheme;
|
||||
return ThemeStore.getInstance().activeTheme;
|
||||
}
|
||||
Reference in new issue
Block a user