Extensions-api: initial hello-world example (#817)

Signed-off-by: Roman <ixrock@gmail.com>
Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
RomanandAlex Andreev authored and GitHub committed 2020-09-09 16:19:02 +03:00
1 parent f1b03990ea
commit 5daf53e6cb
19 files changed
+184 -85

No files matched your search

@@ -1,17 +0,0 @@
import { LensExtension, Icon, LensRuntimeRendererEnv } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts"
// todo: register custom icon in cluster-menu
// todo: register custom view by clicking the item
export default class ExampleExtension extends LensExtension {
async enable(runtime: /*LensRuntimeRendererEnv*/ any): Promise<any> {
try {
super.enable(runtime);
runtime.logger.info('EXAMPLE EXTENSION: ENABLE() override');
} catch (err){
console.error(err)
}
}
}
// <Icon material="camera" onClick={() => console.log("done")}/>
@@ -0,0 +1,47 @@
import { Button, DynamicPageType, Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.ts"
import React from "react";
import path from "path";
export default class ExampleExtension extends LensExtension {
onActivate() {
console.log('EXAMPLE EXTENSION: ACTIVATED', this.getMeta());
this.registerPage({
type: DynamicPageType.CLUSTER,
path: "/extension-example",
menuTitle: "Example Extension",
components: {
Page: () => <ExtensionPage extension={this}/>,
MenuIcon: ExtensionIcon,
}
})
}
onDeactivate() {
console.log('EXAMPLE EXTENSION: DEACTIVATED', this.getMeta());
}
}
export function ExtensionIcon(props: {} /*IconProps |*/) {
return <Icon {...props} material="camera" tooltip={path.basename(__filename)}/>
}
export class ExtensionPage extends React.Component<{ extension: ExampleExtension }> {
deactivate = () => {
const { extension } = this.props;
extension.runtime.navigate("/")
extension.disable();
}
render() {
const { MainLayout } = this.props.extension.runtime.components;
return (
<MainLayout className="ExampleExtension">
<div className="flex column gaps align-flex-start">
<p>Hello from extensions-api!</p>
<p>File: <i>{__filename}</i></p>
<Button accent label="Deactivate" onClick={this.deactivate}/>
</div>
</MainLayout>
)
}
}
@@ -2,9 +2,10 @@
"name": "extension-example",
"version": "1.0.0",
"description": "Example extension",
"main": "example-extension.ts",
"main": "example-extension.js",
"lens": {
"metadata": {}
"metadata": {},
"styles": []
},
"dependencies": {
}
@@ -8,6 +8,6 @@
},
"include": [
"../../../types",
"./example-extension.ts"
"./example-extension.tsx"
]
}
+2 -1
View File
@@ -2,7 +2,8 @@
export type { LensRuntimeRendererEnv } from "./lens-runtime";
// APIs
export * from "./extension"
export * from "./lens-extension"
export { DynamicPageType } from "./register-page";
// Common UI components
export * from "../renderer/components/icon"
+7 -9
View File
@@ -3,17 +3,17 @@ import path from "path";
import fs from "fs-extra";
import { action, observable, reaction, toJS, } from "mobx";
import { BaseStore } from "../common/base-store";
import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./extension";
import { isDevelopment, isProduction, isTestEnv } from "../common/vars";
import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./lens-extension";
import { isDevelopment } from "../common/vars";
import logger from "../main/logger";
export interface ExtensionStoreModel {
version: ExtensionVersion;
extensions: Record<ExtensionId, ExtensionModel>
extensions: [ExtensionId, ExtensionModel][]
}
export interface ExtensionModel {
id?: ExtensionId; // available in lens-extension instance
id: ExtensionId;
version: ExtensionVersion;
name: string;
manifestPath: string;
@@ -35,7 +35,6 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
private constructor() {
super({
configName: "lens-extension-store",
syncEnabled: false,
});
}
@@ -48,7 +47,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
if (isDevelopment) {
return path.resolve(__static, "../src/extensions");
}
return path.resolve(__static, "../extensions"); //todo figure out prod
return path.resolve(__static, "../extensions");
}
async load() {
@@ -80,7 +79,6 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
try {
manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call
mainJs = path.resolve(path.dirname(manifestPath), manifestJson.main);
mainJs = mainJs.replace(/\.ts$/i, ".js"); // todo: compile *.ts on the fly?
const extensionModule = __non_webpack_require__(mainJs);
return {
manifestPath: manifestPath,
@@ -132,7 +130,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
this.version = version;
}
if (extensions) {
const currentExtensions = new Map(Object.entries(extensions));
const currentExtensions = new Map(extensions);
this.extensions.forEach(extension => {
if (!currentExtensions.has(extension.id)) {
this.removed.set(extension.id, extension);
@@ -164,7 +162,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
toJSON(): ExtensionStoreModel {
return toJS({
version: this.version,
extensions: this.extensions.toJSON(),
extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]),
}, {
recurseEverything: true,
})
@@ -1,17 +1,20 @@
import type { ExtensionModel } from "./extension-store";
import type { LensRuntimeRendererEnv } from "./lens-runtime";
import type { PageRegistration } from "./register-page";
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import extensionManifest from "./example-extension/package.json"
import logger from "../main/logger";
export type ExtensionId = string; // instance-id or abs path to "%lens-extension/manifest.json"
export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
export type ExtensionVersion = string | number;
export type ExtensionManifest = typeof extensionManifest & ExtensionModel;
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: Function[] = [];
@observable name = "";
@observable description = "";
@@ -19,7 +22,7 @@ export class LensExtension implements ExtensionModel {
@observable manifest: ExtensionManifest;
@observable manifestPath: string;
@observable isEnabled = false;
@observable.ref runtime: Partial<LensRuntimeRendererEnv> = {};
@observable.ref runtime: LensRuntimeRendererEnv;
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest);
@@ -41,14 +44,27 @@ export class LensExtension implements ExtensionModel {
this.isEnabled = true;
this.runtime = runtime;
console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta());
this.onActivate();
}
async disable() {
this.onDeactivate();
this.isEnabled = false;
this.runtime = {};
this.runtime = null;
this.disposers.forEach(cleanUp => cleanUp());
this.disposers.length = 0;
console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta());
}
// todo: add more hooks
protected onActivate() {
// mock
}
protected onDeactivate() {
// mock
}
// todo
async install(downloadUrl?: string) {
return;
@@ -76,7 +92,7 @@ export class LensExtension implements ExtensionModel {
}
toJSON(): ExtensionModel {
return {
return toJS({
id: this.id,
name: this.name,
version: this.version,
@@ -84,6 +100,16 @@ export class LensExtension implements ExtensionModel {
manifestPath: this.manifestPath,
enabled: this.isEnabled,
updateUrl: this.updateUrl,
}, {
recurseEverything: true,
})
}
// Runtime helpers
protected registerPage(params: PageRegistration, autoDisable = true) {
const dispose = this.runtime.dynamicPages.register(params);
if (autoDisable) {
this.disposers.push(dispose);
}
}
}
+13 -6
View File
@@ -1,19 +1,26 @@
// Lens runtime for injecting to extension on activation
import { apiManager } from "../renderer/api/api-manager";
// Lens renderer runtime params available to extensions after activation
import logger from "../main/logger";
import { dynamicPages } from "../renderer/components/cluster-manager/register-page";
import { dynamicPages } from "./register-page";
import { MainLayout } from "../renderer/components/layout/main-layout";
import { navigate } from "../renderer/navigation";
export interface LensRuntimeRendererEnv {
apiManager: typeof apiManager;
navigate: typeof navigate;
logger: typeof logger;
dynamicPages: typeof dynamicPages
components: {
MainLayout: typeof MainLayout
}
}
// todo: expose more public runtime apis: stores, managers, etc.
export function getLensRuntime(): LensRuntimeRendererEnv {
return {
apiManager,
logger,
navigate,
dynamicPages,
components: {
MainLayout // fixme: refactor, import as pure component from "@lens/extensions"
}
}
}
+46
View File
@@ -0,0 +1,46 @@
// Extensions-api -> Dynamic pages
import { computed, observable } from "mobx";
import React from "react";
import type { IconProps } from "../renderer/components/icon";
export enum DynamicPageType {
GLOBAL = "lens-scope",
CLUSTER = "cluster-view-scope",
}
export interface PageRegistration {
path: string; // route-path
menuTitle: string;
type: DynamicPageType;
components: PageComponents;
}
export interface PageComponents {
Page: React.ComponentType<any>;
MenuIcon: React.ComponentType<IconProps>;
}
export class PagesStore {
protected pages = observable.array<PageRegistration>([], { deep: false });
@computed get globalPages() {
return this.pages.filter(page => page.type === DynamicPageType.GLOBAL);
}
@computed get clusterPages() {
return this.pages.filter(page => page.type === DynamicPageType.CLUSTER);
}
// todo: verify paths to avoid collision with existing pages
register(params: PageRegistration) {
this.pages.push(params);
return () => {
this.pages.replace(
this.pages.filter(page => page.components !== params.components)
)
};
}
}
export const dynamicPages = new PagesStore();