mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
parent
f1b03990ea
commit
5daf53e6cb
@ -24,7 +24,8 @@ module.exports = {
|
|||||||
files: [
|
files: [
|
||||||
"build/*.ts",
|
"build/*.ts",
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"integration/**/*.ts"
|
"integration/**/*.ts",
|
||||||
|
"src/extensions/**/*.ts*"
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: [
|
extends: [
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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")}/>
|
|
||||||
47
src/extensions/example-extension/example-extension.tsx
Normal file
47
src/extensions/example-extension/example-extension.tsx
Normal file
@ -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",
|
"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": {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"../../../types",
|
"../../../types",
|
||||||
"./example-extension.ts"
|
"./example-extension.tsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
export type { LensRuntimeRendererEnv } from "./lens-runtime";
|
export type { LensRuntimeRendererEnv } from "./lens-runtime";
|
||||||
|
|
||||||
// APIs
|
// APIs
|
||||||
export * from "./extension"
|
export * from "./lens-extension"
|
||||||
|
export { DynamicPageType } from "./register-page";
|
||||||
|
|
||||||
// Common UI components
|
// Common UI components
|
||||||
export * from "../renderer/components/icon"
|
export * from "../renderer/components/icon"
|
||||||
|
|||||||
@ -3,17 +3,17 @@ import path from "path";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import { action, observable, reaction, toJS, } from "mobx";
|
import { action, observable, reaction, toJS, } from "mobx";
|
||||||
import { BaseStore } from "../common/base-store";
|
import { BaseStore } from "../common/base-store";
|
||||||
import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./extension";
|
import { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./lens-extension";
|
||||||
import { isDevelopment, isProduction, isTestEnv } from "../common/vars";
|
import { isDevelopment } from "../common/vars";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export interface ExtensionStoreModel {
|
export interface ExtensionStoreModel {
|
||||||
version: ExtensionVersion;
|
version: ExtensionVersion;
|
||||||
extensions: Record<ExtensionId, ExtensionModel>
|
extensions: [ExtensionId, ExtensionModel][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionModel {
|
export interface ExtensionModel {
|
||||||
id?: ExtensionId; // available in lens-extension instance
|
id: ExtensionId;
|
||||||
version: ExtensionVersion;
|
version: ExtensionVersion;
|
||||||
name: string;
|
name: string;
|
||||||
manifestPath: string;
|
manifestPath: string;
|
||||||
@ -35,7 +35,6 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-extension-store",
|
configName: "lens-extension-store",
|
||||||
syncEnabled: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
return path.resolve(__static, "../src/extensions");
|
return path.resolve(__static, "../src/extensions");
|
||||||
}
|
}
|
||||||
return path.resolve(__static, "../extensions"); //todo figure out prod
|
return path.resolve(__static, "../extensions");
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -80,7 +79,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,
|
||||||
@ -132,7 +130,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
this.version = version;
|
this.version = version;
|
||||||
}
|
}
|
||||||
if (extensions) {
|
if (extensions) {
|
||||||
const currentExtensions = new Map(Object.entries(extensions));
|
const currentExtensions = new Map(extensions);
|
||||||
this.extensions.forEach(extension => {
|
this.extensions.forEach(extension => {
|
||||||
if (!currentExtensions.has(extension.id)) {
|
if (!currentExtensions.has(extension.id)) {
|
||||||
this.removed.set(extension.id, extension);
|
this.removed.set(extension.id, extension);
|
||||||
@ -164,7 +162,7 @@ export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
|
|||||||
toJSON(): ExtensionStoreModel {
|
toJSON(): ExtensionStoreModel {
|
||||||
return toJS({
|
return toJS({
|
||||||
version: this.version,
|
version: this.version,
|
||||||
extensions: this.extensions.toJSON(),
|
extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]),
|
||||||
}, {
|
}, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
import type { ExtensionModel } from "./extension-store";
|
import type { ExtensionModel } from "./extension-store";
|
||||||
import type { LensRuntimeRendererEnv } from "./lens-runtime";
|
import type { LensRuntimeRendererEnv } from "./lens-runtime";
|
||||||
|
import type { PageRegistration } from "./register-page";
|
||||||
import { readJsonSync } from "fs-extra";
|
import { readJsonSync } from "fs-extra";
|
||||||
import { action, observable, toJS } from "mobx";
|
import { action, observable, toJS } from "mobx";
|
||||||
import extensionManifest from "./example-extension/package.json"
|
import extensionManifest from "./example-extension/package.json"
|
||||||
import logger from "../main/logger";
|
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 ExtensionVersion = string | number;
|
||||||
export type ExtensionManifest = typeof extensionManifest & ExtensionModel;
|
export type ExtensionManifest = typeof extensionManifest & ExtensionModel;
|
||||||
|
|
||||||
export class LensExtension implements ExtensionModel {
|
export class LensExtension implements ExtensionModel {
|
||||||
public id: ExtensionId;
|
public id: ExtensionId;
|
||||||
public updateUrl: string;
|
public updateUrl: string;
|
||||||
|
protected disposers: Function[] = [];
|
||||||
|
|
||||||
@observable name = "";
|
@observable name = "";
|
||||||
@observable description = "";
|
@observable description = "";
|
||||||
@ -19,7 +22,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 +44,27 @@ 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;
|
||||||
|
this.disposers.forEach(cleanUp => cleanUp());
|
||||||
|
this.disposers.length = 0;
|
||||||
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;
|
||||||
@ -76,7 +92,7 @@ export class LensExtension implements ExtensionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): ExtensionModel {
|
toJSON(): ExtensionModel {
|
||||||
return {
|
return toJS({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
@ -84,6 +100,16 @@ export class LensExtension implements ExtensionModel {
|
|||||||
manifestPath: this.manifestPath,
|
manifestPath: this.manifestPath,
|
||||||
enabled: this.isEnabled,
|
enabled: this.isEnabled,
|
||||||
updateUrl: this.updateUrl,
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,19 +1,26 @@
|
|||||||
// Lens runtime for injecting to extension on activation
|
// Lens renderer runtime params available to extensions after activation
|
||||||
import { apiManager } from "../renderer/api/api-manager";
|
|
||||||
import logger from "../main/logger";
|
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 {
|
export interface LensRuntimeRendererEnv {
|
||||||
apiManager: typeof apiManager;
|
navigate: typeof navigate;
|
||||||
logger: typeof logger;
|
logger: typeof logger;
|
||||||
dynamicPages: typeof dynamicPages
|
dynamicPages: typeof dynamicPages
|
||||||
|
components: {
|
||||||
|
MainLayout: typeof MainLayout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: expose more public runtime apis: stores, managers, etc.
|
|
||||||
export function getLensRuntime(): LensRuntimeRendererEnv {
|
export function getLensRuntime(): LensRuntimeRendererEnv {
|
||||||
return {
|
return {
|
||||||
apiManager,
|
|
||||||
logger,
|
logger,
|
||||||
|
navigate,
|
||||||
dynamicPages,
|
dynamicPages,
|
||||||
|
components: {
|
||||||
|
MainLayout // fixme: refactor, import as pure component from "@lens/extensions"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/extensions/register-page.tsx
Normal file
46
src/extensions/register-page.tsx
Normal 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();
|
||||||
@ -10,6 +10,7 @@ import { i18nStore } from "./i18n";
|
|||||||
import { themeStore } from "./theme.store";
|
import { themeStore } from "./theme.store";
|
||||||
import { App } from "./components/app";
|
import { App } from "./components/app";
|
||||||
import { LensApp } from "./lens-app";
|
import { LensApp } from "./lens-app";
|
||||||
|
import { getLensRuntime } from "../extensions/lens-runtime";
|
||||||
|
|
||||||
type AppComponent = React.ComponentType & {
|
type AppComponent = React.ComponentType & {
|
||||||
init?(): void;
|
init?(): void;
|
||||||
@ -32,6 +33,7 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
await App.init();
|
await App.init();
|
||||||
|
extensionStore.autoEnableOnLoad(getLensRuntime);
|
||||||
}
|
}
|
||||||
render(<App/>, rootElem);
|
render(<App/>, rootElem);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
|
|||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { clusterIpc } from "../../common/cluster-ipc";
|
import { clusterIpc } from "../../common/cluster-ipc";
|
||||||
import { webFrame } from "electron";
|
import { webFrame } from "electron";
|
||||||
|
import { dynamicPages } from "../../extensions/register-page";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -71,6 +72,9 @@ export class App extends React.Component {
|
|||||||
<Route component={CustomResources} {...crdRoute}/>
|
<Route component={CustomResources} {...crdRoute}/>
|
||||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||||
<Route component={Apps} {...appsRoute}/>
|
<Route component={Apps} {...appsRoute}/>
|
||||||
|
{dynamicPages.clusterPages.map(({ path, components: { Page } }) => {
|
||||||
|
return <Route key={path} path={path} component={Page}/>
|
||||||
|
})}
|
||||||
<Redirect exact from="/" to={this.startURL}/>
|
<Redirect exact from="/" to={this.startURL}/>
|
||||||
<Route component={NotFound}/>
|
<Route component={NotFound}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -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 "../../../extensions/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}/>
|
||||||
|
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
|
||||||
|
return <Route key={path} path={path} component={Page}/>
|
||||||
|
})}
|
||||||
<Redirect exact to={this.startUrl}/>
|
<Redirect exact to={this.startUrl}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -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,8 +20,8 @@ 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 "../../../extensions/register-page";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -149,9 +149,8 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="dynamic-pages">
|
<div className="dynamic-pages">
|
||||||
{Array.from(dynamicPages.all).map(([path, { MenuIcon }]) => {
|
{dynamicPages.globalPages.map(({ path, components: { MenuIcon } }) => {
|
||||||
if (!MenuIcon) return;
|
return <MenuIcon key={path} onClick={() => navigate(path)}/>
|
||||||
return <MenuIcon onClick={() => navigate(path)}/>
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
// Dynamic pages
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import type { IconProps } from "../icon";
|
|
||||||
|
|
||||||
export interface PageComponents {
|
|
||||||
Main: React.ComponentType<any>;
|
|
||||||
MenuIcon: React.ComponentType<IconProps>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PagesStore {
|
|
||||||
all = observable.map<string, PageComponents>();
|
|
||||||
|
|
||||||
getComponents(path: string): PageComponents | null {
|
|
||||||
return this.all.get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
register(path: string, components: PageComponents) {
|
|
||||||
this.all.set(path, components);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(path: string) {
|
|
||||||
this.all.delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamicPages = new PagesStore();
|
|
||||||
@ -17,7 +17,7 @@ export interface TabRoute extends RouteProps {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
export interface MainLayoutProps {
|
||||||
className?: any;
|
className?: any;
|
||||||
tabs?: TabRoute[];
|
tabs?: TabRoute[];
|
||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
@ -27,7 +27,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class MainLayout extends React.Component<Props> {
|
export class MainLayout extends React.Component<MainLayoutProps> {
|
||||||
public storage = createStorage("main_layout", { pinnedSidebar: true });
|
public storage = createStorage("main_layout", { pinnedSidebar: true });
|
||||||
|
|
||||||
@observable isPinned = this.storage.get().pinnedSidebar;
|
@observable isPinned = this.storage.get().pinnedSidebar;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resourc
|
|||||||
import { CustomResources } from "../+custom-resources/custom-resources";
|
import { CustomResources } from "../+custom-resources/custom-resources";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { isAllowedResource } from "../../../common/rbac"
|
import { isAllowedResource } from "../../../common/rbac"
|
||||||
|
import { dynamicPages } from "../../../extensions/register-page";
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
||||||
type SidebarContextValue = {
|
type SidebarContextValue = {
|
||||||
@ -183,6 +184,18 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
>
|
>
|
||||||
{this.renderCustomResources()}
|
{this.renderCustomResources()}
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
|
{dynamicPages.clusterPages.map(({ path, menuTitle, components: { MenuIcon } }) => {
|
||||||
|
return (
|
||||||
|
<SidebarNavItem
|
||||||
|
key={path}
|
||||||
|
id={`extension-${path}`}
|
||||||
|
url={path}
|
||||||
|
routePath={path}
|
||||||
|
text={menuTitle}
|
||||||
|
icon={<MenuIcon/>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
|
|||||||
@ -11,15 +11,9 @@ import { ErrorBoundary } from "./components/error-boundary";
|
|||||||
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
|
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
|
||||||
import { Notifications } from "./components/notifications";
|
import { Notifications } from "./components/notifications";
|
||||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||||
import { extensionStore } from "../extensions/extension-store";
|
|
||||||
import { getLensRuntime } from "../extensions/lens-runtime";
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
componentDidMount() {
|
|
||||||
extensionStore.autoEnableOnLoad(getLensRuntime);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<I18nProvider i18n={_i18n}>
|
<I18nProvider i18n={_i18n}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user