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

extensions-api: initial hello-world example which adds new app section: menu icon + page

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-09-07 16:48:10 +03:00 committed by Lauri Nevala
parent f1b03990ea
commit f1f9a364e2
10 changed files with 74 additions and 34 deletions

1
.gitignore vendored
View File

@ -10,5 +10,6 @@ binaries/client/
binaries/server/ binaries/server/
src/extensions/*/*.js src/extensions/*/*.js
src/extensions/*/*.d.ts src/extensions/*/*.d.ts
src/extensions/example-extension/src/**
locales/**/**.js locales/**/**.js
lens.log lens.log

View File

@ -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")}/>

View File

@ -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 <Icon {...props} material="camera" tooltip={path.basename(__filename)}/>
}
// todo: provide extension instance and runtime params (via context or props)
export class ExtensionPage extends React.Component {
render() {
return (
<div className="ExampleExtension" style={{ padding: "20px" }}>
<div className="content flex column gaps align-flex-start">
<p>Hello from extensions-api!</p>
<p>File: <i>{__filename}</i></p>
</div>
</div>
)
}
}

View File

@ -2,9 +2,10 @@
"name": "extension-example", "name": "extension-example",
"version": "1.0.0", "version": "1.0.0",
"description": "Example extension", "description": "Example extension",
"main": "example-extension.ts", "main": "example-extension.js",
"lens": { "lens": {
"metadata": {} "metadata": {},
"styles": []
}, },
"dependencies": { "dependencies": {
} }

View File

@ -8,6 +8,6 @@
}, },
"include": [ "include": [
"../../../types", "../../../types",
"./example-extension.ts" "./example-extension.tsx"
] ]
} }

View File

@ -80,7 +80,6 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
try { try {
manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call
mainJs = path.resolve(path.dirname(manifestPath), manifestJson.main); 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); const extensionModule = __non_webpack_require__(mainJs);
return { return {
manifestPath: manifestPath, manifestPath: manifestPath,

View File

@ -19,7 +19,7 @@ export class LensExtension implements ExtensionModel {
@observable manifest: ExtensionManifest; @observable manifest: ExtensionManifest;
@observable manifestPath: string; @observable manifestPath: string;
@observable isEnabled = false; @observable isEnabled = false;
@observable.ref runtime: Partial<LensRuntimeRendererEnv> = {}; @observable.ref runtime: LensRuntimeRendererEnv;
constructor(model: ExtensionModel, manifest: ExtensionManifest) { constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest); this.importModel(model, manifest);
@ -41,14 +41,25 @@ export class LensExtension implements ExtensionModel {
this.isEnabled = true; this.isEnabled = true;
this.runtime = runtime; this.runtime = runtime;
console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta()); console.log(`[EXTENSION]: enabled ${this.name}@${this.version}`, this.getMeta());
this.onActivate();
} }
async disable() { async disable() {
this.onDeactivate();
this.isEnabled = false; this.isEnabled = false;
this.runtime = {}; this.runtime = null;
console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta()); console.log(`[EXTENSION]: disabled ${this.name}@${this.version}`, this.getMeta());
} }
// todo: add more hooks
protected onActivate() {
// mock
}
protected onDeactivate() {
// mock
}
// todo // todo
async install(downloadUrl?: string) { async install(downloadUrl?: string) {
return; return;

View File

@ -15,6 +15,7 @@ import { Extensions, extensionsRoute } from "../+extensions";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route"; import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { dynamicPages } from "./register-page";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {
@ -62,6 +63,9 @@ export class ClusterManager extends React.Component {
<Route component={ClusterView} {...clusterViewRoute}/> <Route component={ClusterView} {...clusterViewRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/> <Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Extensions} {...extensionsRoute}/> <Route component={Extensions} {...extensionsRoute}/>
{Array.from(dynamicPages.routes).map(([path, { Main }]) => {
return <Route key={path} path={path} component={Main}/>
})}
<Redirect exact to={this.startUrl}/> <Redirect exact to={this.startUrl}/>
</Switch> </Switch>
</main> </main>

View File

@ -10,7 +10,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon"; import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { cssNames, IClassName, autobind } from "../../utils"; import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster"; import { addClusterURL } from "../+add-cluster";
@ -20,7 +20,7 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route"; 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"; import { dynamicPages } from "./register-page";
interface Props { interface Props {
@ -143,15 +143,16 @@ export class ClustersMenu extends React.Component<Props> {
<Tooltip targetId="add-cluster-icon"> <Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans> <Trans>Add Cluster</Trans>
</Tooltip> </Tooltip>
<Icon big material="add" id="add-cluster-icon" /> <Icon big material="add" id="add-cluster-icon"/>
{newContexts.size > 0 && ( {newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} /> <Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
)} )}
</div> </div>
<div className="dynamic-pages"> <div className="dynamic-pages">
{Array.from(dynamicPages.all).map(([path, { MenuIcon }]) => { {Array.from(dynamicPages.routes).map(([path, { MenuIcon }]) => {
if (!MenuIcon) return; if (MenuIcon) {
return <MenuIcon onClick={() => navigate(path)}/> return <MenuIcon key={path} onClick={() => navigate(path)}/>
}
})} })}
</div> </div>
</div> </div>

View File

@ -10,18 +10,18 @@ export interface PageComponents {
} }
export class PagesStore { export class PagesStore {
all = observable.map<string, PageComponents>(); routes = observable.map<string, PageComponents>();
getComponents(path: string): PageComponents | null { getComponents(path: string): PageComponents | null {
return this.all.get(path); return this.routes.get(path);
} }
register(path: string, components: PageComponents) { register(path: string, components: PageComponents) {
this.all.set(path, components); this.routes.set(path, components);
} }
unregister(path: string) { unregister(path: string) {
this.all.delete(path); this.routes.delete(path);
} }
} }