From f1f9a364e265f8dda4333cc007bd94d89f2a4719 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 7 Sep 2020 16:48:10 +0300 Subject: [PATCH 1/8] extensions-api: initial hello-world example which adds new app section: menu icon + page Signed-off-by: Roman --- .gitignore | 1 + .../example-extension/example-extension.ts | 17 -------- .../example-extension/example-extension.tsx | 40 +++++++++++++++++++ src/extensions/example-extension/package.json | 5 ++- .../example-extension/tsconfig.json | 2 +- src/extensions/extension-store.ts | 1 - src/extensions/extension.ts | 15 ++++++- .../cluster-manager/cluster-manager.tsx | 4 ++ .../cluster-manager/clusters-menu.tsx | 15 +++---- .../cluster-manager/register-page.ts | 8 ++-- 10 files changed, 74 insertions(+), 34 deletions(-) delete mode 100644 src/extensions/example-extension/example-extension.ts create mode 100644 src/extensions/example-extension/example-extension.tsx diff --git a/.gitignore b/.gitignore index 6ddf3f489c2b..11cc6298ab62 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ binaries/client/ binaries/server/ src/extensions/*/*.js src/extensions/*/*.d.ts +src/extensions/example-extension/src/** locales/**/**.js lens.log diff --git a/src/extensions/example-extension/example-extension.ts b/src/extensions/example-extension/example-extension.ts deleted file mode 100644 index 28b4da5825be..000000000000 --- a/src/extensions/example-extension/example-extension.ts +++ /dev/null @@ -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 { - try { - super.enable(runtime); - runtime.logger.info('EXAMPLE EXTENSION: ENABLE() override'); - } catch (err){ - console.error(err) - } - } -} - -// console.log("done")}/> \ No newline at end of file diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx new file mode 100644 index 000000000000..5a4e7032b9f9 --- /dev/null +++ b/src/extensions/example-extension/example-extension.tsx @@ -0,0 +1,40 @@ +import { Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts" +import React from "react"; +import path from "path"; + +export default class ExampleExtension extends LensExtension { + protected routePath = "/extension-example" + + onActivate() { + console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) + const { dynamicPages } = this.runtime; + dynamicPages.register(this.routePath, { + Main: ExtensionPage, + MenuIcon: ExtensionIcon, + }) + } + + onDeactivate() { + console.log('EXAMPLE EXTENSION: DEACTIVATE', this.getMeta()); + const { dynamicPages } = this.runtime; + dynamicPages.unregister(this.routePath); + } +} + +export function ExtensionIcon(props: {} /*IconProps |*/) { + return +} + +// todo: provide extension instance and runtime params (via context or props) +export class ExtensionPage extends React.Component { + render() { + return ( +
+
+

Hello from extensions-api!

+

File: {__filename}

+
+
+ ) + } +} diff --git a/src/extensions/example-extension/package.json b/src/extensions/example-extension/package.json index d57a78aab567..663d5d432adf 100644 --- a/src/extensions/example-extension/package.json +++ b/src/extensions/example-extension/package.json @@ -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": { } diff --git a/src/extensions/example-extension/tsconfig.json b/src/extensions/example-extension/tsconfig.json index 16081f6055d3..993992112a53 100644 --- a/src/extensions/example-extension/tsconfig.json +++ b/src/extensions/example-extension/tsconfig.json @@ -8,6 +8,6 @@ }, "include": [ "../../../types", - "./example-extension.ts" + "./example-extension.tsx" ] } diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 655c4d6ca738..e79462694264 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -80,7 +80,6 @@ export class ExtensionStore extends BaseStore { 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, diff --git a/src/extensions/extension.ts b/src/extensions/extension.ts index 698422fb546b..ec40245a4971 100644 --- a/src/extensions/extension.ts +++ b/src/extensions/extension.ts @@ -19,7 +19,7 @@ export class LensExtension implements ExtensionModel { @observable manifest: ExtensionManifest; @observable manifestPath: string; @observable isEnabled = false; - @observable.ref runtime: Partial = {}; + @observable.ref runtime: LensRuntimeRendererEnv; constructor(model: ExtensionModel, manifest: ExtensionManifest) { this.importModel(model, manifest); @@ -41,14 +41,25 @@ 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; 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; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 987f22658d63..08cbfa920730 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -15,6 +15,7 @@ import { Extensions, extensionsRoute } from "../+extensions"; import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; +import { dynamicPages } from "./register-page"; @observer export class ClusterManager extends React.Component { @@ -62,6 +63,9 @@ export class ClusterManager extends React.Component { + {Array.from(dynamicPages.routes).map(([path, { Main }]) => { + return + })} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 495ce2176a53..6068a5bbe1ce 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -10,7 +10,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { ClusterIcon } from "../cluster-icon"; import { Icon } from "../icon"; -import { cssNames, IClassName, autobind } from "../../utils"; +import { autobind, cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; import { navigate } from "../../navigation"; import { addClusterURL } from "../+add-cluster"; @@ -20,7 +20,7 @@ import { Tooltip } from "../tooltip"; import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route"; -import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd"; +import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { dynamicPages } from "./register-page"; interface Props { @@ -143,15 +143,16 @@ export class ClustersMenu extends React.Component { Add Cluster - + {newContexts.size > 0 && ( - new} /> + new}/> )}
- {Array.from(dynamicPages.all).map(([path, { MenuIcon }]) => { - if (!MenuIcon) return; - return navigate(path)}/> + {Array.from(dynamicPages.routes).map(([path, { MenuIcon }]) => { + if (MenuIcon) { + return navigate(path)}/> + } })}
diff --git a/src/renderer/components/cluster-manager/register-page.ts b/src/renderer/components/cluster-manager/register-page.ts index 71a6345cae4f..23db2c36ddb2 100644 --- a/src/renderer/components/cluster-manager/register-page.ts +++ b/src/renderer/components/cluster-manager/register-page.ts @@ -10,18 +10,18 @@ export interface PageComponents { } export class PagesStore { - all = observable.map(); + routes = observable.map(); getComponents(path: string): PageComponents | null { - return this.all.get(path); + return this.routes.get(path); } register(path: string, components: PageComponents) { - this.all.set(path, components); + this.routes.set(path, components); } unregister(path: string) { - this.all.delete(path); + this.routes.delete(path); } } From 1c5c394637f4f18f72cadb50073594586b2aa514 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 7 Sep 2020 18:21:58 +0300 Subject: [PATCH 2/8] extension-store sync fix Signed-off-by: Roman --- src/extensions/extension-store.ts | 7 +++---- src/extensions/extension.ts | 6 ++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index e79462694264..0ce0c1cab84a 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -9,7 +9,7 @@ import logger from "../main/logger"; export interface ExtensionStoreModel { version: ExtensionVersion; - extensions: Record + extensions: [ExtensionId, ExtensionModel][] } export interface ExtensionModel { @@ -35,7 +35,6 @@ export class ExtensionStore extends BaseStore { private constructor() { super({ configName: "lens-extension-store", - syncEnabled: false, }); } @@ -131,7 +130,7 @@ export class ExtensionStore extends BaseStore { 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); @@ -163,7 +162,7 @@ export class ExtensionStore extends BaseStore { toJSON(): ExtensionStoreModel { return toJS({ version: this.version, - extensions: this.extensions.toJSON(), + extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]), }, { recurseEverything: true, }) diff --git a/src/extensions/extension.ts b/src/extensions/extension.ts index ec40245a4971..e4bc683ea958 100644 --- a/src/extensions/extension.ts +++ b/src/extensions/extension.ts @@ -87,7 +87,7 @@ export class LensExtension implements ExtensionModel { } toJSON(): ExtensionModel { - return { + return toJS({ id: this.id, name: this.name, version: this.version, @@ -95,6 +95,8 @@ export class LensExtension implements ExtensionModel { manifestPath: this.manifestPath, enabled: this.isEnabled, updateUrl: this.updateUrl, - } + }, { + recurseEverything: true, + }) } } From f5cea39c647b113562b0481dc1535df08383f613 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 8 Sep 2020 12:07:21 +0300 Subject: [PATCH 3/8] example-extension reworks -- part 1 Signed-off-by: Roman --- .../example-extension/example-extension.tsx | 16 +++++----- src/extensions/extension-store.ts | 2 +- .../cluster-manager/cluster-manager.tsx | 4 +-- .../cluster-manager/clusters-menu.tsx | 2 +- .../cluster-manager/register-page.ts | 29 +++++++++++++------ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx index 5a4e7032b9f9..5c1582057f33 100644 --- a/src/extensions/example-extension/example-extension.tsx +++ b/src/extensions/example-extension/example-extension.tsx @@ -3,21 +3,23 @@ import React from "react"; import path from "path"; export default class ExampleExtension extends LensExtension { - protected routePath = "/extension-example" + protected unRegisterPage = Function(); onActivate() { console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) - const { dynamicPages } = this.runtime; - dynamicPages.register(this.routePath, { - Main: ExtensionPage, - MenuIcon: ExtensionIcon, + this.unRegisterPage = this.runtime.dynamicPages.register({ + type: "cluster-view", + path: "/extension-example", + components: { + Main: ExtensionPage, + MenuIcon: ExtensionIcon, + } }) } onDeactivate() { console.log('EXAMPLE EXTENSION: DEACTIVATE', this.getMeta()); - const { dynamicPages } = this.runtime; - dynamicPages.unregister(this.routePath); + this.unRegisterPage(); } } diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 0ce0c1cab84a..640a0e88e414 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -13,7 +13,7 @@ export interface ExtensionStoreModel { } export interface ExtensionModel { - id?: ExtensionId; // available in lens-extension instance + id: ExtensionId; version: ExtensionVersion; name: string; manifestPath: string; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 08cbfa920730..9f46c88e194e 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -63,8 +63,8 @@ export class ClusterManager extends React.Component { - {Array.from(dynamicPages.routes).map(([path, { Main }]) => { - return + {dynamicPages.globalPages.map(({ path, components: { Page } }) => { + return })} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 6068a5bbe1ce..64f69fb736ac 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -149,7 +149,7 @@ export class ClustersMenu extends React.Component { )}
- {Array.from(dynamicPages.routes).map(([path, { MenuIcon }]) => { + {dynamicPages.globalPages.map(({ path, components: { MenuIcon } }) => { if (MenuIcon) { return navigate(path)}/> } diff --git a/src/renderer/components/cluster-manager/register-page.ts b/src/renderer/components/cluster-manager/register-page.ts index 23db2c36ddb2..49d32dcb3055 100644 --- a/src/renderer/components/cluster-manager/register-page.ts +++ b/src/renderer/components/cluster-manager/register-page.ts @@ -1,27 +1,38 @@ // Dynamic pages import React from "react"; -import { observable } from "mobx"; +import { computed, observable } from "mobx"; import type { IconProps } from "../icon"; +export interface PageRegistration { + path: string; + type: "global" | "cluster-view"; + components: PageComponents; +} + export interface PageComponents { - Main: React.ComponentType; + Page: React.ComponentType; MenuIcon: React.ComponentType; } export class PagesStore { - routes = observable.map(); + protected pages = observable.array([], { deep: false }); - getComponents(path: string): PageComponents | null { - return this.routes.get(path); + @computed get globalPages() { + return this.pages.filter(page => page.type === "global"); } - register(path: string, components: PageComponents) { - this.routes.set(path, components); + @computed get clusterPages() { + return this.pages.filter(page => page.type === "cluster-view"); } - unregister(path: string) { - this.routes.delete(path); + register(params: PageRegistration) { + this.pages.push(params); + return () => { + this.pages.replace( + this.pages.filter(page => page.components !== params.components) + ) + }; } } From 89bc526e4dea5574c56b01740986cc6cc01f1489 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 8 Sep 2020 15:20:41 +0300 Subject: [PATCH 4/8] example-extension reworks -- part 2 Signed-off-by: Roman --- .../example-extension/example-extension.tsx | 9 ++++---- src/extensions/extension-api.ts | 1 + src/extensions/lens-runtime.ts | 8 ++----- .../register-page.tsx} | 21 ++++++++++++------- src/renderer/bootstrap.tsx | 2 ++ src/renderer/components/app.tsx | 4 ++++ .../cluster-manager/cluster-manager.tsx | 2 +- .../cluster-manager/clusters-menu.tsx | 6 ++---- src/renderer/components/layout/sidebar.tsx | 13 ++++++++++++ src/renderer/lens-app.tsx | 6 ------ 10 files changed, 44 insertions(+), 28 deletions(-) rename src/{renderer/components/cluster-manager/register-page.ts => extensions/register-page.tsx} (58%) diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx index 5c1582057f33..9a95f30f6ee4 100644 --- a/src/extensions/example-extension/example-extension.tsx +++ b/src/extensions/example-extension/example-extension.tsx @@ -1,4 +1,4 @@ -import { Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts" +import { DynamicPageType, Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts" import React from "react"; import path from "path"; @@ -8,10 +8,11 @@ export default class ExampleExtension extends LensExtension { onActivate() { console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) this.unRegisterPage = this.runtime.dynamicPages.register({ - type: "cluster-view", + type: DynamicPageType.CLUSTER, path: "/extension-example", + menuTitle: "Example Extension", components: { - Main: ExtensionPage, + Page: ExtensionPage, MenuIcon: ExtensionIcon, } }) @@ -27,7 +28,7 @@ export function ExtensionIcon(props: {} /*IconProps |*/) { return } -// todo: provide extension instance and runtime params (via context or props) +// todo: provide extension-instance and lens-runtime (context/props/extension-js-scope) export class ExtensionPage extends React.Component { render() { return ( diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts index 3dff72d00c02..803698f71f5f 100644 --- a/src/extensions/extension-api.ts +++ b/src/extensions/extension-api.ts @@ -3,6 +3,7 @@ export type { LensRuntimeRendererEnv } from "./lens-runtime"; // APIs export * from "./extension" +export { DynamicPageType } from "./register-page"; // Common UI components export * from "../renderer/components/icon" diff --git a/src/extensions/lens-runtime.ts b/src/extensions/lens-runtime.ts index e456bb435f92..333282a734ae 100644 --- a/src/extensions/lens-runtime.ts +++ b/src/extensions/lens-runtime.ts @@ -1,18 +1,14 @@ -// Lens runtime for injecting to extension on activation -import { apiManager } from "../renderer/api/api-manager"; +// Lens renderer runtime params available to the extension after activation import logger from "../main/logger"; -import { dynamicPages } from "../renderer/components/cluster-manager/register-page"; +import { dynamicPages } from "./register-page"; export interface LensRuntimeRendererEnv { - apiManager: typeof apiManager; logger: typeof logger; dynamicPages: typeof dynamicPages } -// todo: expose more public runtime apis: stores, managers, etc. export function getLensRuntime(): LensRuntimeRendererEnv { return { - apiManager, logger, dynamicPages, } diff --git a/src/renderer/components/cluster-manager/register-page.ts b/src/extensions/register-page.tsx similarity index 58% rename from src/renderer/components/cluster-manager/register-page.ts rename to src/extensions/register-page.tsx index 49d32dcb3055..34317c1d9377 100644 --- a/src/renderer/components/cluster-manager/register-page.ts +++ b/src/extensions/register-page.tsx @@ -1,12 +1,18 @@ -// Dynamic pages +// Extensions-api -> Dynamic pages -import React from "react"; import { computed, observable } from "mobx"; -import type { IconProps } from "../icon"; +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; - type: "global" | "cluster-view"; + path: string; // route-path + menuTitle: string; + type: DynamicPageType; components: PageComponents; } @@ -19,13 +25,14 @@ export class PagesStore { protected pages = observable.array([], { deep: false }); @computed get globalPages() { - return this.pages.filter(page => page.type === "global"); + return this.pages.filter(page => page.type === DynamicPageType.GLOBAL); } @computed get clusterPages() { - return this.pages.filter(page => page.type === "cluster-view"); + 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 () => { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index e4b24c3035d6..ee4dd6dedf9d 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -10,6 +10,7 @@ import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { App } from "./components/app"; import { LensApp } from "./lens-app"; +import { getLensRuntime } from "../extensions/lens-runtime"; type AppComponent = React.ComponentType & { init?(): void; @@ -32,6 +33,7 @@ export async function bootstrap(App: AppComponent) { // init app's dependencies if any if (App.init) { await App.init(); + extensionStore.autoEnableOnLoad(getLensRuntime); } render(, rootElem); } diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 23e71e2be927..a878fd47b747 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -35,6 +35,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store import logger from "../../main/logger"; import { clusterIpc } from "../../common/cluster-ipc"; import { webFrame } from "electron"; +import { dynamicPages } from "../../extensions/register-page"; @observer export class App extends React.Component { @@ -71,6 +72,9 @@ export class App extends React.Component { + {dynamicPages.clusterPages.map(({ path, components: { Page } }) => { + return + })} diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 9f46c88e194e..10ee59f46736 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -15,7 +15,7 @@ import { Extensions, extensionsRoute } from "../+extensions"; import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; -import { dynamicPages } from "./register-page"; +import { dynamicPages } from "../../../extensions/register-page"; @observer export class ClusterManager extends React.Component { diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 64f69fb736ac..30edfc22c723 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -21,7 +21,7 @@ import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; -import { dynamicPages } from "./register-page"; +import { dynamicPages } from "../../../extensions/register-page"; interface Props { className?: IClassName; @@ -150,9 +150,7 @@ export class ClustersMenu extends React.Component {
{dynamicPages.globalPages.map(({ path, components: { MenuIcon } }) => { - if (MenuIcon) { - return navigate(path)}/> - } + return navigate(path)}/> })}
diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index d46a1548cf9f..91cd4940a68b 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -28,6 +28,7 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc import { CustomResources } from "../+custom-resources/custom-resources"; import { navigation } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac" +import { dynamicPages } from "../../../extensions/register-page"; const SidebarContext = React.createContext({ pinned: false }); type SidebarContextValue = { @@ -183,6 +184,18 @@ export class Sidebar extends React.Component { > {this.renderCustomResources()} + {dynamicPages.clusterPages.map(({ path, menuTitle, components: { MenuIcon } }) => { + return ( + } + /> + ) + })} diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 7c1f228fd308..9b4fffc6a19b 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -11,15 +11,9 @@ import { ErrorBoundary } from "./components/error-boundary"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; import { Notifications } from "./components/notifications"; import { ConfirmDialog } from "./components/confirm-dialog"; -import { extensionStore } from "../extensions/extension-store"; -import { getLensRuntime } from "../extensions/lens-runtime"; @observer export class LensApp extends React.Component { - componentDidMount() { - extensionStore.autoEnableOnLoad(getLensRuntime); - } - render() { return ( From c6cb8adbcfbfd6c3ec36beccc15958483d57c76b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 8 Sep 2020 16:03:47 +0300 Subject: [PATCH 5/8] example-extension reworks (3): wrap example-extension into main-layout Signed-off-by: Roman --- .../example-extension/example-extension.tsx | 12 +++++++++--- src/extensions/lens-runtime.ts | 10 +++++++++- src/renderer/components/layout/main-layout.tsx | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx index 9a95f30f6ee4..f360cbf3cfbc 100644 --- a/src/extensions/example-extension/example-extension.tsx +++ b/src/extensions/example-extension/example-extension.tsx @@ -1,4 +1,4 @@ -import { DynamicPageType, Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.d.ts" +import { DynamicPageType, Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.ts" import React from "react"; import path from "path"; @@ -7,12 +7,18 @@ export default class ExampleExtension extends LensExtension { onActivate() { console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) - this.unRegisterPage = this.runtime.dynamicPages.register({ + const { dynamicPages, components: { MainLayout } } = this.runtime; + + this.unRegisterPage = dynamicPages.register({ type: DynamicPageType.CLUSTER, path: "/extension-example", menuTitle: "Example Extension", components: { - Page: ExtensionPage, + Page: () => ( + + + + ), MenuIcon: ExtensionIcon, } }) diff --git a/src/extensions/lens-runtime.ts b/src/extensions/lens-runtime.ts index 333282a734ae..4e335317e142 100644 --- a/src/extensions/lens-runtime.ts +++ b/src/extensions/lens-runtime.ts @@ -1,15 +1,23 @@ -// Lens renderer runtime params available to the extension after activation +// Lens renderer runtime params available to extensions after activation + import logger from "../main/logger"; import { dynamicPages } from "./register-page"; +import { MainLayout } from "../renderer/components/layout/main-layout"; export interface LensRuntimeRendererEnv { logger: typeof logger; dynamicPages: typeof dynamicPages + components: { + MainLayout: typeof MainLayout + } } export function getLensRuntime(): LensRuntimeRendererEnv { return { logger, dynamicPages, + components: { + MainLayout // fixme: refactor, import as pure component from "@lens/extensions" + } } } diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index def55961fa52..8672e4ba235b 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -17,7 +17,7 @@ export interface TabRoute extends RouteProps { url: string; } -interface Props { +export interface MainLayoutProps { className?: any; tabs?: TabRoute[]; footer?: React.ReactNode; @@ -27,7 +27,7 @@ interface Props { } @observer -export class MainLayout extends React.Component { +export class MainLayout extends React.Component { public storage = createStorage("main_layout", { pinnedSidebar: true }); @observable isPinned = this.storage.get().pinnedSidebar; From 966aa38cbf7ae8fd96894659f3c5f9da3ddb5d89 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 8 Sep 2020 16:48:29 +0300 Subject: [PATCH 6/8] example-extension: allow to disable extension from page component (demo-only) Signed-off-by: Roman --- .../example-extension/example-extension.tsx | 28 +++++++++++-------- src/extensions/lens-runtime.ts | 3 ++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx index f360cbf3cfbc..8ee52e6f664f 100644 --- a/src/extensions/example-extension/example-extension.tsx +++ b/src/extensions/example-extension/example-extension.tsx @@ -1,30 +1,30 @@ -import { DynamicPageType, Icon, LensExtension } from "@lens/extensions"; // fixme: map to generated types from "extension-api.ts" +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"; +let extension: ExampleExtension; // todo: provide instance from context + export default class ExampleExtension extends LensExtension { protected unRegisterPage = Function(); onActivate() { + extension = this console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) - const { dynamicPages, components: { MainLayout } } = this.runtime; + const { dynamicPages } = this.runtime; this.unRegisterPage = dynamicPages.register({ type: DynamicPageType.CLUSTER, path: "/extension-example", menuTitle: "Example Extension", components: { - Page: () => ( - - - - ), + Page: ExtensionPage, MenuIcon: ExtensionIcon, } }) } onDeactivate() { + extension = null; console.log('EXAMPLE EXTENSION: DEACTIVATE', this.getMeta()); this.unRegisterPage(); } @@ -34,16 +34,22 @@ export function ExtensionIcon(props: {} /*IconProps |*/) { return } -// todo: provide extension-instance and lens-runtime (context/props/extension-js-scope) export class ExtensionPage extends React.Component { + deactivate = () => { + extension.runtime.navigate("/") + extension.disable(); + } + render() { + const { MainLayout } = extension.runtime.components; return ( -
-
+ +

Hello from extensions-api!

File: {__filename}

+
-
+ ) } } diff --git a/src/extensions/lens-runtime.ts b/src/extensions/lens-runtime.ts index 4e335317e142..5fdc4c35ebae 100644 --- a/src/extensions/lens-runtime.ts +++ b/src/extensions/lens-runtime.ts @@ -3,8 +3,10 @@ import logger from "../main/logger"; import { dynamicPages } from "./register-page"; import { MainLayout } from "../renderer/components/layout/main-layout"; +import { navigate } from "../renderer/navigation"; export interface LensRuntimeRendererEnv { + navigate: typeof navigate; logger: typeof logger; dynamicPages: typeof dynamicPages components: { @@ -15,6 +17,7 @@ export interface LensRuntimeRendererEnv { export function getLensRuntime(): LensRuntimeRendererEnv { return { logger, + navigate, dynamicPages, components: { MainLayout // fixme: refactor, import as pure component from "@lens/extensions" From 8f4c7eea6d91157a598e4a652736f5f5774b05da Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 9 Sep 2020 13:49:52 +0300 Subject: [PATCH 7/8] example-extension: added extension.registerPage(), clean up Signed-off-by: Roman --- .../example-extension/example-extension.tsx | 22 ++++++------------- src/extensions/extension-api.ts | 2 +- src/extensions/extension-store.ts | 6 ++--- .../{extension.ts => lens-extension.ts} | 15 ++++++++++++- 4 files changed, 25 insertions(+), 20 deletions(-) rename src/extensions/{extension.ts => lens-extension.ts} (83%) diff --git a/src/extensions/example-extension/example-extension.tsx b/src/extensions/example-extension/example-extension.tsx index 8ee52e6f664f..52a169a2c976 100644 --- a/src/extensions/example-extension/example-extension.tsx +++ b/src/extensions/example-extension/example-extension.tsx @@ -2,31 +2,22 @@ import { Button, DynamicPageType, Icon, LensExtension } from "@lens/extensions"; import React from "react"; import path from "path"; -let extension: ExampleExtension; // todo: provide instance from context - export default class ExampleExtension extends LensExtension { - protected unRegisterPage = Function(); - onActivate() { - extension = this - console.log('EXAMPLE EXTENSION: ACTIVATE', this.getMeta()) - const { dynamicPages } = this.runtime; - - this.unRegisterPage = dynamicPages.register({ + console.log('EXAMPLE EXTENSION: ACTIVATED', this.getMeta()); + this.registerPage({ type: DynamicPageType.CLUSTER, path: "/extension-example", menuTitle: "Example Extension", components: { - Page: ExtensionPage, + Page: () => , MenuIcon: ExtensionIcon, } }) } onDeactivate() { - extension = null; - console.log('EXAMPLE EXTENSION: DEACTIVATE', this.getMeta()); - this.unRegisterPage(); + console.log('EXAMPLE EXTENSION: DEACTIVATED', this.getMeta()); } } @@ -34,14 +25,15 @@ export function ExtensionIcon(props: {} /*IconProps |*/) { return } -export class ExtensionPage extends React.Component { +export class ExtensionPage extends React.Component<{ extension: ExampleExtension }> { deactivate = () => { + const { extension } = this.props; extension.runtime.navigate("/") extension.disable(); } render() { - const { MainLayout } = extension.runtime.components; + const { MainLayout } = this.props.extension.runtime.components; return (
diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts index 803698f71f5f..8e5951a12e24 100644 --- a/src/extensions/extension-api.ts +++ b/src/extensions/extension-api.ts @@ -2,7 +2,7 @@ export type { LensRuntimeRendererEnv } from "./lens-runtime"; // APIs -export * from "./extension" +export * from "./lens-extension" export { DynamicPageType } from "./register-page"; // Common UI components diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 640a0e88e414..a7eb75db8810 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -3,8 +3,8 @@ 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 { @@ -47,7 +47,7 @@ export class ExtensionStore extends BaseStore { if (isDevelopment) { return path.resolve(__static, "../src/extensions"); } - return path.resolve(__static, "../extensions"); //todo figure out prod + return path.resolve(__static, "../extensions"); } async load() { diff --git a/src/extensions/extension.ts b/src/extensions/lens-extension.ts similarity index 83% rename from src/extensions/extension.ts rename to src/extensions/lens-extension.ts index e4bc683ea958..a285cf095eb3 100644 --- a/src/extensions/extension.ts +++ b/src/extensions/lens-extension.ts @@ -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 = ""; @@ -48,6 +51,8 @@ export class LensExtension implements ExtensionModel { this.onDeactivate(); this.isEnabled = false; this.runtime = null; + this.disposers.forEach(cleanUp => cleanUp()); + this.disposers.length = 0; console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta()); } @@ -99,4 +104,12 @@ export class LensExtension implements ExtensionModel { recurseEverything: true, }) } + + // Runtime helpers + protected registerPage(params: PageRegistration, autoDisable = true) { + const dispose = this.runtime.dynamicPages.register(params); + if (autoDisable) { + this.disposers.push(dispose); + } + } } From 3015dd77bb36553e21b0435a07514f25b75bbdc4 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 9 Sep 2020 15:37:48 +0300 Subject: [PATCH 8/8] Adding extensions path to .eslintrc.js Signed-off-by: Alex Andreev --- .eslintrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 516029cdd43c..3de849de34df 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,7 +24,8 @@ module.exports = { files: [ "build/*.ts", "src/**/*.ts", - "integration/**/*.ts" + "integration/**/*.ts", + "src/extensions/**/*.ts*" ], parser: "@typescript-eslint/parser", extends: [