From d19eb57037d4852c6285eba8456654e44e2ff915 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 18 Nov 2020 08:55:57 +0200 Subject: [PATCH 1/6] Track service start and stop events (#1414) Signed-off-by: Lauri Nevala --- src/main/cluster-manager.ts | 4 +++- src/main/exit-app.ts | 18 ++++++++++++++++++ src/main/index.ts | 5 +++-- src/main/menu.ts | 13 ++++++++++--- src/main/tray.ts | 5 +++-- src/main/window-manager.ts | 6 ++++++ 6 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/main/exit-app.ts diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 1792fb953b..1a479e724e 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -6,9 +6,11 @@ import { clusterStore, getClusterIdFromHost } from "../common/cluster-store" import { Cluster } from "./cluster" import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; +import { Singleton } from "../common/utils"; -export class ClusterManager { +export class ClusterManager extends Singleton { constructor(public readonly port: number) { + super() // auto-init clusters autorun(() => { clusterStore.enabledClustersList.forEach(cluster => { diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts new file mode 100644 index 0000000000..bf73e022f3 --- /dev/null +++ b/src/main/exit-app.ts @@ -0,0 +1,18 @@ +import { app } from "electron"; +import { WindowManager } from "./window-manager"; +import { appEventBus } from "../common/event-bus"; +import { ClusterManager } from "./cluster-manager"; +import logger from "./logger"; + + +export function exitApp() { + const windowManager = WindowManager.getInstance() + const clusterManager = ClusterManager.getInstance() + appEventBus.emit({ name: "service", action: "close" }) + windowManager.hide(); + clusterManager.stop(); + logger.info('SERVICE:QUIT'); + setTimeout(() => { + app.exit() + }, 1000) +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 21acda801b..b25aed5bb2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -67,7 +67,7 @@ app.on("ready", async () => { } // create cluster manager - clusterManager = new ClusterManager(proxyPort); + clusterManager = ClusterManager.getInstance(proxyPort); // run proxy try { @@ -82,7 +82,7 @@ app.on("ready", async () => { extensionLoader.init(await extensionManager.load()); // call after windowManager to see splash earlier setTimeout(() => { - appEventBus.emit({ name: "app", action: "start" }) + appEventBus.emit({ name: "service", action: "start" }) }, 1000) }); @@ -96,6 +96,7 @@ app.on("activate", (event, hasVisibleWindows) => { // Quit app on Cmd+Q (MacOS) app.on("will-quit", (event) => { logger.info('APP:QUIT'); + appEventBus.emit({name: "app", action: "close"}) event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) clusterManager?.stop(); // close cluster connections return; // skip exit to make tray work, to quit go to app's global menu or tray's menu diff --git a/src/main/menu.ts b/src/main/menu.ts index fbd6af8d3c..08260d8e53 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -9,6 +9,7 @@ import { clusterSettingsURL } from "../renderer/components/+cluster-settings/clu import { extensionsURL } from "../renderer/components/+extensions/extensions.route"; import { menuRegistry } from "../extensions/registries/menu-registry"; import logger from "./logger"; +import { exitApp } from "./exit-app"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help" @@ -89,7 +90,7 @@ export function buildMenu(windowManager: WindowManager) { label: 'Quit', accelerator: 'Cmd+Q', click() { - app.exit(); // force quit since might be blocked within app.on("will-quit") + exitApp() } } ] @@ -135,7 +136,13 @@ export function buildMenu(windowManager: WindowManager) { } }, { type: 'separator' }, - { role: 'quit' } + { + label: 'Quit', + accelerator: 'Alt+F4', + click() { + exitApp() + } + } ]), { type: 'separator' }, { role: 'close' } // close current window @@ -259,7 +266,7 @@ export function buildMenu(windowManager: WindowManager) { } menu = menuItem.submenu; } - + const menuPath: string = parentLabels.join(" -> ") if (!menuItem) { logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`); diff --git a/src/main/tray.ts b/src/main/tray.ts index 5bc6ce7091..f170517e62 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,6 +1,6 @@ import path from "path" import packageInfo from "../../package.json" -import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron" +import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron" import { autorun } from "mobx"; import { showAbout } from "./menu"; import { AppUpdater } from "./app-updater"; @@ -11,6 +11,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences. import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route"; import logger from "./logger"; import { isDevelopment } from "../common/vars"; +import { exitApp } from "./exit-app"; // note: instance of Tray should be saved somewhere, otherwise it disappears export let tray: Tray; @@ -119,7 +120,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu { { label: 'Quit App', click() { - app.exit(); + exitApp() } } ]); diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index fc61ca8345..034e14b61a 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -85,6 +85,7 @@ export class WindowManager extends Singleton { await this.mainWindow.loadURL(this.mainUrl); this.mainWindow.show(); this.splashWindow?.close(); + appEventBus.emit({ name: "app", action: "start" }) } catch (err) { dialog.showErrorBox("ERROR!", err.toString()) } @@ -156,6 +157,11 @@ export class WindowManager extends Singleton { this.splashWindow.show(); } + hide() { + if (!this.mainWindow?.isDestroyed()) this.mainWindow.hide(); + if (!this.splashWindow.isDestroyed()) this.splashWindow.hide(); + } + destroy() { this.mainWindow.destroy(); this.splashWindow.destroy(); From b0b2a993724f109cef169fb93291eaea5179c9fe Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Wed, 18 Nov 2020 02:01:42 -0500 Subject: [PATCH 2/6] Extension Guides documentation (#1427) - overview - main extension guide Signed-off-by: Jim Ehrismann --- docs/extensions/guides/README.md | 28 +++++++++ docs/extensions/guides/main-extension.md | 76 ++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 105 insertions(+) create mode 100644 docs/extensions/guides/main-extension.md diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index e69de29bb2..2917090212 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -0,0 +1,28 @@ +# Extension Guides + +The basics of the Lens Extension API are covered in [Your First Extension](../get-started/your-first-extension.md). In this section detailed code guides and samples are used to explain how to use specific Lens Extension APIs. + +Each guide or sample will include: + +- Clearly commented source code. +- Instructions for running the sample extension. +- Image of the sample extension's appearance and usage. +- Listing of Extension API being used. +- Explanation of Extension API concepts. + +## Guides + +| Guide | APIs | +| ----- | ----- | +| [Main process extension](main-extension.md) | LensMainExtension | +| [Renderer process extension](renderer-extension.md) | LensRendererExtension | +| [Stores](stores.md) | | +| [Components](components.md) | | +| [KubeObjectListLayout](kube-object-list-layout.md) | | + +## Samples + +| Sample | APIs | +| ----- | ----- | +[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | +[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension
Store.clusterStore
Store.workspaceStore | \ No newline at end of file diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md new file mode 100644 index 0000000000..c9ad9e378b --- /dev/null +++ b/docs/extensions/guides/main-extension.md @@ -0,0 +1,76 @@ +# Main Extension + +The main extension api is the interface to Lens' main process (Lens runs in main and renderer processes). It allows you to access, configure, and customize Lens data, add custom application menu items, and generally run custom code in Lens' main process. + +## `LensMainExtension` Class + +To create a main extension simply extend the `LensMainExtension` class: + +``` typescript +import { LensMainExtension } from "@k8slens/extensions"; + +export default class ExampleExtensionMain extends LensMainExtension { + onActivate() { + console.log('custom main process extension code started'); + } + + onDeactivate() { + console.log('custom main process extension de-activated'); + } +} +``` + +There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By overriding `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled. Note that to see standard output from the main process there must be a console connected to it. This is typically achieved by starting Lens from the command prompt. + +The following example is a little more interesting in that it accesses some Lens state data and periodically logs the name of the currently active cluster in Lens. + +``` typescript +import { LensMainExtension, Store } from "@k8slens/extensions"; + +const clusterStore = Store.clusterStore + +export default class ActiveClusterExtensionMain extends LensMainExtension { + + timer: NodeJS.Timeout + + onActivate() { + console.log("Cluster logger activated"); + this.timer = setInterval(() => { + if (!clusterStore.active) { + console.log("No active cluster"); + return; + } + console.log("active cluster is", clusterStore.active.contextName) + }, 5000) + } + + onDeactivate() { + clearInterval(this.timer) + console.log("Cluster logger deactivated"); + } +} +``` + +See the [Stores](../stores) guide for more details on accessing Lens state data. + +### `appMenus` + +The only UI feature customizable in the main extension api is the application menu. Custom menu items can be inserted and linked to custom functionality, such as navigating to a specific page. The following example demonstrates adding a menu item to the Help menu. + +``` typescript +import { LensMainExtension } from "@k8slens/extensions"; + +export default class SamplePageMainExtension extends LensMainExtension { + appMenus = [ + { + parentId: "help", + label: "Sample", + click() { + console.log("Sample clicked"); + } + } + ] +} +``` + +`appMenus` is an array of objects satisfying the `MenuRegistration` interface. `MenuRegistration` extends React's `MenuItemConstructorOptions` interface. `parentId` is the id of the menu to put this menu item under (todo: is this case sensitive and how do we know what the available ids are?), `label` is the text to show on the menu item, and `click()` is called when the menu item is selected. In this example we simply log a message, but typically you would navigate to a specific page or perform some operation. Pages are associated with the [`LensRendererExtension`](renderer-extension.md) class and can be defined when you extend it. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 7101f6897d..cf0c066dc8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ nav: - Color Reference: extensions/capabilities/color-reference.md - Extension Guides: - Overview: extensions/guides/README.md + - Main Extension: extensions/guides/main-extension.md - Renderer Extension: extensions/guides/renderer-extension.md - Testing and Publishing: - Testing Extensions: extensions/testing-and-publishing/testing.md From 04517148c08565a0d0d7c4f4d9f907a8d6cee195 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 18 Nov 2020 09:12:11 +0200 Subject: [PATCH 3/6] Add computed name property to cluster (#1428) Signed-off-by: Lauri Nevala --- src/main/cluster.ts | 4 ++++ src/main/tray.ts | 2 +- src/renderer/components/cluster-icon/cluster-icon.tsx | 8 ++++---- src/renderer/components/layout/main-layout.tsx | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 600b377729..b808f6d997 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -86,6 +86,10 @@ export class Cluster implements ClusterModel, ClusterState { return this.accessible && !this.disconnected; } + @computed get name() { + return this.preferences.clusterName || this.contextName + } + get version(): string { return String(this.metadata?.version) || "" } diff --git a/src/main/tray.ts b/src/main/tray.ts index f170517e62..268e006089 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -89,7 +89,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu { label: workspace.name, toolTip: workspace.description, submenu: clusters.map(cluster => { - const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster; + const { id: clusterId, name: label, online, workspace } = cluster; return { label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`, toolTip: clusterId, diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx index ccc65892de..34fbb9ebf8 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ b/src/renderer/components/cluster-icon/cluster-icon.tsx @@ -34,8 +34,8 @@ export class ClusterIcon extends React.Component { cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, children, ...elemProps } = this.props; - const { isAdmin, eventCount, preferences, id: clusterId } = cluster; - const { clusterName, icon } = preferences; + const { isAdmin, name, eventCount, preferences, id: clusterId } = cluster; + const { icon } = preferences; const clusterIconId = `cluster-icon-${clusterId}`; const className = cssNames("ClusterIcon flex inline", this.props.className, { interactive: interactive !== undefined ? interactive : !!this.props.onClick, @@ -44,9 +44,9 @@ export class ClusterIcon extends React.Component { return (
{showTooltip && ( - {clusterName} + {name} )} - {icon && {clusterName}/} + {icon && {name}/} {!icon && } {showErrors && isAdmin && eventCount > 0 && ( { return (
- {cluster.preferences.clusterName || cluster.contextName} + {cluster.name}