Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions-api: initial hello-world example #817

Merged
merged 8 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ module.exports = {
files: [
"build/*.ts",
"src/**/*.ts",
"integration/**/*.ts"
"integration/**/*.ts",
"src/extensions/**/*.ts*"
],
parser: "@typescript-eslint/parser",
extends: [
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ binaries/client/
binaries/server/
src/extensions/*/*.js
src/extensions/*/*.d.ts
src/extensions/example-extension/src/**
locales/**/**.js
lens.log
17 changes: 0 additions & 17 deletions src/extensions/example-extension/example-extension.ts

This file was deleted.

47 changes: 47 additions & 0 deletions src/extensions/example-extension/example-extension.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
}
5 changes: 3 additions & 2 deletions src/extensions/example-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
}
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/example-extension/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
},
"include": [
"../../../types",
"./example-extension.ts"
"./example-extension.tsx"
]
}
3 changes: 2 additions & 1 deletion src/extensions/extension-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 7 additions & 9 deletions src/extensions/extension-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,7 +35,6 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
private constructor() {
super({
configName: "lens-extension-store",
syncEnabled: false,
});
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
})
Expand Down
34 changes: 30 additions & 4 deletions src/extensions/extension.ts → src/extensions/lens-extension.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
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 = "";
@observable version: ExtensionVersion = "0.0.0";
@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);
Expand All @@ -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;
Expand Down Expand Up @@ -76,14 +92,24 @@ export class LensExtension implements ExtensionModel {
}

toJSON(): ExtensionModel {
return {
return toJS({
id: this.id,
name: this.name,
version: this.version,
description: this.description,
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);
}
}
}
19 changes: 13 additions & 6 deletions src/extensions/lens-runtime.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 46 additions & 0 deletions src/extensions/register-page.tsx
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 2 additions & 0 deletions src/renderer/bootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(<App/>, rootElem);
}
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -71,6 +72,9 @@ export class App extends React.Component {
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/cluster-manager/cluster-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 "../../../extensions/register-page";

@observer
export class ClusterManager extends React.Component {
Expand Down Expand Up @@ -62,6 +63,9 @@ export class ClusterManager extends React.Component {
<Route component={ClusterView} {...clusterViewRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Extensions} {...extensionsRoute}/>
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl}/>
</Switch>
</main>
Expand Down
Loading