mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Visualize extension loading (#1635)
* Visualize extension loading Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
parent
b342ea0bd6
commit
ea98d68e08
@ -1,8 +1,11 @@
|
||||
import chokidar from "chokidar";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import fs from "fs-extra";
|
||||
import { observable, reaction, toJS, when } 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 logger from "../main/logger";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
@ -28,6 +31,10 @@ const logModule = "[EXTENSION-DISCOVERY]";
|
||||
|
||||
export const manifestFilename = "package.json";
|
||||
|
||||
interface ExtensionDiscoveryChannelMessage {
|
||||
isLoaded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
|
||||
* @param lstat the stats to compare
|
||||
@ -49,22 +56,16 @@ export class ExtensionDiscovery {
|
||||
|
||||
private loadStarted = false;
|
||||
|
||||
// This promise is resolved when .load() is finished.
|
||||
// This allows operations to be added after .load() success.
|
||||
private loaded: Promise<void>;
|
||||
// True if extensions have been loaded from the disk after app startup
|
||||
@observable isLoaded = false;
|
||||
whenLoaded = when(() => this.isLoaded);
|
||||
|
||||
// These are called to either resolve or reject this.loaded promise
|
||||
private resolveLoaded: () => void;
|
||||
private rejectLoaded: (error: any) => void;
|
||||
// IPC channel to broadcast changes to extension-discovery from main
|
||||
protected static readonly extensionDiscoveryChannel = "extension-discovery:main";
|
||||
|
||||
public events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
this.loaded = new Promise((resolve, reject) => {
|
||||
this.resolveLoaded = resolve;
|
||||
this.rejectLoaded = reject;
|
||||
});
|
||||
|
||||
this.events = new EventEmitter();
|
||||
}
|
||||
|
||||
@ -98,8 +99,32 @@ export class ExtensionDiscovery {
|
||||
/**
|
||||
* Initializes the class and setups the file watcher for added/removed local extensions.
|
||||
*/
|
||||
init() {
|
||||
async init() {
|
||||
if (ipcRenderer) {
|
||||
await this.initRenderer();
|
||||
} else {
|
||||
await this.initMain();
|
||||
}
|
||||
}
|
||||
|
||||
async initRenderer() {
|
||||
const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => {
|
||||
this.isLoaded = isLoaded;
|
||||
};
|
||||
|
||||
requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage);
|
||||
subscribeToBroadcast(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
|
||||
onMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
async initMain() {
|
||||
this.watchExtensions();
|
||||
handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
|
||||
|
||||
reaction(() => this.toJSON(), () => {
|
||||
this.broadcast();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,7 +135,7 @@ export class ExtensionDiscovery {
|
||||
logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`);
|
||||
|
||||
// Wait until .load() has been called and has been resolved
|
||||
await this.loaded;
|
||||
await this.whenLoaded;
|
||||
|
||||
// chokidar works better than fs.watch
|
||||
chokidar.watch(this.localFolderPath, {
|
||||
@ -208,36 +233,31 @@ export class ExtensionDiscovery {
|
||||
|
||||
this.loadStarted = true;
|
||||
|
||||
try {
|
||||
logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`);
|
||||
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);
|
||||
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();
|
||||
|
||||
this.isLoaded = true;
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false }: {
|
||||
@ -356,6 +376,18 @@ export class ExtensionDiscovery {
|
||||
|
||||
return this.getByManifest(manifestPath, { isBundled });
|
||||
}
|
||||
|
||||
toJSON(): ExtensionDiscoveryChannelMessage {
|
||||
return toJS({
|
||||
isLoaded: this.isLoaded
|
||||
}, {
|
||||
recurseEverything: true
|
||||
});
|
||||
}
|
||||
|
||||
broadcast() {
|
||||
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionDiscovery = new ExtensionDiscovery();
|
||||
|
||||
@ -3,19 +3,20 @@ import "./components/app.scss";
|
||||
import React from "react";
|
||||
import * as Mobx from "mobx";
|
||||
import * as MobxReact from "mobx-react";
|
||||
import * as LensExtensions from "../extensions/extension-api";
|
||||
import { App } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import { isMac } from "../common/vars";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { i18nStore } from "./i18n";
|
||||
import { themeStore } from "./theme.store";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { isMac } from "../common/vars";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import * as LensExtensions from "../extensions/extension-api";
|
||||
import { extensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { App } from "./components/app";
|
||||
import { i18nStore } from "./i18n";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { themeStore } from "./theme.store";
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
init?(): Promise<void>;
|
||||
@ -34,6 +35,7 @@ export async function bootstrap(App: AppComponent) {
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
extensionLoader.init();
|
||||
extensionDiscovery.init();
|
||||
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
|
||||
@ -22,7 +22,8 @@ jest.mock("../../../../extensions/extension-discovery", () => ({
|
||||
...jest.requireActual("../../../../extensions/extension-discovery"),
|
||||
extensionDiscovery: {
|
||||
localFolderPath: "/fake/path",
|
||||
uninstallExtension: jest.fn(() => Promise.resolve())
|
||||
uninstallExtension: jest.fn(() => Promise.resolve()),
|
||||
isLoaded: true
|
||||
}
|
||||
}));
|
||||
|
||||
@ -112,4 +113,17 @@ describe("Extensions", () => {
|
||||
expect(Notifications.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays spinner while extensions are loading", () => {
|
||||
extensionDiscovery.isLoaded = false;
|
||||
const { container } = render(<Extensions />);
|
||||
|
||||
expect(container.querySelector(".Spinner")).toBeInTheDocument();
|
||||
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
waitFor(() =>
|
||||
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -37,6 +37,11 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
> .spinner-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
|
||||
@ -21,6 +21,7 @@ import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } fr
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Spinner } from "../spinner/spinner";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { ExtensionStateStore } from "./extension-install.store";
|
||||
import "./extensions.scss";
|
||||
@ -541,7 +542,7 @@ export class Extensions extends React.Component {
|
||||
value={this.search}
|
||||
onChange={(value) => this.search = value}
|
||||
/>
|
||||
{this.renderExtensions()}
|
||||
{extensionDiscovery.isLoaded ? this.renderExtensions() : <div className="spinner-wrapper"><Spinner/></div>}
|
||||
</div>
|
||||
</PageLayout>
|
||||
</DropFileInput>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user