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

PageRegistration & BaseRegistry refactoring #1334

Merged
merged 15 commits into from Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 23 additions & 10 deletions docs/extensions/capabilities/common-capabilities.md
Expand Up @@ -100,13 +100,20 @@ import { ExamplePage } from "./src/example-page"
export default class ExampleRendererExtension extends LensRendererExtension {
globalPages = [
{
path: "/example-route",
hideInMenu: true,
components: {
Page: ExamplePage,
}
}
]

globalPageMenus = [
{
title: "Example page", // used in icon's tooltip
components: {
Icon: () => <Component.Icon material="arrow"/>,
}
}
]
}
```

Expand Down Expand Up @@ -146,11 +153,20 @@ import { ExampleIcon, ExamplePage } from "./src/page"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
path: "/extension-example",
title: "Example Extension",
routePath: "/extension-example", // optional
exact: true, // optional
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]

clusterPageMenus = [
{
url: "/extension-example", // optional
title: "Example Extension",
components: {
Icon: ExampleIcon,
}
}
]
Expand Down Expand Up @@ -199,11 +215,8 @@ export default class ExampleExtension extends LensRendererExtension {
statusBarItems = [
{
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate("/example-page")}
>
<Component.Icon material="favorite" smallest />
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
<Component.Icon material="favorite" />
</div>
)
}
Expand Down
4 changes: 1 addition & 3 deletions docs/extensions/get-started/anatomy.md
Expand Up @@ -84,11 +84,9 @@ import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
path: "/extension-example",
title: "Hello World",
routePath: "/extension-example",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
Expand Down
3 changes: 1 addition & 2 deletions extensions/support-page/main.ts
@@ -1,13 +1,12 @@
import { LensMainExtension } from "@k8slens/extensions";
import { supportPageURL } from "./src/support.route";

export default class SupportPageMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "Support",
click: () => {
this.navigate(supportPageURL());
this.navigate();
}
}
]
Expand Down
21 changes: 7 additions & 14 deletions extensions/support-page/renderer.tsx
@@ -1,28 +1,21 @@
import React from "react";
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
import { supportPageRoute, supportPageURL } from "./src/support.route";
import { Support } from "./src/support";
import { Component, Interface, LensRendererExtension } from "@k8slens/extensions";
import { SupportPage } from "./src/support";

export default class SupportPageRendererExtension extends LensRendererExtension {
globalPages = [
globalPages: Interface.PageRegistration[] = [
{
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
Page: SupportPage,
}
}
]

statusBarItems = [
statusBarItems: Interface.StatusBarRegistration[] = [
{
item: (
<div
className="SupportPageIcon flex align-center"
onClick={() => Navigation.navigate(supportPageURL())}
>
<Component.Icon interactive material="help" smallest />
<div className="SupportPageIcon flex align-center" onClick={() => this.navigate()}>
<Component.Icon interactive material="help" smallest/>
</div>
)
}
Expand Down
7 changes: 0 additions & 7 deletions extensions/support-page/src/support.route.ts

This file was deleted.

2 changes: 1 addition & 1 deletion extensions/support-page/src/support.scss
@@ -1,4 +1,4 @@
.PageLayout.Support {
.SupportPage {
a[target=_blank] {
text-decoration: none;
border-bottom: 1px solid;
Expand Down
4 changes: 2 additions & 2 deletions extensions/support-page/src/support.tsx
Expand Up @@ -6,12 +6,12 @@ import { observer } from "mobx-react"
import { App, Component } from "@k8slens/extensions";

@observer
export class Support extends React.Component {
export class SupportPage extends React.Component {
render() {
const { PageLayout } = Component;
const { slackUrl, issuesTrackerUrl } = App;
return (
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
<PageLayout showOnTop className="SupportPage" header={<h2>Support</h2>}>
<h2>Community Slack Channel</h2>
<p>
Ask a question, see what's being discussed, join the conversation <a href={slackUrl} target="_blank">here</a>
Expand Down
15 changes: 0 additions & 15 deletions src/extensions/dynamic-page.tsx

This file was deleted.

27 changes: 14 additions & 13 deletions src/extensions/extension-loader.ts
Expand Up @@ -56,30 +56,31 @@ export class ExtensionLoader {

loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoInitExtensions((extension: LensMainExtension) => [
registries.menuRegistry.add(...extension.appMenus)
this.autoInitExtensions((ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus, { key: ext })
]);
}

loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.globalPageRegistry.add(...extension.globalPages),
registries.appPreferenceRegistry.add(...extension.appPreferences),
registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
registries.statusBarRegistry.add(...extension.statusBarItems),
this.autoInitExtensions((ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, { key: ext }),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, { key: ext }),
registries.appPreferenceRegistry.add(ext.appPreferences, { key: ext }),
registries.clusterFeatureRegistry.add(ext.clusterFeatures, { key: ext }),
registries.statusBarRegistry.add(ext.statusBarItems, { key: ext }),
]);
}

loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.clusterPageRegistry.add(...extension.clusterPages),
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(...extension.kubeObjectStatusTexts)
this.autoInitExtensions((ext: LensRendererExtension) => [
registries.clusterPageRegistry.add(ext.clusterPages, { key: ext }),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, { key: ext }),
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems, { key: ext }),
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems, { key: ext }),
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts, { key: ext })
])

}

protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
Expand Down
1 change: 1 addition & 0 deletions src/extensions/interfaces/registrations.ts
Expand Up @@ -4,4 +4,5 @@ export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from ".
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"
export type { PageRegistration, PageComponents } from "../registries/page-registry"
export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"
export type { StatusBarRegistration } from "../registries/status-bar-registry"
10 changes: 10 additions & 0 deletions src/extensions/lens-extension.ts
@@ -1,5 +1,6 @@
import type { InstalledExtension } from "./extension-manager";
import { action, observable, reaction } from "mobx";
import { compile } from "path-to-regexp"
import logger from "../main/logger";

export type LensExtensionId = string; // path to manifest (package.json)
Expand All @@ -14,6 +15,7 @@ export interface LensExtensionManifest {
}

export class LensExtension {
readonly routePrefix = "/extension/:name"
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
Expand Down Expand Up @@ -42,6 +44,14 @@ export class LensExtension {
return this.manifest.description
}

getPageUrl(baseUrl = "") {
return compile(this.routePrefix)({ name: this.name }) + baseUrl;
}

getPageRoute(baseRoute = "") {
return this.routePrefix + baseRoute;
}

@action
async enable() {
if (this.isEnabled) return;
Expand Down
6 changes: 4 additions & 2 deletions src/extensions/lens-main-extension.ts
Expand Up @@ -6,7 +6,9 @@ import { WindowManager } from "../main/window-manager";
export class LensMainExtension extends LensExtension {
@observable.shallow appMenus: MenuRegistration[] = []

async navigate(location: string, frameId?: number) {
await WindowManager.getInstance<WindowManager>().navigate(location, frameId)
async navigate(location?: string, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>();
const url = this.getPageUrl(location); // get full path to extension's page
await windowManager.navigate(url, frameId);
}
}
16 changes: 7 additions & 9 deletions src/extensions/lens-renderer-extension.ts
@@ -1,23 +1,21 @@
import type {
AppPreferenceRegistration, ClusterFeatureRegistration,
KubeObjectMenuRegistration, KubeObjectDetailRegistration,
PageRegistration, StatusBarRegistration, KubeObjectStatusRegistration
} from "./registries"
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import { ipcRenderer } from "electron"

export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = []
@observable.shallow clusterPages: PageRegistration[] = []
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []
@observable.shallow globalPageMenus: PageMenuRegistration[] = []
@observable.shallow clusterPageMenus: PageMenuRegistration[] = []
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []
@observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
@observable.shallow statusBarItems: StatusBarRegistration[] = []
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []

navigate(location: string) {
ipcRenderer.emit("renderer:navigate", location)
async navigate(location?: string) {
const { navigate } = await import("../renderer/navigation");
navigate(this.getPageUrl(location));
}
}
4 changes: 2 additions & 2 deletions src/extensions/registries/app-preference-registry.ts
@@ -1,12 +1,12 @@
import type React from "react"
import { BaseRegistry } from "./base-registry";
import { BaseRegistry, BaseRegistryItem } from "./base-registry";

export interface AppPreferenceComponents {
Hint: React.ComponentType<any>;
Input: React.ComponentType<any>;
}

export interface AppPreferenceRegistration {
export interface AppPreferenceRegistration extends BaseRegistryItem {
title: string;
components: AppPreferenceComponents;
}
Expand Down
64 changes: 53 additions & 11 deletions src/extensions/registries/base-registry.ts
@@ -1,23 +1,65 @@
// Base class for extensions-api registries
import { action, observable } from "mobx";
import { LensExtension } from "../lens-extension";
import { getRandId } from "../../common/utils";

export class BaseRegistry<T = any> {
protected items = observable<T>([], { deep: false });
export type BaseRegistryKey = LensExtension | null;
export type BaseRegistryItemId = string | symbol;

getItems(): T[] {
return this.items.toJS();
export interface BaseRegistryItem {
id?: BaseRegistryItemId; // uniq id, generated automatically when not provided
}

export interface BaseRegistryAddMeta {
key?: BaseRegistryKey;
merge?: boolean
}

export class BaseRegistry<T extends BaseRegistryItem = any> {
private items = observable.map<BaseRegistryKey, T[]>([], { deep: false });

getItems(): (T & { extension?: LensExtension | null })[] {
return Array.from(this.items).map(([ext, items]) => {
return items.map(item => ({
...item,
extension: ext,
}))
}).flat()
}

getById(itemId: BaseRegistryItemId, key?: BaseRegistryKey): T {
const byId = (item: BaseRegistryItem) => item.id === itemId;
if (key) {
return this.items.get(key)?.find(byId)
}
return this.getItems().find(byId);
}

@action
add(...items: T[]) {
this.items.push(...items);
return () => this.remove(...items);
add(items: T | T[], { key = null, merge = true }: BaseRegistryAddMeta = {}) {
const normalizedItems = (Array.isArray(items) ? items : [items]).map((item: T) => {
item.id = item.id || getRandId();
return item;
});
if (merge && this.items.has(key)) {
const newItems = new Set(this.items.get(key));
normalizedItems.forEach(item => newItems.add(item))
this.items.set(key, [...newItems]);
} else {
this.items.set(key, normalizedItems);
}
return () => this.remove(normalizedItems, key)
}

@action
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
})
remove(items: T[], key: BaseRegistryKey = null) {
const storedItems = this.items.get(key);
if (!storedItems) return;
const newItems = storedItems.filter(item => !items.includes(item)); // works because of {deep: false};
if (newItems.length > 0) {
this.items.set(key, newItems)
} else {
this.items.delete(key);
}
}
}