From 6a33944f527f607a7e3a14a8cfdf863276228b46 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 17 May 2021 16:57:52 +0300 Subject: [PATCH 1/9] Remove universal analytics types (#2781) Signed-off-by: Jari Kolehmainen --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index bf85e16fec..cc4e278557 100644 --- a/package.json +++ b/package.json @@ -291,7 +291,6 @@ "@types/tar": "^4.0.4", "@types/tcp-port-used": "^1.0.0", "@types/tempy": "^0.3.0", - "@types/universal-analytics": "^0.4.4", "@types/url-parse": "^1.4.3", "@types/uuid": "^8.3.0", "@types/webdriverio": "^4.13.0", diff --git a/yarn.lock b/yarn.lock index 7492bed758..2be3cbfbcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1803,11 +1803,6 @@ resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.0.tgz#5cb0dff2a5f616fc8e0c61b482bf01fa20a03cec" integrity sha512-ZAbqul7QAKpM2h1PFGa5ETN27ulmqtj0QviYHasw9LffvXZvVHuraOx/FOsIPPDNGZN0Qo1nASxxSfMYOtSoCw== -"@types/universal-analytics@^0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.4.tgz#496a52b92b599a0112bec7c12414062de6ea8449" - integrity sha512-9g3F0SGxVr4UDd6y07bWtFnkpSSX1Ake7U7AGHgSFrwM6pF53/fV85bfxT2JLWS/3sjLCcyzoYzQlCxpkVo7wA== - "@types/url-parse@^1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329" From e188cf45e607f382f461c5a21e0088414c36774f Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 18 May 2021 12:23:17 +0300 Subject: [PATCH 2/9] Delete node shell container on exit (#2793) Signed-off-by: Lauri Nevala --- src/main/shell-session/node-shell-session.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session.ts index 1848716380..72413c7953 100644 --- a/src/main/shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session.ts @@ -120,6 +120,11 @@ export class NodeShellSession extends ShellSession { }); } + protected exit() { + super.exit(); + this.deleteNodeShellPod(); + } + protected deleteNodeShellPod() { this .kc From 6be465858b438813887222f60bc2927eaaf6b637 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 18 May 2021 05:24:43 -0400 Subject: [PATCH 3/9] Add IPC capabilities for Extensions (#2775) * Add IPC capabilities for Extensions Signed-off-by: Sebastian Malton * revert onA|D change: Signed-off-by: Sebastian Malton * Switch to pushing the disposer in the methods Signed-off-by: Sebastian Malton * improve documentation, switch to a singleton instead of extension methods Signed-off-by: Sebastian Malton * fix build Signed-off-by: Sebastian Malton * make exported class abstract, improve guide Signed-off-by: Sebastian Malton * fix docs Signed-off-by: Sebastian Malton * fix lint Signed-off-by: Sebastian Malton * Change guide demo to initialization in constructor Signed-off-by: Sebastian Malton --- docs/extensions/guides/README.md | 1 + docs/extensions/guides/ipc.md | 131 ++++++++++++++++++++++++++ mkdocs.yml | 1 + src/common/ipc/ipc.ts | 40 ++++---- src/common/ipc/type-enforced-ipc.ts | 68 +++++++++---- src/extensions/core-api/index.ts | 2 + src/extensions/core-api/stores.ts | 2 + src/extensions/core-api/types.ts | 24 +++++ src/extensions/ipc-store.ts | 39 ++++++++ src/extensions/lens-extension.ts | 27 +++--- src/extensions/lens-main-extension.ts | 2 +- src/extensions/main-ipc-store.ts | 45 +++++++++ src/extensions/registries/index.ts | 1 + src/extensions/renderer-ipc-store.ts | 44 +++++++++ 14 files changed, 375 insertions(+), 52 deletions(-) create mode 100644 docs/extensions/guides/ipc.md create mode 100644 src/extensions/core-api/types.ts create mode 100644 src/extensions/ipc-store.ts create mode 100644 src/extensions/main-ipc-store.ts create mode 100644 src/extensions/renderer-ipc-store.ts diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index 06bbbe9e3c..012794a39e 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -24,6 +24,7 @@ Each guide or code sample includes the following: | [KubeObjectListLayout](kube-object-list-layout.md) | | | [Working with mobx](working-with-mobx.md) | | | [Protocol Handlers](protocol-handlers.md) | | +| [Sending Data between main and renderer](ipc.md) | | ## Samples diff --git a/docs/extensions/guides/ipc.md b/docs/extensions/guides/ipc.md new file mode 100644 index 0000000000..91d811bdf0 --- /dev/null +++ b/docs/extensions/guides/ipc.md @@ -0,0 +1,131 @@ +# Inter Process Communication + +A Lens Extension can utilize IPC to send information between its `LensRendererExtension` and its `LensMainExtension`. +This is useful when wanting to communicate directly within your extension. +For example, if a user logs into a service that your extension is a facade for and `main` needs to know some information so that you can start syncing items to the `Catalog`, this would be a good way to send that information along. + +IPC channels are blocked off per extension. +Meaning that each extension can only communicate with itself. + +## Types of IPC + +There are two flavours of IPC that are provided: + +- Event based +- Request based + +### Event Based IPC + +This is the same as an [Event Emitter](https://nodejs.org/api/events.html#events_class_eventemitter) but is not limited to just one Javascript process. +This is a good option when you need to report that something has happened but you don't need a response. + +This is a fully two-way form of communication. +Both `LensMainExtension` and `LensRendererExtension` can do this sort of IPC. + +### Request Based IPC + +This is more like a Remote Procedure Call (RPC). +With this sort of IPC the caller waits for the result from the other side. +This is accomplished by returning a `Promise` which needs to be `await`-ed. + +This is a unidirectional form of communication. +Only `LensRendererExtension` can initiate this kind of request, and only `LensMainExtension` can and respond this this kind of request. + +## Registering IPC Handlers and Listeners + +The general terminology is as follows: + +- A "handler" is the function that responds to a "Request Based IPC" event. +- A "listener" is the function that is called when a "Event Based IPC" event is emitted. + +To register either a handler or a listener, you should do something like the following: + +`main.ts`: +```typescript +import { LensMainExtension, Interface, Types, Store } from "@k8slens/extensions"; +import { registerListeners, IpcMain } from "./helpers/main"; + +export class ExampleExtensionMain extends LensMainExtension { + onActivate() { + IpcMain.createInstance(this); + } +} +``` + +This file shows that you need to create an instance of the store to be able to use IPC. +Lens will automatically clean up that store and all the handlers on deactivation and uninstall. + +--- + +`helpers/main.ts`: +```typescript +import { Store } from "@k8slens/extensions"; + +export class IpcMain extends Store.MainIpcStore { + constructor(extension: LensMainExtension) { + super(extension); + + this.listenIpc("initialize", onInitialize); + } +} + +function onInitialize(event: Types.IpcMainEvent, id: string) { + console.log(`starting to initialize: ${id}`); +} +``` + +In other files, it is not necessary to pass around any instances. +It should be able to just call `getInstance()` everywhere in your extension as needed. + +--- + +`renderer.ts`: +```typescript +import { LensRendererExtension, Interface, Types } from "@k8slens/extensions"; +import { IpcRenderer } from "./helpers/renderer"; + +export class ExampleExtensionRenderer extends LensRendererExtension { + onActivate() { + const ipc = IpcRenderer.createInstance(this); + + setTimeout(() => ipc.broadcastIpc("initialize", "an-id"), 5000); + } +} +``` + +It is also needed to create an instance to broadcast messages too. + +--- + +`helpers/renderer.ts`: +```typescript +import { Store } from "@k8slens/extensions"; + +export class IpcMain extends Store.RendererIpcStore {} +``` + +It is necessary to create child classes of these `abstract class`'s in your extension before you can use them. + +--- + +As this example shows: the channel names *must* be the same. +It should also be noted that "listeners" and "handlers" are specific to either `LensRendererExtension` and `LensMainExtension`. +There is no behind the scenes transfer of these functions. + +If you want to register a "handler" you would call `Store.MainIpcStore.handleIpc(...)` instead. +The cleanup of these handlers is handled by Lens itself. + +`Store.RendererIpcStore.broadcastIpc(...)` and `Store.MainIpcStore.broadcastIpc(...)` sends an event to all renderer frames and to main. +Because of this, no matter where you broadcast from, all listeners in `main` and `renderer` will be notified. + +### Allowed Values + +This IPC mechanism utilizes the [Structured Clone Algorithm](developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for serialization. +This means that more types than what are JSON serializable can be used, but not all the information will be passed through. + +## Using IPC + +Calling IPC is very simple. +If you are meaning to do an event based call, merely call `broadcastIpc(, ...)` from within your extension. + +If you are meaning to do a request based call from `renderer`, you should do `const res = await Store.RendererIpcStore.invokeIpc(, ...));` instead. diff --git a/mkdocs.yml b/mkdocs.yml index 0496f3001e..fe42cbccf5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - Stores: extensions/guides/stores.md - Working with MobX: extensions/guides/working-with-mobx.md - Protocol Handlers: extensions/guides/protocol-handlers.md + - IPC: extensions/guides/ipc.md - Testing and Publishing: - Testing Extensions: extensions/testing-and-publishing/testing.md - Publishing Extensions: extensions/testing-and-publishing/publishing.md diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 74305e0484..66e591e765 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -42,35 +42,33 @@ function getSubFrames(): ClusterFrameInfo[] { return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); } -export async function broadcastMessage(channel: string, ...args: any[]) { +export function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; - if (ipcRenderer) { - ipcRenderer.send(channel, ...args); - } else if (ipcMain) { - ipcMain.emit(channel, ...args); - } + ipcRenderer?.send(channel, ...args); + ipcMain?.emit(channel, ...args); - for (const view of views) { - const type = view.getType(); + const subFramesP = ipcRenderer + ? requestMain(subFramesChannel) + : Promise.resolve(getSubFrames()); - logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); - view.send(channel, ...args); + subFramesP + .then(subFrames => { + for (const view of views) { + try { + logger.silly(`[IPC]: broadcasting "${channel}" to ${view.getType()}=${view.id}`, { args }); + view.send(channel, ...args); - try { - const subFrames: ClusterFrameInfo[] = ipcRenderer - ? await requestMain(subFramesChannel) - : getSubFrames(); - - for (const frameInfo of subFrames) { - view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + for (const frameInfo of subFrames) { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } + } catch (error) { + logger.error("[IPC]: failed to send IPC message", { error: String(error) }); + } } - } catch (error) { - logger.error("[IPC]: failed to send IPC message", { error: String(error) }); - } - } + }); } export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { diff --git a/src/common/ipc/type-enforced-ipc.ts b/src/common/ipc/type-enforced-ipc.ts index 1c4c7d301d..035ffcd77e 100644 --- a/src/common/ipc/type-enforced-ipc.ts +++ b/src/common/ipc/type-enforced-ipc.ts @@ -19,10 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { ipcMain } from "electron"; import { EventEmitter } from "events"; import logger from "../../main/logger"; +import { Disposer } from "../utils"; -export type HandlerEvent = Parameters[1]>[0]; +export type ListenerEvent = Parameters[1]>[0]; export type ListVerifier = (args: unknown[]) => args is T; export type Rest = T extends [any, ...infer R] ? R : []; @@ -34,22 +36,22 @@ export type Rest = T extends [any, ...infer R] ? R : []; * @param verifier The function to be called to verify that the args are the correct type */ export function onceCorrect< - EM extends EventEmitter, - L extends (event: HandlerEvent, ...args: any[]) => any + IPC extends EventEmitter, + Listener extends (event: ListenerEvent, ...args: any[]) => any >({ source, channel, listener, verifier, }: { - source: EM, - channel: string | symbol, - listener: L, - verifier: ListVerifier>>, + source: IPC, + channel: string, + listener: Listener, + verifier: ListVerifier>>, }): void { - function handler(event: HandlerEvent, ...args: unknown[]): void { + function wrappedListener(event: ListenerEvent, ...args: unknown[]): void { if (verifier(args)) { - source.removeListener(channel, handler); // remove immediately + source.removeListener(channel, wrappedListener); // remove immediately (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject .catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error })); @@ -58,7 +60,7 @@ export function onceCorrect< } } - source.on(channel, handler); + source.on(channel, wrappedListener); } /** @@ -68,25 +70,53 @@ export function onceCorrect< * @param verifier The function to be called to verify that the args are the correct type */ export function onCorrect< - EM extends EventEmitter, - L extends (event: HandlerEvent, ...args: any[]) => any + IPC extends EventEmitter, + Listener extends (event: ListenerEvent, ...args: any[]) => any >({ source, channel, listener, verifier, }: { - source: EM, - channel: string | symbol, - listener: L, - verifier: ListVerifier>>, -}): void { - source.on(channel, (event, ...args: unknown[]) => { + source: IPC, + channel: string, + listener: Listener, + verifier: ListVerifier>>, +}): Disposer { + function wrappedListener(event: ListenerEvent, ...args: unknown[]) { if (verifier(args)) { (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject .catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error })); } else { logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); } - }); + } + + source.on(channel, wrappedListener); + + return () => source.off(channel, wrappedListener); +} + +export function handleCorrect< + Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any, +>({ + channel, + handler, + verifier, +}: { + channel: string, + handler: Handler, + verifier: ListVerifier>>, +}): Disposer { + function wrappedHandler(event: Electron.IpcMainInvokeEvent, ...args: unknown[]): ReturnType { + if (verifier(args)) { + return handler(event, ...args); + } + + throw new TypeError(`Invalid args for invoke on channel: ${channel}`); + } + + ipcMain.handle(channel, wrappedHandler); + + return () => ipcMain.removeHandler(channel); } diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts index 258fdbb6af..4fdca344d9 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -31,6 +31,7 @@ import * as Util from "./utils"; import * as ClusterFeature from "./cluster-feature"; import * as Interface from "../interfaces"; import * as Catalog from "./catalog"; +import * as Types from "./types"; export { App, @@ -39,5 +40,6 @@ export { ClusterFeature, Interface, Store, + Types, Util, }; diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index 17eac539d6..a9dfe9bf1e 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -20,3 +20,5 @@ */ export { ExtensionStore } from "../extension-store"; +export { MainIpcStore } from "../main-ipc-store"; +export { RendererIpcStore } from "../renderer-ipc-store"; diff --git a/src/extensions/core-api/types.ts b/src/extensions/core-api/types.ts new file mode 100644 index 0000000000..ed78b5c017 --- /dev/null +++ b/src/extensions/core-api/types.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent; +export type IpcRendererEvent = Electron.IpcRendererEvent; +export type IpcMainEvent = Electron.IpcMainEvent; diff --git a/src/extensions/ipc-store.ts b/src/extensions/ipc-store.ts new file mode 100644 index 0000000000..541f7b2914 --- /dev/null +++ b/src/extensions/ipc-store.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { Singleton } from "../common/utils"; +import { LensExtension } from "./lens-extension"; +import { createHash } from "crypto"; +import { broadcastMessage } from "../common/ipc"; + +export const IpcPrefix = Symbol(); + +export abstract class IpcStore extends Singleton { + readonly [IpcPrefix]: string; + + constructor(protected extension: LensExtension) { + super(); + this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex"); + } + + broadcastIpc(channel: string, ...args: any[]): void { + broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args); + } +} diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index cf9077209c..d9df2c9993 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -23,7 +23,8 @@ import type { InstalledExtension } from "./extension-discovery"; import { action, observable, reaction } from "mobx"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; -import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry"; +import { ProtocolHandlerRegistration } from "./registries"; +import { disposer } from "../common/utils"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -37,6 +38,8 @@ export interface LensExtensionManifest { lens?: object; // fixme: add more required fields for validation } +export const Disposers = Symbol(); + export class LensExtension { readonly id: LensExtensionId; readonly manifest: LensExtensionManifest; @@ -46,6 +49,7 @@ export class LensExtension { protocolHandlers: ProtocolHandlerRegistration[] = []; @observable private isEnabled = false; + [Disposers] = disposer(); constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) { this.id = id; @@ -62,6 +66,10 @@ export class LensExtension { return this.manifest.version; } + get description() { + return this.manifest.description; + } + /** * getExtensionFileFolder returns the path to an already created folder. This * folder is for the sole use of this extension. @@ -73,15 +81,11 @@ export class LensExtension { return FilesystemProvisionerStore.getInstance().requestDirectory(this.id); } - get description() { - return this.manifest.description; - } - @action async enable() { if (this.isEnabled) return; this.isEnabled = true; - this.onActivate(); + this.onActivate?.(); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); } @@ -89,7 +93,8 @@ export class LensExtension { async disable() { if (!this.isEnabled) return; this.isEnabled = false; - this.onDeactivate(); + this.onDeactivate?.(); + this[Disposers](); logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); } @@ -125,12 +130,12 @@ export class LensExtension { }; } - protected onActivate() { - // mock + protected onActivate(): void { + return; } - protected onDeactivate() { - // mock + protected onDeactivate(): void { + return; } } diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 7ae2f06693..b02456c6d4 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -19,12 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { MenuRegistration } from "./registries/menu-registry"; import { LensExtension } from "./lens-extension"; import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; import { CatalogEntity, catalogEntityRegistry } from "../common/catalog"; import { IObservableArray } from "mobx"; +import { MenuRegistration } from "./registries"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; diff --git a/src/extensions/main-ipc-store.ts b/src/extensions/main-ipc-store.ts new file mode 100644 index 0000000000..1f9241a8b1 --- /dev/null +++ b/src/extensions/main-ipc-store.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { ipcMain } from "electron"; +import { IpcPrefix, IpcStore } from "./ipc-store"; +import { Disposers } from "./lens-extension"; +import { LensMainExtension } from "./lens-main-extension"; + +export abstract class MainIpcStore extends IpcStore { + constructor(extension: LensMainExtension) { + super(extension); + extension[Disposers].push(() => MainIpcStore.resetInstance()); + } + + handleIpc(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + + ipcMain.handle(prefixedChannel, handler); + this.extension[Disposers].push(() => ipcMain.removeHandler(prefixedChannel)); + } + + listenIpc(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): void { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + + ipcMain.addListener(prefixedChannel, listener); + this.extension[Disposers].push(() => ipcMain.removeListener(prefixedChannel, listener)); + } +} diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 13d07b2249..7056206daf 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -32,3 +32,4 @@ export * from "./kube-object-status-registry"; export * from "./command-registry"; export * from "./entity-setting-registry"; export * from "./welcome-menu-registry"; +export * from "./protocol-handler-registry"; diff --git a/src/extensions/renderer-ipc-store.ts b/src/extensions/renderer-ipc-store.ts new file mode 100644 index 0000000000..930027d0e2 --- /dev/null +++ b/src/extensions/renderer-ipc-store.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { ipcRenderer } from "electron"; +import { IpcPrefix, IpcStore } from "./ipc-store"; +import { Disposers } from "./lens-extension"; +import { LensRendererExtension } from "./lens-renderer-extension"; + +export abstract class RendererIpcStore extends IpcStore { + constructor(extension: LensRendererExtension) { + super(extension); + extension[Disposers].push(() => RendererIpcStore.resetInstance()); + } + + listenIpc(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): void { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + + ipcRenderer.addListener(prefixedChannel, listener); + this.extension[Disposers].push(() => ipcRenderer.removeListener(prefixedChannel, listener)); + } + + invokeIpc(channel: string, ...args: any[]): Promise { + const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; + + return ipcRenderer.invoke(prefixedChannel, ...args); + } +} From 0537792c56f395ba990f61f14c73da25d862dc77 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 18 May 2021 12:26:29 +0300 Subject: [PATCH 4/9] Update node shell to use Alpine 3.13 (#2792) Signed-off-by: Lauri Nevala --- src/main/shell-session/node-shell-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session.ts index 72413c7953..92831939ed 100644 --- a/src/main/shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session.ts @@ -77,7 +77,7 @@ export class NodeShellSession extends ShellSession { }], containers: [{ name: "shell", - image: "docker.io/alpine:3.12", + image: "docker.io/alpine:3.13", securityContext: { privileged: true, }, From d08d5a24d7b1f1b07ef64cc46549e04451b2aea5 Mon Sep 17 00:00:00 2001 From: steve richards Date: Tue, 18 May 2021 10:29:13 +0100 Subject: [PATCH 5/9] Remove custom GA Tags script. Use built in GA. (#2794) Signed-off-by: Steve Richards --- docs/custom_theme/main.html | 11 ----------- mkdocs.yml | 3 +++ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/docs/custom_theme/main.html b/docs/custom_theme/main.html index b6ad2467dd..94d9808cc7 100644 --- a/docs/custom_theme/main.html +++ b/docs/custom_theme/main.html @@ -1,12 +1 @@ {% extends "base.html" %} - -{% block analytics %} - - - -{% endblock %} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index fe42cbccf5..7001e41ce5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,9 @@ repo_name: GitHub repo_url: https://github.com/lensapp/lens copyright: Copyright © 2021 Mirantis Inc. - All rights reserved. edit_uri: "" +google_analytics: + - UA-159377374-2 + - auto nav: - Overview: README.md - Getting Started: getting-started/README.md From 752f49821a1217dc22d7d3ea13e787dbbb2b2fab Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 18 May 2021 13:13:33 +0300 Subject: [PATCH 6/9] Show active item in hotbar & allow to pin/unpin (#2790) * show active item in hotbar & allow to pin it Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * fix styles Signed-off-by: Jari Kolehmainen --- src/common/__tests__/hotbar-store.test.ts | 52 +++++---------- src/common/hotbar-store.ts | 12 ++-- src/renderer/components/+catalog/catalog.tsx | 4 +- .../components/hotbar/hotbar-entity-icon.tsx | 64 +++++++++---------- .../components/hotbar/hotbar-icon.scss | 32 ++++------ .../components/hotbar/hotbar-icon.tsx | 18 ++---- .../components/hotbar/hotbar-menu.tsx | 38 +++++++++-- 7 files changed, 109 insertions(+), 111 deletions(-) diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 92e5a3f35b..51002fee23 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -20,7 +20,6 @@ */ import mockFs from "mock-fs"; -import { CatalogEntityItem } from "../../renderer/components/+catalog/catalog-entity.store"; import { ClusterStore } from "../cluster-store"; import { HotbarStore } from "../hotbar-store"; @@ -159,10 +158,9 @@ describe("HotbarStore", () => { it("adds items", () => { const hotbarStore = HotbarStore.createInstance(); - const entity = new CatalogEntityItem(testCluster); hotbarStore.load(); - hotbarStore.addToHotbar(entity); + hotbarStore.addToHotbar(testCluster); const items = hotbarStore.getActive().items.filter(Boolean); expect(items.length).toEqual(1); @@ -170,10 +168,9 @@ describe("HotbarStore", () => { it("removes items", () => { const hotbarStore = HotbarStore.createInstance(); - const entity = new CatalogEntityItem(testCluster); hotbarStore.load(); - hotbarStore.addToHotbar(entity); + hotbarStore.addToHotbar(testCluster); hotbarStore.removeFromHotbar("test"); const items = hotbarStore.getActive().items.filter(Boolean); @@ -182,10 +179,9 @@ describe("HotbarStore", () => { it("does nothing if removing with invalid uid", () => { const hotbarStore = HotbarStore.createInstance(); - const entity = new CatalogEntityItem(testCluster); hotbarStore.load(); - hotbarStore.addToHotbar(entity); + hotbarStore.addToHotbar(testCluster); hotbarStore.removeFromHotbar("invalid uid"); const items = hotbarStore.getActive().items.filter(Boolean); @@ -194,14 +190,11 @@ describe("HotbarStore", () => { it("moves item to empty cell", () => { const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); - const minikube = new CatalogEntityItem(minikubeCluster); - const aws = new CatalogEntityItem(awsCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); - hotbarStore.addToHotbar(minikube); - hotbarStore.addToHotbar(aws); + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); expect(hotbarStore.getActive().items[5]).toBeNull(); @@ -213,14 +206,11 @@ describe("HotbarStore", () => { it("moves items down", () => { const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); - const minikube = new CatalogEntityItem(minikubeCluster); - const aws = new CatalogEntityItem(awsCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); - hotbarStore.addToHotbar(minikube); - hotbarStore.addToHotbar(aws); + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); // aws -> test hotbarStore.restackItems(2, 0); @@ -232,14 +222,11 @@ describe("HotbarStore", () => { it("moves items up", () => { const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); - const minikube = new CatalogEntityItem(minikubeCluster); - const aws = new CatalogEntityItem(awsCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); - hotbarStore.addToHotbar(minikube); - hotbarStore.addToHotbar(aws); + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); // test -> aws hotbarStore.restackItems(0, 2); @@ -251,10 +238,9 @@ describe("HotbarStore", () => { it("does nothing when item moved to same cell", () => { const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); + hotbarStore.addToHotbar(testCluster); hotbarStore.restackItems(0, 0); expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test"); @@ -262,15 +248,12 @@ describe("HotbarStore", () => { it("new items takes first empty cell", () => { const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); - const minikube = new CatalogEntityItem(minikubeCluster); - const aws = new CatalogEntityItem(awsCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); - hotbarStore.addToHotbar(aws); + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(awsCluster); hotbarStore.restackItems(0, 3); - hotbarStore.addToHotbar(minikube); + hotbarStore.addToHotbar(minikubeCluster); expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); }); @@ -282,10 +265,9 @@ describe("HotbarStore", () => { console.error = jest.fn(); const hotbarStore = HotbarStore.createInstance(); - const test = new CatalogEntityItem(testCluster); hotbarStore.load(); - hotbarStore.addToHotbar(test); + hotbarStore.addToHotbar(testCluster); expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); expect(() => hotbarStore.restackItems(2, -1)).toThrow(); diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index b2d5af0f2a..53c1c5b3ef 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -23,8 +23,8 @@ import { action, comparer, observable, toJS } from "mobx"; import { BaseStore } from "./base-store"; import migrations from "../migrations/hotbar-store"; import * as uuid from "uuid"; -import { CatalogEntityItem } from "../renderer/components/+catalog/catalog-entity.store"; import isNull from "lodash/isNull"; +import { CatalogEntity } from "./catalog"; export interface HotbarItem { entity: { @@ -151,15 +151,15 @@ export class HotbarStore extends BaseStore { } @action - addToHotbar(item: CatalogEntityItem, cellIndex = -1) { + addToHotbar(item: CatalogEntity, cellIndex = -1) { const hotbar = this.getActive(); const newItem = { entity: { - uid: item.id, - name: item.name, - source: item.source + uid: item.metadata.uid, + name: item.metadata.name, + source: item.metadata.source }}; - if (hotbar.items.find(i => i?.entity.uid === item.id)) { + if (hotbar.items.find(i => i?.entity.uid === item.metadata.uid)) { return; } diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 2fb6ce8f9f..3f0505d3c1 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -68,7 +68,7 @@ export class Catalog extends React.Component { } addToHotbar(item: CatalogEntityItem): void { - HotbarStore.getInstance().addToHotbar(item); + HotbarStore.getInstance().addToHotbar(item.entity); } onDetails(item: CatalogEntityItem) { @@ -137,7 +137,7 @@ export class Catalog extends React.Component { return ( item.onContextMenuOpen(this.contextMenu)}> this.addToHotbar(item) }> - Add to Hotbar + Pin to Hotbar { menuItems.map((menuItem, index) => ( diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 7e2b7f79bf..e71ab2fdb2 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -18,26 +18,26 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./hotbar-icon.scss"; import React, { DOMAttributes } from "react"; import { observable } from "mobx"; import { observer } from "mobx-react"; -import randomColor from "randomcolor"; -import { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../../common/catalog"; +import { CatalogEntity, CatalogEntityContextMenuContext } from "../../../common/catalog"; import { catalogCategoryRegistry } from "../../api/catalog-category-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { navigate } from "../../navigation"; import { cssNames, IClassName } from "../../utils"; -import { ConfirmDialog } from "../confirm-dialog"; import { Icon } from "../icon"; import { HotbarIcon } from "./hotbar-icon"; +import { HotbarStore } from "../../../common/hotbar-store"; interface Props extends DOMAttributes { entity: CatalogEntity; + index: number; className?: IClassName; errorClass?: IClassName; + add: (item: CatalogEntity, index: number) => void; remove: (uid: string) => void; } @@ -77,33 +77,18 @@ export class HotbarEntityIcon extends React.Component { return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId(); } - onMenuItemClick(menuItem: CatalogEntityContextMenu) { - if (menuItem.confirm) { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - }, - ok: () => { - menuItem.onClick(); - }, - message: menuItem.confirm.message - }); - } else { - menuItem.onClick(); - } - } - - generateAvatarStyle(entity: CatalogEntity): React.CSSProperties { - return { - "backgroundColor": randomColor({ seed: `${entity.metadata.name}-${entity.metadata.source}`, luminosity: "dark" }) - }; + isPersisted(entity: CatalogEntity) { + return HotbarStore.getInstance().getActive().items.find((item) => item?.entity?.uid === entity.metadata.uid) !== undefined; } render() { + if (!this.contextMenu) { + return null; + } + const { - entity, errorClass, remove, - children, ...elemProps + entity, errorClass, add, remove, + index, children, ...elemProps } = this.props; const className = cssNames("HotbarEntityIcon", this.props.className, { interactive: true, @@ -113,16 +98,31 @@ export class HotbarEntityIcon extends React.Component { const onOpen = async () => { await entity.onContextMenuOpen(this.contextMenu); }; + const isActive = this.isActive(entity); + const isPersisted = this.isPersisted(entity); const menuItems = this.contextMenu?.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source); + if (!isPersisted) { + menuItems.unshift({ + title: "Pin to Hotbar", + icon: "push_pin", + onClick: () => add(entity, index) + }); + } else { + menuItems.unshift({ + title: "Unpin from Hotbar", + icon: "push_pin", + onClick: () => remove(entity.metadata.uid) + }); + } + return ( .led { + div.MuiAvatar-root { + &.active { + box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent); + transition: all 0s 0.8s; + } + + &:hover { + &:not(.active) { + box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30; + } + } + } + + .led { position: absolute; left: 3px; top: 3px; diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 74154505e3..2f681a05c1 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -32,12 +32,12 @@ import { ConfirmDialog } from "../confirm-dialog"; import { Icon } from "../icon"; import { Menu, MenuItem } from "../menu"; import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip"; +import { observer } from "mobx-react"; interface Props extends DOMAttributes { uid: string; title: string; source: string; - remove: (uid: string) => void; onMenuOpen?: () => void; className?: IClassName; active?: boolean; @@ -84,8 +84,8 @@ function getNameParts(name: string): string[] { return name.split(/@+/); } -export function HotbarIcon(props: Props) { - const { uid, title, className, source, active, remove, disabled, menuItems, onMenuOpen, children, ...rest } = props; +export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => { + const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props; const id = `hotbarIcon-${uid}`; const [menuOpen, setMenuOpen] = useState(false); @@ -134,12 +134,6 @@ export function HotbarIcon(props: Props) { toggleMenu(); }} close={() => toggleMenu()}> - { - evt.stopPropagation(); - remove(uid); - }}> - Remove from Hotbar - { menuItems.map((menuItem) => { return ( onMenuItemClick(menuItem) }> @@ -150,8 +144,4 @@ export function HotbarIcon(props: Props) { ); -} - -HotbarIcon.defaultProps = { - menuItems: [] -}; +}); diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 319b05f946..33a8d4e0b7 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -28,11 +28,12 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store"; -import { catalogEntityRunContext } from "../../api/catalog-entity"; +import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; import { HotbarCell } from "./hotbar-cell"; import { HotbarIcon } from "./hotbar-icon"; +import { computed } from "mobx"; interface Props { className?: IClassName; @@ -64,6 +65,10 @@ export class HotbarMenu extends React.Component { const from = parseInt(source.droppableId); const to = parseInt(destination.droppableId); + if (!this.hotbar.items[from]) { // Dropped non-persisted item + this.hotbar.items[from] = this.items[from]; + } + HotbarStore.getInstance().restackItems(from, to); } @@ -73,14 +78,38 @@ export class HotbarMenu extends React.Component { hotbar.removeFromHotbar(uid); } + addItem(entity: CatalogEntity, index = -1) { + const hotbar = HotbarStore.getInstance(); + + hotbar.addToHotbar(entity, index); + } + getMoveAwayDirection(entityId: string, cellIndex: number) { const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId); return draggableItemIndex > cellIndex ? "animateDown" : "animateUp"; } + @computed get items() { + const items = this.hotbar.items; + const activeEntity = catalogEntityRegistry.activeEntity; + + if (!activeEntity) return items; + + const emptyIndex = items.indexOf(null); + + if (emptyIndex === -1) return items; + if (items.find((item) => item?.entity?.uid === activeEntity.metadata.uid)) return items; + + const modifiedItems = [...items]; + + modifiedItems.splice(emptyIndex, 1, { entity: { uid: activeEntity.metadata.uid }}); + + return modifiedItems; + } + renderGrid() { - return this.hotbar.items.map((item, index) => { + return this.items.map((item, index) => { const entity = this.getEntity(item); return ( @@ -116,17 +145,18 @@ export class HotbarMenu extends React.Component { {entity ? ( entity.onRun(catalogEntityRunContext)} className={cssNames({ isDragging: snapshot.isDragging })} remove={this.removeItem} + add={this.addItem} /> ) : ( )} @@ -151,7 +181,7 @@ export class HotbarMenu extends React.Component { return (
- + {this.renderGrid()}
From ef4bae341f157c66327a2412cd7a048b254a0795 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 18 May 2021 13:25:10 +0300 Subject: [PATCH 7/9] Re-implement deployment revisions (#2795) * Re-implement deployment revisions * Fix css Signed-off-by: Lauri Nevala --- .../deployment-details.tsx | 5 + .../deployment-replicasets.scss | 36 ++++++ .../deployment-replicasets.tsx | 119 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/renderer/components/+workloads-deployments/deployment-replicasets.scss create mode 100644 src/renderer/components/+workloads-deployments/deployment-replicasets.tsx diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 641eae79e7..112953e63a 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -42,6 +42,8 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; import { ClusterStore } from "../../../common/cluster-store"; +import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; +import { DeploymentReplicaSets } from "./deployment-replicasets"; interface Props extends KubeObjectDetailsProps { } @@ -55,6 +57,7 @@ export class DeploymentDetails extends React.Component { componentDidMount() { podsStore.reloadAll(); + replicaSetStore.reloadAll(); } componentWillUnmount() { @@ -69,6 +72,7 @@ export class DeploymentDetails extends React.Component { const nodeSelector = deployment.getNodeSelectors(); const selectors = deployment.getSelectors(); const childPods = deploymentStore.getChildPods(deployment); + const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment); const metrics = deploymentStore.metrics; const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Deployment); @@ -131,6 +135,7 @@ export class DeploymentDetails extends React.Component { +
); diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.scss b/src/renderer/components/+workloads-deployments/deployment-replicasets.scss new file mode 100644 index 0000000000..0c04f54709 --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployment-replicasets.scss @@ -0,0 +1,36 @@ +.DeploymentDetails { + .ReplicaSets { + position: relative; + min-height: 80px; + + .Table { + margin: 0 (-$margin * 3); + } + + .TableCell { + &:first-child { + margin-left: $margin; + } + + &:last-child { + margin-right: $margin; + } + + &.name { + flex-grow: 2; + } + + &.warning { + @include table-cell-warning; + } + + &.namespace { + flex-grow: 1.2; + } + + &.actions { + @include table-cell-action; + } + } + } +} diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx new file mode 100644 index 0000000000..8fbce9564d --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import "./deployment-replicasets.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { ReplicaSet } from "../../api/endpoints"; +import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; +import { Spinner } from "../spinner"; +import { prevDefault, stopPropagation } from "../../utils"; +import { DrawerTitle } from "../drawer"; +import { Table, TableCell, TableHead, TableRow } from "../table"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; +import { showDetails } from "../kube-object"; + + +enum sortBy { + name = "name", + namespace = "namespace", + pods = "pods", + age = "age", +} + +interface Props { + replicaSets: ReplicaSet[]; +} + +@observer +export class DeploymentReplicaSets extends React.Component { + private sortingCallbacks = { + [sortBy.name]: (replicaSet: ReplicaSet) => replicaSet.getName(), + [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), + [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, + [sortBy.pods]: (replicaSet: ReplicaSet) => this.getPodsLength(replicaSet), + }; + + getPodsLength(replicaSet: ReplicaSet) { + return replicaSetStore.getChildPods(replicaSet).length; + } + + render() { + const { replicaSets } = this.props; + + if (!replicaSets.length && !replicaSetStore.isLoaded) return ( +
+ ); + if (!replicaSets.length) return null; + + return ( +
+ + + + Name + + Namespace + Pods + Age + + + { + replicaSets.map(replica => { + return ( + showDetails(replica.selfLink, false))} + > + {replica.getName()} + + {replica.getNs()} + {this.getPodsLength(replica)} + {replica.getAge()} + + + + + ); + }) + } +
+
+ ); + } +} + +export function ReplicaSetMenu(props: KubeObjectMenuProps) { + return ( + + ); +} From b6b1b467e0abd79f6478fb85d8a658a0459c0617 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 18 May 2021 13:42:33 +0300 Subject: [PATCH 8/9] Add connect/disconnect methods to KubernetesCluster kind (#2758) * add connect/disconnect methods to kubernetes cluster Signed-off-by: Jari Kolehmainen * return void Signed-off-by: Jari Kolehmainen --- .../catalog-entities/kubernetes-cluster.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index cd217d3a02..7c6727ada5 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -21,11 +21,12 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; -import { clusterDisconnectHandler } from "../cluster-ipc"; +import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store"; import { requestMain } from "../ipc"; import { productName } from "../vars"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; +import { app } from "electron"; export type KubernetesClusterSpec = { kubeconfigPath: string; @@ -40,6 +41,38 @@ export class KubernetesCluster extends CatalogEntity { + if (app) { + const cluster = ClusterStore.getInstance().getById(this.metadata.uid); + + if (!cluster) return; + + await cluster.activate(); + + return; + } + + await requestMain(clusterActivateHandler, this.metadata.uid, false); + + return; + } + + async disconnect(): Promise { + if (app) { + const cluster = ClusterStore.getInstance().getById(this.metadata.uid); + + if (!cluster) return; + + cluster.disconnect(); + + return; + } + + await requestMain(clusterDisconnectHandler, this.metadata.uid, false); + + return; + } + async onRun(context: CatalogEntityActionContext) { context.navigate(`/cluster/${this.metadata.uid}`); } From d9c6e5c52fcb6282847b4093f9cee695362e433e Mon Sep 17 00:00:00 2001 From: chh <1474479+chenhunghan@users.noreply.github.com> Date: Tue, 18 May 2021 14:13:07 +0300 Subject: [PATCH 9/9] Catalog entity WebLink.kind 'KubernetesCluster' > 'WebLink' (#2799) Signed-off-by: Hung-Han (Henry) Chen --- src/common/catalog-entities/web-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index 6223b6dbfc..7e0f024421 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -32,7 +32,7 @@ export type WebLinkSpec = { export class WebLink extends CatalogEntity { public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; - public readonly kind = "KubernetesCluster"; + public readonly kind = "WebLink"; async onRun() { window.open(this.spec.url, "_blank");