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

Install missing extension if no extension route found and extension name is not installed

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti 2020-12-09 17:14:01 +02:00 committed by Sebastian Malton
parent 6db52e70dd
commit 7a44a2ebc0
4 changed files with 60 additions and 18 deletions

View File

@ -33,7 +33,6 @@ module.exports = {
"indent": ["error", 2, { "indent": ["error", 2, {
"SwitchCase": 1, "SwitchCase": 1,
}], }],
"no-invalid-this": "error",
"no-unused-vars": "off", "no-unused-vars": "off",
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [ "unused-imports/no-unused-vars": [
@ -96,7 +95,6 @@ module.exports = {
"indent": ["error", 2, { "indent": ["error", 2, {
"SwitchCase": 1, "SwitchCase": 1,
}], }],
"no-invalid-this": "error",
"quotes": ["error", "double", { "quotes": ["error", "double", {
"avoidEscape": true, "avoidEscape": true,
"allowTemplateLiterals": true, "allowTemplateLiterals": true,
@ -162,7 +160,6 @@ module.exports = {
"avoidEscape": true, "avoidEscape": true,
"allowTemplateLiterals": true, "allowTemplateLiterals": true,
}], }],
"no-invalid-this": "error",
"semi": "off", "semi": "off",
"@typescript-eslint/semi": ["error"], "@typescript-eslint/semi": ["error"],
"object-shorthand": "error", "object-shorthand": "error",

View File

@ -84,6 +84,18 @@ export class ExtensionLoader {
this.extensions.replace(extensions); this.extensions.replace(extensions);
} }
isInstalled(name: string) {
for (const extensionEntry of this.extensions) {
const [, extension ] = extensionEntry;
if (extension.manifest.name === name) {
return true;
}
}
return false;
}
addExtension(extension: InstalledExtension) { addExtension(extension: InstalledExtension) {
this.extensions.set(extension.id, extension); this.extensions.set(extension.id, extension);
} }
@ -103,7 +115,6 @@ export class ExtensionLoader {
} catch (error) { } catch (error) {
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
} }
} }
removeExtension(lensExtensionId: LensExtensionId) { removeExtension(lensExtensionId: LensExtensionId) {

View File

@ -46,20 +46,23 @@ export type RouteHandler = (params: RouteParams) => void;
export type ExtensionId = string; export type ExtensionId = string;
const EXT_ID_MATCH = "LENS_INTERNAL_EXTENSION_ID_MATCH"; const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel. // IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const lensProtocolChannel = "protocol-handler"; export const lensProtocolChannel = "protocol-handler";
interface ExtensionIdMatch { interface ExtensionUrlMatch {
[EXT_ID_MATCH]: string; [EXTENSION_PUBLISHER_MATCH]: string;
[EXTENSION_NAME_MATCH]: string;
} }
export class LensProtocolRouter extends Singleton { export class LensProtocolRouter extends Singleton {
private extentionRoutes = new Map<ExtensionId, Map<string, RouteHandler>>(); private extentionRoutes = new Map<ExtensionId, Map<string, RouteHandler>>();
private internalRoutes = new Map<string, RouteHandler>(); private internalRoutes = new Map<string, RouteHandler>();
private missingExtensionHandler?: (name: string) => Promise<void>;
private static ExtensionIDSchema = `/:${EXT_ID_MATCH}`; private static ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}/:${EXTENSION_NAME_MATCH}`;
public init() { public init() {
subscribeToBroadcast(lensProtocolChannel, ((_event, { rawUrl }) => { subscribeToBroadcast(lensProtocolChannel, ((_event, { rawUrl }) => {
@ -87,26 +90,39 @@ export class LensProtocolRouter extends Singleton {
case "internal": case "internal":
return this._route(this.internalRoutes, url); return this._route(this.internalRoutes, url);
case "extension": case "extension":
return this._routeToExtension(url); // Possible rejected promise is ignored
this._routeToExtension(url);
default: default:
throw new RoutingError(RoutingErrorType.INVALID_HOST, url); throw new RoutingError(RoutingErrorType.INVALID_HOST, url);
} }
} }
private _routeToExtension(url: Url): void { private async _routeToExtension(url: Url) {
const match = matchPath<ExtensionIdMatch>(url.pathname, LensProtocolRouter.ExtensionIDSchema); const match = matchPath<ExtensionUrlMatch>(url.pathname, LensProtocolRouter.ExtensionUrlSchema);
if (!match) { if (!match) {
throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url); throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url);
} }
const { [EXT_ID_MATCH]: id } = match.params; const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const routes = this.extentionRoutes.get(id); const name = `${publisher}/${partialName}`;
logger.info(`[PROTOCOL ROUTER] Extension ${name} matched`);
const routes = this.extentionRoutes.get(name);
if (!routes) { if (!routes) {
if (this.missingExtensionHandler) {
await this.missingExtensionHandler(name);
// TODO: After installation we can continue to route to the extension..
// but this is difficult, since the promise resolves before extension installation is complete.
return;
} else {
throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url); throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url);
} }
}
this._route(routes, url, true); this._route(routes, url, true);
} }
@ -117,7 +133,7 @@ export class LensProtocolRouter extends Singleton {
if (matchExtension) { if (matchExtension) {
const joinChar = schema.startsWith("/") ? "" : "/"; const joinChar = schema.startsWith("/") ? "" : "/";
schema = `${LensProtocolRouter.ExtensionIDSchema}${joinChar}${schema}`; schema = `${LensProtocolRouter.ExtensionUrlSchema}${joinChar}${schema}`;
} }
return [matchPath(url.pathname, { path: schema }), handler]; return [matchPath(url.pathname, { path: schema }), handler];
@ -132,7 +148,7 @@ export class LensProtocolRouter extends Singleton {
const [match, handler] = route; const [match, handler] = route;
delete match.params[EXT_ID_MATCH]; delete match.params[EXTENSION_NAME_MATCH];
handler({ handler({
pathname: match.params, pathname: match.params,
search: url.query, search: url.query,
@ -151,10 +167,14 @@ export class LensProtocolRouter extends Singleton {
this.extentionRoutes.set(id, new Map()); this.extentionRoutes.set(id, new Map());
} }
if (urlSchema.includes(`:${EXT_ID_MATCH}`)) { if (urlSchema.includes(`:${EXTENSION_NAME_MATCH}`)) {
throw new TypeError("Invalid url path schema"); throw new TypeError("Invalid url path schema");
} }
this.extentionRoutes.get(id).set(urlSchema, handler); this.extentionRoutes.get(id).set(urlSchema, handler);
} }
public onMissingExtension(handler: (name: string) => Promise<void>) {
this.missingExtensionHandler = handler;
}
} }

View File

@ -19,6 +19,8 @@ import { LensApp } from "./lens-app";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import protocolEndpoints from "./api/protocol-endpoints"; import protocolEndpoints from "./api/protocol-endpoints";
import { LensProtocolRouter } from "../main/protocol-handler"; import { LensProtocolRouter } from "../main/protocol-handler";
import logger from "../main/logger";
import { installFromNpm } from "./components/+extensions";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -38,7 +40,19 @@ export async function bootstrap(App: AppComponent) {
extensionLoader.init(); extensionLoader.init();
extensionDiscovery.init(); extensionDiscovery.init();
LensProtocolRouter.getInstance<LensProtocolRouter>().init(); const lensProtocolRouter = LensProtocolRouter.getInstance<LensProtocolRouter>();
lensProtocolRouter.init();
lensProtocolRouter.onMissingExtension(async name => {
if (!extensionLoader.isInstalled(name)) {
logger.info(`[PROTOCOL ROUTER] Extension ${name} not installed, installing..`);
// TODO: This actually resolves before the extension installation is complete
return installFromNpm(name);
} else {
logger.info(`[PROTOCOL ROUTER] Extension already installed, but route is missing.`);
}
});
protocolEndpoints.registerHandlers(); protocolEndpoints.registerHandlers();
// preload common stores // preload common stores