From 205225f6a4300177dd39d022b35a64c5e16b1400 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 27 Jan 2022 16:18:48 +0300 Subject: [PATCH 01/13] Docs: add styling Custom Views block (#4760) --- docs/extensions/guides/catalog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/extensions/guides/catalog.md b/docs/extensions/guides/catalog.md index f428f323e0..6ddc5755c7 100644 --- a/docs/extensions/guides/catalog.md +++ b/docs/extensions/guides/catalog.md @@ -57,6 +57,14 @@ Will register a new view for the KubernetesCluster category, and because the pri The default list view has a priority of 50 and and custom views with priority (defaulting to 50) >= 50 will be displayed afterwards. +#### Styling Custom Views + +By default, custom view blocks are styled with [Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). Some details comes from this. + +- To set fixed height of a custom block, use `max-height` css rule. +- To set flexible height, use `height`. +- Otherwise, custom view will have height of it's contents. + ## Entities An entity is the data within the catalog. From 1cac3ca74c9d986d39a399fe90e5d7be0ad81130 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 27 Jan 2022 17:23:36 +0200 Subject: [PATCH 02/13] Upgrade to Electron 14.2.4 (#4625) Co-authored-by: Sebastian Malton Co-authored-by: Jim Ehrismann --- .yarnrc | 2 +- .../__tests__/app-preferences.tests.ts | 4 +- .../__tests__/command-palette.tests.ts | 4 +- integration/helpers/utils.ts | 15 ++- package.json | 3 +- .../catalog-entities/kubernetes-cluster.ts | 10 +- src/common/cluster-store/cluster-store.ts | 18 ++-- src/common/cluster/cluster.ts | 5 +- src/common/hotbar-store.ts | 5 +- src/common/ipc/catalog.ts | 23 +++-- src/common/ipc/cluster.ipc.ts | 16 --- src/common/{cluster-ipc.ts => ipc/cluster.ts} | 13 +++ .../index.ts => common/ipc/dialog.ts} | 4 +- src/common/ipc/extension-handling.ts | 9 ++ src/common/ipc/extension-loader.ipc.ts | 5 - src/common/ipc/hotbar.ts | 2 +- src/common/ipc/index.ts | 8 +- src/common/ipc/ipc.ts | 99 ++++++++----------- ...e-available.ipc.ts => update-available.ts} | 0 src/common/ipc/window.ts | 39 ++++++++ src/common/k8s/resource-stack.ts | 11 +-- .../__tests__/extension-loader.test.ts | 7 +- .../extension-discovery.ts | 20 ++-- .../extension-loader/extension-loader.ts | 22 ++--- src/main/__test__/cluster.test.ts | 7 +- src/main/catalog-pusher.ts | 6 +- src/main/cluster-manager.ts | 2 +- src/main/index.ts | 8 +- .../init-ipc-main-handlers.ts | 31 +++--- src/main/ipc/dialog.ts | 12 +++ src/main/ipc/window.ts | 71 +++++++++++++ src/main/window-manager.ts | 16 ++- src/renderer/api/catalog-entity-registry.ts | 8 +- .../components/+extensions/extensions.tsx | 36 +++---- ...tall-from-select-file-dialog.injectable.ts | 39 ++++++++ ...tall-from-select-file-dialog.injectable.ts | 20 ---- .../install-from-select-file-dialog.ts | 29 ------ .../+preferences/add-helm-repo-dialog.tsx | 4 +- .../__tests__/pod-tolerations.test.tsx | 3 +- .../activate-entity-command.tsx | 3 +- .../cluster-manager/cluster-status.tsx | 6 +- .../cluster-manager/cluster-view.tsx | 5 +- .../components/cluster-manager/lens-views.ts | 2 +- .../delete-cluster-dialog.tsx | 9 +- .../layout/top-bar/top-bar-win-linux.test.tsx | 32 ++---- .../layout/top-bar/top-bar.test.tsx | 43 ++++---- .../components/layout/top-bar/top-bar.tsx | 55 ++++++----- .../components/path-picker/path-picker.tsx | 7 +- .../frames/cluster-frame/cluster-frame.tsx | 1 - .../init-cluster-frame/init-cluster-frame.ts | 5 +- .../init-root-frame/init-root-frame.ts | 7 +- src/renderer/hooks/index.ts | 1 + src/renderer/hooks/use-toggle.ts | 11 +++ src/renderer/ipc/index.ts | 83 ++++++++++++++++ .../ipc/{index.tsx => register-listeners.tsx} | 8 +- src/renderer/remote-helpers/dialog.ts | 10 -- .../remote-helpers/history-updater.ts | 26 +---- yarn.lock | 13 +-- 58 files changed, 545 insertions(+), 418 deletions(-) delete mode 100644 src/common/ipc/cluster.ipc.ts rename src/common/{cluster-ipc.ts => ipc/cluster.ts} (60%) rename src/{renderer/remote-helpers/index.ts => common/ipc/dialog.ts} (67%) create mode 100644 src/common/ipc/extension-handling.ts delete mode 100644 src/common/ipc/extension-loader.ipc.ts rename src/common/ipc/{update-available.ipc.ts => update-available.ts} (100%) create mode 100644 src/common/ipc/window.ts create mode 100644 src/main/ipc/dialog.ts create mode 100644 src/main/ipc/window.ts create mode 100644 src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts create mode 100644 src/renderer/hooks/use-toggle.ts create mode 100644 src/renderer/ipc/index.ts rename src/renderer/ipc/{index.tsx => register-listeners.tsx} (92%) delete mode 100644 src/renderer/remote-helpers/dialog.ts diff --git a/.yarnrc b/.yarnrc index 756f21586a..c8e7a49bbe 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "13.6.1" +target "14.2.4" runtime "electron" diff --git a/integration/__tests__/app-preferences.tests.ts b/integration/__tests__/app-preferences.tests.ts index 98b7fd41d0..d342106631 100644 --- a/integration/__tests__/app-preferences.tests.ts +++ b/integration/__tests__/app-preferences.tests.ts @@ -11,6 +11,7 @@ */ import type { ElectronApplication, Page } from "playwright"; import * as utils from "../helpers/utils"; +import { isWindows } from "../../src/common/vars"; describe("preferences page tests", () => { let window: Page, cleanup: () => Promise; @@ -33,7 +34,8 @@ describe("preferences page tests", () => { await cleanup(); }, 10*60*1000); - it('shows "preferences" and can navigate through the tabs', async () => { + // skip on windows due to suspected playwright issue with Electron 14 + utils.itIf(!isWindows)('shows "preferences" and can navigate through the tabs', async () => { const pages = [ { id: "application", diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts index da45b078f6..b1d43bcd2e 100644 --- a/integration/__tests__/command-palette.tests.ts +++ b/integration/__tests__/command-palette.tests.ts @@ -5,6 +5,7 @@ import type { ElectronApplication, Page } from "playwright"; import * as utils from "../helpers/utils"; +import { isWindows } from "../../src/common/vars"; describe("Lens command palette", () => { let window: Page, cleanup: () => Promise, app: ElectronApplication; @@ -19,7 +20,8 @@ describe("Lens command palette", () => { }, 10*60*1000); describe("menu", () => { - it("opens command dialog from menu", async () => { + // skip on windows due to suspected playwright issue with Electron 14 + utils.itIf(!isWindows)("opens command dialog from menu", async () => { await app.evaluate(async ({ app }) => { await app.applicationMenu .getMenuItemById("view") diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index e79ff1ca72..b3994eb931 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -40,7 +40,7 @@ async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promis throw new Error(`Lens did not open the main window within ${timeout}ms`); } -export async function start() { +async function attemptStart() { const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4()); // Make sure that the directory is clear @@ -76,6 +76,19 @@ export async function start() { } } +export async function start() { + // this is an attempted workaround for an issue with playwright not always getting the main window when using Electron 14.2.4 (observed on windows) + for (let i = 0; ; i++) { + try { + return await attemptStart(); + } catch (error) { + if (i === 4) { + throw error; + } + } + } +} + export async function clickWelcomeButton(window: Page) { await window.click("[data-testid=welcome-menu-container] li a"); } diff --git a/package.json b/package.json index 03ff28fa93..398eff127c 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,6 @@ } }, "dependencies": { - "@electron/remote": "^1.2.2", "@hapi/call": "^8.0.1", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.1", @@ -334,7 +333,7 @@ "css-loader": "^5.2.7", "deepdash": "^5.3.9", "dompurify": "^2.3.4", - "electron": "^13.6.1", + "electron": "^14.2.4", "electron-builder": "^22.14.5", "electron-notarize": "^0.3.0", "esbuild": "^0.13.15", diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 813d5d3866..dcc732a2ca 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -5,12 +5,12 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog"; -import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store/cluster-store"; -import { broadcastMessage, requestMain } from "../ipc"; +import { broadcastMessage } from "../ipc"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; export interface KubernetesClusterPrometheusMetrics { address?: { @@ -69,7 +69,7 @@ export class KubernetesCluster extends CatalogEntity requestMain(clusterDisconnectHandler, this.metadata.uid), + onClick: () => requestClusterDisconnection(this.metadata.uid), }); break; case LensKubernetesClusterStatus.DISCONNECTED: diff --git a/src/common/cluster-store/cluster-store.ts b/src/common/cluster-store/cluster-store.ts index 41617e11a8..aebdb79045 100644 --- a/src/common/cluster-store/cluster-store.ts +++ b/src/common/cluster-store/cluster-store.ts @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - + import { ipcMain, ipcRenderer, webFrame } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; @@ -11,16 +11,16 @@ import { Cluster } from "../cluster/cluster"; import migrations from "../../migrations/cluster-store"; import logger from "../../main/logger"; import { appEventBus } from "../app-event-bus/event-bus"; -import { ipcMainHandle, requestMain } from "../ipc"; +import { ipcMainHandle } from "../ipc"; import { disposer, toJS } from "../utils"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; +import { requestInitialClusterStates } from "../../renderer/ipc"; +import { clusterStates } from "../ipc/cluster"; export interface ClusterStoreModel { clusters?: ClusterModel[]; } -const initialStates = "cluster:states"; - interface Dependencies { createCluster: (model: ClusterModel) => Cluster } @@ -49,18 +49,18 @@ export class ClusterStore extends BaseStore { async loadInitialOnRenderer() { logger.info("[CLUSTER-STORE] requesting initial state sync"); - for (const { id, state } of await requestMain(initialStates)) { + for (const { id, state } of await requestInitialClusterStates()) { this.getById(id)?.setState(state); } } provideInitialFromMain() { - ipcMainHandle(initialStates, () => { - return this.clustersList.map(cluster => ({ + ipcMainHandle(clusterStates, () => ( + this.clustersList.map(cluster => ({ id: cluster.id, state: cluster.getState(), - })); - }); + })) + )); } protected pushStateToViewsAutomatically() { diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 911c2dd8fa..6adf91151f 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -5,7 +5,7 @@ import { ipcMain } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; -import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../ipc"; +import { broadcastMessage } from "../ipc"; import type { ContextHandler } from "../../main/context-handler/context-handler"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import type { Kubectl } from "../../main/kubectl/kubectl"; @@ -20,6 +20,7 @@ import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, C import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; import { disposer, toJS } from "../utils"; import type { Response } from "request"; +import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; interface Dependencies { directoryForKubeConfigs: string, @@ -641,7 +642,7 @@ export class Cluster implements ClusterModel, ClusterState { const { response } = error as HttpError & { response: Response }; logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); - broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); + broadcastMessage(clusterListNamespaceForbiddenChannel, this.id); } return namespaceList; diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 9375ba7231..3b4593111e 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -10,8 +10,9 @@ import { toJS } from "./utils"; import { CatalogEntity } from "./catalog"; import { catalogEntity } from "../main/catalog-sources/general"; import logger from "../main/logger"; -import { broadcastMessage, HotbarTooManyItems } from "./ipc"; +import { broadcastMessage } from "./ipc"; import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; +import { hotbarTooManyItemsChannel } from "./ipc/hotbar"; export interface HotbarStoreModel { hotbars: Hotbar[]; @@ -182,7 +183,7 @@ export class HotbarStore extends BaseStore { if (emptyCellIndex != -1) { hotbar.items[emptyCellIndex] = newItem; } else { - broadcastMessage(HotbarTooManyItems); + broadcastMessage(hotbarTooManyItemsChannel); } } else if (0 <= cellIndex && cellIndex < hotbar.items.length) { hotbar.items[cellIndex] = newItem; diff --git a/src/common/ipc/catalog.ts b/src/common/ipc/catalog.ts index 6bc4492083..3d316d211c 100644 --- a/src/common/ipc/catalog.ts +++ b/src/common/ipc/catalog.ts @@ -3,14 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export enum CatalogIpcEvents { - /** - * This is broadcast on whenever there is an update to any catalog item - */ - ITEMS = "catalog:items", +/** + * This is used to activate a specific entity in the renderer main frame + */ +export const catalogEntityRunListener = "catalog-entity:run"; - /** - * This can be sent from renderer to main to initialize a broadcast of ITEMS - */ - INIT = "catalog:init", -} +/** + * This is broadcast on whenever there is an update to any catalog item + */ +export const catalogItemsChannel = "catalog:items"; + +/** + * This can be sent from renderer to main to initialize a broadcast of ITEMS + */ +export const catalogInitChannel = "catalog:init"; diff --git a/src/common/ipc/cluster.ipc.ts b/src/common/ipc/cluster.ipc.ts deleted file mode 100644 index 5e44735b23..0000000000 --- a/src/common/ipc/cluster.ipc.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * This channel is broadcast on whenever the cluster fails to list namespaces - * during a refresh and no `accessibleNamespaces` have been set. - */ -export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden"; - -export type ListNamespaceForbiddenArgs = [clusterId: string]; - -export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs { - return args.length === 1 && typeof args[0] === "string"; -} diff --git a/src/common/cluster-ipc.ts b/src/common/ipc/cluster.ts similarity index 60% rename from src/common/cluster-ipc.ts rename to src/common/ipc/cluster.ts index 373bafcfad..9f69ff42d5 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/ipc/cluster.ts @@ -13,3 +13,16 @@ export const clusterSetDeletingHandler = "cluster:deleting:set"; export const clusterClearDeletingHandler = "cluster:deleting:clear"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all"; +export const clusterStates = "cluster:states"; + +/** + * This channel is broadcast on whenever the cluster fails to list namespaces + * during a refresh and no `accessibleNamespaces` have been set. + */ +export const clusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden"; + +export type ListNamespaceForbiddenArgs = [clusterId: string]; + +export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs { + return args.length === 1 && typeof args[0] === "string"; +} diff --git a/src/renderer/remote-helpers/index.ts b/src/common/ipc/dialog.ts similarity index 67% rename from src/renderer/remote-helpers/index.ts rename to src/common/ipc/dialog.ts index b15edbcc83..eab621a280 100644 --- a/src/renderer/remote-helpers/index.ts +++ b/src/common/ipc/dialog.ts @@ -3,6 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import * as dialog from "./dialog"; - -export { dialog }; +export const openFilePickingDialogChannel = "dialog:open:file-picking"; diff --git a/src/common/ipc/extension-handling.ts b/src/common/ipc/extension-handling.ts new file mode 100644 index 0000000000..1474a6d18d --- /dev/null +++ b/src/common/ipc/extension-handling.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export const extensionDiscoveryStateChannel = "extension-discovery:state"; +export const bundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded"; +export const extensionLoaderFromMainChannel = "extension-loader:main:state"; +export const extensionLoaderFromRendererChannel = "extension-loader:renderer:state"; diff --git a/src/common/ipc/extension-loader.ipc.ts b/src/common/ipc/extension-loader.ipc.ts deleted file mode 100644 index 48bfedd970..0000000000 --- a/src/common/ipc/extension-loader.ipc.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded"; diff --git a/src/common/ipc/hotbar.ts b/src/common/ipc/hotbar.ts index e245e5dfb8..3617ebd6c5 100644 --- a/src/common/ipc/hotbar.ts +++ b/src/common/ipc/hotbar.ts @@ -3,4 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export const HotbarTooManyItems = "hotbar:too-many-items"; +export const hotbarTooManyItemsChannel = "hotbar:too-many-items"; diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index 9b5bb3a663..60ae46438e 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -3,13 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export const dialogShowOpenDialogHandler = "dialog:show-open-dialog"; -export const catalogEntityRunListener = "catalog-entity:run"; - export * from "./ipc"; export * from "./invalid-kubeconfig"; -export * from "./update-available.ipc"; -export * from "./cluster.ipc"; +export * from "./update-available"; export * from "./type-enforced-ipc"; -export * from "./hotbar"; -export * from "./extension-loader.ipc"; diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 78ef1f3a16..b875a7d30d 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -12,25 +12,8 @@ import { toJS } from "../utils/toJS"; import logger from "../../main/logger"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import type { Disposer } from "../utils"; -import type remote from "@electron/remote"; -const electronRemote = (() => { - if (ipcRenderer) { - try { - return require("@electron/remote"); - } catch { - // ignore temp - } - } - - return null; -})(); - -const subFramesChannel = "ipc:get-sub-frames"; - -export async function requestMain(channel: string, ...args: any[]) { - return ipcRenderer.invoke(channel, ...args.map(sanitizePayload)); -} +export const broadcastMainChannel = "ipc:broadcast-main"; export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(channel, async (event, ...args) => { @@ -42,51 +25,55 @@ function getSubFrames(): ClusterFrameInfo[] { return Array.from(clusterFrameMap.values()); } -export function broadcastMessage(channel: string, ...args: any[]) { - const subFramesP = ipcRenderer - ? requestMain(subFramesChannel) - : Promise.resolve(getSubFrames()); +export async function broadcastMessage(channel: string, ...args: any[]): Promise { + if (ipcRenderer) { + return ipcRenderer.invoke(broadcastMainChannel, channel, ...args.map(sanitizePayload)); + } - subFramesP - .then(subFrames => { - const views: undefined | ReturnType | ReturnType = (webContents || electronRemote?.webContents)?.getAllWebContents(); + if (!webContents) { + return; + } - if (!views || !Array.isArray(views) || views.length === 0) return; - args = args.map(sanitizePayload); + ipcMain.listeners(channel).forEach((func) => func({ + processId: undefined, frameId: undefined, sender: undefined, senderFrame: undefined, + }, ...args)); - ipcRenderer?.send(channel, ...args); - ipcMain?.emit(channel, ...args); + const subFrames = getSubFrames(); + const views = webContents.getAllWebContents(); - for (const view of views) { - let viewType = "unknown"; + if (!views || !Array.isArray(views) || views.length === 0) return; - // There will be a uncaught exception if the view is destroyed. - try { - viewType = view.getType(); - } catch { - // We can ignore the view destroyed exception as viewType is only used for logging. - } + args = args.map(sanitizePayload); - // Send message to views. - try { - logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args }); - view.send(channel, ...args); - } catch (error) { - logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error: String(error) }); - } + for (const view of views) { + let viewType = "unknown"; - // Send message to subFrames of views. - for (const frameInfo of subFrames) { - logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args }); + // There will be a uncaught exception if the view is destroyed. + try { + viewType = view.getType(); + } catch { + // We can ignore the view destroyed exception as viewType is only used for logging. + } - try { - view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); - } catch (error) { - logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) }); - } - } + // Send message to views. + try { + logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args }); + view.send(channel, ...args); + } catch (error) { + logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error }); + } + + // Send message to subFrames of views. + for (const frameInfo of subFrames) { + logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args }); + + try { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } catch (error) { + logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) }); } - }); + } + } } export function ipcMainOn(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): Disposer { @@ -101,10 +88,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen return () => ipcRenderer.off(channel, listener); } -export function bindBroadcastHandlers() { - ipcMainHandle(subFramesChannel, () => getSubFrames()); -} - /** * Sanitizing data for IPC-messaging before send. * Removes possible observable values to avoid exceptions like "can't clone object". diff --git a/src/common/ipc/update-available.ipc.ts b/src/common/ipc/update-available.ts similarity index 100% rename from src/common/ipc/update-available.ipc.ts rename to src/common/ipc/update-available.ts diff --git a/src/common/ipc/window.ts b/src/common/ipc/window.ts new file mode 100644 index 0000000000..23a83177c6 --- /dev/null +++ b/src/common/ipc/window.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export const windowActionHandleChannel = "window:window-action"; +export const windowOpenAppMenuAsContextMenuChannel = "window:open-app-context-menu"; +export const windowLocationChangedChannel = "window:location-changed"; + +/** + * The supported actions on the current window. The argument for `windowActionHandleChannel` + */ +export enum WindowAction { + /** + * Request that the current window goes back one step of browser history + */ + GO_BACK = "back", + + /** + * Request that the current window goes forward one step of browser history + */ + GO_FORWARD = "forward", + + /** + * Request that the current window is minimized + */ + MINIMIZE = "minimize", + + /** + * Request that the current window is maximized if it isn't, or unmaximized + * if it is + */ + TOGGLE_MAXIMIZE = "toggle-maximize", + + /** + * Request that the current window is closed + */ + CLOSE = "close", +} diff --git a/src/common/k8s/resource-stack.ts b/src/common/k8s/resource-stack.ts index 08aae15d0b..29ade4b84a 100644 --- a/src/common/k8s/resource-stack.ts +++ b/src/common/k8s/resource-stack.ts @@ -9,11 +9,10 @@ import { ResourceApplier } from "../../main/resource-applier"; import type { KubernetesCluster } from "../catalog-entities"; import logger from "../../main/logger"; import { app } from "electron"; -import { requestMain } from "../ipc"; -import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store/cluster-store"; import yaml from "js-yaml"; import { productName } from "../vars"; +import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc"; export class ResourceStack { constructor(protected cluster: KubernetesCluster, protected name: string) {} @@ -41,7 +40,7 @@ export class ResourceStack { } protected async applyResources(resources: string[], extraArgs?: string[]): Promise { - const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); + const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId()); if (!clusterModel) { throw new Error(`cluster not found`); @@ -54,7 +53,7 @@ export class ResourceStack { if (app) { return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs); } else { - const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); + const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs); if (response.stderr) { throw new Error(response.stderr); @@ -65,7 +64,7 @@ export class ResourceStack { } protected async deleteResources(resources: string[], extraArgs?: string[]): Promise { - const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); + const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId()); if (!clusterModel) { throw new Error(`cluster not found`); @@ -78,7 +77,7 @@ export class ResourceStack { if (app) { return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs); } else { - const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); + const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs); if (response.stderr) { throw new Error(response.stderr); diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index fe72bea585..c9e145f8f9 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -8,8 +8,7 @@ import { Console } from "console"; import { stdout, stderr } from "process"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import { runInAction } from "mobx"; -import updateExtensionsStateInjectable - from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; +import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import mockFs from "mock-fs"; @@ -24,7 +23,7 @@ jest.mock( () => ({ ipcRenderer: { invoke: jest.fn(async (channel: string) => { - if (channel === "extensions:main") { + if (channel === "extension-loader:main:state") { return [ [ manifestPath, @@ -61,7 +60,7 @@ jest.mock( }), on: jest.fn( (channel: string, listener: (event: any, ...args: any[]) => void) => { - if (channel === "extensions:main") { + if (channel === "extension-loader:main:state") { // First initialize with extensions 1 and 2 // and then broadcast event to remove extension 2 and add extension number 3 setTimeout(() => { diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index d563fcbdb5..eb957a4f0c 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -10,12 +10,7 @@ import fse from "fs-extra"; import { makeObservable, observable, reaction, when } from "mobx"; import os from "os"; import path from "path"; -import { - broadcastMessage, - ipcMainHandle, - ipcRendererOn, - requestMain, -} from "../../common/ipc"; +import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc"; import { toJS } from "../../common/utils"; import logger from "../../main/logger"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; @@ -24,6 +19,8 @@ import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; import { isProduction } from "../../common/vars"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { PackageJson } from "type-fest"; +import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; +import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; interface Dependencies { extensionLoader: ExtensionLoader; @@ -96,9 +93,6 @@ export class ExtensionDiscovery { return when(() => this.isLoaded); } - // IPC channel to broadcast changes to extension-discovery from main - protected static readonly extensionDiscoveryChannel = "extension-discovery:main"; - public events = new EventEmitter(); constructor(protected dependencies : Dependencies) { @@ -141,14 +135,14 @@ export class ExtensionDiscovery { this.isLoaded = isLoaded; }; - requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage); - ipcRendererOn(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => { + requestInitialExtensionDiscovery().then(onMessage); + ipcRendererOn(extensionDiscoveryStateChannel, (_event, message: ExtensionDiscoveryChannelMessage) => { onMessage(message); }); } async initMain(): Promise { - ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); + ipcMainHandle(extensionDiscoveryStateChannel, () => this.toJSON()); reaction(() => this.toJSON(), () => { this.broadcast(); @@ -492,6 +486,6 @@ export class ExtensionDiscovery { } broadcast(): void { - broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON()); + broadcastMessage(extensionDiscoveryStateChannel, this.toJSON()); } } diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index afe09cce19..04b552884c 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -8,7 +8,7 @@ import { EventEmitter } from "events"; import { isEqual } from "lodash"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import path from "path"; -import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc"; +import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; import { Disposer, toJS } from "../../common/utils"; import logger from "../../main/logger"; import type { KubernetesCluster } from "../common-api/catalog"; @@ -17,6 +17,8 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ". import type { LensRendererExtension } from "../lens-renderer-extension"; import * as registries from "../registries"; import type { LensExtensionState } from "../extensions-store/extensions-store"; +import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling"; +import { requestExtensionLoaderInitialState } from "../../renderer/ipc"; const logModule = "[EXTENSIONS-LOADER]"; @@ -49,12 +51,6 @@ export class ExtensionLoader { */ protected instancesByName = observable.map(); - // IPC channel to broadcast changes to extensions from main - protected static readonly extensionsMainChannel = "extensions:main"; - - // IPC channel to broadcast changes to extensions from renderer - protected static readonly extensionsRendererChannel = "extensions:renderer"; - // emits event "remove" of type LensExtension when the extension is removed private events = new EventEmitter(); @@ -196,11 +192,11 @@ export class ExtensionLoader { this.isLoaded = true; this.loadOnMain(); - ipcMainHandle(ExtensionLoader.extensionsMainChannel, () => { + ipcMainHandle(extensionLoaderFromMainChannel, () => { return Array.from(this.toJSON()); }); - ipcMainOn(ExtensionLoader.extensionsRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { + ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { this.syncExtensions(extensions); }); } @@ -220,16 +216,16 @@ export class ExtensionLoader { }); }; - requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler); - ipcRendererOn(ExtensionLoader.extensionsMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { + requestExtensionLoaderInitialState().then(extensionListHandler); + ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { extensionListHandler(extensions); }); } broadcastExtensions() { const channel = ipcRenderer - ? ExtensionLoader.extensionsRendererChannel - : ExtensionLoader.extensionsMainChannel; + ? extensionLoaderFromRendererChannel + : extensionLoaderFromMainChannel; broadcastMessage(channel, Array.from(this.extensions)); } diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index d528f9f9ac..4f6c833a3e 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -51,8 +51,7 @@ describe("create clusters", () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - - const mockOpts = { + mockFs({ "minikube-config.yml": JSON.stringify({ apiVersion: "v1", clusters: [{ @@ -74,9 +73,7 @@ describe("create clusters", () => { kind: "Config", preferences: {}, }), - }; - - mockFs(mockOpts); + }); await di.runSetups(); diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts index 7a79c9f50c..14cf8cc5e9 100644 --- a/src/main/catalog-pusher.ts +++ b/src/main/catalog-pusher.ts @@ -10,15 +10,15 @@ import "../common/catalog-entities/kubernetes-cluster"; import { disposer, toJS } from "../common/utils"; import { debounce } from "lodash"; import type { CatalogEntity } from "../common/catalog"; -import { CatalogIpcEvents } from "../common/ipc/catalog"; +import { catalogInitChannel, catalogItemsChannel } from "../common/ipc/catalog"; const broadcaster = debounce((items: CatalogEntity[]) => { - broadcastMessage(CatalogIpcEvents.ITEMS, items); + broadcastMessage(catalogItemsChannel, items); }, 1_000, { leading: true, trailing: true }); export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { return disposer( - ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))), + ipcMainOn(catalogInitChannel, () => broadcaster(toJS(catalog.items))), reaction(() => toJS(catalog.items), (items) => { broadcaster(items); }, { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index b7e17ff762..12924a4b18 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import "../common/cluster-ipc"; +import "../common/ipc/cluster"; import type http from "http"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; import { Cluster } from "../common/cluster/cluster"; diff --git a/src/main/index.ts b/src/main/index.ts index 663e7d8f53..473a92c370 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,7 +6,6 @@ // Main process import { injectSystemCAs } from "../common/system-ca"; -import { initialize as initializeRemote } from "@electron/remote/main"; import * as Mobx from "mobx"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsMainApi from "../extensions/main-api"; @@ -24,7 +23,7 @@ import type { InstalledExtension } from "../extensions/extension-discovery/exten import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; -import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc"; +import { ipcMainOn } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { pushCatalogToRenderer } from "./catalog-pusher"; @@ -81,9 +80,6 @@ di.runSetups().then(() => { app.disableHardwareAcceleration(); } - logger.debug("[APP-MAIN] initializing remote"); - initializeRemote(); - logger.debug("[APP-MAIN] configuring packages"); configurePackages(); @@ -131,8 +127,6 @@ di.runSetups().then(() => { logger.info("🐚 Syncing shell environment"); await shellSync(); - bindBroadcastHandlers(); - powerMonitor.on("shutdown", () => app.exit()); registerFileProtocol("static", __static); diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts index 53d2ed9567..2986d3319e 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts @@ -3,23 +3,27 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { BrowserWindow, dialog, IpcMainInvokeEvent, Menu } from "electron"; +import { BrowserWindow, IpcMainInvokeEvent, Menu } from "electron"; import { clusterFrameMap } from "../../../common/cluster-frames"; -import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/cluster-ipc"; +import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster"; import type { ClusterId } from "../../../common/cluster-types"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { appEventBus } from "../../../common/app-event-bus/event-bus"; -import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../../common/ipc"; +import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc"; import { catalogEntityRegistry } from "../../catalog"; import { pushCatalogToRenderer } from "../../catalog-pusher"; import { ClusterManager } from "../../cluster-manager"; import { ResourceApplier } from "../../resource-applier"; -import { IpcMainWindowEvents, WindowManager } from "../../window-manager"; +import { WindowManager } from "../../window-manager"; import path from "path"; import { remove } from "fs-extra"; import { getAppMenu } from "../../menu/menu"; import type { MenuRegistration } from "../../menu/menu-registration"; import type { IComputedValue } from "mobx"; +import { onLocationChange, handleWindowAction } from "../../ipc/window"; +import { openFilePickingDialogChannel } from "../../../common/ipc/dialog"; +import { showOpenDialog } from "../../ipc/dialog"; +import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window"; interface Dependencies { electronMenuItems: IComputedValue, @@ -136,21 +140,22 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt } }); - ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => { - await WindowManager.getInstance().ensureMainWindow(); + ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action)); - return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts); - }); + ipcMainOn(windowLocationChangedChannel, () => onLocationChange()); - ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => { + ipcMainHandle(openFilePickingDialogChannel, (event, opts) => showOpenDialog(opts)); + + ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args)); + + ipcMainOn(windowOpenAppMenuAsContextMenuChannel, async (event) => { const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get())); - const options = { + + menu.popup({ ...BrowserWindow.fromWebContents(event.sender), // Center of the topbar menu icon x: 20, y: 20, - } as Electron.PopupOptions; - - menu.popup(options); + }); }); }; diff --git a/src/main/ipc/dialog.ts b/src/main/ipc/dialog.ts new file mode 100644 index 0000000000..d192869ebd --- /dev/null +++ b/src/main/ipc/dialog.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { BrowserWindow, dialog, OpenDialogOptions } from "electron"; + +export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[]; }> { + const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOptions); + + return { canceled, filePaths }; +} diff --git a/src/main/ipc/window.ts b/src/main/ipc/window.ts new file mode 100644 index 0000000000..b111eab243 --- /dev/null +++ b/src/main/ipc/window.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { BrowserWindow, webContents } from "electron"; +import { broadcastMessage } from "../../common/ipc"; +import { WindowAction } from "../../common/ipc/window"; + +export function handleWindowAction(action: WindowAction) { + const window = BrowserWindow.getFocusedWindow(); + + if (!window) return; + + switch (action) { + case WindowAction.GO_BACK: { + window.webContents.goBack(); + break; + } + + case WindowAction.GO_FORWARD: { + window.webContents.goForward(); + break; + } + + case WindowAction.MINIMIZE: { + window.minimize(); + break; + } + + case WindowAction.TOGGLE_MAXIMIZE: { + if (window.isMaximized()) { + window.unmaximize(); + } else { + window.maximize(); + } + break; + } + + case WindowAction.CLOSE: { + window.close(); + break; + } + + default: + throw new Error(`Attemped window action ${action} is unknown`); + } +} + +export function onLocationChange(): void { + const getAllWebContents = webContents.getAllWebContents(); + + const canGoBack = getAllWebContents.some((webContent) => { + if (webContent.getType() === "window") { + return webContent.canGoBack(); + } + + return false; + }); + + const canGoForward = getAllWebContents.some((webContent) => { + if (webContent.getType() === "window") { + return webContent.canGoForward(); + } + + return false; + }); + + broadcastMessage("history:can-go-back", canGoBack); + broadcastMessage("history:can-go-forward", canGoForward); +} diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 116f02aefe..903c9d8525 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -8,17 +8,14 @@ import { makeObservable, observable } from "mobx"; import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/app-event-bus/event-bus"; -import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc"; +import { ipcMainOn } from "../common/ipc"; import { delay, iter, Singleton } from "../common/utils"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import logger from "./logger"; import { isMac, productName } from "../common/vars"; import { LensProxy } from "./lens-proxy"; - -export const enum IpcMainWindowEvents { - OPEN_CONTEXT_MENU = "window:open-context-menu", -} +import { bundledExtensionsLoaded } from "../common/ipc/extension-handling"; function isHideable(window: BrowserWindow | null): boolean { return Boolean(window && !window.isDestroyed()); @@ -75,9 +72,9 @@ export class WindowManager extends Singleton { webPreferences: { nodeIntegration: true, nodeIntegrationInSubFrames: true, - enableRemoteModule: true, webviewTag: true, contextIsolation: false, + nativeWindowOpen: true, }, }); this.windowState.manage(this.mainWindow); @@ -135,7 +132,8 @@ export class WindowManager extends Singleton { // Always disable Node.js integration for all webviews webPreferences.nodeIntegration = false; - }).setWindowOpenHandler((details) => { + }) + .setWindowOpenHandler((details) => { shell.openExternal(details.url); return { action: "deny" }; @@ -165,7 +163,7 @@ export class WindowManager extends Singleton { if (!this.mainWindow) { viewHasLoaded = new Promise(resolve => { - ipcMain.once(BundledExtensionsLoaded, () => resolve()); + ipcMain.once(bundledExtensionsLoaded, () => resolve()); }); await this.initMainWindow(showSplash); } @@ -249,9 +247,9 @@ export class WindowManager extends Singleton { show: false, webPreferences: { nodeIntegration: true, - enableRemoteModule: true, contextIsolation: false, nodeIntegrationInSubFrames: true, + nativeWindowOpen: true, }, }); await this.splashWindow.loadURL("static://splash.html"); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 85477245a4..b7c97aa968 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -4,7 +4,7 @@ */ import { computed, observable, makeObservable, action } from "mobx"; -import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc"; +import { ipcRendererOn } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; import type { Cluster } from "../../common/cluster/cluster"; @@ -14,7 +14,7 @@ import { once } from "lodash"; import logger from "../../common/logger"; import { CatalogRunEvent } from "../../common/catalog/catalog-run-event"; import { ipcRenderer } from "electron"; -import { CatalogIpcEvents } from "../../common/ipc/catalog"; +import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../common/ipc/catalog"; import { navigate } from "../navigation"; import { isMainFrame } from "process"; @@ -79,12 +79,12 @@ export class CatalogEntityRegistry { } init() { - ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => { + ipcRendererOn(catalogItemsChannel, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => { this.updateItems(items); }); // Make sure that we get items ASAP and not the next time one of them changes - ipcRenderer.send(CatalogIpcEvents.INIT); + ipcRenderer.send(catalogInitChannel); if (isMainFrame) { ipcRendererOn(catalogEntityRunListener, (event, id: string) => { diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 264118da3b..e9be90e067 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -27,12 +27,11 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable"; import installFromInputInjectable from "./install-from-input/install-from-input.injectable"; -import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog/install-from-select-file-dialog.injectable"; +import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable"; import type { LensExtensionId } from "../../../extensions/lens-extension"; import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; import { supportedExtensionFormats } from "./supported-extension-formats"; -import extensionInstallationStateStoreInjectable - from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; interface Dependencies { @@ -107,22 +106,15 @@ class NonInjectedExtensions extends React.Component { } } -export const Extensions = withInjectables( - NonInjectedExtensions, - { - getProps: (di) => ({ - userExtensions: di.inject(userExtensionsInjectable), - enableExtension: di.inject(enableExtensionInjectable), - disableExtension: di.inject(disableExtensionInjectable), - confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), - installFromInput: di.inject(installFromInputInjectable), - installOnDrop: di.inject(installOnDropInjectable), - - installFromSelectFileDialog: di.inject( - installFromSelectFileDialogInjectable, - ), - - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - }), - }, -); +export const Extensions = withInjectables(NonInjectedExtensions, { + getProps: (di) => ({ + userExtensions: di.inject(userExtensionsInjectable), + enableExtension: di.inject(enableExtensionInjectable), + disableExtension: di.inject(disableExtensionInjectable), + confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), + installFromInput: di.inject(installFromInputInjectable), + installOnDrop: di.inject(installOnDropInjectable), + installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + }), +}); diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts new file mode 100644 index 0000000000..5cf51733d1 --- /dev/null +++ b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { requestOpenFilePickingDialog } from "../../ipc"; +import { supportedExtensionFormats } from "./supported-extension-formats"; +import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable"; +import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; +import { bind } from "../../utils"; + +interface Dependencies { + attemptInstalls: (filePaths: string[]) => Promise + directoryForDownloads: string +} + +async function installFromSelectFileDialog({ attemptInstalls, directoryForDownloads }: Dependencies) { + const { canceled, filePaths } = await requestOpenFilePickingDialog({ + defaultPath: directoryForDownloads, + properties: ["openFile", "multiSelections"], + message: `Select extensions to install (formats: ${supportedExtensionFormats.join(", ")}), `, + buttonLabel: "Use configuration", + filters: [{ name: "tarball", extensions: supportedExtensionFormats }], + }); + + if (!canceled) { + await attemptInstalls(filePaths); + } +} + +const installFromSelectFileDialogInjectable = getInjectable({ + instantiate: (di) => bind(installFromSelectFileDialog, null, { + attemptInstalls: di.inject(attemptInstallsInjectable), + directoryForDownloads: di.inject(directoryForDownloadsInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default installFromSelectFileDialogInjectable; diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts deleted file mode 100644 index 8ea2d9b1a4..0000000000 --- a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { installFromSelectFileDialog } from "./install-from-select-file-dialog"; -import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable"; -import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; - -const installFromSelectFileDialogInjectable = getInjectable({ - instantiate: (di) => - installFromSelectFileDialog({ - attemptInstalls: di.inject(attemptInstallsInjectable), - directoryForDownloads: di.inject(directoryForDownloadsInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default installFromSelectFileDialogInjectable; diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts deleted file mode 100644 index de98b5a7ec..0000000000 --- a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { dialog } from "../../../remote-helpers"; -import { supportedExtensionFormats } from "../supported-extension-formats"; - -interface Dependencies { - attemptInstalls: (filePaths: string[]) => Promise - directoryForDownloads: string -} - -export const installFromSelectFileDialog = - ({ attemptInstalls, directoryForDownloads }: Dependencies) => - async () => { - const { canceled, filePaths } = await dialog.showOpenDialog({ - defaultPath: directoryForDownloads, - properties: ["openFile", "multiSelections"], - message: `Select extensions to install (formats: ${supportedExtensionFormats.join( - ", ", - )}), `, - buttonLabel: "Use configuration", - filters: [{ name: "tarball", extensions: supportedExtensionFormats }], - }); - - if (!canceled) { - await attemptInstalls(filePaths); - } - }; diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx index bd25940c84..eb3d255987 100644 --- a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx +++ b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx @@ -19,7 +19,7 @@ import { SubTitle } from "../layout/sub-title"; import { Icon } from "../icon"; import { Notifications } from "../notifications"; import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager"; -import { dialog } from "../../remote-helpers"; +import { requestOpenFilePickingDialog } from "../../ipc"; interface Props extends Partial { onAddRepo: Function @@ -73,7 +73,7 @@ export class AddHelmRepoDialog extends React.Component { } async selectFileDialog(type: FileType, fileFilter: FileFilter) { - const { canceled, filePaths } = await dialog.showOpenDialog({ + const { canceled, filePaths } = await requestOpenFilePickingDialog({ defaultPath: this.getFilePath(type), properties: ["openFile", "showHiddenFiles"], message: `Select file`, diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx index 79deef7dcb..69102ca38b 100644 --- a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -10,8 +10,7 @@ import type { IToleration } from "../../../../common/k8s-api/workload-kube-objec import { PodTolerations } from "../pod-tolerations"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; -import directoryForLensLocalStorageInjectable - from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; jest.mock("electron", () => ({ app: { diff --git a/src/renderer/components/activate-entity-command/activate-entity-command.tsx b/src/renderer/components/activate-entity-command/activate-entity-command.tsx index 9a7964dee6..939d750da4 100644 --- a/src/renderer/components/activate-entity-command/activate-entity-command.tsx +++ b/src/renderer/components/activate-entity-command/activate-entity-command.tsx @@ -7,7 +7,8 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import { computed, IComputedValue } from "mobx"; import { observer } from "mobx-react"; import React from "react"; -import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc"; +import { broadcastMessage } from "../../../common/ipc"; +import { catalogEntityRunListener } from "../../../common/ipc/catalog"; import type { CatalogEntity } from "../../api/catalog-entity"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 11a16f2689..408f45b1bb 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -8,8 +8,7 @@ import styles from "./cluster-status.module.scss"; import { computed, observable, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; -import { clusterActivateHandler } from "../../../common/cluster-ipc"; -import { ipcRendererOn, requestMain } from "../../../common/ipc"; +import { ipcRendererOn } from "../../../common/ipc"; import type { Cluster } from "../../../common/cluster/cluster"; import { cssNames, IClassName } from "../../utils"; import { Button } from "../button"; @@ -19,6 +18,7 @@ import { navigate } from "../../navigation"; import { entitySettingsURL } from "../../../common/routes"; import type { KubeAuthUpdate } from "../../../common/cluster-types"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; +import { requestClusterActivation } from "../../ipc"; interface Props { className?: IClassName; @@ -60,7 +60,7 @@ export class ClusterStatus extends React.Component { this.isReconnecting = true; try { - await requestMain(clusterActivateHandler, this.cluster.id, true); + await requestClusterActivation(this.cluster.id, true); } catch (error) { this.authOutput.push({ message: error.toString(), diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 29b0f4c739..5187866ef3 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -12,11 +12,10 @@ import { ClusterStatus } from "./cluster-status"; import { ClusterFrameHandler } from "./lens-views"; import type { Cluster } from "../../../common/cluster/cluster"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import { requestMain } from "../../../common/ipc"; -import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { navigate } from "../../navigation"; import { catalogURL, ClusterViewRouteParams } from "../../../common/routes"; +import { requestClusterActivation } from "../../ipc"; interface Props extends RouteComponentProps { } @@ -58,7 +57,7 @@ export class ClusterView extends React.Component { reaction(() => this.clusterId, async (clusterId) => { ClusterFrameHandler.getInstance().setVisibleCluster(clusterId); ClusterFrameHandler.getInstance().initView(clusterId); - requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main + requestClusterActivation(clusterId, false); // activate and fetch cluster's state from main catalogEntityRegistry.activeEntity = clusterId; }, { fireImmediately: true, diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index edd3b6d0c7..22d5cba586 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -5,7 +5,7 @@ import { action, IReactionDisposer, makeObservable, observable, when } from "mobx"; import logger from "../../../main/logger"; -import { clusterVisibilityHandler } from "../../../common/cluster-ipc"; +import { clusterVisibilityHandler } from "../../../common/ipc/cluster"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import type { ClusterId } from "../../../common/cluster-types"; import { getClusterFrameUrl, Singleton } from "../../utils"; diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index 9b30e686fe..f28222a2a2 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -12,8 +12,6 @@ import { Button } from "../button"; import type { KubeConfig } from "@kubernetes/client-node"; import type { Cluster } from "../../../common/cluster/cluster"; import { saveKubeconfig } from "./save-config"; -import { requestMain } from "../../../common/ipc"; -import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc"; import { Notifications } from "../notifications"; import { HotbarStore } from "../../../common/hotbar-store"; import { boundMethod } from "autobind-decorator"; @@ -21,6 +19,7 @@ import { Dialog } from "../dialog"; import { Icon } from "../icon"; import { Select } from "../select"; import { Checkbox } from "../checkbox"; +import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc"; type DialogState = { isOpen: boolean, @@ -87,18 +86,18 @@ export class DeleteClusterDialog extends React.Component { async onDelete() { const { cluster, config } = dialogState; - await requestMain(clusterSetDeletingHandler, cluster.id); + await requestSetClusterAsDeleting(cluster.id); this.removeContext(); this.changeCurrentContext(); try { await saveKubeconfig(config, cluster.kubeConfigPath); HotbarStore.getInstance().removeAllHotbarItems(cluster.id); - await requestMain(clusterDeleteHandler, cluster.id); + await requestDeleteCluster(cluster.id); } catch(error) { Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`); } finally { - await requestMain(clusterClearDeletingHandler, cluster.id); + await requestClearClusterAsDeleting(cluster.id); } this.onClose(); diff --git a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx index 3cc3a75b3f..0190adcc71 100644 --- a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx @@ -7,18 +7,17 @@ import React from "react"; import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { TopBar } from "./top-bar"; -import { IpcMainWindowEvents } from "../../../../main/window-manager"; -import { broadcastMessage } from "../../../../common/ipc"; import * as vars from "../../../../common/vars"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; -import directoryForUserDataInjectable - from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import mockFs from "mock-fs"; +import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc"; const mockConfig = vars as { isWindows: boolean; isLinux: boolean }; jest.mock("../../../../common/ipc"); +jest.mock("../../../ipc"); jest.mock("../../../../common/vars", () => { const SemVer = require("semver").SemVer; @@ -33,23 +32,6 @@ jest.mock("../../../../common/vars", () => { }; }); -const mockMinimize = jest.fn(); -const mockMaximize = jest.fn(); -const mockUnmaximize = jest.fn(); -const mockClose = jest.fn(); - -jest.mock("@electron/remote", () => { - return { - getCurrentWindow: () => ({ - minimize: () => mockMinimize(), - maximize: () => mockMaximize(), - unmaximize: () => mockUnmaximize(), - close: () => mockClose(), - isMaximized: () => false, - }), - }; -}); - describe(" in Windows and Linux", () => { let render: DiRender; @@ -104,15 +86,15 @@ describe(" in Windows and Linux", () => { const close = getByTestId("window-close"); fireEvent.click(menu); - expect(broadcastMessage).toHaveBeenCalledWith(IpcMainWindowEvents.OPEN_CONTEXT_MENU); + expect(emitOpenAppMenuAsContextMenu).toHaveBeenCalledWith(); fireEvent.click(minimize); - expect(mockMinimize).toHaveBeenCalled(); + expect(requestWindowAction).toHaveBeenCalledWith("minimize"); fireEvent.click(maximize); - expect(mockMaximize).toHaveBeenCalled(); + expect(requestWindowAction).toHaveBeenCalledWith("toggle-maximize"); fireEvent.click(close); - expect(mockClose).toHaveBeenCalled(); + expect(requestWindowAction).toHaveBeenCalledWith("close"); }); }); diff --git a/src/renderer/components/layout/top-bar/top-bar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx index 9c55b6ec7a..e5d491401c 100644 --- a/src/renderer/components/layout/top-bar/top-bar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -12,8 +12,7 @@ import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injec import { DiRender, renderFor } from "../../test-utils/renderFor"; import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import { computed } from "mobx"; -import directoryForUserDataInjectable - from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import mockFs from "mock-fs"; jest.mock("../../../../common/vars", () => { @@ -27,6 +26,9 @@ jest.mock("../../../../common/vars", () => { }; }); +const goBack = jest.fn(); +const goForward = jest.fn(); + jest.mock( "electron", () => ({ @@ -42,6 +44,25 @@ jest.mock( } }, ), + invoke: jest.fn( + (channel: string, action: string) => { + console.log("channel", channel, action); + + if (channel !== "window:window-action") return; + + switch(action) { + case "back": { + goBack(); + break; + } + + case "forward": { + goForward(); + break; + } + } + }, + ), }, }), ); @@ -50,24 +71,6 @@ jest.mock("../../+catalog", () => ({ previousActiveTab: jest.fn(), })); -const goBack = jest.fn(); -const goForward = jest.fn(); - -jest.mock("@electron/remote", () => { - return { - webContents: { - getAllWebContents: () => { - return [{ - getType: () => "window", - goBack, - goForward, - }]; - }, - }, - getCurrentWindow: () => jest.fn(), - }; -}); - describe("", () => { let di: ConfigurableDependencyInjectionContainer; let render: DiRender; diff --git a/src/renderer/components/layout/top-bar/top-bar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx index 9a122250d4..dc801a5424 100644 --- a/src/renderer/components/layout/top-bar/top-bar.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.tsx @@ -4,22 +4,22 @@ */ import styles from "./top-bar.module.scss"; -import React, { useEffect, useMemo, useRef } from "react"; +import React, { useEffect, useRef } from "react"; import { observer } from "mobx-react"; import type { IComputedValue } from "mobx"; import { Icon } from "../../icon"; -import { webContents, getCurrentWindow } from "@electron/remote"; import { observable } from "mobx"; -import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc"; +import { ipcRendererOn } from "../../../../common/ipc"; import { watchHistoryState } from "../../../remote-helpers/history-updater"; import { isActiveRoute, navigate } from "../../../navigation"; import { catalogRoute, catalogURL } from "../../../../common/routes"; -import { IpcMainWindowEvents } from "../../../../main/window-manager"; import { isLinux, isWindows } from "../../../../common/vars"; import { cssNames } from "../../../utils"; import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { TopBarRegistration } from "./top-bar-registration"; +import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc"; +import { WindowAction } from "../../../../common/ipc/window"; interface Props extends React.HTMLAttributes {} @@ -40,10 +40,9 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => { const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => { const elem = useRef(); - const window = useMemo(() => getCurrentWindow(), []); - const openContextMenu = () => { - broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU); + const openAppContextMenu = () => { + emitOpenAppMenuAsContextMenu(); }; const goHome = () => { @@ -51,11 +50,11 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) }; const goBack = () => { - webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goBack(); + requestWindowAction(WindowAction.GO_BACK); }; const goForward = () => { - webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goForward(); + requestWindowAction(WindowAction.GO_FORWARD); }; const windowSizeToggle = (evt: React.MouseEvent) => { @@ -68,34 +67,30 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) }; const minimizeWindow = () => { - window.minimize(); + requestWindowAction(WindowAction.MINIMIZE); }; const toggleMaximize = () => { - if (window.isMaximized()) { - window.unmaximize(); - } else { - window.maximize(); - } + requestWindowAction(WindowAction.TOGGLE_MAXIMIZE); }; const closeWindow = () => { - window.close(); + requestWindowAction(WindowAction.CLOSE); }; - useEffect(() => { - const disposer = watchHistoryState(); - - return () => disposer(); - }, []); + useEffect(() => watchHistoryState(), []); return (
{(isWindows || isLinux) && (
-
- +
+ + + + +
)} @@ -127,12 +122,19 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) {(isWindows || isLinux) && (
-
+ + + +
- + + +
- + + +
)} @@ -162,7 +164,6 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => ( export const TopBar = withInjectables(observer(NonInjectedTopBar), { getProps: (di, props) => ({ items: di.inject(topBarItemsInjectable), - ...props, }), }); diff --git a/src/renderer/components/path-picker/path-picker.tsx b/src/renderer/components/path-picker/path-picker.tsx index 9a0d81f597..416bd3fd42 100644 --- a/src/renderer/components/path-picker/path-picker.tsx +++ b/src/renderer/components/path-picker/path-picker.tsx @@ -3,11 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { FileFilter, OpenDialogOptions, remote } from "electron"; +import type { FileFilter, OpenDialogOptions } from "electron"; import { observer } from "mobx-react"; import React from "react"; import { cssNames } from "../../utils"; import { Button } from "../button"; +import { requestOpenFilePickingDialog } from "../../ipc"; export interface PathPickOpts { label: string; @@ -29,8 +30,8 @@ export interface PathPickerProps extends PathPickOpts { export class PathPicker extends React.Component { static async pick(opts: PathPickOpts) { const { onPick, onCancel, label, ...dialogOptions } = opts; - const { dialog, BrowserWindow } = remote; - const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + + const { canceled, filePaths } = await requestOpenFilePickingDialog({ message: label, ...dialogOptions, }); diff --git a/src/renderer/frames/cluster-frame/cluster-frame.tsx b/src/renderer/frames/cluster-frame/cluster-frame.tsx index cad742f485..9ef25ab844 100755 --- a/src/renderer/frames/cluster-frame/cluster-frame.tsx +++ b/src/renderer/frames/cluster-frame/cluster-frame.tsx @@ -71,7 +71,6 @@ class NonInjectedClusterFrame extends React.Component { this.props.subscribeStores([ this.props.namespaceStore, ]), - watchHistoryState(), ]); } diff --git a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 22acc43be5..71d71cf8be 100644 --- a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -6,8 +6,6 @@ import type { Cluster } from "../../../../common/cluster/cluster"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; import logger from "../../../../main/logger"; import { Terminal } from "../../../components/dock/terminal/terminal"; -import { requestMain } from "../../../../common/ipc"; -import { clusterSetFrameIdHandler } from "../../../../common/cluster-ipc"; import type { KubernetesCluster } from "../../../../common/catalog-entities"; import { Notifications } from "../../../components/notifications"; import type { AppEvent } from "../../../../common/app-event-bus/event-bus"; @@ -16,6 +14,7 @@ import { when } from "mobx"; import { unmountComponentAtNode } from "react-dom"; import type { ClusterFrameContext } from "../../../cluster-frame-context/cluster-frame-context"; import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; +import { requestSetClusterFrameId } from "../../../ipc"; interface Dependencies { hostedCluster: Cluster; @@ -42,7 +41,7 @@ export const initClusterFrame = ); await Terminal.preloadFonts(); - await requestMain(clusterSetFrameIdHandler, hostedCluster.id); + await requestSetClusterFrameId(hostedCluster.id); await hostedCluster.whenReady; // cluster.activate() is done at this point catalogEntityRegistry.activeEntity = hostedCluster.id; diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts index c166847e2c..511c0baab0 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts @@ -3,12 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { delay } from "../../../../common/utils"; -import { broadcastMessage, BundledExtensionsLoaded } from "../../../../common/ipc"; -import { registerIpcListeners } from "../../../ipc"; +import { broadcastMessage } from "../../../../common/ipc"; +import { registerIpcListeners } from "../../../ipc/register-listeners"; import logger from "../../../../common/logger"; import { unmountComponentAtNode } from "react-dom"; import type { ExtensionLoading } from "../../../../extensions/extension-loader"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; +import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; interface Dependencies { loadExtensions: () => Promise; @@ -50,7 +51,7 @@ export const initRootFrame = await Promise.race([bundledExtensionsFinished, timeout]); } finally { - ipcRenderer.send(BundledExtensionsLoaded); + ipcRenderer.send(bundledExtensionsLoaded); } lensProtocolRouterRenderer.init(); diff --git a/src/renderer/hooks/index.ts b/src/renderer/hooks/index.ts index 2e5281076b..d790ddf145 100644 --- a/src/renderer/hooks/index.ts +++ b/src/renderer/hooks/index.ts @@ -8,3 +8,4 @@ export * from "./useOnUnmount"; export * from "./useInterval"; export * from "./useMutationObserver"; +export * from "./use-toggle"; diff --git a/src/renderer/hooks/use-toggle.ts b/src/renderer/hooks/use-toggle.ts new file mode 100644 index 0000000000..123c7d55d7 --- /dev/null +++ b/src/renderer/hooks/use-toggle.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { useState } from "react"; + +export function useToggle(initial: boolean): [value: boolean, toggle: () => void] { + const [val, setVal] = useState(initial); + + return [val, () => setVal(!val)]; +} diff --git a/src/renderer/ipc/index.ts b/src/renderer/ipc/index.ts new file mode 100644 index 0000000000..7a14f1e014 --- /dev/null +++ b/src/renderer/ipc/index.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { ipcRenderer, OpenDialogOptions } from "electron"; +import { clusterActivateHandler, clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; +import type { ClusterId, ClusterState } from "../../common/cluster-types"; +import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window"; +import { openFilePickingDialogChannel } from "../../common/ipc/dialog"; +import { extensionDiscoveryStateChannel, extensionLoaderFromMainChannel } from "../../common/ipc/extension-handling"; +import type { InstalledExtension } from "../../extensions/extension-discovery/extension-discovery"; +import type { LensExtensionId } from "../../extensions/lens-extension"; +import { toJS } from "../utils"; +import type { Location } from "history"; + +function requestMain(channel: string, ...args: any[]) { + return ipcRenderer.invoke(channel, ...args.map(toJS)); +} + +function emitToMain(channel: string, ...args: any[]) { + return ipcRenderer.send(channel, ...args.map(toJS)); +} + +export function emitOpenAppMenuAsContextMenu(): void { + emitToMain(windowOpenAppMenuAsContextMenuChannel); +} + +export function emitWindowLocationChanged(location: Location): void { + emitToMain(windowLocationChangedChannel, location); +} + +export function requestWindowAction(type: WindowAction): Promise { + return requestMain(windowActionHandleChannel, type); +} + +export function requestOpenFilePickingDialog(opts: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[]; }> { + return requestMain(openFilePickingDialogChannel, opts); +} + +export function requestSetClusterFrameId(clusterId: ClusterId): Promise { + return requestMain(clusterSetFrameIdHandler, clusterId); +} + +export function requestClusterActivation(clusterId: ClusterId, force?: boolean): Promise { + return requestMain(clusterActivateHandler, clusterId, force); +} + +export function requestClusterDisconnection(clusterId: ClusterId, force?: boolean): Promise { + return requestMain(clusterDisconnectHandler, clusterId, force); +} + +export function requestSetClusterAsDeleting(clusterId: ClusterId): Promise { + return requestMain(clusterSetDeletingHandler, clusterId); +} + +export function requestClearClusterAsDeleting(clusterId: ClusterId): Promise { + return requestMain(clusterClearDeletingHandler, clusterId); +} + +export function requestDeleteCluster(clusterId: ClusterId): Promise { + return requestMain(clusterDeleteHandler, clusterId); +} + +export function requestInitialClusterStates(): Promise<{ id: string, state: ClusterState }[]> { + return requestMain(clusterStates); +} + +export function requestKubectlApplyAll(clusterId: ClusterId, resources: string[], kubectlArgs: string[]): Promise<{ stderr?: string; stdout?: string }> { + return requestMain(clusterKubectlApplyAllHandler, clusterId, resources, kubectlArgs); +} + +export function requestKubectlDeleteAll(clusterId: ClusterId, resources: string[], kubectlArgs: string[]): Promise<{ stderr?: string; stdout?: string }> { + return requestMain(clusterKubectlDeleteAllHandler, clusterId, resources, kubectlArgs); +} + +export function requestInitialExtensionDiscovery(): Promise<{ isLoaded: boolean }> { + return requestMain(extensionDiscoveryStateChannel); +} + +export function requestExtensionLoaderInitialState(): Promise<[LensExtensionId, InstalledExtension][]> { + return requestMain(extensionLoaderFromMainChannel); +} diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/register-listeners.tsx similarity index 92% rename from src/renderer/ipc/index.tsx rename to src/renderer/ipc/register-listeners.tsx index 3a4272c96f..99429a4596 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/register-listeners.tsx @@ -5,7 +5,7 @@ import React from "react"; import { ipcRenderer, IpcRendererEvent } from "electron"; -import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs, HotbarTooManyItems, ipcRendererOn, AutoUpdateChecking, AutoUpdateNoUpdateAvailable } from "../../common/ipc"; +import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ipcRendererOn, AutoUpdateChecking, AutoUpdateNoUpdateAvailable } from "../../common/ipc"; import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; @@ -13,6 +13,8 @@ import { ClusterStore } from "../../common/cluster-store/cluster-store"; import { navigate } from "../navigation"; import { entitySettingsURL } from "../../common/routes"; import { defaultHotbarCells } from "../../common/hotbar-types"; +import { type ListNamespaceForbiddenArgs, clusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; +import { hotbarTooManyItemsChannel } from "../../common/ipc/hotbar"; function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { notificationsStore.remove(notificationId); @@ -127,13 +129,13 @@ export function registerIpcListeners() { }); onCorrect({ source: ipcRenderer, - channel: ClusterListNamespaceForbiddenChannel, + channel: clusterListNamespaceForbiddenChannel, listener: ListNamespacesForbiddenHandler, verifier: isListNamespaceForbiddenArgs, }); onCorrect({ source: ipcRenderer, - channel: HotbarTooManyItems, + channel: hotbarTooManyItemsChannel, listener: HotbarTooManyItemsHandler, verifier: (args: unknown[]): args is [] => args.length === 0, }); diff --git a/src/renderer/remote-helpers/dialog.ts b/src/renderer/remote-helpers/dialog.ts deleted file mode 100644 index 038409eb39..0000000000 --- a/src/renderer/remote-helpers/dialog.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { dialogShowOpenDialogHandler, requestMain } from "../../common/ipc"; - -export async function showOpenDialog(options: Electron.OpenDialogOptions): Promise { - return requestMain(dialogShowOpenDialogHandler, options); -} diff --git a/src/renderer/remote-helpers/history-updater.ts b/src/renderer/remote-helpers/history-updater.ts index d0bbe9384a..2927843776 100644 --- a/src/renderer/remote-helpers/history-updater.ts +++ b/src/renderer/remote-helpers/history-updater.ts @@ -3,32 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { webContents } from "@electron/remote"; import { reaction } from "mobx"; -import { broadcastMessage } from "../../common/ipc"; +import { emitWindowLocationChanged } from "../ipc"; import { navigation } from "../navigation"; export function watchHistoryState() { - return reaction(() => navigation.location, () => { - const getAllWebContents = webContents.getAllWebContents(); - - const canGoBack = getAllWebContents.some((webContent) => { - if (webContent.getType() === "window") { - return webContent.canGoBack(); - } - - return false; - }); - - const canGoForward = getAllWebContents.some((webContent) => { - if (webContent.getType() === "window") { - return webContent.canGoForward(); - } - - return false; - }); - - broadcastMessage("history:can-go-back", canGoBack); - broadcastMessage("history:can-go-forward", canGoForward); - }); + return reaction(() => navigation.location, emitWindowLocationChanged); } diff --git a/yarn.lock b/yarn.lock index db6e481d1f..26caa86d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -396,11 +396,6 @@ global-agent "^2.0.2" global-tunnel-ng "^2.7.1" -"@electron/remote@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.2.tgz#4c390a2e669df47af973c09eec106162a296c323" - integrity sha512-PfnXpQGWh4vpX866NNucJRnNOzDRZcsLcLaT32fUth9k0hccsohfxprqEDYLzRg+ZK2xRrtyUN5wYYoHimMCJg== - "@electron/universal@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.0.5.tgz#b812340e4ef21da2b3ee77b2b4d35c9b86defe37" @@ -5055,10 +5050,10 @@ electron-window-state@^5.0.3: jsonfile "^4.0.0" mkdirp "^0.5.1" -electron@^13.6.1: - version "13.6.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-13.6.1.tgz#f61c4f269b57c7dc27e0d5476216a988caa9c752" - integrity sha512-rZ6Y7RberigruefQpWOiI4bA9ppyT88GQF8htY6N1MrAgal5RrBc+Mh92CcGU7zT9QO+XO3DarSgZafNTepffQ== +electron@^14.2.4: + version "14.2.4" + resolved "https://registry.yarnpkg.com/electron/-/electron-14.2.4.tgz#243c71a16a85a4f70086d003b3437cd30b541da0" + integrity sha512-uskCIp+fpohqVYtM2Q28rbXLqGjZ6sWYylXcX6N+K8jR8kR2eHuDMIkO8DzWrTsqA6t4UNAzn+bJnA3VfIIjQw== dependencies: "@electron/get" "^1.0.1" "@types/node" "^14.6.2" From 8d8491a0353882fe8b72f680fe71b3ab08855478 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 27 Jan 2022 14:42:19 -0500 Subject: [PATCH 03/13] Ensure that CatalogEntity.getName() and CatalogEntity.getId() are always used (#4763) --- src/common/__tests__/hotbar-store.test.ts | 39 +++++-- .../catalog-entities/kubernetes-cluster.ts | 16 +-- src/common/catalog-entities/web-link.ts | 4 +- src/common/catalog/catalog-entity.ts | 21 +++- src/common/hotbar-store.ts | 33 +++--- src/main/catalog/catalog-entity-registry.ts | 2 +- src/main/cluster-manager.ts | 6 +- src/migrations/hotbar-store/5.0.0-beta.5.ts | 2 +- src/renderer/api/catalog-entity-registry.ts | 2 +- .../+catalog/catalog-entity-item.tsx | 100 ------------------ .../+entity-settings/entity-settings.tsx | 6 +- .../cluster-settings/cluster-settings.tsx | 2 +- .../components/cluster-icon-settings.tsx | 4 +- .../components/cluster-name-setting.tsx | 2 +- .../components/hotbar/hotbar-entity-icon.tsx | 8 +- .../components/layout/sidebar-cluster.tsx | 15 ++- src/renderer/initializers/catalog.tsx | 2 +- 17 files changed, 98 insertions(+), 166 deletions(-) delete mode 100644 src/renderer/components/+catalog/catalog-entity-item.tsx diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index b73a63b308..b9fc9d1e99 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -4,45 +4,61 @@ */ import { anyObject } from "jest-mock-extended"; -import { merge } from "lodash"; import mockFs from "mock-fs"; import logger from "../../main/logger"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import { HotbarStore } from "../hotbar-store"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import directoryForUserDataInjectable - from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { items: [ - { + getMockCatalogEntity({ + apiVersion: "v1", + kind: "Cluster", + status: { + phase: "Running", + }, metadata: { uid: "1dfa26e2ebab15780a3547e9c7fa785c", name: "mycluster", source: "local", + labels: {}, + }, + }), + getMockCatalogEntity({ + apiVersion: "v1", + kind: "Cluster", + status: { + phase: "Running", }, - }, - { metadata: { uid: "55b42c3c7ba3b04193416cda405269a5", name: "my_shiny_cluster", source: "remote", + labels: {}, + }, + }), + getMockCatalogEntity({ + apiVersion: "v1", + kind: "Cluster", + status: { + phase: "Running", }, - }, - { metadata: { uid: "catalog-entity", name: "Catalog", source: "app", + labels: {}, }, - }, + }), ], }, })); function getMockCatalogEntity(data: Partial & CatalogEntityKindData): CatalogEntity { - return merge(data, { + return { getName: jest.fn(() => data.metadata?.name), getId: jest.fn(() => data.metadata?.uid), getSource: jest.fn(() => data.metadata?.source ?? "unknown"), @@ -52,7 +68,8 @@ function getMockCatalogEntity(data: Partial & CatalogEntityKi metadata: {}, spec: {}, status: {}, - }) as CatalogEntity; + ...data, + } as CatalogEntity; } const testCluster = getMockCatalogEntity({ diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index dcc732a2ca..71cefe6bfc 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -67,22 +67,22 @@ export class KubernetesCluster extends CatalogEntity { if (app) { - await ClusterStore.getInstance().getById(this.metadata.uid)?.activate(); + await ClusterStore.getInstance().getById(this.getId())?.activate(); } else { - await requestClusterActivation(this.metadata.uid, false); + await requestClusterActivation(this.getId(), false); } } async disconnect(): Promise { if (app) { - ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect(); + ClusterStore.getInstance().getById(this.getId())?.disconnect(); } else { - await requestClusterDisconnection(this.metadata.uid, false); + await requestClusterDisconnection(this.getId(), false); } } async onRun(context: CatalogEntityActionContext) { - context.navigate(`/cluster/${this.metadata.uid}`); + context.navigate(`/cluster/${this.getId()}`); } onDetailsOpen(): void { @@ -100,7 +100,7 @@ export class KubernetesCluster extends CatalogEntity broadcastMessage( IpcRendererNavigationEvents.NAVIGATE_IN_APP, - `/entity/${this.metadata.uid}/settings`, + `/entity/${this.getId()}/settings`, ), }); } @@ -111,14 +111,14 @@ export class KubernetesCluster extends CatalogEntity requestClusterDisconnection(this.metadata.uid), + onClick: () => requestClusterDisconnection(this.getId()), }); break; case LensKubernetesClusterStatus.DISCONNECTED: context.menuItems.push({ title: "Connect", icon: "link", - onClick: () => context.navigate(`/cluster/${this.metadata.uid}`), + onClick: () => context.navigate(`/cluster/${this.getId()}`), }); break; } diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index be59135ae1..a014b22aa4 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -38,9 +38,9 @@ export class WebLink extends CatalogEntity WeblinkStore.getInstance().removeById(this.metadata.uid), + onClick: async () => WeblinkStore.getInstance().removeById(this.getId()), confirm: { - message: `Remove Web Link "${this.metadata.name}" from ${productName}?`, + message: `Remove Web Link "${this.getName()}" from ${productName}?`, }, }); } diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index 46ce8228ff..7f5f8bbe73 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -315,11 +315,24 @@ export abstract class CatalogEntity< @observable status: Status; @observable spec: Spec; - constructor(data: CatalogEntityData) { + constructor({ metadata, status, spec }: CatalogEntityData) { makeObservable(this); - this.metadata = data.metadata; - this.status = data.status; - this.spec = data.spec; + + if (!metadata || typeof metadata !== "object") { + throw new TypeError("CatalogEntity's metadata must be a defined object"); + } + + if (!status || typeof status !== "object") { + throw new TypeError("CatalogEntity's status must be a defined object"); + } + + if (!spec || typeof spec !== "object") { + throw new TypeError("CatalogEntity's spec must be a defined object"); + } + + this.metadata = metadata; + this.status = status; + this.spec = spec; } /** diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 3b4593111e..82c697d0d7 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -154,28 +154,28 @@ export class HotbarStore extends BaseStore { @action addToHotbar(item: CatalogEntity, cellIndex?: number) { const hotbar = this.getActive(); - const uid = item.metadata?.uid; - const name = item.metadata?.name; + const uid = item.getId(); + const name = item.getName(); if (typeof uid !== "string") { - throw new TypeError("CatalogEntity.metadata.uid must be a string"); + throw new TypeError("CatalogEntity's ID must be a string"); } if (typeof name !== "string") { - throw new TypeError("CatalogEntity.metadata.name must be a string"); + throw new TypeError("CatalogEntity's NAME must be a string"); } - const newItem = { entity: { - uid, - name, - source: item.metadata.source, - }}; - - if (this.isAddedToActive(item)) { return; } + const entity = { + uid, + name, + source: item.metadata.source, + }; + const newItem = { entity }; + if (cellIndex === undefined) { // Add item to empty cell const emptyCellIndex = hotbar.items.indexOf(null); @@ -278,11 +278,14 @@ export class HotbarStore extends BaseStore { } /** - * Checks if entity already pinned to hotbar - * @returns boolean + * Checks if entity already pinned to the active hotbar */ - isAddedToActive(entity: CatalogEntity) { - return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); + isAddedToActive(entity: CatalogEntity | null | undefined): boolean { + if (!entity) { + return false; + } + + return this.getActive().items.findIndex(item => item?.entity.uid === entity.getId()) >= 0; } getDisplayLabel(hotbar: Hotbar): string { diff --git a/src/main/catalog/catalog-entity-registry.ts b/src/main/catalog/catalog-entity-registry.ts index 65f60d8e4e..5c6e760052 100644 --- a/src/main/catalog/catalog-entity-registry.ts +++ b/src/main/catalog/catalog-entity-registry.ts @@ -36,7 +36,7 @@ export class CatalogEntityRegistry { } getById(id: string): T | undefined { - return this.items.find((entity) => entity.metadata.uid === id) as T | undefined; + return this.items.find(entity => entity.getId() === id) as T | undefined; } getItemsForApiKind(apiVersion: string, kind: string): T[] { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 12924a4b18..d42a0c60a7 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -85,7 +85,7 @@ export class ClusterManager extends Singleton { } protected updateEntityFromCluster(cluster: Cluster) { - const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); + const index = catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id); if (index === -1) { return; @@ -169,11 +169,11 @@ export class ClusterManager extends Singleton { @action protected syncClustersFromCatalog(entities: KubernetesCluster[]) { for (const entity of entities) { - const cluster = this.store.getById(entity.metadata.uid); + const cluster = this.store.getById(entity.getId()); if (!cluster) { const model = { - id: entity.metadata.uid, + id: entity.getId(), kubeConfigPath: entity.spec.kubeconfigPath, contextName: entity.spec.kubeconfigContext, accessibleNamespaces: entity.spec.accessibleNamespaces ?? [], diff --git a/src/migrations/hotbar-store/5.0.0-beta.5.ts b/src/migrations/hotbar-store/5.0.0-beta.5.ts index 421bc2b908..bb7ceb2567 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.5.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.5.ts @@ -16,7 +16,7 @@ export default { for (const hotbar of hotbars) { for (let i = 0; i < hotbar.items.length; i += 1) { const item = hotbar.items[i]; - const entity = catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item?.entity.uid); + const entity = catalogEntityRegistry.items.find((entity) => entity.getId() === item?.entity.uid); if (!entity) { // Clear disabled item diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index b7c97aa968..cc6af170c8 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -120,7 +120,7 @@ export class CatalogEntityRegistry { const entity = this.categoryRegistry.getEntityForData(item); if (entity) { - this._entities.set(entity.metadata.uid, entity); + this._entities.set(entity.getId(), entity); } else { this.rawEntities.push(item); } diff --git a/src/renderer/components/+catalog/catalog-entity-item.tsx b/src/renderer/components/+catalog/catalog-entity-item.tsx deleted file mode 100644 index f909eaf602..0000000000 --- a/src/renderer/components/+catalog/catalog-entity-item.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import styles from "./catalog.module.scss"; -import React from "react"; -import { action, computed } from "mobx"; -import { CatalogEntity } from "../../api/catalog-entity"; -import type { ItemObject } from "../../../common/item.store"; -import { Badge } from "../badge"; -import { navigation } from "../../navigation"; -import { searchUrlParam } from "../input"; -import { KubeObject } from "../../../common/k8s-api/kube-object"; -import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry"; - -export class CatalogEntityItem implements ItemObject { - constructor(public entity: T, private registry: CatalogEntityRegistry) { - if (!(entity instanceof CatalogEntity)) { - throw Object.assign(new TypeError("CatalogEntityItem cannot wrap a non-CatalogEntity type"), { typeof: typeof entity, prototype: Object.getPrototypeOf(entity) }); - } - } - - get kind() { - return this.entity.kind; - } - - get apiVersion() { - return this.entity.apiVersion; - } - - get name() { - return this.entity.metadata.name; - } - - getName() { - return this.entity.metadata.name; - } - - get id() { - return this.entity.metadata.uid; - } - - getId() { - return this.id; - } - - @computed get phase() { - return this.entity.status.phase; - } - - get enabled() { - return this.entity.status.enabled ?? true; - } - - get labels() { - return KubeObject.stringifyLabels(this.entity.metadata.labels); - } - - getLabelBadges(onClick?: React.MouseEventHandler) { - return this.labels - .map(label => ( - { - navigation.searchParams.set(searchUrlParam.name, label); - onClick?.(event); - event.stopPropagation(); - }} - expandable={false} - /> - )); - } - - get source() { - return this.entity.metadata.source || "unknown"; - } - - get searchFields() { - return [ - this.name, - this.id, - this.phase, - `source=${this.source}`, - ...this.labels, - ]; - } - - onRun() { - this.registry.onRun(this.entity); - } - - @action - async onContextMenuOpen(ctx: any) { - return this.entity.onContextMenuOpen(ctx); - } -} diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index 7b5e9c816e..6bfa97195f 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -81,14 +81,14 @@ export class EntitySettings extends React.Component { <>
- {this.entity.metadata.name} + {this.entity.getName()}
diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index bfd89bda36..e89a600095 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -11,7 +11,7 @@ import type { CatalogEntity } from "../../api/catalog-entity"; import * as components from "./components"; function getClusterForEntity(entity: CatalogEntity) { - return ClusterStore.getInstance().getById(entity.metadata.uid); + return ClusterStore.getInstance().getById(entity.getId()); } export function GeneralSettings({ entity }: EntitySettingViewProps) { diff --git a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx index 7300dc92e0..afd23b221f 100644 --- a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx @@ -75,8 +75,8 @@ export class ClusterIconSetting extends React.Component { accept="image/*" label={ diff --git a/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx index 3e80fdbb1f..4802d79d41 100644 --- a/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx @@ -29,7 +29,7 @@ export class ClusterNameSetting extends React.Component { componentDidMount() { disposeOnUnmount(this, autorun(() => { - this.name = this.props.cluster.preferences.clusterName || this.props.entity.metadata.name; + this.name = this.props.cluster.preferences.clusterName || this.props.entity.getName(); }), ); } diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 63bf7dc6c9..a20160f8df 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -73,7 +73,7 @@ export class HotbarEntityIcon extends React.Component { menuItems.unshift({ title: "Remove from Hotbar", - onClick: () => this.props.remove(this.props.entity.metadata.uid), + onClick: () => this.props.remove(this.props.entity.getId()), }); this.contextMenu.menuItems = menuItems; @@ -90,8 +90,8 @@ export class HotbarEntityIcon extends React.Component { return ( { onMenuOpen={() => this.onMenuOpen()} disabled={!entity} menuItems={this.contextMenu.menuItems} - tooltip={`${entity.metadata.name} (${entity.metadata.source})`} + tooltip={`${entity.getName()} (${entity.metadata.source})`} {...elemProps} > { this.ledIcon } diff --git a/src/renderer/components/layout/sidebar-cluster.tsx b/src/renderer/components/layout/sidebar-cluster.tsx index 0126059468..58cae55c99 100644 --- a/src/renderer/components/layout/sidebar-cluster.tsx +++ b/src/renderer/components/layout/sidebar-cluster.tsx @@ -73,7 +73,7 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity ? "Remove from Hotbar" : "Add to Hotbar"; const onClick = isAddedToActive - ? () => hotbarStore.removeFromHotbar(metadata.uid) + ? () => hotbarStore.removeFromHotbar(clusterEntity.getId()) : () => hotbarStore.addToHotbar(clusterEntity); contextMenu.menuItems = [{ title, onClick }]; @@ -92,8 +92,7 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity setOpened(!opened); }; - const { metadata, spec } = clusterEntity; - const id = `cluster-${metadata.uid}`; + const id = `cluster-${clusterEntity.getId()}`; const tooltipId = `tooltip-${id}`; return ( @@ -106,17 +105,17 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity data-testid="sidebar-cluster-dropdown" >
- {metadata.name} + {clusterEntity.getName()}
- {metadata.name} + {clusterEntity.getName()} onClusterDelete(entity.metadata.uid), + onClick: () => onClusterDelete(entity.getId()), }); } }); From 09b33633f755c187fb036d70ae7f6f3c0e379f71 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Fri, 28 Jan 2022 14:40:58 +0100 Subject: [PATCH 04/13] Replace StatusBarRegistry with reactive solution (#4728) --- src/extensions/common-api/registrations.ts | 3 +- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../bottom-bar-items.injectable.ts | 28 +++++ .../cluster-manager/bottom-bar.test.tsx | 119 ++++++++++++------ .../components/cluster-manager/bottom-bar.tsx | 34 +++-- .../status-bar-registration.d.ts} | 9 -- src/renderer/initializers/registries.ts | 1 - 9 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 src/renderer/components/cluster-manager/bottom-bar-items.injectable.ts rename src/{extensions/registries/status-bar-registry.ts => renderer/components/cluster-manager/status-bar-registration.d.ts} (74%) diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts index 20512afb22..5178dd9ee4 100644 --- a/src/extensions/common-api/registrations.ts +++ b/src/extensions/common-api/registrations.ts @@ -2,13 +2,12 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - +export type { StatusBarRegistration } from "../../renderer/components/cluster-manager/status-bar-registration"; export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry"; -export type { StatusBarRegistration } from "../registries/status-bar-registry"; export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler"; export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views"; diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 04b552884c..11ace2cbf1 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -249,7 +249,6 @@ export class ExtensionLoader { const removeItems = [ registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), - registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), ]; diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 7d511c5504..9104f8871b 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -18,6 +18,7 @@ import type { CommandRegistration } from "../renderer/components/command-palette import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration"; import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns"; import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views"; +import type { StatusBarRegistration } from "../renderer/components/cluster-manager/status-bar-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -26,7 +27,7 @@ export class LensRendererExtension extends LensExtension { kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = []; appPreferences: AppPreferenceRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = []; - statusBarItems: registries.StatusBarRegistration[] = []; + statusBarItems: StatusBarRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 477f406b2c..464d19cd4e 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -7,7 +7,6 @@ export * from "./page-registry"; export * from "./page-menu-registry"; -export * from "./status-bar-registry"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; diff --git a/src/renderer/components/cluster-manager/bottom-bar-items.injectable.ts b/src/renderer/components/cluster-manager/bottom-bar-items.injectable.ts new file mode 100644 index 0000000000..9f32831ca6 --- /dev/null +++ b/src/renderer/components/cluster-manager/bottom-bar-items.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import type { StatusBarRegistration } from "./status-bar-registration"; + +const bottomBarItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => + extensions + .get() + .flatMap((extension) => extension.statusBarItems) + .sort(leftItemsBeforeRight), + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default bottomBarItemsInjectable; + +const leftItemsBeforeRight = (firstItem: StatusBarRegistration, secondItem: StatusBarRegistration) => + firstItem.components?.position?.localeCompare(secondItem.components?.position); diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index 97539f0ea3..523500629c 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -4,10 +4,15 @@ */ import React from "react"; -import { render } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { BottomBar } from "./bottom-bar"; -import { StatusBarRegistry } from "../../../extensions/registries"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../test-utils/renderFor"; +import { renderFor } from "../test-utils/renderFor"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import { computed, IObservableArray, observable, runInAction } from "mobx"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; jest.mock("electron", () => ({ app: { @@ -15,13 +20,43 @@ jest.mock("electron", () => ({ }, })); -describe("", () => { - beforeEach(() => { - StatusBarRegistry.createInstance(); - }); +class SomeTestExtension extends LensRendererExtension { + constructor(statusBarItems: IObservableArray) { + super({ + id: "some-id", + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: "some-id", version: "some-version" }, + manifestPath: "irrelevant", + }); - afterEach(() => { - StatusBarRegistry.resetInstance(); + this.statusBarItems = statusBarItems; + } +} + +describe("", () => { + let render: DiRender; + let statusBarItems: IObservableArray; + + beforeEach(async () => { + + statusBarItems = observable.array([]); + + const someTestExtension = new SomeTestExtension(statusBarItems); + + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + di.override(rendererExtensionsInjectable, () => { + return computed(() => [someTestExtension]); + }); + + render = renderFor(di); + + await di.runSetups(); }); it("renders w/o errors", () => { @@ -30,6 +65,7 @@ describe("", () => { expect(container).toBeInstanceOf(HTMLElement); }); + it.each([ undefined, "hello", @@ -39,7 +75,10 @@ describe("", () => { [{}], {}, ])("renders w/o errors when .getItems() returns not type compliant (%p)", val => { - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => val); + runInAction(() => { + statusBarItems.replace([val]); + }); + expect(() => render()).not.toThrow(); }); @@ -47,9 +86,12 @@ describe("", () => { const testId = "testId"; const text = "heee"; - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { item: {text} }, - ]); + runInAction(() => { + statusBarItems.replace([ + { item: {text} }, + ]); + }); + const { getByTestId } = render(); expect(getByTestId(testId)).toHaveTextContent(text); @@ -59,9 +101,12 @@ describe("", () => { const testId = "testId"; const text = "heee"; - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { item: () => {text} }, - ]); + runInAction(() => { + statusBarItems.replace([ + { item: () => {text} }, + ]); + }); + const { getByTestId } = render(); expect(getByTestId(testId)).toHaveTextContent(text); @@ -69,31 +114,33 @@ describe("", () => { it("sort positioned items properly", () => { - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - components: { - Item: () =>
right
, + runInAction(() => { + statusBarItems.replace([ + { + components: { + Item: () =>
right
, + }, }, - }, - { - components: { - Item: () =>
right
, - position: "right", + { + components: { + Item: () =>
right
, + position: "right", + }, }, - }, - { - components: { - Item: () =>
left
, - position: "left", + { + components: { + Item: () =>
left
, + position: "left", + }, }, - }, - { - components: { - Item: () =>
left
, - position: "left", + { + components: { + Item: () =>
left
, + position: "left", + }, }, - }, - ]); + ]); + }); const { getAllByTestId } = render(); const elems = getAllByTestId("sortedElem"); diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx index 84e0f7bbc8..ecbb1634cc 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.tsx @@ -7,11 +7,18 @@ import styles from "./bottom-bar.module.scss"; import React from "react"; import { observer } from "mobx-react"; -import { StatusBarRegistration, StatusBarRegistry } from "../../../extensions/registries"; import { cssNames } from "../../utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import bottomBarItemsInjectable from "./bottom-bar-items.injectable"; +import type { IComputedValue } from "mobx"; +import type { StatusBarRegistration } from "./status-bar-registration"; + +interface Dependencies { + items: IComputedValue +} @observer -export class BottomBar extends React.Component { +class NonInjectedBottomBar extends React.Component { renderRegisteredItem(registration: StatusBarRegistration) { const { item } = registration; @@ -23,19 +30,9 @@ export class BottomBar extends React.Component { } renderRegisteredItems() { - const items = StatusBarRegistry.getInstance().getItems(); - - if (!Array.isArray(items)) { - return null; - } - - items.sort(function sortLeftPositionFirst(a, b) { - return a.components?.position?.localeCompare(b.components?.position); - }); - return ( <> - {items.map((registration, index) => { + {this.props.items.get().map((registration, index) => { if (!registration?.item && !registration?.components?.Item) { return null; } @@ -64,3 +61,14 @@ export class BottomBar extends React.Component { ); } } + +export const BottomBar = withInjectables( + NonInjectedBottomBar, + + { + getProps: (di, props) => ({ + items: di.inject(bottomBarItemsInjectable), + ...props, + }), + }, +); diff --git a/src/extensions/registries/status-bar-registry.ts b/src/renderer/components/cluster-manager/status-bar-registration.d.ts similarity index 74% rename from src/extensions/registries/status-bar-registry.ts rename to src/renderer/components/cluster-manager/status-bar-registration.d.ts index e8d4661abc..2faea55cc5 100644 --- a/src/extensions/registries/status-bar-registry.ts +++ b/src/renderer/components/cluster-manager/status-bar-registration.d.ts @@ -2,12 +2,6 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -// Extensions API -> Status bar customizations - -import type React from "react"; -import { BaseRegistry } from "./base-registry"; - interface StatusBarComponents { Item?: React.ComponentType; /** @@ -28,6 +22,3 @@ export interface StatusBarRegistration extends StatusBarRegistrationV2 { */ item?: React.ReactNode; } - -export class StatusBarRegistry extends BaseRegistry { -} diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 93caeee51e..1c9e5662d4 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -14,6 +14,5 @@ export function initRegistries() { registries.KubeObjectDetailRegistry.createInstance(); registries.KubeObjectMenuRegistry.createInstance(); registries.KubeObjectStatusRegistry.createInstance(); - registries.StatusBarRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); } From 2ad78c2a86cff37abd959a946035862a8ce7043f Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 28 Jan 2022 11:08:06 -0500 Subject: [PATCH 05/13] Fix item menus for several pages (#4770) --- .../components/+user-management/+service-accounts/view.tsx | 3 --- src/renderer/components/+workloads-cronjobs/cronjobs.tsx | 3 --- src/renderer/components/+workloads-deployments/deployments.tsx | 1 - src/renderer/components/+workloads-replicasets/replicasets.tsx | 3 --- .../components/+workloads-statefulsets/statefulsets.tsx | 1 - 5 files changed, 11 deletions(-) diff --git a/src/renderer/components/+user-management/+service-accounts/view.tsx b/src/renderer/components/+user-management/+service-accounts/view.tsx index 84721fe8d5..b8405781b0 100644 --- a/src/renderer/components/+user-management/+service-accounts/view.tsx +++ b/src/renderer/components/+user-management/+service-accounts/view.tsx @@ -58,9 +58,6 @@ export class ServiceAccounts extends React.Component { account.getNs(), account.getAge(), ]} - renderItemMenu={(item: ServiceAccount) => { - return ; - }} addRemoveButtons={{ onAdd: () => CreateServiceAccountDialog.open(), addTooltip: "Create new Service Account", diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 2bb2de3277..c4605ff45f 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -82,9 +82,6 @@ export class CronJobs extends React.Component { cronJob.getLastScheduleTime(), cronJob.getAge(), ]} - renderItemMenu={(item: CronJob) => { - return ; - }} /> ); } diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 09c7c676f4..79ce4af810 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -91,7 +91,6 @@ export class Deployments extends React.Component { deployment.getAge(), this.renderConditions(deployment), ]} - renderItemMenu={item => } /> ); } diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index c76f11131d..f3986125b6 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -70,9 +70,6 @@ export class ReplicaSets extends React.Component { replicaSet.getReady(), replicaSet.getAge(), ]} - renderItemMenu={(item: ReplicaSet) => { - return ; - }} /> ); } diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 1b4a967282..c00f9456ae 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -72,7 +72,6 @@ export class StatefulSets extends React.Component { , statefulSet.getAge(), ]} - renderItemMenu={item => } /> ); } From 38790eb8860a84bf1f820facfc29b6a8914b1c1f Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Sat, 29 Jan 2022 07:46:37 -0500 Subject: [PATCH 06/13] Upgrade to latest for all linting deps (#4755) * Upgrade to eslint latest Signed-off-by: Sebastian Malton * Fix OOM error Signed-off-by: Sebastian Malton * resolve PR comments Signed-off-by: Sebastian Malton --- .eslintrc.js | 106 +---- package.json | 13 +- .../ipc-channel/{channel.d.ts => channel.ts} | 0 src/common/item.store.ts | 2 +- ...e-watch-event.d.ts => kube-watch-event.ts} | 0 src/common/utils/extended-map.ts | 10 +- .../extensions-store/extensions-store.ts | 5 +- ...registration.d.ts => menu-registration.ts} | 0 ...tration.d.ts => tray-menu-registration.ts} | 0 .../components/+add-cluster/add-cluster.tsx | 10 +- .../+apps-helm-charts/helm-chart-details.tsx | 3 +- .../+catalog/catalog-add-button.tsx | 5 +- src/renderer/components/+catalog/catalog.tsx | 5 +- ...nstall-request.d.ts => install-request.ts} | 0 ...on.d.ts => app-preference-registration.ts} | 0 .../+preferences/kubeconfig-syncs.tsx | 3 +- .../+cluster-role-bindings/dialog.tsx | 10 +- .../+role-bindings/dialog.tsx | 10 +- .../+service-accounts/details.tsx | 39 +- ...on.d.ts => welcome-banner-registration.ts} | 0 ...tion.d.ts => welcome-menu-registration.ts} | 0 .../+workloads-pods/pod-details-secrets.tsx | 25 +- .../{commands.d.ts => commands.ts} | 0 src/renderer/components/dialog/dialog.tsx | 11 +- .../components/dock/dock-store/dock.store.ts | 5 +- .../components/dock/install-chart.tsx | 10 +- src/renderer/components/dock/logs/list.tsx | 10 +- .../components/dock/upgrade-chart.tsx | 10 +- src/renderer/components/icon/icon.tsx | 16 +- .../components/input/search-input-url.tsx | 8 +- .../kube-object-details.tsx | 45 ++- ...istration.d.ts => top-bar-registration.ts} | 0 .../monaco-editor/monaco-editor.tsx | 5 +- .../resizing-anchor/resizing-anchor.tsx | 10 +- yarn.lock | 378 +++++++----------- 35 files changed, 284 insertions(+), 470 deletions(-) rename src/common/ipc-channel/{channel.d.ts => channel.ts} (100%) rename src/common/k8s-api/{kube-watch-event.d.ts => kube-watch-event.ts} (100%) rename src/main/menu/{menu-registration.d.ts => menu-registration.ts} (100%) rename src/main/tray/{tray-menu-registration.d.ts => tray-menu-registration.ts} (100%) rename src/renderer/components/+extensions/attempt-install/{install-request.d.ts => install-request.ts} (100%) rename src/renderer/components/+preferences/app-preferences/{app-preference-registration.d.ts => app-preference-registration.ts} (100%) rename src/renderer/components/+welcome/welcome-banner-items/{welcome-banner-registration.d.ts => welcome-banner-registration.ts} (100%) rename src/renderer/components/+welcome/welcome-menu-items/{welcome-menu-registration.d.ts => welcome-menu-registration.ts} (100%) rename src/renderer/components/command-palette/registered-commands/{commands.d.ts => commands.ts} (100%) rename src/renderer/components/layout/top-bar/{top-bar-registration.d.ts => top-bar-registration.ts} (100%) diff --git a/.eslintrc.js b/.eslintrc.js index 3fda195286..cd50650709 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,17 +11,12 @@ module.exports = { "**/dist/**/*", "**/static/**/*", "**/site/**/*", + "extensions/*/*.tgz", ], settings: { react: { version: packageJson.devDependencies.react || "detect", }, - // the package eslint-import-resolver-typescript is required for this line which fixes errors when using .d.ts files - "import/resolver": { - "typescript": { - "alwaysTryTypes": true, - }, - }, }, overrides: [ { @@ -95,95 +90,9 @@ module.exports = { { files: [ "**/*.ts", - ], - parser: "@typescript-eslint/parser", - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - plugins: [ - "header", - "unused-imports", - ], - parserOptions: { - ecmaVersion: 2018, - sourceType: "module", - }, - rules: { - "no-constant-condition": ["error", { "checkLoops": false }], - "header/header": [2, "./license-header"], - "no-invalid-this": "off", - "@typescript-eslint/no-invalid-this": ["error"], - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": "off", - "space-before-function-paren": "off", - "@typescript-eslint/space-before-function-paren": ["error", { - "anonymous": "always", - "named": "never", - "asyncArrow": "always", - }], - "unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn", - "unused-imports/no-unused-vars-ts": [ - "warn", { - "vars": "all", - "args": "after-used", - "ignoreRestSiblings": true, - }, - ], - "comman-dangle": "off", - "@typescript-eslint/comma-dangle": ["error", "always-multiline"], - "comma-spacing": "off", - "@typescript-eslint/comma-spacing": "error", - "indent": ["error", 2, { - "SwitchCase": 1, - }], - "quotes": ["error", "double", { - "avoidEscape": true, - "allowTemplateLiterals": true, - }], - "object-curly-spacing": "off", - "@typescript-eslint/object-curly-spacing": ["error", "always", { - "objectsInObjects": false, - "arraysInObjects": true, - }], - "react/prop-types": "off", - "semi": "off", - "@typescript-eslint/semi": ["error"], - "linebreak-style": ["error", "unix"], - "eol-last": ["error", "always"], - "object-shorthand": "error", - "prefer-template": "error", - "template-curly-spacing": "error", - "no-unused-expressions": "off", - "@typescript-eslint/no-unused-expressions": "error", - "padding-line-between-statements": [ - "error", - { "blankLine": "always", "prev": "*", "next": "return" }, - { "blankLine": "always", "prev": "*", "next": "block-like" }, - { "blankLine": "always", "prev": "*", "next": "function" }, - { "blankLine": "always", "prev": "*", "next": "class" }, - { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, - { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] }, - ], - "no-template-curly-in-string": "error", - }, - }, - { - files: [ "**/*.tsx", ], parser: "@typescript-eslint/parser", - plugins: [ - "header", - "unused-imports", - ], extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", @@ -191,13 +100,19 @@ module.exports = { "plugin:import/recommended", "plugin:import/typescript", ], + plugins: [ + "header", + "unused-imports", + "react-hooks", + ], parserOptions: { ecmaVersion: 2018, sourceType: "module", - jsx: true, }, rules: { - "no-constant-condition": ["error", { "checkLoops": false }], + "no-constant-condition": ["error", { + "checkLoops": false, + }], "header/header": [2, "./license-header"], "react/prop-types": "off", "no-invalid-this": "off", @@ -211,9 +126,10 @@ module.exports = { "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-function": "off", - "react/display-name": "off", "@typescript-eslint/no-unused-vars": "off", + "react/display-name": "off", "space-before-function-paren": "off", "@typescript-eslint/space-before-function-paren": ["error", { "anonymous": "always", diff --git a/package.json b/package.json index 398eff127c..829b7d7df0 100644 --- a/package.json +++ b/package.json @@ -323,8 +323,8 @@ "@types/webpack-dev-server": "^3.11.6", "@types/webpack-env": "^1.16.3", "@types/webpack-node-externals": "^1.7.1", - "@typescript-eslint/eslint-plugin": "^5.7.0", - "@typescript-eslint/parser": "^5.7.0", + "@typescript-eslint/eslint-plugin": "^5.10.1", + "@typescript-eslint/parser": "^5.10.1", "ansi_up": "^5.1.0", "chart.js": "^2.9.4", "circular-dependency-plugin": "^5.2.2", @@ -338,13 +338,12 @@ "electron-notarize": "^0.3.0", "esbuild": "^0.13.15", "esbuild-loader": "^2.16.0", - "eslint": "^7.32.0", - "eslint-import-resolver-typescript": "^2.5.0", + "eslint": "^8.7.0", "eslint-plugin-header": "^3.1.1", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-react": "^7.27.1", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unused-imports": "^1.1.5", + "eslint-plugin-unused-imports": "^2.0.0", "file-loader": "^6.2.0", "flex.box": "^3.4.4", "fork-ts-checker-webpack-plugin": "^5.2.1", diff --git a/src/common/ipc-channel/channel.d.ts b/src/common/ipc-channel/channel.ts similarity index 100% rename from src/common/ipc-channel/channel.d.ts rename to src/common/ipc-channel/channel.ts diff --git a/src/common/item.store.ts b/src/common/item.store.ts index c2296a7515..fee402491b 100644 --- a/src/common/item.store.ts +++ b/src/common/item.store.ts @@ -111,7 +111,7 @@ export abstract class ItemStore { } } - protected async loadItem(...args: any[]): Promise + protected async loadItem(...args: any[]): Promise; @action protected async loadItem(request: () => Promise, sortItems = true) { const item = await Promise.resolve(request()).catch(() => null); diff --git a/src/common/k8s-api/kube-watch-event.d.ts b/src/common/k8s-api/kube-watch-event.ts similarity index 100% rename from src/common/k8s-api/kube-watch-event.d.ts rename to src/common/k8s-api/kube-watch-event.ts diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index fe8520e31d..5db6bffec1 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -3,12 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, ObservableMap } from "mobx"; +import { action, ObservableMap, runInAction } from "mobx"; export function multiSet(map: Map, newEntries: [T, V][]): void { - for (const [key, val] of newEntries) { - map.set(key, val); - } + runInAction(() => { + for (const [key, val] of newEntries) { + map.set(key, val); + } + }); } export class ExtendedMap extends Map { diff --git a/src/extensions/extensions-store/extensions-store.ts b/src/extensions/extensions-store/extensions-store.ts index 682a5a25cb..e019071836 100644 --- a/src/extensions/extensions-store/extensions-store.ts +++ b/src/extensions/extensions-store/extensions-store.ts @@ -42,10 +42,9 @@ export class ExtensionsStore extends BaseStore { return isBundled || Boolean(this.state.get(id)?.enabled); } - @action - mergeState = (extensionsState: Record) => { + mergeState = action((extensionsState: Record) => { this.state.merge(extensionsState); - }; + }); @action protected fromStore({ extensions }: LensExtensionsStoreModel) { diff --git a/src/main/menu/menu-registration.d.ts b/src/main/menu/menu-registration.ts similarity index 100% rename from src/main/menu/menu-registration.d.ts rename to src/main/menu/menu-registration.ts diff --git a/src/main/tray/tray-menu-registration.d.ts b/src/main/tray/tray-menu-registration.ts similarity index 100% rename from src/main/tray/tray-menu-registration.d.ts rename to src/main/tray/tray-menu-registration.ts diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index ba06039d4c..ed69819cfd 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -70,8 +70,7 @@ class NonInjectedAddCluster extends React.Component { ].filter(Boolean); } - @action - refreshContexts = debounce(() => { + readonly refreshContexts = debounce(action(() => { const { config, error } = loadConfigFromString(this.customConfig.trim() || "{}"); this.kubeContexts.replace(getContexts(config)); @@ -83,10 +82,9 @@ class NonInjectedAddCluster extends React.Component { if (config.contexts.length === 0) { this.errors.push('No contexts defined, either missing the "contexts" field, or it is empty.'); } - }, 500); + }), 500); - @action - addClusters = async () => { + addClusters = action(async () => { this.isWaiting = true; appEventBus.emit({ name: "cluster-add", action: "click" }); @@ -102,7 +100,7 @@ class NonInjectedAddCluster extends React.Component { } catch (error) { Notifications.error(`Failed to add clusters: ${error}`); } - }; + }); render() { return ( diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index ea31f628d7..936859dbd0 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -18,8 +18,7 @@ import { Select, SelectOption } from "../select"; import { Badge } from "../badge"; import { Tooltip, withStyles } from "@material-ui/core"; import { withInjectables } from "@ogre-tools/injectable-react"; -import createInstallChartTabInjectable - from "../dock/create-install-chart-tab/create-install-chart-tab.injectable"; +import createInstallChartTabInjectable from "../dock/create-install-chart-tab/create-install-chart-tab.injectable"; interface Props { chart: HelmChart; diff --git a/src/renderer/components/+catalog/catalog-add-button.tsx b/src/renderer/components/+catalog/catalog-add-button.tsx index b20f361fb4..e9f3a0743f 100644 --- a/src/renderer/components/+catalog/catalog-add-button.tsx +++ b/src/renderer/components/+catalog/catalog-add-button.tsx @@ -57,8 +57,7 @@ export class CatalogAddButton extends React.Component { } } - @action - updateCategoryItems = (category: CatalogCategory) => { + updateCategoryItems = action((category: CatalogCategory) => { if (category instanceof EventEmitter) { const menuItems: CatalogEntityAddMenu[] = []; const context: CatalogEntityAddMenuContext = { @@ -69,7 +68,7 @@ export class CatalogAddButton extends React.Component { category.emit("catalogAddMenu", context); this.menuItems.set(category.getId(), menuItems); } - }; + }); getCategoryFilteredItems = (category: CatalogCategory) => { return category.filteredItems(this.menuItems.get(category.getId()) || []); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 3edca9ea92..c99c24bf6b 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -143,8 +143,7 @@ class NonInjectedCatalog extends React.Component { return catalogCategoryRegistry.items; } - @action - onTabChange = (tabId: string | null) => { + onTabChange = action((tabId: string | null) => { const activeCategory = this.categories.find(category => category.getId() === tabId); if (activeCategory) { @@ -152,7 +151,7 @@ class NonInjectedCatalog extends React.Component { } else { navigate(catalogURL({ params: { group: browseCatalogTab }})); } - }; + }); renderNavigation() { return ( diff --git a/src/renderer/components/+extensions/attempt-install/install-request.d.ts b/src/renderer/components/+extensions/attempt-install/install-request.ts similarity index 100% rename from src/renderer/components/+extensions/attempt-install/install-request.d.ts rename to src/renderer/components/+extensions/attempt-install/install-request.ts diff --git a/src/renderer/components/+preferences/app-preferences/app-preference-registration.d.ts b/src/renderer/components/+preferences/app-preferences/app-preference-registration.ts similarity index 100% rename from src/renderer/components/+preferences/app-preferences/app-preference-registration.d.ts rename to src/renderer/components/+preferences/app-preferences/app-preference-registration.ts diff --git a/src/renderer/components/+preferences/kubeconfig-syncs.tsx b/src/renderer/components/+preferences/kubeconfig-syncs.tsx index 0594ea4de9..bbe4438226 100644 --- a/src/renderer/components/+preferences/kubeconfig-syncs.tsx +++ b/src/renderer/components/+preferences/kubeconfig-syncs.tsx @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import fse from "fs-extra"; -import { action, computed, makeObservable, observable, reaction } from "mobx"; +import { computed, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { Notice } from "../+extensions/notice"; @@ -93,7 +93,6 @@ export class KubeconfigSyncs extends React.Component { return Array.from(this.syncs.entries(), ([filePath, value]) => ({ filePath, ...value })); } - @action onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths)); getIconName(entry: Entry) { diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx index ed11dfd800..3823b2213e 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx @@ -115,8 +115,7 @@ export class ClusterRoleBindingDialog extends React.Component { return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value)); } - @action - onOpen = () => { + onOpen = action(() => { const binding = this.clusterRoleBinding; if (!binding) { @@ -137,16 +136,15 @@ export class ClusterRoleBindingDialog extends React.Component { ); this.selectedUsers.replace(uSubjects.map(user => user.name)); this.selectedGroups.replace(gSubjects.map(group => group.name)); - }; + }); - @action - reset = () => { + reset = action(() => { this.selectedRoleRef = undefined; this.bindingName = ""; this.selectedAccounts.clear(); this.selectedUsers.clear(); this.selectedGroups.clear(); - }; + }); createBindings = async () => { const { selectedRoleRef, selectedBindings, bindingName } = this; diff --git a/src/renderer/components/+user-management/+role-bindings/dialog.tsx b/src/renderer/components/+user-management/+role-bindings/dialog.tsx index 8cb4d893d9..29cb771432 100644 --- a/src/renderer/components/+user-management/+role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+role-bindings/dialog.tsx @@ -116,8 +116,7 @@ export class RoleBindingDialog extends React.Component { return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value)); } - @action - onOpen = () => { + onOpen = action(() => { const binding = this.roleBinding; if (!binding) { @@ -140,17 +139,16 @@ export class RoleBindingDialog extends React.Component { ); this.selectedUsers.replace(uSubjects.map(user => user.name)); this.selectedGroups.replace(gSubjects.map(group => group.name)); - }; + }); - @action - reset = () => { + reset = action(() => { this.selectedRoleRef = undefined; this.bindingName = ""; this.bindingNamespace = ""; this.selectedAccounts.clear(); this.selectedUsers.clear(); this.selectedGroups.clear(); - }; + }); createBindings = async () => { const { selectedRoleRef, bindingNamespace: namespace, selectedBindings } = this; diff --git a/src/renderer/components/+user-management/+service-accounts/details.tsx b/src/renderer/components/+user-management/+service-accounts/details.tsx index 081d44aa81..dec10fa678 100644 --- a/src/renderer/components/+user-management/+service-accounts/details.tsx +++ b/src/renderer/components/+user-management/+service-accounts/details.tsx @@ -28,27 +28,30 @@ export class ServiceAccountsDetails extends React.Component { @observable secrets: Secret[]; @observable imagePullSecrets: Secret[]; - @disposeOnUnmount - loadSecrets = autorun(async () => { - this.secrets = null; - this.imagePullSecrets = null; - const { object: serviceAccount } = this.props; + componentDidMount(): void { + disposeOnUnmount(this, [ + autorun(async () => { + this.secrets = null; + this.imagePullSecrets = null; + const { object: serviceAccount } = this.props; - if (!serviceAccount) { - return; - } - const namespace = serviceAccount.getNs(); - const secrets = serviceAccount.getSecrets().map(({ name }) => { - return secretsStore.load({ name, namespace }); - }); + if (!serviceAccount) { + return; + } + const namespace = serviceAccount.getNs(); + const secrets = serviceAccount.getSecrets().map(({ name }) => { + return secretsStore.load({ name, namespace }); + }); - this.secrets = await Promise.all(secrets); - const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async ({ name }) => { - return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name)); - }); + this.secrets = await Promise.all(secrets); + const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async ({ name }) => { + return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name)); + }); - this.imagePullSecrets = await Promise.all(imagePullSecrets); - }); + this.imagePullSecrets = await Promise.all(imagePullSecrets); + }), + ]); + } constructor(props: Props) { super(props); diff --git a/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.ts similarity index 100% rename from src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts rename to src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.ts diff --git a/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.ts similarity index 100% rename from src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts rename to src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.ts diff --git a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx index 1c354b227f..86896ce934 100644 --- a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx @@ -20,19 +20,22 @@ interface Props { export class PodDetailsSecrets extends Component { @observable secrets: Map = observable.map(); - @disposeOnUnmount - secretsLoader = autorun(async () => { - const { pod } = this.props; + componentDidMount(): void { + disposeOnUnmount(this, [ + autorun(async () => { + const { pod } = this.props; - const secrets = await Promise.all( - pod.getSecrets().map(secretName => secretsApi.get({ - name: secretName, - namespace: pod.getNs(), - })), - ); + const secrets = await Promise.all( + pod.getSecrets().map(secretName => secretsApi.get({ + name: secretName, + namespace: pod.getNs(), + })), + ); - secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret)); - }); + secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret)); + }), + ]); + } constructor(props: Props) { super(props); diff --git a/src/renderer/components/command-palette/registered-commands/commands.d.ts b/src/renderer/components/command-palette/registered-commands/commands.ts similarity index 100% rename from src/renderer/components/command-palette/registered-commands/commands.d.ts rename to src/renderer/components/command-palette/registered-commands/commands.ts diff --git a/src/renderer/components/dialog/dialog.tsx b/src/renderer/components/dialog/dialog.tsx index f339ec973e..f40f1ee91d 100644 --- a/src/renderer/components/dialog/dialog.tsx +++ b/src/renderer/components/dialog/dialog.tsx @@ -48,9 +48,6 @@ export class Dialog extends React.PureComponent { pinned: false, }; - @disposeOnUnmount - closeOnNavigate = reaction(() => navigation.toString(), () => this.close()); - public state: DialogState = { isOpen: this.props.isOpen, }; @@ -64,7 +61,13 @@ export class Dialog extends React.PureComponent { } componentDidMount() { - if (this.isOpen) this.onOpen(); + if (this.isOpen) { + this.onOpen(); + } + + disposeOnUnmount(this, [ + reaction(() => navigation.toString(), () => this.close()), + ]); } componentDidUpdate(prevProps: DialogProps) { diff --git a/src/renderer/components/dock/dock-store/dock.store.ts b/src/renderer/components/dock/dock-store/dock.store.ts index b551496e7e..6cca0e4d06 100644 --- a/src/renderer/components/dock/dock-store/dock.store.ts +++ b/src/renderer/components/dock/dock-store/dock.store.ts @@ -276,8 +276,7 @@ export class DockStore implements DockStorageState { } } - @action - createTab = (rawTabDesc: DockTabCreate, addNumber = true): DockTab => { + createTab = action((rawTabDesc: DockTabCreate, addNumber = true): DockTab => { const { id = uuid.v4(), kind, @@ -307,7 +306,7 @@ export class DockStore implements DockStorageState { this.open(); return tab; - }; + }); @action closeTab(tabId: TabId) { diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index d159a8df08..a240b50979 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -93,16 +93,14 @@ class NonInjectedInstallChart extends Component { this.props.installChartStore.loadValues(this.tabId); }; - @action - onChange = (values: string) => { + onChange = action((values: string) => { this.error = ""; this.save({ values }); - }; + }); - @action - onError = (error: Error | string) => { + onError = action((error: Error | string) => { this.error = error.toString(); - }; + }); onNamespaceChange = (opt: SelectOption) => { this.save({ namespace: opt.value }); diff --git a/src/renderer/components/dock/logs/list.tsx b/src/renderer/components/dock/logs/list.tsx index 358889f2f3..b9274087b4 100644 --- a/src/renderer/components/dock/logs/list.tsx +++ b/src/renderer/components/dock/logs/list.tsx @@ -103,8 +103,7 @@ export class LogList extends React.Component { * Checks if JumpToBottom button should be visible and sets its observable * @param props Scrolling props from virtual list core */ - @action - setButtonVisibility = (props: ListOnScrollProps) => { + setButtonVisibility = action((props: ListOnScrollProps) => { const offset = 100 * this.lineHeight; const { scrollHeight } = this.virtualListDiv.current; const { scrollOffset } = props; @@ -114,19 +113,18 @@ export class LogList extends React.Component { } else { this.isJumpButtonVisible = true; } - }; + }); /** * Checks if last log line considered visible to user, setting its observable * @param props Scrolling props from virtual list core */ - @action - setLastLineVisibility = (props: ListOnScrollProps) => { + setLastLineVisibility = action((props: ListOnScrollProps) => { const { scrollHeight, clientHeight } = this.virtualListDiv.current; const { scrollOffset } = props; this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight; - }; + }); /** * Check if user scrolled to top and new logs should be loaded diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index 270656e9f4..ce25d1815f 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -86,16 +86,14 @@ export class NonInjectedUpgradeChart extends React.Component { + onChange = action((value: string) => { this.error = ""; this.props.upgradeChartStore.values.setData(this.tabId, value); - }; + }); - @action - onError = (error: Error | string) => { + onError = action((error: Error | string) => { this.error = error.toString(); - }; + }); upgrade = async () => { if (this.error) return null; diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index d84f7df6de..a0c3551434 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -5,8 +5,7 @@ import "./icon.scss"; -import React, { ReactNode } from "react"; -import { findDOMNode } from "react-dom"; +import React, { createRef, ReactNode } from "react"; import { NavLink } from "react-router-dom"; import type { LocationDescriptor } from "history"; import { boundMethod, cssNames } from "../../utils"; @@ -31,6 +30,8 @@ export interface IconProps extends React.HTMLAttributes, TooltipDecoratorPr @withTooltip export class Icon extends React.PureComponent { + private readonly ref = createRef(); + static defaultProps: IconProps = { focusable: true, }; @@ -59,10 +60,7 @@ export class Icon extends React.PureComponent { // fallthrough case "Enter": { - // eslint-disable-next-line react/no-find-dom-node - const icon = findDOMNode(this) as HTMLElement; - - setTimeout(() => icon.click()); + this.ref.current?.click(); evt.preventDefault(); break; } @@ -123,16 +121,16 @@ export class Icon extends React.PureComponent { const { className, children } = iconProps; return ( - + {children} ); } if (href) { - return ; + return ; } - return ; + return ; } } diff --git a/src/renderer/components/input/search-input-url.tsx b/src/renderer/components/input/search-input-url.tsx index 74c57b0849..9ee4c8a5bb 100644 --- a/src/renderer/components/input/search-input-url.tsx +++ b/src/renderer/components/input/search-input-url.tsx @@ -24,10 +24,14 @@ export interface SearchInputUrlProps extends InputProps { export class SearchInputUrl extends React.Component { @observable inputVal = ""; // fix: use empty string on init to avoid react warnings - @disposeOnUnmount - updateInput = autorun(() => this.inputVal = searchUrlParam.get()); updateUrl = debounce((val: string) => searchUrlParam.set(val), 250); + componentDidMount(): void { + disposeOnUnmount(this, [ + autorun(() => this.inputVal = searchUrlParam.get()), + ]); + } + setValue = (value: string) => { this.inputVal = value; this.updateUrl(value); diff --git a/src/renderer/components/kube-object-details/kube-object-details.tsx b/src/renderer/components/kube-object-details/kube-object-details.tsx index 0301302714..68b7199ced 100644 --- a/src/renderer/components/kube-object-details/kube-object-details.tsx +++ b/src/renderer/components/kube-object-details/kube-object-details.tsx @@ -51,31 +51,34 @@ export class KubeObjectDetails extends React.Component { } } - @disposeOnUnmount - loader = reaction(() => [ - this.path, - this.object, // resource might be updated via watch-event or from already opened details - crdStore.items.length, // crd stores initialized after loading - ], async () => { - this.loadingError = ""; - const { path, object } = this; + componentDidMount(): void { + disposeOnUnmount(this, [ + reaction(() => [ + this.path, + this.object, // resource might be updated via watch-event or from already opened details + crdStore.items.length, // crd stores initialized after loading + ], async () => { + this.loadingError = ""; + const { path, object } = this; - if (!object) { - const store = apiManager.getStore(path); + if (!object) { + const store = apiManager.getStore(path); - if (store) { - this.isLoading = true; + if (store) { + this.isLoading = true; - try { - await store.loadFromPath(path); - } catch (err) { - this.loadingError = <>Resource loading has failed: {err.toString()}; - } finally { - this.isLoading = false; + try { + await store.loadFromPath(path); + } catch (err) { + this.loadingError = <>Resource loading has failed: {err.toString()}; + } finally { + this.isLoading = false; + } + } } - } - } - }); + }), + ]); + } render() { const { object, isLoading, loadingError } = this; diff --git a/src/renderer/components/layout/top-bar/top-bar-registration.d.ts b/src/renderer/components/layout/top-bar/top-bar-registration.ts similarity index 100% rename from src/renderer/components/layout/top-bar/top-bar-registration.d.ts rename to src/renderer/components/layout/top-bar/top-bar-registration.ts diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index 788d6d941e..b5f7a57364 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -249,8 +249,7 @@ export class MonacoEditor extends React.Component { this.editor?.focus(); } - @action - validate = (value = this.getValue()) => { + validate = action((value = this.getValue()) => { const validators: MonacoValidator[] = [ monacoValidators[this.props.language], // parsing syntax check ].filter(Boolean); @@ -262,7 +261,7 @@ export class MonacoEditor extends React.Component { this.props.onError?.(error); // emit error outside } } - }; + }); // avoid excessive validations during typing validateLazy = debounce(this.validate, 250); diff --git a/src/renderer/components/resizing-anchor/resizing-anchor.tsx b/src/renderer/components/resizing-anchor/resizing-anchor.tsx index 4842dff719..7629b6610f 100644 --- a/src/renderer/components/resizing-anchor/resizing-anchor.tsx +++ b/src/renderer/components/resizing-anchor/resizing-anchor.tsx @@ -194,8 +194,7 @@ export class ResizingAnchor extends React.PureComponent { document.removeEventListener("mouseup", this.onDragEnd); } - @action - onDragInit = (event: React.MouseEvent) => { + onDragInit = action((event: React.MouseEvent) => { const { onStart, onlyButtons } = this.props; if (typeof onlyButtons === "number" && onlyButtons !== event.buttons) { @@ -209,7 +208,7 @@ export class ResizingAnchor extends React.PureComponent { this.lastMouseEvent = undefined; onStart(); - }; + }); calculateDelta(from: Position, to: Position): number | false { const node = this.ref.current; @@ -285,8 +284,7 @@ export class ResizingAnchor extends React.PureComponent { } }, 100); - @action - onDragEnd = () => { + onDragEnd = action(() => { this.props.onEnd(); document.removeEventListener("mousemove", this.onDrag); document.removeEventListener("mouseup", this.onDragEnd); @@ -295,7 +293,7 @@ export class ResizingAnchor extends React.PureComponent { this.wasDragging = true; setTimeout(() => this.wasDragging = false, 200); - }; + }); render() { const { disabled, direction, placement, onDoubleClick } = this.props; diff --git a/yarn.lock b/yarn.lock index 26caa86d58..dd2ff2256c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,13 +12,6 @@ resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.5.3.tgz#42be6c0e8ba5ccd737e006ca600e7e319fe2a591" integrity sha512-iQ9gAPZFW5U6TNcFS99ffwYYsB9LNecTnvG73BaDc/zAD0qOWctY1imEACC1pLymmm/xaf/OUq9I9QenfkatTA== -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1", "@babel/code-frame@^7.10.4": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" @@ -155,11 +148,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - "@babel/helper-validator-identifier@^7.15.7": version "7.15.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" @@ -174,15 +162,6 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" @@ -484,18 +463,18 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint/eslintrc@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" + integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" + debug "^4.3.2" + espree "^9.2.0" globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" - js-yaml "^3.13.1" + js-yaml "^4.1.0" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -598,19 +577,19 @@ "@hapi/bourne" "2.x.x" "@hapi/hoek" "9.x.x" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== +"@humanwhocodes/config-array@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" + integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== dependencies: - "@humanwhocodes/object-schema" "^1.2.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" minimatch "^3.0.4" -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@iarna/cli@^1.2.0": version "1.2.0" @@ -2078,13 +2057,14 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.7.0.tgz#12d54709f8ea1da99a01d8a992cd0474ad0f0aa9" - integrity sha512-8RTGBpNn5a9M628wBPrCbJ+v3YTEOE2qeZb7TDkGKTDXSj36KGRg92SpFFaR/0S3rSXQxM0Og/kV9EyadsYSBg== +"@typescript-eslint/eslint-plugin@^5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.1.tgz#870195d0f2146b36d11fc71131b75aba52354c69" + integrity sha512-xN3CYqFlyE/qOcy978/L0xLR2HlcAGIyIK5sMOasxaaAPfQRj/MmMV6OC3I7NZO84oEUdWCOju34Z9W8E0pFDQ== dependencies: - "@typescript-eslint/experimental-utils" "5.7.0" - "@typescript-eslint/scope-manager" "5.7.0" + "@typescript-eslint/scope-manager" "5.10.1" + "@typescript-eslint/type-utils" "5.10.1" + "@typescript-eslint/utils" "5.10.1" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -2092,60 +2072,69 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.7.0.tgz#2b1633e6613c3238036156f70c32634843ad034f" - integrity sha512-u57eZ5FbEpzN5kSjmVrSesovWslH2ZyNPnaXQMXWgH57d5+EVHEt76W75vVuI9qKZ5BMDKNfRN+pxcPEjQjb2A== +"@typescript-eslint/parser@^5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.1.tgz#4ce9633cc33fc70bc13786cb793c1a76fe5ad6bd" + integrity sha512-GReo3tjNBwR5RnRO0K2wDIDN31cM3MmDtgyQ85oAxAmC5K3j/g85IjP+cDfcqDsDDBf1HNKQAD0WqOYL8jXqUA== dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.7.0" - "@typescript-eslint/types" "5.7.0" - "@typescript-eslint/typescript-estree" "5.7.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/parser@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.7.0.tgz#4dca6de463d86f02d252e681136a67888ea3b181" - integrity sha512-m/gWCCcS4jXw6vkrPQ1BjZ1vomP01PArgzvauBqzsoZ3urLbsRChexB8/YV8z9HwE3qlJM35FxfKZ1nfP/4x8g== - dependencies: - "@typescript-eslint/scope-manager" "5.7.0" - "@typescript-eslint/types" "5.7.0" - "@typescript-eslint/typescript-estree" "5.7.0" + "@typescript-eslint/scope-manager" "5.10.1" + "@typescript-eslint/types" "5.10.1" + "@typescript-eslint/typescript-estree" "5.10.1" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.7.0.tgz#70adf960e5a58994ad50438ba60d98ecadd79452" - integrity sha512-7mxR520DGq5F7sSSgM0HSSMJ+TFUymOeFRMfUfGFAVBv8BR+Jv1vHgAouYUvWRZeszVBJlLcc9fDdktxb5kmxA== +"@typescript-eslint/scope-manager@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.1.tgz#f0539c73804d2423506db2475352a4dec36cd809" + integrity sha512-Lyvi559Gvpn94k7+ElXNMEnXu/iundV5uFmCUNnftbFrUbAJ1WBoaGgkbOBm07jVZa682oaBU37ao/NGGX4ZDg== dependencies: - "@typescript-eslint/types" "5.7.0" - "@typescript-eslint/visitor-keys" "5.7.0" + "@typescript-eslint/types" "5.10.1" + "@typescript-eslint/visitor-keys" "5.10.1" -"@typescript-eslint/types@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.7.0.tgz#2d4cae0105ba7d08bffa69698197a762483ebcbe" - integrity sha512-5AeYIF5p2kAneIpnLFve8g50VyAjq7udM7ApZZ9JYjdPjkz0LvODfuSHIDUVnIuUoxafoWzpFyU7Sqbxgi79mA== - -"@typescript-eslint/typescript-estree@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.7.0.tgz#968fad899050ccce4f08a40cd5fabc0798525006" - integrity sha512-aO1Ql+izMrTnPj5aFFlEJkpD4jRqC4Gwhygu2oHK2wfVQpmOPbyDSveJ+r/NQo+PWV43M6uEAeLVbTi09dFLhg== +"@typescript-eslint/type-utils@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.1.tgz#5e526c00142585e40ab1503e83f1ff608c367405" + integrity sha512-AfVJkV8uck/UIoDqhu+ptEdBoQATON9GXnhOpPLzkQRJcSChkvD//qsz9JVffl2goxX+ybs5klvacE9vmrQyCw== dependencies: - "@typescript-eslint/types" "5.7.0" - "@typescript-eslint/visitor-keys" "5.7.0" + "@typescript-eslint/utils" "5.10.1" + debug "^4.3.2" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.1.tgz#dca9bd4cb8c067fc85304a31f38ec4766ba2d1ea" + integrity sha512-ZvxQ2QMy49bIIBpTqFiOenucqUyjTQ0WNLhBM6X1fh1NNlYAC6Kxsx8bRTY3jdYsYg44a0Z/uEgQkohbR0H87Q== + +"@typescript-eslint/typescript-estree@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.1.tgz#b268e67be0553f8790ba3fe87113282977adda15" + integrity sha512-PwIGnH7jIueXv4opcwEbVGDATjGPO1dx9RkUl5LlHDSe+FXxPwFL5W/qYd5/NHr7f6lo/vvTrAzd0KlQtRusJQ== + dependencies: + "@typescript-eslint/types" "5.10.1" + "@typescript-eslint/visitor-keys" "5.10.1" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.7.0.tgz#e05164239eb7cb8aa9fa06c516ede480ce260178" - integrity sha512-hdohahZ4lTFcglZSJ3DGdzxQHBSxsLVqHzkiOmKi7xVAWC4y2c1bIMKmPJSrA4aOEoRUPOKQ87Y/taC7yVHpFg== +"@typescript-eslint/utils@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.1.tgz#fa682a33af47080ba2c4368ee0ad2128213a1196" + integrity sha512-RRmlITiUbLuTRtn/gcPRi4202niF+q7ylFLCKu4c+O/PcpRvZ/nAUwQ2G00bZgpWkhrNLNnvhZLbDn8Ml0qsQw== dependencies: - "@typescript-eslint/types" "5.7.0" + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.10.1" + "@typescript-eslint/types" "5.10.1" + "@typescript-eslint/typescript-estree" "5.10.1" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.1.tgz#29102de692f59d7d34ecc457ed59ab5fc558010b" + integrity sha512-NjQ0Xinhy9IL979tpoTRuLKxMc0zJC7QVSdeerXs2/QvOy2yRkzX5dRb10X5woNUdJgU8G3nYRDlI33sq1K4YQ== + dependencies: + "@typescript-eslint/types" "5.10.1" eslint-visitor-keys "^3.0.0" "@webassemblyjs/ast@1.9.0": @@ -2373,7 +2362,7 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.0.0, acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -2383,6 +2372,11 @@ acorn@^8.2.4, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== +acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + agent-base@4, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -2439,16 +2433,6 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.4, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: - version "8.6.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" - integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -2463,7 +2447,7 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@^3.0.0, ansi-colors@^3.2.1: +ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== @@ -2782,11 +2766,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -4442,7 +4421,7 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -5130,13 +5109,6 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" - integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== - dependencies: - ansi-colors "^3.2.1" - entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -5455,59 +5427,47 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" -eslint-import-resolver-typescript@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a" - integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ== - dependencies: - debug "^4.3.1" - glob "^7.1.7" - is-glob "^4.0.1" - resolve "^1.20.0" - tsconfig-paths "^3.9.0" - -eslint-module-utils@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c" - integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ== +eslint-module-utils@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz#1d0aa455dcf41052339b63cada8ab5fd57577129" + integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg== dependencies: debug "^3.2.7" find-up "^2.1.0" - pkg-dir "^2.0.0" eslint-plugin-header@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6" integrity sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg== -eslint-plugin-import@^2.25.3: - version "2.25.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" - integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg== +eslint-plugin-import@^2.25.4: + version "2.25.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" + integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== dependencies: array-includes "^3.1.4" array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.1" + eslint-module-utils "^2.7.2" has "^1.0.3" is-core-module "^2.8.0" is-glob "^4.0.3" minimatch "^3.0.4" object.values "^1.1.5" resolve "^1.20.0" - tsconfig-paths "^3.11.0" + tsconfig-paths "^3.12.0" eslint-plugin-react-hooks@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== -eslint-plugin-react@^7.27.1: - version "7.27.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz#469202442506616f77a854d91babaae1ec174b45" - integrity sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA== +eslint-plugin-react@^7.28.0: + version "7.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" + integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== dependencies: array-includes "^3.1.4" array.prototype.flatmap "^1.2.5" @@ -5524,10 +5484,10 @@ eslint-plugin-react@^7.27.1: semver "^6.3.0" string.prototype.matchall "^4.0.6" -eslint-plugin-unused-imports@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-1.1.5.tgz#a2b992ef0faf6c6c75c3815cc47bde76739513c2" - integrity sha512-TeV8l8zkLQrq9LBeYFCQmYVIXMjfHgdRQLw7dEZp4ZB3PeR10Y5Uif11heCsHRmhdRIYMoewr1d9ouUHLbLHew== +eslint-plugin-unused-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz#d8db8c4d0cfa0637a8b51ce3fd7d1b6bc3f08520" + integrity sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A== dependencies: eslint-rule-composer "^0.3.0" @@ -5552,12 +5512,13 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" + integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== dependencies: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -5566,11 +5527,6 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" @@ -5581,60 +5537,60 @@ eslint-visitor-keys@^3.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== -eslint@^7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" + integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== + +eslint@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.7.0.tgz#22e036842ee5b7cf87b03fe237731675b4d3633c" + integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" + "@eslint/eslintrc" "^1.0.5" + "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + eslint-scope "^7.1.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.2.0" + espree "^9.3.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" + glob-parent "^6.0.1" globals "^13.6.0" - ignore "^4.0.6" + ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" + regexpp "^3.2.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.2.0, espree@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" + integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== dependencies: - acorn "^7.4.0" + acorn "^8.7.0" acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + eslint-visitor-keys "^3.1.0" esprima@1.2.2: version "1.2.2" @@ -6510,7 +6466,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.2: +glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -6522,7 +6478,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -7196,6 +7152,11 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -8456,11 +8417,6 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - json-schema-typed@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9" @@ -9066,11 +9022,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - lodash.union@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" @@ -10891,13 +10842,6 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -11205,7 +11149,7 @@ progress-bar-webpack-plugin@^2.1.0: chalk "^3.0.0" progress "^2.0.3" -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -11851,11 +11795,6 @@ regexp.prototype.flags@^1.3.1: call-bind "^1.0.2" define-properties "^1.1.3" -regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -11968,11 +11907,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -12576,15 +12510,6 @@ slice-ansi@^1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -12994,15 +12919,6 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string.prototype.matchall@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" @@ -13245,18 +13161,6 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^6.0.9: - version "6.7.2" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0" - integrity sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g== - dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tailwindcss@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.7.tgz#15936881f042a7eb8d6f2b6a454bac9f51181bbd" @@ -13665,7 +13569,7 @@ ts-node@^10.4.0: make-error "^1.1.1" yn "3.1.1" -tsconfig-paths@^3.11.0: +tsconfig-paths@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== From a9abcf99e4707c1def85f04ebfbf9d06e8773354 Mon Sep 17 00:00:00 2001 From: Dmitriy Noa Date: Mon, 31 Jan 2022 11:03:49 +0100 Subject: [PATCH 07/13] Fix failing lint in master (#4775) Signed-off-by: DmitriyNoa --- .../{status-bar-registration.d.ts => status-bar-registration.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/renderer/components/cluster-manager/{status-bar-registration.d.ts => status-bar-registration.ts} (100%) diff --git a/src/renderer/components/cluster-manager/status-bar-registration.d.ts b/src/renderer/components/cluster-manager/status-bar-registration.ts similarity index 100% rename from src/renderer/components/cluster-manager/status-bar-registration.d.ts rename to src/renderer/components/cluster-manager/status-bar-registration.ts From 65669f6a6473235bac1267d31358c8a751110dd2 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 31 Jan 2022 17:04:15 +0300 Subject: [PATCH 08/13] Fix: increase catalog entity name col width (#4776) * Increase .entityName col width Signed-off-by: Alex Andreev * Adjust catalog menu paddings Signed-off-by: Alex Andreev * Fix table head border color Signed-off-by: Alex Andreev --- src/renderer/components/+catalog/catalog-tree.module.scss | 4 ++++ src/renderer/components/+catalog/catalog.module.scss | 1 + src/renderer/components/table/table-head.scss | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/+catalog/catalog-tree.module.scss b/src/renderer/components/+catalog/catalog-tree.module.scss index 284b04083f..f190d04417 100644 --- a/src/renderer/components/+catalog/catalog-tree.module.scss +++ b/src/renderer/components/+catalog/catalog-tree.module.scss @@ -22,6 +22,8 @@ .content { min-height: 26px; + line-height: 1.3; + padding: 2px var(--padding) 2px 0; &:hover { background-color: var(--sidebarItemHoverBackground); @@ -39,6 +41,8 @@ .iconContainer { margin-left: 28px; + margin-top: 2px; + align-self: flex-start; } } diff --git a/src/renderer/components/+catalog/catalog.module.scss b/src/renderer/components/+catalog/catalog.module.scss index 25d564eba8..5948d64a62 100644 --- a/src/renderer/components/+catalog/catalog.module.scss +++ b/src/renderer/components/+catalog/catalog.module.scss @@ -20,6 +20,7 @@ padding: 0 var(--padding); padding-bottom: 0; padding-right: 24px; // + reserved space for .pinIcon + flex-grow: 2.5!important; > span { overflow: hidden; diff --git a/src/renderer/components/table/table-head.scss b/src/renderer/components/table/table-head.scss index 03e64b4add..381fb68b9c 100644 --- a/src/renderer/components/table/table-head.scss +++ b/src/renderer/components/table/table-head.scss @@ -4,7 +4,7 @@ */ .TableHead { - $border: 1px solid var(--layoutBackground); + $border: 1px solid var(--tableHeaderBorderColor); background-color: var(--tableHeaderBackground); border-bottom-width: var(--tableHeaderBorderWidth); From 0ce4e3d793cb788979a722b6a6d99903abc41a66 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 31 Jan 2022 09:49:36 -0500 Subject: [PATCH 09/13] Full dependency inversion of and all current tab kinds (#4757) Co-authored-by: Mikko Aspiala Co-authored-by: Sebastian Malton Co-authored-by: Janne Savolainen --- integration/__tests__/cluster-pages.tests.ts | 6 +- package.json | 1 + .../fs/read-dir.injectable.ts} | 8 +- ....injectable.ts => read-file.injectable.ts} | 9 +- .../read-json-file.injectable.ts | 8 +- .../fs/read-json-file/read-json-file.ts | 16 - src/common/fs/write-json-file.injectable.ts | 38 ++ .../fs/write-json-file/write-json-file.ts | 31 - src/common/utils/__tests__/bind.test.ts | 19 - src/common/utils/index.ts | 8 +- src/common/utils/wait-for-path.ts | 55 ++ src/common/vars/is-linux.injectable.ts | 13 + src/common/vars/is-windows.injectable.ts | 13 + ...ct-for-extension-api-with-modifications.ts | 47 ++ ...gacy-global-singleton-for-extension-api.ts | 43 -- src/extensions/renderer-api/components.ts | 41 +- src/main/getDiForUnitTesting.ts | 4 +- .../+apps-helm-charts/helm-chart-details.tsx | 2 +- .../+apps-helm-charts/helm-charts.tsx | 2 +- .../release-details/release-details.tsx | 2 +- .../+apps-releases/release-menu.tsx | 2 +- .../components/+apps-releases/releases.tsx | 15 +- .../get-category-columns.injectable.ts | 8 +- ...tall-from-select-file-dialog.injectable.ts | 8 +- .../+network-port-forwards/port-forwards.tsx | 2 +- .../internal-commands.injectable.tsx | 5 +- .../dock/__test__/dock-tabs.test.tsx | 4 +- .../create-install-chart-tab.injectable.ts | 19 - .../create-install-chart-tab.ts | 44 -- .../create-resource.store.ts | 70 --- .../create-resource-tab.injectable.ts | 17 - .../create-resource-tab.ts | 18 - .../components/dock/create-resource.scss | 7 - .../components/dock/create-resource.tsx | 184 ------ ...ear-create-resource-tab-data.injectable.ts | 21 + .../create-resource-tab.injectable.ts | 24 + .../create-resource-templates.injectable.ts | 34 ++ .../create-resource/has-correct-extension.ts | 18 + .../lens-templates.injectable.ts | 48 ++ .../store.injectable.ts} | 10 +- .../components/dock/create-resource/store.ts | 19 + .../user-templates.injectable.ts | 102 ++++ .../components/dock/create-resource/view.tsx | 154 +++++ .../create-terminal-tab.injectable.ts | 17 - .../create-terminal-tab.ts | 22 - .../create-upgrade-chart-tab.injectable.ts | 19 - .../create-upgrade-chart-tab.ts | 41 -- .../dock/dock-store/dock-store.injectable.ts | 18 - .../create-dock-tab-store.injectable.ts | 2 - .../dock/dock-tab-store/dock-tab.store.ts | 43 +- src/renderer/components/dock/dock-tab.tsx | 4 +- src/renderer/components/dock/dock-tabs.tsx | 6 +- src/renderer/components/dock/dock.tsx | 25 +- .../dock/dock/close-dock-tab.injectable.ts | 21 + .../dock/dock/create-dock-tab.injectable.ts | 20 + .../dock-storage.injectable.ts | 4 +- .../dock/dock/rename-tab.injectable.ts | 22 + .../dock/dock/select-dock-tab.injectable.ts | 21 + .../components/dock/dock/store.injectable.ts | 35 ++ .../dock.store.ts => dock/store.ts} | 18 +- .../edit-resource-store.injectable.ts | 20 - .../edit-resource.store.ts | 109 ---- .../edit-resource-tab.injectable.ts | 19 - .../edit-resource-tab/edit-resource-tab.ts | 41 -- .../components/dock/edit-resource.scss | 7 - ...clear-edit-resource-tab-data.injectable.ts | 21 + .../edit-resource-tab.injectable.ts | 56 ++ .../dock/edit-resource/store.injectable.ts | 17 + .../components/dock/edit-resource/store.ts | 57 ++ .../view.tsx} | 64 +- src/renderer/components/dock/editor-panel.tsx | 4 +- src/renderer/components/dock/info-panel.tsx | 8 +- ...clear-install-chart-tab-data.injectable.ts | 21 + .../create-install-chart-tab.injectable.ts | 55 ++ .../{ => install-chart}/install-chart.scss | 0 .../store.injectable.ts} | 10 +- .../store.ts} | 30 +- .../view.tsx} | 51 +- .../__test__/log-resource-selector.test.tsx | 107 +++- .../dock/logs/__test__/log-search.test.tsx | 147 +++++ .../dock/logs/__test__/log-tab.store.test.ts | 146 ----- .../dock/logs/are-logs-present.injectable.ts | 19 + .../logs/clear-log-tab-data.injectable.ts | 21 + .../components/dock/logs/controls.tsx | 27 +- .../dock/logs/create-logs-tab.injectable.ts | 48 ++ .../logs/create-pod-logs-tab.injectable.ts | 31 + .../create-workload-logs-tab.injectable.ts | 48 ++ .../components/dock/logs/dock-tab.tsx | 101 ---- .../dock/logs/get-log-tab-data.injectable.ts | 8 +- .../get-logs-without-timestamps.injectable.ts | 8 +- .../dock/logs/get-logs.injectable.ts | 7 +- .../get-timestamp-split-logs.injectable.ts | 8 +- .../logs/is-logs-tab-data-valid.injectable.ts | 19 + src/renderer/components/dock/logs/list.tsx | 13 +- .../dock/logs/load-logs.injectable.ts | 14 +- .../dock/logs/log-tab-data.validator.ts | 38 ++ .../dock/logs/logs-view-model.injectable.ts | 17 +- .../components/dock/logs/logs-view-model.ts | 51 +- .../dock/logs/reload-logs.injectable.ts | 18 +- .../dock/logs/resource-selector.tsx | 83 +-- src/renderer/components/dock/logs/search.tsx | 40 +- .../dock/logs/set-log-tab-data.injectable.ts | 8 +- .../dock/logs/stop-loading-logs.injectable.ts | 7 +- src/renderer/components/dock/logs/store.ts | 99 ++-- .../dock/logs/tab-store.injectable.ts | 4 +- .../components/dock/logs/tab-store.ts | 81 +++ .../components/dock/logs/tab.store.ts | 139 ----- src/renderer/components/dock/logs/view.tsx | 101 ++++ .../dock/terminal-store/terminal.store.ts | 152 ----- .../components/dock/terminal-tab.scss | 8 - .../components/dock/terminal-window.tsx | 74 --- .../clear-terminal-tab-data.injectable.ts | 21 + .../create-terminal-tab.injectable.ts | 24 + .../terminal/create-terminal.injectable.ts | 13 +- .../dock-tab.tsx} | 34 +- .../terminal/get-terminal-api.injectable.ts | 20 + .../dock/terminal/send-command.injectable.ts | 91 +++ .../store.injectable.ts} | 8 +- .../components/dock/terminal/store.ts | 87 +++ .../dock/{ => terminal}/terminal-window.scss | 0 .../components/dock/terminal/terminal.ts | 18 +- .../components/dock/terminal/view.tsx | 74 +++ .../upgrade-chart.store.ts | 128 ---- ...clear-upgrade-chart-tab-data.injectable.ts | 21 + .../create-upgrade-chart-tab.injectable.ts | 56 ++ .../store.injectable.ts} | 16 +- .../components/dock/upgrade-chart/store.ts | 51 ++ .../{ => upgrade-chart}/upgrade-chart.scss | 0 .../view.tsx} | 95 +-- .../components/input/search-input.tsx | 9 +- .../components/item-object-list/content.tsx | 271 +++++++++ .../components/item-object-list/filters.tsx | 29 + .../components/item-object-list/header.tsx | 105 ++++ .../components/item-object-list/index.tsx | 2 +- .../item-object-list/item-list-layout.tsx | 549 ------------------ .../item-object-list/list-layout.tsx | 313 ++++++++++ ...ge.injectable.ts => storage.injectable.ts} | 2 +- .../kube-object-list-layout.tsx | 6 +- .../kube-object-menu.test.tsx | 10 +- .../kube-object-menu/kube-object-menu.tsx | 8 +- .../layout/top-bar/top-bar-win-linux.test.tsx | 7 +- .../layout/top-bar/top-bar.test.tsx | 53 +- .../components/layout/top-bar/top-bar.tsx | 13 +- src/renderer/getDiForUnitTesting.tsx | 14 +- src/renderer/kube-watch-api/kube-watch-api.ts | 18 +- .../subscribe-stores.injectable.ts | 13 + .../search-store/search-store.injectable.ts | 13 + .../search-store/search-store.test.ts | 3 +- .../create-storage.injectable.ts | 4 +- src/renderer/utils/save-file.injectable.ts | 13 + webpack.extensions.ts | 4 + yarn.lock | 5 + 152 files changed, 3423 insertions(+), 2617 deletions(-) rename src/{renderer/components/dock/logs/update-tab-name.injectable.ts => common/fs/read-dir.injectable.ts} (54%) rename src/common/fs/{write-json-file/write-json-file.injectable.ts => read-file.injectable.ts} (50%) rename src/common/fs/{read-json-file => }/read-json-file.injectable.ts (66%) delete mode 100644 src/common/fs/read-json-file/read-json-file.ts create mode 100644 src/common/fs/write-json-file.injectable.ts delete mode 100644 src/common/fs/write-json-file/write-json-file.ts delete mode 100644 src/common/utils/__tests__/bind.test.ts create mode 100644 src/common/utils/wait-for-path.ts create mode 100644 src/common/vars/is-linux.injectable.ts create mode 100644 src/common/vars/is-windows.injectable.ts create mode 100644 src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.ts delete mode 100644 src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api.ts delete mode 100644 src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts delete mode 100644 src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts delete mode 100644 src/renderer/components/dock/create-resource-store/create-resource.store.ts delete mode 100644 src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts delete mode 100644 src/renderer/components/dock/create-resource-tab/create-resource-tab.ts delete mode 100644 src/renderer/components/dock/create-resource.scss delete mode 100644 src/renderer/components/dock/create-resource.tsx create mode 100644 src/renderer/components/dock/create-resource/clear-create-resource-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/create-resource/create-resource-tab.injectable.ts create mode 100644 src/renderer/components/dock/create-resource/create-resource-templates.injectable.ts create mode 100644 src/renderer/components/dock/create-resource/has-correct-extension.ts create mode 100644 src/renderer/components/dock/create-resource/lens-templates.injectable.ts rename src/renderer/components/dock/{create-resource-store/create-resource-store.injectable.ts => create-resource/store.injectable.ts} (55%) create mode 100644 src/renderer/components/dock/create-resource/store.ts create mode 100644 src/renderer/components/dock/create-resource/user-templates.injectable.ts create mode 100644 src/renderer/components/dock/create-resource/view.tsx delete mode 100644 src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts delete mode 100644 src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts delete mode 100644 src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts delete mode 100644 src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts delete mode 100644 src/renderer/components/dock/dock-store/dock-store.injectable.ts create mode 100644 src/renderer/components/dock/dock/close-dock-tab.injectable.ts create mode 100644 src/renderer/components/dock/dock/create-dock-tab.injectable.ts rename src/renderer/components/dock/{dock-store/dock-storage => dock}/dock-storage.injectable.ts (81%) create mode 100644 src/renderer/components/dock/dock/rename-tab.injectable.ts create mode 100644 src/renderer/components/dock/dock/select-dock-tab.injectable.ts create mode 100644 src/renderer/components/dock/dock/store.injectable.ts rename src/renderer/components/dock/{dock-store/dock.store.ts => dock/store.ts} (93%) delete mode 100644 src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts delete mode 100644 src/renderer/components/dock/edit-resource-store/edit-resource.store.ts delete mode 100644 src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts delete mode 100644 src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts delete mode 100644 src/renderer/components/dock/edit-resource.scss create mode 100644 src/renderer/components/dock/edit-resource/clear-edit-resource-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts create mode 100644 src/renderer/components/dock/edit-resource/store.injectable.ts create mode 100644 src/renderer/components/dock/edit-resource/store.ts rename src/renderer/components/dock/{edit-resource.tsx => edit-resource/view.tsx} (67%) create mode 100644 src/renderer/components/dock/install-chart/clear-install-chart-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts rename src/renderer/components/dock/{ => install-chart}/install-chart.scss (100%) rename src/renderer/components/dock/{install-chart-store/install-chart-store.injectable.ts => install-chart/store.injectable.ts} (73%) rename src/renderer/components/dock/{install-chart-store/install-chart.store.ts => install-chart/store.ts} (72%) rename src/renderer/components/dock/{install-chart.tsx => install-chart/view.tsx} (79%) create mode 100644 src/renderer/components/dock/logs/__test__/log-search.test.tsx delete mode 100644 src/renderer/components/dock/logs/__test__/log-tab.store.test.ts create mode 100644 src/renderer/components/dock/logs/are-logs-present.injectable.ts create mode 100644 src/renderer/components/dock/logs/clear-log-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/logs/create-logs-tab.injectable.ts create mode 100644 src/renderer/components/dock/logs/create-pod-logs-tab.injectable.ts create mode 100644 src/renderer/components/dock/logs/create-workload-logs-tab.injectable.ts delete mode 100644 src/renderer/components/dock/logs/dock-tab.tsx create mode 100644 src/renderer/components/dock/logs/is-logs-tab-data-valid.injectable.ts create mode 100644 src/renderer/components/dock/logs/log-tab-data.validator.ts create mode 100644 src/renderer/components/dock/logs/tab-store.ts delete mode 100644 src/renderer/components/dock/logs/tab.store.ts create mode 100644 src/renderer/components/dock/logs/view.tsx delete mode 100644 src/renderer/components/dock/terminal-store/terminal.store.ts delete mode 100644 src/renderer/components/dock/terminal-tab.scss delete mode 100644 src/renderer/components/dock/terminal-window.tsx create mode 100644 src/renderer/components/dock/terminal/clear-terminal-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/terminal/create-terminal-tab.injectable.ts rename src/renderer/components/dock/{terminal-tab.tsx => terminal/dock-tab.tsx} (70%) create mode 100644 src/renderer/components/dock/terminal/get-terminal-api.injectable.ts create mode 100644 src/renderer/components/dock/terminal/send-command.injectable.ts rename src/renderer/components/dock/{terminal-store/terminal-store.injectable.ts => terminal/store.injectable.ts} (52%) create mode 100644 src/renderer/components/dock/terminal/store.ts rename src/renderer/components/dock/{ => terminal}/terminal-window.scss (100%) create mode 100644 src/renderer/components/dock/terminal/view.tsx delete mode 100644 src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts create mode 100644 src/renderer/components/dock/upgrade-chart/clear-upgrade-chart-tab-data.injectable.ts create mode 100644 src/renderer/components/dock/upgrade-chart/create-upgrade-chart-tab.injectable.ts rename src/renderer/components/dock/{upgrade-chart-store/upgrade-chart-store.injectable.ts => upgrade-chart/store.injectable.ts} (55%) create mode 100644 src/renderer/components/dock/upgrade-chart/store.ts rename src/renderer/components/dock/{ => upgrade-chart}/upgrade-chart.scss (100%) rename src/renderer/components/dock/{upgrade-chart.tsx => upgrade-chart/view.tsx} (64%) create mode 100644 src/renderer/components/item-object-list/content.tsx create mode 100644 src/renderer/components/item-object-list/filters.tsx create mode 100644 src/renderer/components/item-object-list/header.tsx delete mode 100644 src/renderer/components/item-object-list/item-list-layout.tsx create mode 100644 src/renderer/components/item-object-list/list-layout.tsx rename src/renderer/components/item-object-list/{item-list-layout-storage/item-list-layout-storage.injectable.ts => storage.injectable.ts} (85%) create mode 100644 src/renderer/kube-watch-api/subscribe-stores.injectable.ts create mode 100644 src/renderer/search-store/search-store.injectable.ts create mode 100644 src/renderer/utils/save-file.injectable.ts diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 6dc89020e3..375321aaaf 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -354,8 +354,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { } }, 10*60*1000); - // TODO: Make re-rendering of KubeObjectListLayout not cause namespaceSelector to be closed - xit("show logs and highlight the log search entries", async () => { + it("show logs and highlight the log search entries", async () => { await frame.click(`a[href="/workloads"]`); await frame.click(`a[href="/pods"]`); @@ -400,8 +399,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { await frame.waitForSelector("div.TableCell >> text='kube-system'"); }, 10*60*1000); - // TODO: Make re-rendering of KubeObjectListLayout not cause namespaceSelector to be closed - xit(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => { + it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => { await frame.click('a[href="/namespaces"]'); await frame.click("button.add-button"); await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'"); diff --git a/package.json b/package.json index 829b7d7df0..ebb8d290e6 100644 --- a/package.json +++ b/package.json @@ -349,6 +349,7 @@ "fork-ts-checker-webpack-plugin": "^5.2.1", "hoist-non-react-statics": "^3.3.2", "html-webpack-plugin": "^4.5.2", + "ignore-loader": "^0.1.2", "include-media": "^1.4.9", "jest": "26.6.3", "jest-canvas-mock": "^2.3.1", diff --git a/src/renderer/components/dock/logs/update-tab-name.injectable.ts b/src/common/fs/read-dir.injectable.ts similarity index 54% rename from src/renderer/components/dock/logs/update-tab-name.injectable.ts rename to src/common/fs/read-dir.injectable.ts index 24074e4cb6..501ecc1cdd 100644 --- a/src/renderer/components/dock/logs/update-tab-name.injectable.ts +++ b/src/common/fs/read-dir.injectable.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import logTabStoreInjectable from "./tab-store.injectable"; +import fsInjectable from "./fs.injectable"; -const updateTabNameInjectable = getInjectable({ - instantiate: (di) => di.inject(logTabStoreInjectable).updateTabName, +const readDirInjectable = getInjectable({ + instantiate: (di) => di.inject(fsInjectable).readdir, lifecycle: lifecycleEnum.singleton, }); -export default updateTabNameInjectable; +export default readDirInjectable; diff --git a/src/common/fs/write-json-file/write-json-file.injectable.ts b/src/common/fs/read-file.injectable.ts similarity index 50% rename from src/common/fs/write-json-file/write-json-file.injectable.ts rename to src/common/fs/read-file.injectable.ts index 5ff6cbaec3..5e2871a03d 100644 --- a/src/common/fs/write-json-file/write-json-file.injectable.ts +++ b/src/common/fs/read-file.injectable.ts @@ -3,12 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { writeJsonFile } from "./write-json-file"; -import fsInjectable from "../fs.injectable"; +import fsInjectable from "./fs.injectable"; -const writeJsonFileInjectable = getInjectable({ - instantiate: (di) => writeJsonFile({ fs: di.inject(fsInjectable) }), +const readFileInjectable = getInjectable({ + instantiate: (di) => di.inject(fsInjectable).readFile, lifecycle: lifecycleEnum.singleton, }); -export default writeJsonFileInjectable; +export default readFileInjectable; diff --git a/src/common/fs/read-json-file/read-json-file.injectable.ts b/src/common/fs/read-json-file.injectable.ts similarity index 66% rename from src/common/fs/read-json-file/read-json-file.injectable.ts rename to src/common/fs/read-json-file.injectable.ts index b94c904885..b944decadc 100644 --- a/src/common/fs/read-json-file/read-json-file.injectable.ts +++ b/src/common/fs/read-json-file.injectable.ts @@ -2,15 +2,11 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { readJsonFile } from "./read-json-file"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import fsInjectable from "../fs.injectable"; +import fsInjectable from "./fs.injectable"; const readJsonFileInjectable = getInjectable({ - instantiate: (di) => readJsonFile({ - fs: di.inject(fsInjectable), - }), - + instantiate: (di) => di.inject(fsInjectable).readJson, lifecycle: lifecycleEnum.singleton, }); diff --git a/src/common/fs/read-json-file/read-json-file.ts b/src/common/fs/read-json-file/read-json-file.ts deleted file mode 100644 index cb22e78ad3..0000000000 --- a/src/common/fs/read-json-file/read-json-file.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { JsonObject } from "type-fest"; - -interface Dependencies { - fs: { - readJson: (filePath: string) => Promise; - }; -} - -export const readJsonFile = - ({ fs }: Dependencies) => - (filePath: string) => - fs.readJson(filePath); diff --git a/src/common/fs/write-json-file.injectable.ts b/src/common/fs/write-json-file.injectable.ts new file mode 100644 index 0000000000..7bef449444 --- /dev/null +++ b/src/common/fs/write-json-file.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { EnsureOptions, WriteOptions } from "fs-extra"; +import path from "path"; +import type { JsonValue } from "type-fest"; +import fsInjectable from "./fs.injectable"; + +interface Dependencies { + writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise; + ensureDir: (dir: string, options?: EnsureOptions | number) => Promise; +} + +const writeJsonFile = ({ writeJson, ensureDir }: Dependencies) => async (filePath: string, content: JsonValue) => { + await ensureDir(path.dirname(filePath), { mode: 0o755 }); + + await writeJson(filePath, content, { + encoding: "utf-8", + spaces: 2, + }); +}; + +const writeJsonFileInjectable = getInjectable({ + instantiate: (di) => { + const { writeJson, ensureDir } = di.inject(fsInjectable); + + return writeJsonFile({ + writeJson, + ensureDir, + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default writeJsonFileInjectable; diff --git a/src/common/fs/write-json-file/write-json-file.ts b/src/common/fs/write-json-file/write-json-file.ts deleted file mode 100644 index 8db664593e..0000000000 --- a/src/common/fs/write-json-file/write-json-file.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import path from "path"; -import type { JsonObject } from "type-fest"; - -interface Dependencies { - fs: { - ensureDir: ( - directoryName: string, - options: { mode: number } - ) => Promise; - - writeJson: ( - filePath: string, - contentObject: JsonObject, - options: { spaces: number } - ) => Promise; - }; -} - -export const writeJsonFile = - ({ fs }: Dependencies) => - async (filePath: string, contentObject: JsonObject) => { - const directoryName = path.dirname(filePath); - - await fs.ensureDir(directoryName, { mode: 0o755 }); - - await fs.writeJson(filePath, contentObject, { spaces: 2 }); - }; diff --git a/src/common/utils/__tests__/bind.test.ts b/src/common/utils/__tests__/bind.test.ts deleted file mode 100644 index 77850a88d9..0000000000 --- a/src/common/utils/__tests__/bind.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { bind } from "../index"; - -describe("bind", () => { - it("should work correctly", () => { - function foobar(bound: number, nonBound: number): number { - expect(typeof bound).toBe("number"); - expect(typeof nonBound).toBe("number"); - - return bound + nonBound; - } - const foobarBound = bind(foobar, null, 5); - - expect(foobarBound(10)).toBe(15); - }); -}); diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 1400608aab..d349bd3b14 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -10,13 +10,6 @@ export function noop(...args: T): void { return void args; } -/** - * A typecorrect version of .bind() - */ -export function bind(fn: (...args: [...BoundArgs, ...NonBoundArgs]) => ReturnType, thisArg: any, ...boundArgs: BoundArgs): (...args: NonBoundArgs) => ReturnType { - return fn.bind(thisArg, ...boundArgs); -} - export * from "./app-version"; export * from "./autobind"; export * from "./camelCase"; @@ -49,6 +42,7 @@ export * from "./toggle-set"; export * from "./toJS"; export * from "./type-narrowing"; export * from "./types"; +export * from "./wait-for-path"; import * as iter from "./iter"; import * as array from "./array"; diff --git a/src/common/utils/wait-for-path.ts b/src/common/utils/wait-for-path.ts new file mode 100644 index 0000000000..f5a068075b --- /dev/null +++ b/src/common/utils/wait-for-path.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { FSWatcher } from "chokidar"; +import path from "path"; + +/** + * Wait for `filePath` and all parent directories to exist. + * @param pathname The file path to wait until it exists + * + * NOTE: There is technically a race condition in this function of the form + * "time-of-check to time-of-use" because we have to wait for each parent + * directory to exist first. + */ +export async function waitForPath(pathname: string): Promise { + const dirOfPath = path.dirname(pathname); + + if (dirOfPath === pathname) { + // The root of this filesystem, assume it exists + return; + } else { + await waitForPath(dirOfPath); + } + + return new Promise((resolve, reject) => { + const watcher = new FSWatcher({ + depth: 0, + disableGlobbing: true, + }); + const onAddOrAddDir = (filePath: string) => { + if (filePath === pathname) { + watcher.unwatch(dirOfPath); + watcher + .close() + .then(() => resolve()) + .catch(reject); + } + }; + const onError = (error: any) => { + watcher.unwatch(dirOfPath); + watcher + .close() + .then(() => reject(error)) + .catch(() => reject(error)); + }; + + watcher + .on("add", onAddOrAddDir) + .on("addDir", onAddOrAddDir) + .on("error", onError) + .add(dirOfPath); + }); +} diff --git a/src/common/vars/is-linux.injectable.ts b/src/common/vars/is-linux.injectable.ts new file mode 100644 index 0000000000..a603f01951 --- /dev/null +++ b/src/common/vars/is-linux.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { isLinux } from "../vars"; + +const isLinuxInjectable = getInjectable({ + instantiate: () => isLinux, + lifecycle: lifecycleEnum.singleton, +}); + +export default isLinuxInjectable; diff --git a/src/common/vars/is-windows.injectable.ts b/src/common/vars/is-windows.injectable.ts new file mode 100644 index 0000000000..76a08a4af2 --- /dev/null +++ b/src/common/vars/is-windows.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { isWindows } from "../vars"; + +const isWindowsInjectable = getInjectable({ + instantiate: () => isWindows, + lifecycle: lifecycleEnum.singleton, +}); + +export default isWindowsInjectable; diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.ts new file mode 100644 index 0000000000..8961ea8424 --- /dev/null +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { Injectable } from "@ogre-tools/injectable"; +import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; + +type TentativeTuple = T extends object ? [T] : [undefined?]; + +type MapInjectables = { + [Key in keyof T]: T[Key] extends () => infer Res ? Res : never; +}; + +export const asLegacyGlobalObjectForExtensionApiWithModifications = < + TInjectable extends Injectable, + TInstantiationParameter, + OtherFields extends Record any>, +>( + injectableKey: TInjectable, + otherFields: OtherFields, + ...instantiationParameter: TentativeTuple + ) => + new Proxy( + {}, + { + get(target, propertyName) { + if (propertyName === "$$typeof") { + return undefined; + } + + const instance: any = getLegacyGlobalDiForExtensionApi().inject( + injectableKey, + ...instantiationParameter, + ); + + const propertyValue = instance[propertyName] ?? otherFields[propertyName as any](); + + if (typeof propertyValue === "function") { + return function (...args: any[]) { + return propertyValue.apply(instance, args); + }; + } + + return propertyValue; + }, + }, + ) as ReturnType & MapInjectables; diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api.ts deleted file mode 100644 index 4e77961ca8..0000000000 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { Injectable } from "@ogre-tools/injectable"; -import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; - -type TentativeTuple = T extends object ? [T] : [undefined?]; - -export const asLegacyGlobalSingletonForExtensionApi = < - TClass extends abstract new (...args: any[]) => any, - TInjectable extends Injectable, - TInstantiationParameter, ->( - Class: TClass, - injectableKey: TInjectable, - ...instantiationParameter: TentativeTuple - ) => - new Proxy(Class, { - construct: () => { - throw new Error("A legacy singleton class must be created by createInstance()"); - }, - - get: (target: any, propertyName) => { - if (propertyName === "getInstance" || propertyName === "createInstance") { - return () => - getLegacyGlobalDiForExtensionApi().inject( - injectableKey, - ...instantiationParameter, - ); - } - - if (propertyName === "resetInstance") { - return () => getLegacyGlobalDiForExtensionApi().purge(injectableKey); - } - - return target[propertyName]; - }, - }) as InstanceType & { - getInstance: () => InstanceType; - createInstance: () => InstanceType; - resetInstance: () => void; - }; diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index 54a0f2d2d1..8e6df41fa1 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -3,14 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; -import createTerminalTabInjectable from "../../renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable"; -import terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable"; +import createTerminalTabInjectable from "../../renderer/components/dock/terminal/create-terminal-tab.injectable"; +import terminalStoreInjectable from "../../renderer/components/dock/terminal/store.injectable"; import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable"; -import { asLegacyGlobalSingletonForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api"; -import { TerminalStore as TerminalStoreClass } from "../../renderer/components/dock/terminal-store/terminal.store"; import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable"; +import { asLegacyGlobalObjectForExtensionApiWithModifications } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications"; +import createPodLogsTabInjectable from "../../renderer/components/dock/logs/create-pod-logs-tab.injectable"; +import createWorkloadLogsTabInjectable from "../../renderer/components/dock/logs/create-workload-logs-tab.injectable"; +import sendCommandInjectable from "../../renderer/components/dock/terminal/send-command.injectable"; +import { podsStore } from "../../renderer/components/+workloads-pods/pods.store"; +import renameTabInjectable from "../../renderer/components/dock/dock/rename-tab.injectable"; // layouts export * from "../../renderer/components/layout/main-layout"; @@ -71,7 +75,30 @@ export * from "../../renderer/components/+events/kube-event-details"; export * from "../../renderer/components/status-brick"; export const createTerminalTab = asLegacyGlobalFunctionForExtensionApi(createTerminalTabInjectable); -export const TerminalStore = asLegacyGlobalSingletonForExtensionApi(TerminalStoreClass, terminalStoreInjectable); -export const terminalStore = asLegacyGlobalObjectForExtensionApi(terminalStoreInjectable); -export const logTabStore = asLegacyGlobalObjectForExtensionApi(logTabStoreInjectable); +export const terminalStore = asLegacyGlobalObjectForExtensionApiWithModifications(terminalStoreInjectable, { + sendCommand: () => asLegacyGlobalFunctionForExtensionApi(sendCommandInjectable), +}); +export const logTabStore = asLegacyGlobalObjectForExtensionApiWithModifications(logTabStoreInjectable, { + createPodTab: () => asLegacyGlobalFunctionForExtensionApi(createPodLogsTabInjectable), + createWorkloadTab: () => asLegacyGlobalFunctionForExtensionApi(createWorkloadLogsTabInjectable), + renameTab: () => (tabId: string): void => { + const renameTab = asLegacyGlobalFunctionForExtensionApi(renameTabInjectable); + const tabData = logTabStore.getData(tabId); + const pod = podsStore.getById(tabData.selectedPodId); + renameTab(tabId, `Pod ${pod.getName()}`); + }, + tabs: () => undefined, +}); + +export class TerminalStore { + static getInstance() { + return terminalStore; + } + static createInstance() { + return terminalStore; + } + static resetInstance() { + console.warn("TerminalStore.resetInstance() does nothing"); + } +} diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 54db181333..ae71fb9618 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -12,8 +12,8 @@ import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get- import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable"; import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; -import writeJsonFileInjectable from "../common/fs/write-json-file/write-json-file.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file/read-json-file.injectable"; +import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; +import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index 936859dbd0..ff7d929e8f 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -18,7 +18,7 @@ import { Select, SelectOption } from "../select"; import { Badge } from "../badge"; import { Tooltip, withStyles } from "@material-ui/core"; import { withInjectables } from "@ogre-tools/injectable-react"; -import createInstallChartTabInjectable from "../dock/create-install-chart-tab/create-install-chart-tab.injectable"; +import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable"; interface Props { chart: HelmChart; diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index 36b84d5267..49f885a093 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -12,7 +12,7 @@ import { helmChartStore } from "./helm-chart.store"; import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import { HelmChartDetails } from "./helm-chart-details"; import { navigation } from "../../navigation"; -import { ItemListLayout } from "../item-object-list/item-list-layout"; +import { ItemListLayout } from "../item-object-list/list-layout"; import { helmChartsURL } from "../../../common/routes"; import type { HelmChartsRouteParams } from "../../../common/routes"; diff --git a/src/renderer/components/+apps-releases/release-details/release-details.tsx b/src/renderer/components/+apps-releases/release-details/release-details.tsx index 77f6ab2d87..30d34e1857 100644 --- a/src/renderer/components/+apps-releases/release-details/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details/release-details.tsx @@ -27,7 +27,7 @@ import { getDetailsUrl } from "../../kube-detail-params"; import { Checkbox } from "../../checkbox"; import { MonacoEditor } from "../../monaco-editor"; import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; -import createUpgradeChartTabInjectable from "../../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; +import createUpgradeChartTabInjectable from "../../dock/upgrade-chart/create-upgrade-chart-tab.injectable"; import updateReleaseInjectable from "../update-release/update-release.injectable"; import releaseInjectable from "./release.injectable"; import releaseDetailsInjectable from "./release-details.injectable"; diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx index a1dc263806..30824dba15 100644 --- a/src/renderer/components/+apps-releases/release-menu.tsx +++ b/src/renderer/components/+apps-releases/release-menu.tsx @@ -10,7 +10,7 @@ import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; import { withInjectables } from "@ogre-tools/injectable-react"; -import createUpgradeChartTabInjectable from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; +import createUpgradeChartTabInjectable from "../dock/upgrade-chart/create-upgrade-chart-tab.injectable"; import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; import deleteReleaseInjectable from "./delete-release/delete-release.injectable"; diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index a71cc8e7a1..34f9798783 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -23,10 +23,8 @@ import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { ReleaseDetails } from "./release-details/release-details"; import removableReleasesInjectable from "./removable-releases.injectable"; import type { RemovableHelmRelease } from "./removable-releases"; -import { observer } from "mobx-react"; import type { IComputedValue } from "mobx"; import releasesInjectable from "./releases.injectable"; -import { Spinner } from "../spinner"; enum columnId { name = "name", @@ -48,7 +46,6 @@ interface Dependencies { selectNamespace: (namespace: string) => void } -@observer class NonInjectedHelmReleases extends Component { componentDidMount() { const { match: { params: { namespace }}} = this.props; @@ -89,12 +86,8 @@ class NonInjectedHelmReleases extends Component { } render() { - if (this.props.releasesArePending.get()) { - // TODO: Make Spinner "center" work properly - return
; - } - const releases = this.props.releases; + const releasesArePending = this.props.releasesArePending; // TODO: Implement ItemListLayout without stateful stores const legacyReleaseStore = { @@ -103,7 +96,11 @@ class NonInjectedHelmReleases extends Component { }, loadAll: () => Promise.resolve(), - isLoaded: true, + + get isLoaded() { + return !releasesArePending.get(); + }, + failedLoading: false, getTotalCount: () => releases.get().length, diff --git a/src/renderer/components/+catalog/get-category-columns.injectable.ts b/src/renderer/components/+catalog/get-category-columns.injectable.ts index 66fe0cddee..080fde7e29 100644 --- a/src/renderer/components/+catalog/get-category-columns.injectable.ts +++ b/src/renderer/components/+catalog/get-category-columns.injectable.ts @@ -6,7 +6,6 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { orderBy } from "lodash"; import type { IComputedValue } from "mobx"; import type { CatalogCategory, CatalogEntity } from "../../../common/catalog"; -import { bind } from "../../utils"; import type { ItemListLayoutProps } from "../item-object-list"; import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns"; import categoryColumnsInjectable from "./custom-category-columns.injectable"; @@ -50,7 +49,7 @@ function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] { ]; } -function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory }: GetCategoryColumnsParams): CategoryColumns { +const getCategoryColumns = ({ extensionColumns }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => { const allRegistrations = orderBy( activeCategory ? getSpecificCategoryColumns(activeCategory, extensionColumns) @@ -83,12 +82,13 @@ function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)), searchFilters, }; -} +}; const getCategoryColumnsInjectable = getInjectable({ - instantiate: (di) => bind(getCategoryColumns, null, { + instantiate: (di) => getCategoryColumns({ extensionColumns: di.inject(categoryColumnsInjectable), }), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts index 5cf51733d1..eb1597f61f 100644 --- a/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts +++ b/src/renderer/components/+extensions/install-from-select-file-dialog.injectable.ts @@ -7,14 +7,13 @@ import { requestOpenFilePickingDialog } from "../../ipc"; import { supportedExtensionFormats } from "./supported-extension-formats"; import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable"; import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; -import { bind } from "../../utils"; interface Dependencies { attemptInstalls: (filePaths: string[]) => Promise directoryForDownloads: string } -async function installFromSelectFileDialog({ attemptInstalls, directoryForDownloads }: Dependencies) { +const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => { const { canceled, filePaths } = await requestOpenFilePickingDialog({ defaultPath: directoryForDownloads, properties: ["openFile", "multiSelections"], @@ -26,13 +25,14 @@ async function installFromSelectFileDialog({ attemptInstalls, directoryForDownlo if (!canceled) { await attemptInstalls(filePaths); } -} +}; const installFromSelectFileDialogInjectable = getInjectable({ - instantiate: (di) => bind(installFromSelectFileDialog, null, { + instantiate: (di) => installFromSelectFileDialog({ attemptInstalls: di.inject(attemptInstallsInjectable), directoryForDownloads: di.inject(directoryForDownloadsInjectable), }), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index b3e7e4c1f3..9ceeadba38 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -8,7 +8,7 @@ import "./port-forwards.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import type { RouteComponentProps } from "react-router-dom"; -import { ItemListLayout } from "../item-object-list/item-list-layout"; +import { ItemListLayout } from "../item-object-list/list-layout"; import type { PortForwardItem, PortForwardStore } from "../../port-forward"; import { PortForwardMenu } from "./port-forward-menu"; import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx index e1a3d7c46d..76e8ed7743 100644 --- a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx +++ b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx @@ -14,9 +14,8 @@ import { ActivateEntityCommand } from "../../activate-entity-command"; import type { CommandContext, CommandRegistration } from "./commands"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import commandOverlayInjectable from "../command-overlay.injectable"; -import createTerminalTabInjectable - from "../../dock/create-terminal-tab/create-terminal-tab.injectable"; -import type { DockTabCreate } from "../../dock/dock-store/dock.store"; +import createTerminalTabInjectable from "../../dock/terminal/create-terminal-tab.injectable"; +import type { DockTabCreate } from "../../dock/dock/store"; export function isKubernetesClusterActive(context: CommandContext): boolean { return context.entity?.kind === "KubernetesCluster"; diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 705680317f..4be169bf7f 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -8,12 +8,12 @@ import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import fse from "fs-extra"; import { DockTabs } from "../dock-tabs"; -import { DockStore, DockTab, TabKind } from "../dock-store/dock.store"; +import { DockStore, DockTab, TabKind } from "../dock/store"; import { noop } from "../../../utils"; import { ThemeStore } from "../../../theme.store"; import { UserStore } from "../../../../common/user-store"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import dockStoreInjectable from "../dock/store.injectable"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; import directoryForUserDataInjectable diff --git a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts deleted file mode 100644 index 8544cff719..0000000000 --- a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { createInstallChartTab } from "./create-install-chart-tab"; -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import installChartStoreInjectable from "../install-chart-store/install-chart-store.injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; - -const createInstallChartTabInjectable = getInjectable({ - instantiate: (di) => createInstallChartTab({ - installChartStore: di.inject(installChartStoreInjectable), - createDockTab: di.inject(dockStoreInjectable).createTab, - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default createInstallChartTabInjectable; diff --git a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts deleted file mode 100644 index d4f6aba833..0000000000 --- a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api"; -import { - DockTab, - DockTabCreate, - DockTabCreateSpecific, - TabKind, -} from "../dock-store/dock.store"; - -import type { InstallChartStore } from "../install-chart-store/install-chart.store"; - -interface Dependencies { - createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab; - installChartStore: InstallChartStore; -} - -export const createInstallChartTab = - ({ createDockTab, installChartStore }: Dependencies) => - (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => { - const { name, repo, version } = chart; - - const tab = createDockTab( - { - title: `Helm Install: ${repo}/${name}`, - ...tabParams, - kind: TabKind.INSTALL_CHART, - }, - false, - ); - - installChartStore.setData(tab.id, { - name, - repo, - version, - namespace: "default", - releaseName: "", - description: "", - }); - - return tab; - }; diff --git a/src/renderer/components/dock/create-resource-store/create-resource.store.ts b/src/renderer/components/dock/create-resource-store/create-resource.store.ts deleted file mode 100644 index 7c0c62f30b..0000000000 --- a/src/renderer/components/dock/create-resource-store/create-resource.store.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import fs from "fs-extra"; -import path from "path"; -import os from "os"; -import groupBy from "lodash/groupBy"; -import filehound from "filehound"; -import { watch } from "chokidar"; -import { autoBind, StorageHelper } from "../../../utils"; -import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; -import type { DockStore } from "../dock-store/dock.store"; - -interface Dependencies { - dockStore: DockStore, - createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> -} - -export class CreateResourceStore extends DockTabStore { - constructor(protected dependencies: Dependencies) { - super(dependencies, { - storageKey: "create_resource", - }); - - autoBind(this); - fs.ensureDirSync(this.userTemplatesFolder); - } - - get lensTemplatesFolder():string { - return path.resolve(__static, "../templates/create-resource"); - } - - get userTemplatesFolder():string { - return path.join(os.homedir(), ".k8slens", "templates"); - } - - async getTemplates(templatesPath: string, defaultGroup: string) { - const templates = await filehound.create().path(templatesPath).ext(["yaml", "json"]).depth(1).find(); - - return templates ? this.groupTemplates(templates, templatesPath, defaultGroup) : {}; - } - - groupTemplates(templates: string[], templatesPath: string, defaultGroup: string) { - return groupBy(templates, (v:string) => - path.relative(templatesPath, v).split(path.sep).length>1 - ? path.parse(path.relative(templatesPath, v)).dir - : defaultGroup); - } - - async getMergedTemplates() { - const userTemplates = await this.getTemplates(this.userTemplatesFolder, "ungrouped"); - const lensTemplates = await this.getTemplates(this.lensTemplatesFolder, "lens"); - - return { ...userTemplates, ...lensTemplates }; - } - - async watchUserTemplates(callback: ()=> void){ - watch(this.userTemplatesFolder, { - depth: 1, - ignoreInitial: true, - awaitWriteFinish: { - stabilityThreshold: 500, - }, - }).on("all", () => { - callback(); - }); - } -} diff --git a/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts b/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts deleted file mode 100644 index daacd34c33..0000000000 --- a/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { createResourceTab } from "./create-resource-tab"; -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; - -const createResourceTabInjectable = getInjectable({ - instantiate: (di) => createResourceTab({ - dockStore: di.inject(dockStoreInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default createResourceTabInjectable; diff --git a/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts b/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts deleted file mode 100644 index bea9dc05aa..0000000000 --- a/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; - -interface Dependencies { - dockStore: DockStore -} - -export const createResourceTab = - ({ dockStore }: Dependencies) => - (tabParams: DockTabCreateSpecific = {}) => - dockStore.createTab({ - title: "Create resource", - ...tabParams, - kind: TabKind.CREATE_RESOURCE, - }); diff --git a/src/renderer/components/dock/create-resource.scss b/src/renderer/components/dock/create-resource.scss deleted file mode 100644 index 7191de3134..0000000000 --- a/src/renderer/components/dock/create-resource.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -.CreateResource { -} diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx deleted file mode 100644 index 2b6728ede6..0000000000 --- a/src/renderer/components/dock/create-resource.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./create-resource.scss"; - -import React from "react"; -import path from "path"; -import fs from "fs-extra"; -import { GroupSelectOption, Select, SelectOption } from "../select"; -import yaml from "js-yaml"; -import { makeObservable, observable } from "mobx"; -import { observer } from "mobx-react"; -import type { CreateResourceStore } from "./create-resource-store/create-resource.store"; -import type { DockTab } from "./dock-store/dock.store"; -import { EditorPanel } from "./editor-panel"; -import { InfoPanel } from "./info-panel"; -import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api"; -import { Notifications } from "../notifications"; -import logger from "../../../common/logger"; -import type { KubeJsonApiData } from "../../../common/k8s-api/kube-json-api"; -import { getDetailsUrl } from "../kube-detail-params"; -import { apiManager } from "../../../common/k8s-api/api-manager"; -import { prevDefault } from "../../utils"; -import { navigate } from "../../navigation"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import createResourceStoreInjectable - from "./create-resource-store/create-resource-store.injectable"; - -interface Props { - tab: DockTab; -} - -interface Dependencies { - createResourceStore: CreateResourceStore -} - -@observer -class NonInjectedCreateResource extends React.Component { - @observable currentTemplates: Map = new Map(); - @observable error = ""; - @observable templates: GroupSelectOption[] = []; - - constructor(props: Props & Dependencies) { - super(props); - makeObservable(this); - } - - componentDidMount() { - this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)); - this.props.createResourceStore.watchUserTemplates(() => this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v))); - } - - updateGroupSelectOptions(templates: Record) { - this.templates = Object.entries(templates) - .map(([name, grouping]) => this.convertToGroup(name, grouping)); - } - - convertToGroup(group: string, items: string[]): GroupSelectOption { - const options = items.map(v => ({ label: path.parse(v).name, value: v })); - - return { label: group, options }; - } - - get tabId() { - return this.props.tab.id; - } - - get data() { - return this.props.createResourceStore.getData(this.tabId); - } - - get currentTemplate() { - return this.currentTemplates.get(this.tabId) ?? null; - } - - onChange = (value: string) => { - this.error = ""; // reset first, validation goes later - this.props.createResourceStore.setData(this.tabId, value); - }; - - onError = (error: Error | string) => { - this.error = error.toString(); - }; - - onSelectTemplate = (item: SelectOption) => { - this.currentTemplates.set(this.tabId, item); - fs.readFile(item.value, "utf8").then(v => { - this.props.createResourceStore.setData(this.tabId, v); - }); - }; - - create = async (): Promise => { - if (this.error || !this.data.trim()) { - // do not save when field is empty or there is an error - return null; - } - - // skip empty documents - const resources = yaml.loadAll(this.data).filter(Boolean); - - if (resources.length === 0) { - return void logger.info("Nothing to create"); - } - - const creatingResources = resources.map(async (resource: string) => { - try { - const data: KubeJsonApiData = await resourceApplierApi.update(resource); - const { kind, apiVersion, metadata: { name, namespace }} = data; - const resourceLink = apiManager.lookupApiLink({ kind, apiVersion, name, namespace }); - - const showDetails = () => { - navigate(getDetailsUrl(resourceLink)); - close(); - }; - - const close = Notifications.ok( -
, - ); - } catch (error) { - Notifications.error(error?.toString() ?? "Unknown error occured"); - } - }); - - await Promise.allSettled(creatingResources); - - return undefined; - }; - - renderControls() { - return ( -
- +
+ ); + } + + render() { + const { tabId, data, error } = this; + + return ( +
+ + +
+ ); + } +} + +export const CreateResource = withInjectables(NonInjectedCreateResource, { + getPlaceholder: () => , + + getProps: async (di, props) => ({ + createResourceTabStore: di.inject(createResourceTabStoreInjectable), + createResourceTemplates: await di.inject(createResourceTemplatesInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts deleted file mode 100644 index e22ceaf273..0000000000 --- a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { createTerminalTab } from "./create-terminal-tab"; -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; - -const createTerminalTabInjectable = getInjectable({ - instantiate: (di) => createTerminalTab({ - dockStore: di.inject(dockStoreInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default createTerminalTabInjectable; diff --git a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts deleted file mode 100644 index cd3c501158..0000000000 --- a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { - DockStore, - DockTabCreateSpecific, - TabKind, -} from "../dock-store/dock.store"; - -interface Dependencies { - dockStore: DockStore; -} - -export const createTerminalTab = - ({ dockStore }: Dependencies) => - (tabParams: DockTabCreateSpecific = {}) => - dockStore.createTab({ - title: `Terminal`, - ...tabParams, - kind: TabKind.TERMINAL, - }); diff --git a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts deleted file mode 100644 index 0ba73c3abe..0000000000 --- a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { createUpgradeChartTab } from "./create-upgrade-chart-tab"; -import upgradeChartStoreInjectable from "../upgrade-chart-store/upgrade-chart-store.injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; - -const createUpgradeChartTabInjectable = getInjectable({ - instantiate: (di) => createUpgradeChartTab({ - upgradeChartStore: di.inject(upgradeChartStoreInjectable), - dockStore: di.inject(dockStoreInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default createUpgradeChartTabInjectable; diff --git a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts deleted file mode 100644 index 17c94c6741..0000000000 --- a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; -import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; -import type { UpgradeChartStore } from "../upgrade-chart-store/upgrade-chart.store"; - -interface Dependencies { - upgradeChartStore: UpgradeChartStore; - dockStore: DockStore -} - -export const createUpgradeChartTab = - ({ upgradeChartStore, dockStore }: Dependencies) => - (release: HelmRelease, tabParams: DockTabCreateSpecific = {}) => { - let tab = upgradeChartStore.getTabByRelease(release.getName()); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - } - - if (!tab) { - tab = dockStore.createTab( - { - title: `Helm Upgrade: ${release.getName()}`, - ...tabParams, - kind: TabKind.UPGRADE_CHART, - }, - false, - ); - - upgradeChartStore.setData(tab.id, { - releaseName: release.getName(), - releaseNamespace: release.getNs(), - }); - } - - return tab; - }; diff --git a/src/renderer/components/dock/dock-store/dock-store.injectable.ts b/src/renderer/components/dock/dock-store/dock-store.injectable.ts deleted file mode 100644 index 19a47dead6..0000000000 --- a/src/renderer/components/dock/dock-store/dock-store.injectable.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { DockStore } from "./dock.store"; -import dockStorageInjectable from "./dock-storage/dock-storage.injectable"; - -const dockStoreInjectable = getInjectable({ - instantiate: (di) => - new DockStore({ - storage: di.inject(dockStorageInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default dockStoreInjectable; diff --git a/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts b/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts index 19b5af4084..ed76d8d9a6 100644 --- a/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts +++ b/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts @@ -4,13 +4,11 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; const createDockTabStoreInjectable = getInjectable({ instantiate: (di) => { const dependencies = { - dockStore: di.inject(dockStoreInjectable), createStorage: di.inject(createStorageInjectable), }; diff --git a/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts index 5bfcdd504c..ab8d4b2940 100644 --- a/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { autorun, observable, reaction } from "mobx"; +import { action, observable, reaction } from "mobx"; import { autoBind, StorageHelper, toJS } from "../../../utils"; -import type { DockStore, TabId } from "../dock-store/dock.store"; +import type { TabId } from "../dock/store"; export interface DockTabStoreOptions { autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true @@ -14,16 +14,15 @@ export interface DockTabStoreOptions { export type DockTabStorageState = Record; -interface Dependencies { - dockStore: DockStore +interface DockTabStoreDependencies { createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> } export class DockTabStore { protected storage?: StorageHelper>; - protected data = observable.map(); + private data = observable.map(); - constructor(protected dependencies: Dependencies, protected options: DockTabStoreOptions) { + constructor(protected dependencies: DockTabStoreDependencies, protected options: DockTabStoreOptions) { autoBind(this); this.options = { @@ -48,17 +47,6 @@ export class DockTabStore { reaction(() => this.toJSON(), data => this.storage.set(data)); }); } - - // clear data for closed tabs - autorun(() => { - const currentTabs = this.dependencies.dockStore.tabs.map(tab => tab.id); - - Array.from(this.data.keys()).forEach(tabId => { - if (!currentTabs.includes(tabId)) { - this.clearData(tabId); - } - }); - }); } protected finalizeDataForSave(data: T): T { @@ -75,8 +63,22 @@ export class DockTabStore { return Object.fromEntries(deepCopy); } + protected getAllData() { + return this.data.toJSON(); + } + + findTabIdFromData(inspecter: (val: T) => any): TabId | undefined { + for (const [tabId, data] of this.data) { + if (inspecter(data)) { + return tabId; + } + } + + return undefined; + } + isReady(tabId: TabId): boolean { - return Boolean(this.getData(tabId) !== undefined); + return this.getData(tabId) !== undefined; } getData(tabId: TabId) { @@ -91,8 +93,11 @@ export class DockTabStore { this.data.delete(tabId); } + @action reset() { - this.data.clear(); + for (const tabId of this.data.keys()) { + this.clearData(tabId); + } this.storage?.reset(); } } diff --git a/src/renderer/components/dock/dock-tab.tsx b/src/renderer/components/dock/dock-tab.tsx index f5d7be54c2..4b91c609ba 100644 --- a/src/renderer/components/dock/dock-tab.tsx +++ b/src/renderer/components/dock/dock-tab.tsx @@ -8,14 +8,14 @@ import "./dock-tab.scss"; import React from "react"; import { observer } from "mobx-react"; import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils"; -import type { DockStore, DockTab as DockTabModel } from "./dock-store/dock.store"; +import type { DockStore, DockTab as DockTabModel } from "./dock/store"; import { Tab, TabProps } from "../tabs"; import { Icon } from "../icon"; import { Menu, MenuItem } from "../menu"; import { observable, makeObservable } from "mobx"; import { isMac } from "../../../common/vars"; import { withInjectables } from "@ogre-tools/injectable-react"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import dockStoreInjectable from "./dock/store.injectable"; export interface DockTabProps extends TabProps { moreActions?: React.ReactNode; diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx index 8902fe439a..d4443cf6c0 100644 --- a/src/renderer/components/dock/dock-tabs.tsx +++ b/src/renderer/components/dock/dock-tabs.tsx @@ -8,9 +8,9 @@ import React, { Fragment } from "react"; import { Icon } from "../icon"; import { Tabs } from "../tabs/tabs"; import { DockTab } from "./dock-tab"; -import type { DockTab as DockTabModel } from "./dock-store/dock.store"; -import { TabKind } from "./dock-store/dock.store"; -import { TerminalTab } from "./terminal-tab"; +import type { DockTab as DockTabModel } from "./dock/store"; +import { TabKind } from "./dock/store"; +import { TerminalTab } from "./terminal/dock-tab"; interface Props { tabs: DockTabModel[] diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index dcc655c60d..d067931762 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -13,18 +13,19 @@ import { Icon } from "../icon"; import { MenuItem } from "../menu"; import { MenuActions } from "../menu/menu-actions"; import { ResizeDirection, ResizingAnchor } from "../resizing-anchor"; -import { CreateResource } from "./create-resource"; +import { CreateResource } from "./create-resource/view"; import { DockTabs } from "./dock-tabs"; -import { DockStore, DockTab, TabKind } from "./dock-store/dock.store"; -import { EditResource } from "./edit-resource"; -import { InstallChart } from "./install-chart"; -import { LogsDockTab } from "./logs/dock-tab"; -import { TerminalWindow } from "./terminal-window"; -import { UpgradeChart } from "./upgrade-chart"; +import { DockStore, DockTab, TabKind } from "./dock/store"; +import { EditResource } from "./edit-resource/view"; +import { InstallChart } from "./install-chart/view"; +import { LogsDockTab } from "./logs/view"; +import { TerminalWindow } from "./terminal/view"; +import { UpgradeChart } from "./upgrade-chart/view"; import { withInjectables } from "@ogre-tools/injectable-react"; -import createResourceTabInjectable from "./create-resource-tab/create-resource-tab.injectable"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; -import createTerminalTabInjectable from "./create-terminal-tab/create-terminal-tab.injectable"; +import createResourceTabInjectable from "./create-resource/create-resource-tab.injectable"; +import dockStoreInjectable from "./dock/store.injectable"; +import createTerminalTabInjectable from "./terminal/create-terminal-tab.injectable"; +import { ErrorBoundary } from "../error-boundary"; interface Props { className?: string; @@ -160,7 +161,9 @@ class NonInjectedDock extends React.Component { )}

- {kind} {name} successfully created. -

- {this.renderTabContent()} + + {this.renderTabContent()} + ); } diff --git a/src/renderer/components/dock/dock/close-dock-tab.injectable.ts b/src/renderer/components/dock/dock/close-dock-tab.injectable.ts new file mode 100644 index 0000000000..8d44637cd7 --- /dev/null +++ b/src/renderer/components/dock/dock/close-dock-tab.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "./store"; +import dockStoreInjectable from "./store.injectable"; + +const closeDockTabInjectable = getInjectable({ + instantiate: (di) => { + const dockStore = di.inject(dockStoreInjectable); + + return (tabId: TabId): void => { + dockStore.closeTab(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default closeDockTabInjectable; diff --git a/src/renderer/components/dock/dock/create-dock-tab.injectable.ts b/src/renderer/components/dock/dock/create-dock-tab.injectable.ts new file mode 100644 index 0000000000..1469b2e31b --- /dev/null +++ b/src/renderer/components/dock/dock/create-dock-tab.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "./store.injectable"; +import type { DockTab, DockTabCreate } from "./store"; + +const createDockTabInjectable = getInjectable({ + instantiate: (di) => { + const dockStore = di.inject(dockStoreInjectable); + + return (rawTabDesc: DockTabCreate, addNumber?: boolean): DockTab => + dockStore.createTab(rawTabDesc, addNumber); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createDockTabInjectable; diff --git a/src/renderer/components/dock/dock-store/dock-storage/dock-storage.injectable.ts b/src/renderer/components/dock/dock/dock-storage.injectable.ts similarity index 81% rename from src/renderer/components/dock/dock-store/dock-storage/dock-storage.injectable.ts rename to src/renderer/components/dock/dock/dock-storage.injectable.ts index 56e6edd37e..e58127761f 100644 --- a/src/renderer/components/dock/dock-store/dock-storage/dock-storage.injectable.ts +++ b/src/renderer/components/dock/dock/dock-storage.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import createStorageInjectable from "../../../../utils/create-storage/create-storage.injectable"; -import { DockStorageState, TabKind } from "../dock.store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; +import { DockStorageState, TabKind } from "./store"; const dockStorageInjectable = getInjectable({ instantiate: (di) => { diff --git a/src/renderer/components/dock/dock/rename-tab.injectable.ts b/src/renderer/components/dock/dock/rename-tab.injectable.ts new file mode 100644 index 0000000000..38ceb4dcbe --- /dev/null +++ b/src/renderer/components/dock/dock/rename-tab.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "./store.injectable"; +import type { TabId } from "./store"; + +const renameTabInjectable = getInjectable({ + + instantiate: (di) => { + const dockStore = di.inject(dockStoreInjectable); + + return (tabId: TabId, title: string): void => { + dockStore.renameTab(tabId, title); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default renameTabInjectable; diff --git a/src/renderer/components/dock/dock/select-dock-tab.injectable.ts b/src/renderer/components/dock/dock/select-dock-tab.injectable.ts new file mode 100644 index 0000000000..39830db90c --- /dev/null +++ b/src/renderer/components/dock/dock/select-dock-tab.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "./store"; +import dockStoreInjectable from "./store.injectable"; + +const selectDockTabInjectable = getInjectable({ + instantiate: (di) => { + const dockStore = di.inject(dockStoreInjectable); + + return (tabId: TabId): void => { + dockStore.selectTab(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default selectDockTabInjectable; diff --git a/src/renderer/components/dock/dock/store.injectable.ts b/src/renderer/components/dock/dock/store.injectable.ts new file mode 100644 index 0000000000..b61450ea54 --- /dev/null +++ b/src/renderer/components/dock/dock/store.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { DockStore, TabKind } from "./store"; +import dockStorageInjectable from "./dock-storage.injectable"; +import clearLogTabDataInjectable from "../logs/clear-log-tab-data.injectable"; +import clearUpgradeChartTabDataInjectable from "../upgrade-chart/clear-upgrade-chart-tab-data.injectable"; +import clearCreateResourceTabDataInjectable from "../create-resource/clear-create-resource-tab-data.injectable"; +import clearEditResourceTabDataInjectable from "../edit-resource/clear-edit-resource-tab-data.injectable"; +import clearTerminalTabDataInjectable from "../terminal/clear-terminal-tab-data.injectable"; +import clearInstallChartTabDataInjectable from "../install-chart/clear-install-chart-tab-data.injectable"; +import isLogsTabDataValidInjectable from "../logs/is-logs-tab-data-valid.injectable"; + +const dockStoreInjectable = getInjectable({ + instantiate: (di) => new DockStore({ + storage: di.inject(dockStorageInjectable), + tabDataClearers: { + [TabKind.POD_LOGS]: di.inject(clearLogTabDataInjectable), + [TabKind.UPGRADE_CHART]: di.inject(clearUpgradeChartTabDataInjectable), + [TabKind.CREATE_RESOURCE]: di.inject(clearCreateResourceTabDataInjectable), + [TabKind.EDIT_RESOURCE]: di.inject(clearEditResourceTabDataInjectable), + [TabKind.INSTALL_CHART]: di.inject(clearInstallChartTabDataInjectable), + [TabKind.TERMINAL]: di.inject(clearTerminalTabDataInjectable), + }, + tabDataValidator: { + [TabKind.POD_LOGS]: di.inject(isLogsTabDataValidInjectable), + }, + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default dockStoreInjectable; diff --git a/src/renderer/components/dock/dock-store/dock.store.ts b/src/renderer/components/dock/dock/store.ts similarity index 93% rename from src/renderer/components/dock/dock-store/dock.store.ts rename to src/renderer/components/dock/dock/store.ts index 6cca0e4d06..3cbe3c03aa 100644 --- a/src/renderer/components/dock/dock-store/dock.store.ts +++ b/src/renderer/components/dock/dock/store.ts @@ -98,11 +98,13 @@ export interface DockTabCloseEvent { } interface Dependencies { - storage: StorageHelper + readonly storage: StorageHelper + readonly tabDataClearers: Record void>; + readonly tabDataValidator: Partial boolean>>; } export class DockStore implements DockStorageState { - constructor(private dependencies: Dependencies) { + constructor(private readonly dependencies: Dependencies) { makeObservable(this); autoBind(this); this.init(); @@ -167,6 +169,16 @@ export class DockStore implements DockStorageState { private init() { // adjust terminal height if window size changes window.addEventListener("resize", throttle(this.adjustHeight, 250)); + + this.whenReady.then(action(() => { + for (const tab of this.tabs) { + const validator = this.dependencies.tabDataValidator[tab.kind]; + + if (validator && !validator(tab.id)) { + this.closeTab(tab.id); + } + } + })); } get maxHeight() { @@ -317,6 +329,7 @@ export class DockStore implements DockStorageState { } this.tabs = this.tabs.filter(tab => tab.id !== tabId); + this.dependencies.tabDataClearers[tab.kind](tab.id); if (this.selectedTabId === tab.id) { if (this.tabs.length) { @@ -330,6 +343,7 @@ export class DockStore implements DockStorageState { } } + @action closeTabs(tabs: DockTab[]) { tabs.forEach(tab => this.closeTab(tab.id)); } diff --git a/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts b/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts deleted file mode 100644 index 393bfa560b..0000000000 --- a/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; -import { EditResourceStore } from "./edit-resource.store"; -import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; - -const editResourceStoreInjectable = getInjectable({ - instantiate: (di) => - new EditResourceStore({ - dockStore: di.inject(dockStoreInjectable), - createStorage: di.inject(createStorageInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default editResourceStoreInjectable; diff --git a/src/renderer/components/dock/edit-resource-store/edit-resource.store.ts b/src/renderer/components/dock/edit-resource-store/edit-resource.store.ts deleted file mode 100644 index 92982cb52d..0000000000 --- a/src/renderer/components/dock/edit-resource-store/edit-resource.store.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { autoBind, noop, StorageHelper } from "../../../utils"; -import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; -import { autorun, IReactionDisposer } from "mobx"; -import type { DockStore, DockTab, TabId } from "../dock-store/dock.store"; -import type { KubeObject } from "../../../../common/k8s-api/kube-object"; -import { apiManager } from "../../../../common/k8s-api/api-manager"; -import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; - -export interface EditingResource { - resource: string; // resource path, e.g. /api/v1/namespaces/default - draft?: string; // edited draft in yaml - firstDraft?: string; -} - -interface Dependencies { - dockStore: DockStore - createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> -} - -export class EditResourceStore extends DockTabStore { - private watchers = new Map(); - - constructor(protected dependencies: Dependencies) { - super(dependencies, { - storageKey: "edit_resource_store", - }); - - autoBind(this); - } - - protected async init() { - super.init(); - await this.storage.whenReady; - - autorun(() => { - Array.from(this.data).forEach(([tabId, { resource }]) => { - if (this.watchers.get(tabId)) { - return; - } - this.watchers.set(tabId, autorun(() => { - const store = apiManager.getStore(resource); - - if (store) { - const isActiveTab = this.dependencies.dockStore.isOpen && this.dependencies.dockStore.selectedTabId === tabId; - const obj = store.getByPath(resource); - - // preload resource for editing - if (!obj && !store.isLoaded && !store.isLoading && isActiveTab) { - store.loadFromPath(resource).catch(noop); - } - // auto-close tab when resource removed from store - else if (!obj && store.isLoaded) { - this.dependencies.dockStore.closeTab(tabId); - } - } - }, { - delay: 100, // make sure all kube-object stores are initialized - })); - }); - }); - } - - protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource { - return data; // skip saving draft to local-storage - } - - isReady(tabId: TabId) { - const tabDataReady = super.isReady(tabId); - - return Boolean(tabDataReady && this.getResource(tabId)); // ready to edit resource - } - - getStore(tabId: TabId): KubeObjectStore | undefined { - return apiManager.getStore(this.getResourcePath(tabId)); - } - - getResource(tabId: TabId): KubeObject | undefined { - return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId)); - } - - getResourcePath(tabId: TabId): string | undefined { - return this.getData(tabId)?.resource; - } - - getTabByResource(object: KubeObject): DockTab { - const [tabId] = Array.from(this.data).find(([, { resource }]) => { - return object.selfLink === resource; - }) || []; - - return this.dependencies.dockStore.getTabById(tabId); - } - - clearInitialDraft(tabId: TabId): void { - delete this.getData(tabId)?.firstDraft; - } - - reset() { - super.reset(); - Array.from(this.watchers).forEach(([tabId, dispose]) => { - this.watchers.delete(tabId); - dispose(); - }); - } -} diff --git a/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts deleted file mode 100644 index ba94e51376..0000000000 --- a/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { editResourceTab } from "./edit-resource-tab"; -import editResourceStoreInjectable from "../edit-resource-store/edit-resource-store.injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; - -const editResourceTabInjectable = getInjectable({ - instantiate: (di) => editResourceTab({ - dockStore: di.inject(dockStoreInjectable), - editResourceStore: di.inject(editResourceStoreInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default editResourceTabInjectable; diff --git a/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts deleted file mode 100644 index e42004ef69..0000000000 --- a/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { KubeObject } from "../../../../common/k8s-api/kube-object"; -import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; -import type { EditResourceStore } from "../edit-resource-store/edit-resource.store"; - -interface Dependencies { - dockStore: DockStore; - editResourceStore: EditResourceStore; -} - -export const editResourceTab = - ({ dockStore, editResourceStore }: Dependencies) => - (object: KubeObject, tabParams: DockTabCreateSpecific = {}) => { - // use existing tab if already opened - let tab = editResourceStore.getTabByResource(object); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - } - - // or create new tab - if (!tab) { - tab = dockStore.createTab( - { - title: `${object.kind}: ${object.getName()}`, - ...tabParams, - kind: TabKind.EDIT_RESOURCE, - }, - false, - ); - editResourceStore.setData(tab.id, { - resource: object.selfLink, - }); - } - - return tab; - }; diff --git a/src/renderer/components/dock/edit-resource.scss b/src/renderer/components/dock/edit-resource.scss deleted file mode 100644 index 2ae07cdd07..0000000000 --- a/src/renderer/components/dock/edit-resource.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -.EditResource { -} diff --git a/src/renderer/components/dock/edit-resource/clear-edit-resource-tab-data.injectable.ts b/src/renderer/components/dock/edit-resource/clear-edit-resource-tab-data.injectable.ts new file mode 100644 index 0000000000..3b73991af8 --- /dev/null +++ b/src/renderer/components/dock/edit-resource/clear-edit-resource-tab-data.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import editResourceTabStoreInjectable from "./store.injectable"; + +const clearEditResourceTabDataInjectable = getInjectable({ + instantiate: (di) => { + const editResourceTabStore = di.inject(editResourceTabStoreInjectable); + + return (tabId: TabId) => { + editResourceTabStore.clearData(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clearEditResourceTabDataInjectable; diff --git a/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts new file mode 100644 index 0000000000..a68639e12b --- /dev/null +++ b/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import editResourceTabStoreInjectable from "./store.injectable"; +import dockStoreInjectable from "../dock/store.injectable"; +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { DockStore, DockTabCreateSpecific, TabId, TabKind } from "../dock/store"; +import type { EditResourceTabStore } from "./store"; +import { runInAction } from "mobx"; + +interface Dependencies { + dockStore: DockStore; + editResourceStore: EditResourceTabStore; +} + +const createEditResourceTab = ({ dockStore, editResourceStore }: Dependencies) => (object: KubeObject, tabParams: DockTabCreateSpecific = {}): TabId => { + // use existing tab if already opened + const tabId = editResourceStore.getTabIdByResource(object); + + if (tabId) { + dockStore.open(); + dockStore.selectTab(tabId); + + return tabId; + } + + return runInAction(() => { + const tab = dockStore.createTab( + { + title: `${object.kind}: ${object.getName()}`, + ...tabParams, + kind: TabKind.EDIT_RESOURCE, + }, + false, + ); + + editResourceStore.setData(tab.id, { + resource: object.selfLink, + }); + + return tab.id; + }); +}; + +const createEditResourceTabInjectable = getInjectable({ + instantiate: (di) => createEditResourceTab({ + dockStore: di.inject(dockStoreInjectable), + editResourceStore: di.inject(editResourceTabStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createEditResourceTabInjectable; diff --git a/src/renderer/components/dock/edit-resource/store.injectable.ts b/src/renderer/components/dock/edit-resource/store.injectable.ts new file mode 100644 index 0000000000..ae2f462262 --- /dev/null +++ b/src/renderer/components/dock/edit-resource/store.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { EditResourceTabStore } from "./store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const editResourceTabStoreInjectable = getInjectable({ + instantiate: (di) => new EditResourceTabStore({ + createStorage: di.inject(createStorageInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default editResourceTabStoreInjectable; diff --git a/src/renderer/components/dock/edit-resource/store.ts b/src/renderer/components/dock/edit-resource/store.ts new file mode 100644 index 0000000000..5b8794d758 --- /dev/null +++ b/src/renderer/components/dock/edit-resource/store.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { StorageHelper } from "../../../utils"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import type { TabId } from "../dock/store"; +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { apiManager } from "../../../../common/k8s-api/api-manager"; +import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; + +export interface EditingResource { + resource: string; // resource path, e.g. /api/v1/namespaces/default + draft?: string; // edited draft in yaml + firstDraft?: string; +} + +interface Dependencies { + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + +export class EditResourceTabStore extends DockTabStore { + constructor(protected dependencies: Dependencies) { + super(dependencies, { + storageKey: "edit_resource_store", + }); + } + + protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource { + return data; // skip saving draft to local-storage + } + + isReady(tabId: TabId) { + return super.isReady(tabId) && Boolean(this.getResource(tabId)); // ready to edit resource + } + + getStore(tabId: TabId): KubeObjectStore | undefined { + return apiManager.getStore(this.getResourcePath(tabId)); + } + + getResource(tabId: TabId): KubeObject | undefined { + return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId)); + } + + getResourcePath(tabId: TabId): string | undefined { + return this.getData(tabId)?.resource; + } + + getTabIdByResource(object: KubeObject): TabId { + return this.findTabIdFromData(({ resource }) => object.selfLink === resource); + } + + clearInitialDraft(tabId: TabId): void { + delete this.getData(tabId)?.firstDraft; + } +} diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource/view.tsx similarity index 67% rename from src/renderer/components/dock/edit-resource.tsx rename to src/renderer/components/dock/edit-resource/view.tsx index ec15e27ab5..aa23af9cea 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource/view.tsx @@ -3,29 +3,30 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import "./edit-resource.scss"; - import React from "react"; -import { computed, makeObservable, observable } from "mobx"; -import { observer } from "mobx-react"; +import { autorun, computed, makeObservable, observable } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import yaml from "js-yaml"; -import type { DockTab } from "./dock-store/dock.store"; -import type { EditResourceStore } from "./edit-resource-store/edit-resource.store"; -import { InfoPanel } from "./info-panel"; -import { Badge } from "../badge"; -import { EditorPanel } from "./editor-panel"; -import { Spinner } from "../spinner"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { DockTab, TabId } from "../dock/store"; +import type { EditResourceTabStore } from "./store"; +import { InfoPanel } from "../info-panel"; +import { Badge } from "../../badge"; +import { EditorPanel } from "../editor-panel"; +import { Spinner } from "../../spinner"; +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; import { createPatch } from "rfc6902"; import { withInjectables } from "@ogre-tools/injectable-react"; -import editResourceStoreInjectable from "./edit-resource-store/edit-resource-store.injectable"; +import editResourceTabStoreInjectable from "./store.injectable"; +import { noop } from "../../../utils"; +import closeDockTabInjectable from "../dock/close-dock-tab.injectable"; interface Props { tab: DockTab; } interface Dependencies { - editResourceStore: EditResourceStore + editResourceStore: EditResourceTabStore; + closeTab: (tabId: TabId) => void; } @observer @@ -37,6 +38,26 @@ class NonInjectedEditResource extends React.Component { makeObservable(this); } + componentDidMount(): void { + disposeOnUnmount(this, [ + autorun(() => { + const store = this.props.editResourceStore.getStore(this.props.tab.id); + const tabData = this.props.editResourceStore.getData(this.props.tab.id); + const obj = this.resource; + + if (!obj) { + if (store.isLoaded) { + // auto-close tab when resource removed from store + this.props.closeTab(this.props.tab.id); + } else if (!store.isLoading) { + // preload resource for editing + store.loadFromPath(tabData.resource).catch(noop); + } + } + }), + ]); + } + get tabId() { return this.props.tab.id; } @@ -132,13 +153,10 @@ class NonInjectedEditResource extends React.Component { } } -export const EditResource = withInjectables( - NonInjectedEditResource, - - { - getProps: (di, props) => ({ - editResourceStore: di.inject(editResourceStoreInjectable), - ...props, - }), - }, -); +export const EditResource = withInjectables(NonInjectedEditResource, { + getProps: (di, props) => ({ + editResourceStore: di.inject(editResourceTabStoreInjectable), + closeTab: di.inject(closeDockTabInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/dock/editor-panel.tsx b/src/renderer/components/dock/editor-panel.tsx index e2500a7513..90e47b1c7f 100644 --- a/src/renderer/components/dock/editor-panel.tsx +++ b/src/renderer/components/dock/editor-panel.tsx @@ -8,11 +8,11 @@ import throttle from "lodash/throttle"; import React from "react"; import { makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import type { DockStore, TabId } from "./dock-store/dock.store"; +import type { DockStore, TabId } from "./dock/store"; import { cssNames } from "../../utils"; import { MonacoEditor, MonacoEditorProps } from "../monaco-editor"; import { withInjectables } from "@ogre-tools/injectable-react"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import dockStoreInjectable from "./dock/store.injectable"; export interface EditorPanelProps { tabId: TabId; diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 0d534b3a6f..91a6d329f8 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -12,14 +12,14 @@ import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; -import type { DockStore, TabId } from "./dock-store/dock.store"; +import type { DockStore, TabId } from "./dock/store"; import { Notifications } from "../notifications"; import { withInjectables } from "@ogre-tools/injectable-react"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import dockStoreInjectable from "./dock/store.injectable"; interface Props extends OptionalProps { tabId: TabId; - submit?: () => Promise; + submit?: () => Promise; } interface OptionalProps { @@ -80,7 +80,7 @@ class NonInjectedInfoPanel extends Component { try { const result = await this.props.submit(); - if (showNotifications) Notifications.ok(result); + if (showNotifications && result) Notifications.ok(result); } catch (error) { if (showNotifications) Notifications.error(error.toString()); } finally { diff --git a/src/renderer/components/dock/install-chart/clear-install-chart-tab-data.injectable.ts b/src/renderer/components/dock/install-chart/clear-install-chart-tab-data.injectable.ts new file mode 100644 index 0000000000..0221d5dc38 --- /dev/null +++ b/src/renderer/components/dock/install-chart/clear-install-chart-tab-data.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import installChartTabStoreInjectable from "./store.injectable"; + +const clearInstallChartTabDataInjectable = getInjectable({ + instantiate: (di) => { + const installChartTabStore = di.inject(installChartTabStoreInjectable); + + return (tabId: TabId) => { + installChartTabStore.clearData(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clearInstallChartTabDataInjectable; diff --git a/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts b/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts new file mode 100644 index 0000000000..2e9920c310 --- /dev/null +++ b/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import installChartTabStoreInjectable from "./store.injectable"; +import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api"; +import { + DockTab, + DockTabCreate, + DockTabCreateSpecific, + TabKind, +} from "../dock/store"; +import type { InstallChartTabStore } from "./store"; +import createDockTabInjectable from "../dock/create-dock-tab.injectable"; + +interface Dependencies { + createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab; + installChartStore: InstallChartTabStore; +} + +const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencies) => (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => { + const { name, repo, version } = chart; + + const tab = createDockTab( + { + title: `Helm Install: ${repo}/${name}`, + ...tabParams, + kind: TabKind.INSTALL_CHART, + }, + false, + ); + + installChartStore.setData(tab.id, { + name, + repo, + version, + namespace: "default", + releaseName: "", + description: "", + }); + + return tab; +}; + +const createInstallChartTabInjectable = getInjectable({ + instantiate: (di) => createInstallChartTab({ + installChartStore: di.inject(installChartTabStoreInjectable), + createDockTab: di.inject(createDockTabInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createInstallChartTabInjectable; diff --git a/src/renderer/components/dock/install-chart.scss b/src/renderer/components/dock/install-chart/install-chart.scss similarity index 100% rename from src/renderer/components/dock/install-chart.scss rename to src/renderer/components/dock/install-chart/install-chart.scss diff --git a/src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts b/src/renderer/components/dock/install-chart/store.injectable.ts similarity index 73% rename from src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts rename to src/renderer/components/dock/install-chart/store.injectable.ts index d0fdb731a0..f1d38d744a 100644 --- a/src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts +++ b/src/renderer/components/dock/install-chart/store.injectable.ts @@ -3,18 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { InstallChartStore } from "./install-chart.store"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import { InstallChartTabStore } from "./store"; import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; -const installChartStoreInjectable = getInjectable({ +const installChartTabStoreInjectable = getInjectable({ instantiate: (di) => { const createDockTabStore = di.inject(createDockTabStoreInjectable); - return new InstallChartStore({ - dockStore: di.inject(dockStoreInjectable), + return new InstallChartTabStore({ createStorage: di.inject(createStorageInjectable), versionsStore: createDockTabStore(), detailsStore: createDockTabStore(), @@ -23,4 +21,4 @@ const installChartStoreInjectable = getInjectable({ lifecycle: lifecycleEnum.singleton, }); -export default installChartStoreInjectable; +export default installChartTabStoreInjectable; diff --git a/src/renderer/components/dock/install-chart-store/install-chart.store.ts b/src/renderer/components/dock/install-chart/store.ts similarity index 72% rename from src/renderer/components/dock/install-chart-store/install-chart.store.ts rename to src/renderer/components/dock/install-chart/store.ts index 9e8a9d4c32..e322a0ec78 100644 --- a/src/renderer/components/dock/install-chart-store/install-chart.store.ts +++ b/src/renderer/components/dock/install-chart/store.ts @@ -3,12 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, autorun, makeObservable } from "mobx"; -import { DockStore, TabId, TabKind } from "../dock-store/dock.store"; +import { action, makeObservable, when } from "mobx"; +import type { TabId } from "../dock/store"; import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api"; import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; -import { Notifications } from "../../notifications"; import type { StorageHelper } from "../../../utils"; export interface IChartInstallData { @@ -23,29 +22,18 @@ export interface IChartInstallData { } interface Dependencies { - dockStore: DockStore, - createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> - - versionsStore: DockTabStore, - detailsStore: DockTabStore + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper>; + versionsStore: DockTabStore; + detailsStore: DockTabStore; } -export class InstallChartStore extends DockTabStore { +export class InstallChartTabStore extends DockTabStore { constructor(protected dependencies: Dependencies) { super( dependencies, { storageKey: "install_charts" }, ); - makeObservable(this); - autorun(() => { - const { selectedTab, isOpen } = dependencies.dockStore; - - if (selectedTab?.kind === TabKind.INSTALL_CHART && isOpen) { - this.loadData(selectedTab.id) - .catch(err => Notifications.error(String(err))); - } - }, { delay: 250 }); } get versions() { @@ -60,6 +48,8 @@ export class InstallChartStore extends DockTabStore { async loadData(tabId: string) { const promises = []; + await when(() => this.isReady(tabId)); + if (!this.getData(tabId).values) { promises.push(this.loadValues(tabId)); } @@ -94,8 +84,4 @@ export class InstallChartStore extends DockTabStore { return this.loadValues(tabId, attempt + 1); } } - - setData(tabId: TabId, data: IChartInstallData){ - super.setData(tabId, data); - } } diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart/view.tsx similarity index 79% rename from src/renderer/components/dock/install-chart.tsx rename to src/renderer/components/dock/install-chart/view.tsx index a240b50979..f162ae13a3 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart/view.tsx @@ -8,29 +8,27 @@ import "./install-chart.scss"; import React, { Component } from "react"; import { action, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; -import type { DockStore, DockTab } from "./dock-store/dock.store"; -import { InfoPanel } from "./info-panel"; -import { Badge } from "../badge"; -import { NamespaceSelect } from "../+namespaces/namespace-select"; -import { prevDefault } from "../../utils"; -import type { IChartInstallData, InstallChartStore } from "./install-chart-store/install-chart.store"; -import { Spinner } from "../spinner"; -import { Icon } from "../icon"; -import { Button } from "../button"; -import { LogsDialog } from "../dialog/logs-dialog"; -import { Select, SelectOption } from "../select"; -import { Input } from "../input"; -import { EditorPanel } from "./editor-panel"; -import { navigate } from "../../navigation"; -import { releaseURL } from "../../../common/routes"; -import type { - IReleaseCreatePayload, - IReleaseUpdateDetails, -} from "../../../common/k8s-api/endpoints/helm-releases.api"; +import type { DockStore, DockTab } from "../dock/store"; +import { InfoPanel } from "../info-panel"; +import { Badge } from "../../badge"; +import { NamespaceSelect } from "../../+namespaces/namespace-select"; +import { prevDefault } from "../../../utils"; +import type { IChartInstallData, InstallChartTabStore } from "./store"; +import { Spinner } from "../../spinner"; +import { Icon } from "../../icon"; +import { Button } from "../../button"; +import { LogsDialog } from "../../dialog/logs-dialog"; +import { Select, SelectOption } from "../../select"; +import { Input } from "../../input"; +import { EditorPanel } from "../editor-panel"; +import { navigate } from "../../../navigation"; +import { releaseURL } from "../../../../common/routes"; +import type { IReleaseCreatePayload, IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import { withInjectables } from "@ogre-tools/injectable-react"; -import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; -import createReleaseInjectable from "../+apps-releases/create-release/create-release.injectable"; +import installChartTabStoreInjectable from "./store.injectable"; +import dockStoreInjectable from "../dock/store.injectable"; +import createReleaseInjectable from "../../+apps-releases/create-release/create-release.injectable"; +import { Notifications } from "../../notifications"; interface Props { tab: DockTab; @@ -38,7 +36,7 @@ interface Props { interface Dependencies { createRelease: (payload: IReleaseCreatePayload) => Promise - installChartStore: InstallChartStore + installChartStore: InstallChartTabStore dockStore: DockStore } @@ -52,6 +50,11 @@ class NonInjectedInstallChart extends Component { makeObservable(this); } + componentDidMount(): void { + this.props.installChartStore.loadData(this.tabId) + .catch(err => Notifications.error(String(err))); + } + get chartData() { return this.props.installChartStore.getData(this.tabId); } @@ -221,7 +224,7 @@ export const InstallChart = withInjectables( { getProps: (di, props) => ({ createRelease: di.inject(createReleaseInjectable), - installChartStore: di.inject(installChartStoreInjectable), + installChartStore: di.inject(installChartTabStoreInjectable), dockStore: di.inject(dockStoreInjectable), ...props, }), diff --git a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx index 9858827f86..b5923b98a0 100644 --- a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx @@ -8,7 +8,7 @@ import "@testing-library/jest-dom/extend-expect"; import * as selectEvent from "react-select-event"; import { Pod } from "../../../../../common/k8s-api/endpoints"; import { LogResourceSelector } from "../resource-selector"; -import { dockerPod, deploymentPod1 } from "./pod.mock"; +import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock"; import { ThemeStore } from "../../../../theme.store"; import { UserStore } from "../../../../../common/user-store"; import mockFs from "mock-fs"; @@ -18,7 +18,9 @@ import { renderFor } from "../../../test-utils/renderFor"; import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import callForLogsInjectable from "../call-for-logs.injectable"; import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model"; -import type { TabId } from "../../dock-store/dock.store"; +import type { TabId } from "../../dock/store"; +import userEvent from "@testing-library/user-event"; +import { SearchStore } from "../../../../search-store/search-store"; jest.mock("electron", () => ({ app: { @@ -36,10 +38,6 @@ jest.mock("electron", () => ({ }, })); -const getComponent = (model: LogTabViewModel) => ( - -); - function mockLogTabViewModel(tabId: TabId, deps: Partial): LogTabViewModel { return new LogTabViewModel(tabId, { getLogs: jest.fn(), @@ -49,34 +47,74 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial { +const getOnePodViewModel = (tabId: TabId, deps: Partial = {}): LogTabViewModel => { const selectedPod = new Pod(dockerPod); return mockLogTabViewModel(tabId, { getLogTabData: () => ({ - pods: [selectedPod], - selectedPod, - selectedContainer: selectedPod.getContainers()[0], + selectedPodId: selectedPod.getId(), + selectedContainer: selectedPod.getContainers()[0].name, + namespace: selectedPod.getNs(), + showPrevious: false, + showTimestamps: false, }), + getPodById: (id) => { + if (id === selectedPod.getId()) { + return selectedPod; + } + + return undefined; + }, + ...deps, }); }; -const getFewPodsTabData = (tabId: TabId): LogTabViewModel => { +const getFewPodsTabData = (tabId: TabId, deps: Partial = {}): LogTabViewModel => { const selectedPod = new Pod(deploymentPod1); - const anotherPod = new Pod(dockerPod); + const anotherPod = new Pod(deploymentPod2); return mockLogTabViewModel(tabId, { getLogTabData: () => ({ - pods: [selectedPod, anotherPod], - selectedPod, - selectedContainer: selectedPod.getContainers()[0], + owner: { + uid: "uuid", + kind: "Deployment", + name: "super-deployment", + }, + selectedPodId: selectedPod.getId(), + selectedContainer: selectedPod.getContainers()[0].name, + namespace: selectedPod.getNs(), + showPrevious: false, + showTimestamps: false, }), + getPodById: (id) => { + if (id === selectedPod.getId()) { + return selectedPod; + } + + if (id === anotherPod.getId()) { + return anotherPod; + } + + return undefined; + }, + getPodsByOwnerId: (id) => { + if (id === "uuid") { + return [selectedPod, anotherPod]; + } + + return []; + }, + ...deps, }); }; @@ -109,14 +147,14 @@ describe("", () => { it("renders w/o errors", () => { const model = getOnePodViewModel("foobar"); - const { container } = render(getComponent(model)); + const { container } = render(); expect(container).toBeInstanceOf(HTMLElement); }); it("renders proper namespace", async () => { const model = getOnePodViewModel("foobar"); - const { findByTestId } = render(getComponent(model)); + const { findByTestId } = render(); const ns = await findByTestId("namespace-badge"); expect(ns).toHaveTextContent("default"); @@ -124,7 +162,7 @@ describe("", () => { it("renders proper selected items within dropdowns", async () => { const model = getOnePodViewModel("foobar"); - const { findByText } = render(getComponent(model)); + const { findByText } = render(); expect(await findByText("dockerExporter")).toBeInTheDocument(); expect(await findByText("docker-exporter")).toBeInTheDocument(); @@ -132,33 +170,40 @@ describe("", () => { it("renders sibling pods in dropdown", async () => { const model = getFewPodsTabData("foobar"); - const { container, findByText } = render(getComponent(model)); + const { container, findByText } = render(); selectEvent.openMenu(container.querySelector(".pod-selector")); - - expect(await findByText("dockerExporter", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument(); + expect(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument(); expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument(); }); it("renders sibling containers in dropdown", async () => { const model = getFewPodsTabData("foobar"); - const { findByText, container } = render(getComponent(model)); - const containerSelector: HTMLElement = container.querySelector(".container-selector"); - - selectEvent.openMenu(containerSelector); + const { findByText, container } = render(); + selectEvent.openMenu(container.querySelector(".container-selector")); expect(await findByText("node-exporter-1")).toBeInTheDocument(); expect(await findByText("init-node-exporter")).toBeInTheDocument(); expect(await findByText("init-node-exporter-1")).toBeInTheDocument(); }); - it("renders pod owner as dropdown title", async () => { + it("renders pod owner as badge", async () => { const model = getFewPodsTabData("foobar"); - const { findByText, container } = render(getComponent(model)); - const podSelector: HTMLElement = container.querySelector(".pod-selector"); + const { findByText } = render(); - selectEvent.openMenu(podSelector); + expect(await findByText("super-deployment", { + exact: false, + })).toBeInTheDocument(); + }); - expect(await findByText("super-deployment")).toBeInTheDocument(); + it("updates tab name if selected pod changes", async () => { + const renameTab = jest.fn(); + const model = getFewPodsTabData("foobar", { renameTab }); + const { findByText, container } = render(); + + selectEvent.openMenu(container.querySelector(".pod-selector")); + + userEvent.click(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" })); + expect(renameTab).toBeCalledWith("foobar", "Pod deploymentPod2"); }); }); diff --git a/src/renderer/components/dock/logs/__test__/log-search.test.tsx b/src/renderer/components/dock/logs/__test__/log-search.test.tsx new file mode 100644 index 0000000000..24c2001eec --- /dev/null +++ b/src/renderer/components/dock/logs/__test__/log-search.test.tsx @@ -0,0 +1,147 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { Pod } from "../../../../../common/k8s-api/endpoints"; +import { dockerPod } from "./pod.mock"; +import { getDiForUnitTesting } from "../../../../getDiForUnitTesting"; +import type { DiRender } from "../../../test-utils/renderFor"; +import { renderFor } from "../../../test-utils/renderFor"; +import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model"; +import type { TabId } from "../../dock/store"; +import { LogSearch } from "../search"; +import userEvent from "@testing-library/user-event"; +import { SearchStore } from "../../../../search-store/search-store"; + +function mockLogTabViewModel(tabId: TabId, deps: Partial): LogTabViewModel { + return new LogTabViewModel(tabId, { + getLogs: jest.fn(), + getLogsWithoutTimestamps: jest.fn(), + getTimestampSplitLogs: jest.fn(), + getLogTabData: jest.fn(), + setLogTabData: jest.fn(), + loadLogs: jest.fn(), + reloadLogs: jest.fn(), + renameTab: jest.fn(), + stopLoadingLogs: jest.fn(), + getPodById: jest.fn(), + getPodsByOwnerId: jest.fn(), + areLogsPresent: jest.fn(), + searchStore: new SearchStore(), + ...deps, + }); +} + +const getOnePodViewModel = (tabId: TabId, deps: Partial = {}): LogTabViewModel => { + const selectedPod = new Pod(dockerPod); + + return mockLogTabViewModel(tabId, { + getLogTabData: () => ({ + selectedPodId: selectedPod.getId(), + selectedContainer: selectedPod.getContainers()[0].name, + namespace: selectedPod.getNs(), + showPrevious: false, + showTimestamps: false, + }), + getPodById: (id) => { + if (id === selectedPod.getId()) { + return selectedPod; + } + + return undefined; + }, + ...deps, + }); +}; + +describe("LogSearch tests", () => { + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + + await di.runSetups(); + }); + + it("renders w/o errors", () => { + const model = getOnePodViewModel("foobar"); + const { container } = render( + , + ); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("should scroll to new active overlay when clicking the previous button", async () => { + const scrollToOverlay = jest.fn(); + const model = getOnePodViewModel("foobar", { + getLogsWithoutTimestamps: () => [ + "hello", + "world", + ], + }); + + render( + , + ); + + userEvent.click(await screen.findByPlaceholderText("Search...")); + userEvent.keyboard("o"); + userEvent.click(await screen.findByText("keyboard_arrow_up")); + expect(scrollToOverlay).toBeCalled(); + }); + + it("should scroll to new active overlay when clicking the next button", async () => { + const scrollToOverlay = jest.fn(); + const model = getOnePodViewModel("foobar", { + getLogsWithoutTimestamps: () => [ + "hello", + "world", + ], + }); + + render( + , + ); + + userEvent.click(await screen.findByPlaceholderText("Search...")); + userEvent.keyboard("o"); + userEvent.click(await screen.findByText("keyboard_arrow_down")); + expect(scrollToOverlay).toBeCalled(); + }); + + it("next and previous should be disabled initially", async () => { + const scrollToOverlay = jest.fn(); + const model = getOnePodViewModel("foobar", { + getLogsWithoutTimestamps: () => [ + "hello", + "world", + ], + }); + + render( + , + ); + + userEvent.click(await screen.findByText("keyboard_arrow_down")); + userEvent.click(await screen.findByText("keyboard_arrow_up")); + expect(scrollToOverlay).not.toBeCalled(); + }); +}); diff --git a/src/renderer/components/dock/logs/__test__/log-tab.store.test.ts b/src/renderer/components/dock/logs/__test__/log-tab.store.test.ts deleted file mode 100644 index b98b46daf9..0000000000 --- a/src/renderer/components/dock/logs/__test__/log-tab.store.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { podsStore } from "../../../+workloads-pods/pods.store"; -import { UserStore } from "../../../../../common/user-store"; -import { Pod } from "../../../../../common/k8s-api/endpoints"; -import { ThemeStore } from "../../../../theme.store"; -import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock"; -import { mockWindow } from "../../../../../../__mocks__/windowMock"; -import { getDiForUnitTesting } from "../../../../getDiForUnitTesting"; -import logTabStoreInjectable from "../tab-store.injectable"; -import type { LogTabStore } from "../tab.store"; -import dockStoreInjectable from "../../dock-store/dock-store.injectable"; -import type { DockStore } from "../../dock-store/dock.store"; -import directoryForUserDataInjectable - from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import mockFs from "mock-fs"; - -mockWindow(); - -podsStore.items.push(new Pod(dockerPod)); -podsStore.items.push(new Pod(deploymentPod1)); -podsStore.items.push(new Pod(deploymentPod2)); - -describe("log tab store", () => { - let logTabStore: LogTabStore; - let dockStore: DockStore; - - beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); - - mockFs(); - - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - - await di.runSetups(); - - dockStore = di.inject(dockStoreInjectable); - logTabStore = di.inject(logTabStoreInjectable); - - UserStore.createInstance(); - ThemeStore.createInstance(); - }); - - afterEach(() => { - UserStore.resetInstance(); - ThemeStore.resetInstance(); - mockFs.restore(); - }); - - it("creates log tab without sibling pods", () => { - const selectedPod = new Pod(dockerPod); - const selectedContainer = selectedPod.getAllContainers()[0]; - - logTabStore.createPodTab({ - selectedPod, - selectedContainer, - }); - - expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ - pods: [selectedPod], - selectedPod, - selectedContainer, - showTimestamps: false, - previous: false, - }); - }); - - it("creates log tab with sibling pods", () => { - const selectedPod = new Pod(deploymentPod1); - const siblingPod = new Pod(deploymentPod2); - const selectedContainer = selectedPod.getInitContainers()[0]; - - logTabStore.createPodTab({ - selectedPod, - selectedContainer, - }); - - expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ - pods: [selectedPod, siblingPod], - selectedPod, - selectedContainer, - showTimestamps: false, - previous: false, - }); - }); - - it("removes item from pods list if pod deleted from store", () => { - const selectedPod = new Pod(deploymentPod1); - const selectedContainer = selectedPod.getInitContainers()[0]; - - logTabStore.createPodTab({ - selectedPod, - selectedContainer, - }); - - podsStore.items.pop(); - - expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ - pods: [selectedPod], - selectedPod, - selectedContainer, - showTimestamps: false, - previous: false, - }); - }); - - it("adds item into pods list if new sibling pod added to store", () => { - const selectedPod = new Pod(deploymentPod1); - const selectedContainer = selectedPod.getInitContainers()[0]; - - logTabStore.createPodTab({ - selectedPod, - selectedContainer, - }); - - podsStore.items.push(new Pod(deploymentPod3)); - - expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ - pods: [selectedPod, deploymentPod3], - selectedPod, - selectedContainer, - showTimestamps: false, - previous: false, - }); - }); - - // FIXME: this is failed when it's not .only == depends on something above - it.only("closes tab if no pods left in store", async () => { - const selectedPod = new Pod(deploymentPod1); - const selectedContainer = selectedPod.getInitContainers()[0]; - - const id = logTabStore.createPodTab({ - selectedPod, - selectedContainer, - }); - - podsStore.items.clear(); - - expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined(); - expect(logTabStore.getData(id)).toBeUndefined(); - expect(dockStore.getTabById(id)).toBeUndefined(); - }); -}); diff --git a/src/renderer/components/dock/logs/are-logs-present.injectable.ts b/src/renderer/components/dock/logs/are-logs-present.injectable.ts new file mode 100644 index 0000000000..f4644c8229 --- /dev/null +++ b/src/renderer/components/dock/logs/are-logs-present.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import logStoreInjectable from "./store.injectable"; + +const areLogsPresentInjectable = getInjectable({ + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return (tabId: TabId) => logStore.areLogsPresent(tabId); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default areLogsPresentInjectable; diff --git a/src/renderer/components/dock/logs/clear-log-tab-data.injectable.ts b/src/renderer/components/dock/logs/clear-log-tab-data.injectable.ts new file mode 100644 index 0000000000..0bc007bdf4 --- /dev/null +++ b/src/renderer/components/dock/logs/clear-log-tab-data.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import logTabStoreInjectable from "./tab-store.injectable"; + +const clearLogTabDataInjectable = getInjectable({ + instantiate: (di) => { + const logTabStore = di.inject(logTabStoreInjectable); + + return (tabId: TabId): void => { + logTabStore.clearData(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clearLogTabDataInjectable; diff --git a/src/renderer/components/dock/logs/controls.tsx b/src/renderer/components/dock/logs/controls.tsx index 954ccb8478..59aead384b 100644 --- a/src/renderer/components/dock/logs/controls.tsx +++ b/src/renderer/components/dock/logs/controls.tsx @@ -8,17 +8,22 @@ import "./controls.scss"; import React from "react"; import { observer } from "mobx-react"; -import { Pod } from "../../../../common/k8s-api/endpoints"; -import { cssNames, saveFileDialog } from "../../../utils"; +import { cssNames } from "../../../utils"; import { Checkbox } from "../../checkbox"; import { Icon } from "../../icon"; import type { LogTabViewModel } from "./logs-view-model"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import openSaveFileDialogInjectable from "../../../utils/save-file.injectable"; export interface LogControlsProps { model: LogTabViewModel; } -export const LogControls = observer(({ model }: LogControlsProps) => { +interface Dependencies { + openSaveFileDialog: (filename: string, contents: BlobPart | BlobPart[], type: string) => void; +} + +const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependencies & LogControlsProps) => { const tabData = model.logTabData.get(); if (!tabData) { @@ -26,26 +31,25 @@ export const LogControls = observer(({ model }: LogControlsProps) => { } const logs = model.timestampSplitLogs.get(); - const { showTimestamps, previous } = tabData; + const { showTimestamps, showPrevious: previous } = tabData; const since = logs.length ? logs[0][0] : null; - const pod = new Pod(tabData.selectedPod); const toggleTimestamps = () => { model.updateLogTabData({ showTimestamps: !showTimestamps }); }; const togglePrevious = () => { - model.updateLogTabData({ previous: !previous }); + model.updateLogTabData({ showPrevious: !previous }); model.reloadLogs(); }; const downloadLogs = () => { - const fileName = pod.getName(); + const fileName = model.pod.get().getName(); const logsToDownload: string[] = showTimestamps ? model.logs.get() : model.logsWithoutTimestamps.get(); - saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain"); + openSaveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain"); }; return ( @@ -81,3 +85,10 @@ export const LogControls = observer(({ model }: LogControlsProps) => { ); }); + +export const LogControls = withInjectables(NonInjectedLogControls, { + getProps: (di, props) => ({ + openSaveFileDialog: di.inject(openSaveFileDialogInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/dock/logs/create-logs-tab.injectable.ts b/src/renderer/components/dock/logs/create-logs-tab.injectable.ts new file mode 100644 index 0000000000..ddef5c16d7 --- /dev/null +++ b/src/renderer/components/dock/logs/create-logs-tab.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { DockTabCreate, DockTab, TabKind, TabId } from "../dock/store"; +import type { LogTabData } from "./tab-store"; +import * as uuid from "uuid"; +import { runInAction } from "mobx"; +import createDockTabInjectable from "../dock/create-dock-tab.injectable"; +import setLogTabDataInjectable from "./set-log-tab-data.injectable"; + +export type CreateLogsTabData = Pick & Omit, "owner" | "selectedPodId" | "selectedContainer" | "namespace">; + +interface Dependencies { + createDockTab: (rawTabDesc: DockTabCreate, addNumber?: boolean) => DockTab; + setLogTabData: (tabId: string, data: LogTabData) => void; +} + +const createLogsTab = ({ createDockTab, setLogTabData }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => { + const id = `log-tab-${uuid.v4()}`; + + runInAction(() => { + createDockTab({ + id, + title, + kind: TabKind.POD_LOGS, + }, false); + setLogTabData(id, { + showTimestamps: false, + showPrevious: false, + ...data, + }); + }); + + return id; +}; + +const createLogsTabInjectable = getInjectable({ + instantiate: (di) => createLogsTab({ + createDockTab: di.inject(createDockTabInjectable), + setLogTabData: di.inject(setLogTabDataInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createLogsTabInjectable; diff --git a/src/renderer/components/dock/logs/create-pod-logs-tab.injectable.ts b/src/renderer/components/dock/logs/create-pod-logs-tab.injectable.ts new file mode 100644 index 0000000000..2d9c3f981d --- /dev/null +++ b/src/renderer/components/dock/logs/create-pod-logs-tab.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints"; +import type { TabId } from "../dock/store"; +import createLogsTabInjectable from "./create-logs-tab.injectable"; + +export interface PodLogsTabData { + selectedPod: Pod; + selectedContainer: IPodContainer; +} + +const createPodLogsTabInjectable = getInjectable({ + instantiate: (di) => { + const createLogsTab = di.inject(createLogsTabInjectable); + + return ({ selectedPod, selectedContainer }: PodLogsTabData): TabId => + createLogsTab(`Pod ${selectedPod.getName()}`, { + owner: selectedPod.getOwnerRefs()[0], + namespace: selectedPod.getNs(), + selectedContainer: selectedContainer.name, + selectedPodId: selectedPod.getId(), + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createPodLogsTabInjectable; diff --git a/src/renderer/components/dock/logs/create-workload-logs-tab.injectable.ts b/src/renderer/components/dock/logs/create-workload-logs-tab.injectable.ts new file mode 100644 index 0000000000..e8e2eb41ed --- /dev/null +++ b/src/renderer/components/dock/logs/create-workload-logs-tab.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { podsStore } from "../../+workloads-pods/pods.store"; +import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object"; +import type { TabId } from "../dock/store"; +import createLogsTabInjectable, { CreateLogsTabData } from "./create-logs-tab.injectable"; + +export interface WorkloadLogsTabData { + workload: WorkloadKubeObject +} + +interface Dependencies { + createLogsTab: (title: string, data: CreateLogsTabData) => TabId; +} + +const createWorkloadLogsTab = ({ createLogsTab }: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => { + const pods = podsStore.getPodsByOwnerId(workload.getId()); + + if (pods.length === 0) { + return undefined; + } + + const selectedPod = pods[0]; + + return createLogsTab(`${workload.kind} ${selectedPod.getName()}`, { + selectedContainer: selectedPod.getAllContainers()[0].name, + selectedPodId: selectedPod.getId(), + namespace: selectedPod.getNs(), + owner: { + kind: workload.kind, + name: workload.getName(), + uid: workload.getId(), + }, + }); +}; + +const createWorkloadLogsTabInjectable = getInjectable({ + instantiate: (di) => createWorkloadLogsTab({ + createLogsTab: di.inject(createLogsTabInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createWorkloadLogsTabInjectable; diff --git a/src/renderer/components/dock/logs/dock-tab.tsx b/src/renderer/components/dock/logs/dock-tab.tsx deleted file mode 100644 index 1ae62b4fe6..0000000000 --- a/src/renderer/components/dock/logs/dock-tab.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { boundMethod } from "../../../utils"; -import { InfoPanel } from "../info-panel"; -import { LogResourceSelector } from "./resource-selector"; -import { LogList } from "./list"; -import { LogSearch } from "./search"; -import { LogControls } from "./controls"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import logsViewModelInjectable from "./logs-view-model.injectable"; -import type { LogTabViewModel } from "./logs-view-model"; -import type { DockTab } from "../dock-store/dock.store"; - -export interface LogsDockTabProps { - className?: string; - tab: DockTab; -} - -interface Dependencies { - model: LogTabViewModel; -} - -@observer -class NonInjectedLogsDockTab extends React.Component { - private logListElement = React.createRef(); // A reference for VirtualList component - - componentDidMount(): void { - this.props.model.reloadLogs(); - } - - componentWillUnmount(): void { - this.props.model.stopLoadingLogs(); - } - - /** - * Scrolling to active overlay (search word highlight) - */ - @boundMethod - scrollToOverlay() { - const { activeOverlayLine } = this.props.model.searchStore; - - if (!this.logListElement.current || activeOverlayLine === undefined) return; - // Scroll vertically - this.logListElement.current.scrollToItem(activeOverlayLine, "center"); - // Scroll horizontally in timeout since virtual list need some time to prepare its contents - setTimeout(() => { - const overlay = document.querySelector(".PodLogs .list span.active"); - - if (!overlay) return; - overlay.scrollIntoViewIfNeeded(); - }, 100); - } - - render() { - const { model, tab } = this.props; - const { logTabData } = model; - const data = logTabData.get(); - - if (!data) { - return null; - } - - return ( -
- - - -
- )} - showSubmitClose={false} - showButtons={false} - showStatusPanel={false} - /> - - - - ); - } -} - -export const LogsDockTab = withInjectables(NonInjectedLogsDockTab, { - getProps: (di, props) => ({ - model: di.inject(logsViewModelInjectable, { - tabId: props.tab.id, - }), - ...props, - }), -}); diff --git a/src/renderer/components/dock/logs/get-log-tab-data.injectable.ts b/src/renderer/components/dock/logs/get-log-tab-data.injectable.ts index e706fdaafb..35fecebb3a 100644 --- a/src/renderer/components/dock/logs/get-log-tab-data.injectable.ts +++ b/src/renderer/components/dock/logs/get-log-tab-data.injectable.ts @@ -3,10 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { LogTabData } from "./tab-store"; import logTabStoreInjectable from "./tab-store.injectable"; const getLogTabDataInjectable = getInjectable({ - instantiate: (di) => di.inject(logTabStoreInjectable).getData, + instantiate: (di) => { + const logTabStore = di.inject(logTabStoreInjectable); + + return (tabId: string): LogTabData => logTabStore.getData(tabId); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/get-logs-without-timestamps.injectable.ts b/src/renderer/components/dock/logs/get-logs-without-timestamps.injectable.ts index 1be01ff443..250039d7e1 100644 --- a/src/renderer/components/dock/logs/get-logs-without-timestamps.injectable.ts +++ b/src/renderer/components/dock/logs/get-logs-without-timestamps.injectable.ts @@ -6,7 +6,13 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import logStoreInjectable from "./store.injectable"; const getLogsWithoutTimestampsInjectable = getInjectable({ - instantiate: (di) => di.inject(logStoreInjectable).getLogsWithoutTimestampsByTabId, + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return (tabId: string): string[] => + logStore.getLogsWithoutTimestamps(tabId); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/get-logs.injectable.ts b/src/renderer/components/dock/logs/get-logs.injectable.ts index 659a87c46f..acd6d10137 100644 --- a/src/renderer/components/dock/logs/get-logs.injectable.ts +++ b/src/renderer/components/dock/logs/get-logs.injectable.ts @@ -6,7 +6,12 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import logStoreInjectable from "./store.injectable"; const getLogsInjectable = getInjectable({ - instantiate: (di) => di.inject(logStoreInjectable).getLogsByTabId, + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return (tabId: string): string[] => logStore.getLogs(tabId); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/get-timestamp-split-logs.injectable.ts b/src/renderer/components/dock/logs/get-timestamp-split-logs.injectable.ts index 7cdac68327..4d2189d27e 100644 --- a/src/renderer/components/dock/logs/get-timestamp-split-logs.injectable.ts +++ b/src/renderer/components/dock/logs/get-timestamp-split-logs.injectable.ts @@ -6,7 +6,13 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import logStoreInjectable from "./store.injectable"; const getTimestampSplitLogsInjectable = getInjectable({ - instantiate: (di) => di.inject(logStoreInjectable).getTimestampSplitLogsByTabId, + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return (tabId: string): [string, string][] => + logStore.getTimestampSplitLogs(tabId); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/is-logs-tab-data-valid.injectable.ts b/src/renderer/components/dock/logs/is-logs-tab-data-valid.injectable.ts new file mode 100644 index 0000000000..03062ecaae --- /dev/null +++ b/src/renderer/components/dock/logs/is-logs-tab-data-valid.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import logTabStoreInjectable from "./tab-store.injectable"; + +const isLogsTabDataValidInjectable = getInjectable({ + instantiate: (di) => { + const logTabStore = di.inject(logTabStoreInjectable); + + return (tabId: TabId) => logTabStore.isDataValid(tabId); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default isLogsTabDataValidInjectable; diff --git a/src/renderer/components/dock/logs/list.tsx b/src/renderer/components/dock/logs/list.tsx index b9274087b4..273a6ee32f 100644 --- a/src/renderer/components/dock/logs/list.tsx +++ b/src/renderer/components/dock/logs/list.tsx @@ -19,6 +19,7 @@ import { array, boundMethod, cssNames } from "../../../utils"; import { VirtualList } from "../../virtual-list"; import { ToBottom } from "./to-bottom"; import type { LogTabViewModel } from "../logs/logs-view-model"; +import { Spinner } from "../../spinner"; export interface LogListProps { model: LogTabViewModel; @@ -210,12 +211,18 @@ export class LogList extends React.Component { }; render() { - const rowHeights = array.filled(this.logs.length, this.lineHeight); + if (this.props.model.isLoading.get()) { + return ( +
+ +
+ ); + } if (!this.logs.length) { return (
- There are no logs available for container + There are no logs available for container {this.props.model.logTabData.get()?.selectedContainer}
); } @@ -224,7 +231,7 @@ export class LogList extends React.Component {
di.inject(logStoreInjectable).load, + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return ( + tabId: string, + pod: IComputedValue, + logTabData: IComputedValue, + ): Promise => logStore.load(tabId, pod, logTabData); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/log-tab-data.validator.ts b/src/renderer/components/dock/logs/log-tab-data.validator.ts new file mode 100644 index 0000000000..32baf38fbb --- /dev/null +++ b/src/renderer/components/dock/logs/log-tab-data.validator.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import Joi from "joi"; +import type { LogTabData, LogTabOwnerRef } from "./tab-store"; + +export const logTabDataValidator = Joi.object({ + owner: Joi + .object({ + uid: Joi + .string() + .required(), + name: Joi + .string() + .required(), + kind: Joi + .string() + .required(), + }) + .unknown(true) + .optional(), + selectedPodId: Joi + .string() + .required(), + namespace: Joi + .string() + .required(), + selectedContainer: Joi + .string() + .optional(), + showTimestamps: Joi + .boolean() + .required(), + showPrevious: Joi + .boolean() + .required(), +}); diff --git a/src/renderer/components/dock/logs/logs-view-model.injectable.ts b/src/renderer/components/dock/logs/logs-view-model.injectable.ts index 7516e47124..d26a34d583 100644 --- a/src/renderer/components/dock/logs/logs-view-model.injectable.ts +++ b/src/renderer/components/dock/logs/logs-view-model.injectable.ts @@ -4,16 +4,19 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { LogTabViewModel } from "./logs-view-model"; -import type { TabId } from "../dock-store/dock.store"; +import type { TabId } from "../dock/store"; import getLogsInjectable from "./get-logs.injectable"; import getLogsWithoutTimestampsInjectable from "./get-logs-without-timestamps.injectable"; import getTimestampSplitLogsInjectable from "./get-timestamp-split-logs.injectable"; -import reloadLoadsInjectable from "./reload-logs.injectable"; +import reloadLogsInjectable from "./reload-logs.injectable"; import getLogTabDataInjectable from "./get-log-tab-data.injectable"; import loadLogsInjectable from "./load-logs.injectable"; import setLogTabDataInjectable from "./set-log-tab-data.injectable"; -import updateTabNameInjectable from "./update-tab-name.injectable"; import stopLoadingLogsInjectable from "./stop-loading-logs.injectable"; +import { podsStore } from "../../+workloads-pods/pods.store"; +import renameTabInjectable from "../dock/rename-tab.injectable"; +import areLogsPresentInjectable from "./are-logs-present.injectable"; +import searchStoreInjectable from "../../../search-store/search-store.injectable"; export interface InstantiateArgs { tabId: TabId; @@ -24,12 +27,16 @@ const logsViewModelInjectable = getInjectable({ getLogs: di.inject(getLogsInjectable), getLogsWithoutTimestamps: di.inject(getLogsWithoutTimestampsInjectable), getTimestampSplitLogs: di.inject(getTimestampSplitLogsInjectable), - reloadLogs: di.inject(reloadLoadsInjectable), + reloadLogs: di.inject(reloadLogsInjectable), getLogTabData: di.inject(getLogTabDataInjectable), setLogTabData: di.inject(setLogTabDataInjectable), loadLogs: di.inject(loadLogsInjectable), - updateTabName: di.inject(updateTabNameInjectable), + renameTab: di.inject(renameTabInjectable), stopLoadingLogs: di.inject(stopLoadingLogsInjectable), + areLogsPresent: di.inject(areLogsPresentInjectable), + getPodById: id => podsStore.getById(id), + getPodsByOwnerId: id => podsStore.getPodsByOwnerId(id), + searchStore: di.inject(searchStoreInjectable), }), lifecycle: lifecycleEnum.transient, }); diff --git a/src/renderer/components/dock/logs/logs-view-model.ts b/src/renderer/components/dock/logs/logs-view-model.ts index 6c2f182355..73ed0c770f 100644 --- a/src/renderer/components/dock/logs/logs-view-model.ts +++ b/src/renderer/components/dock/logs/logs-view-model.ts @@ -2,10 +2,11 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { LogTabData } from "./tab.store"; +import type { LogTabData } from "./tab-store"; import { computed, IComputedValue } from "mobx"; -import type { TabId } from "../dock-store/dock.store"; -import { SearchStore } from "../../../search-store/search-store"; +import type { TabId } from "../dock/store"; +import type { SearchStore } from "../../../search-store/search-store"; +import type { Pod } from "../../../../common/k8s-api/endpoints"; export interface LogTabViewModelDependencies { getLogs: (tabId: TabId) => string[]; @@ -13,27 +14,57 @@ export interface LogTabViewModelDependencies { getTimestampSplitLogs: (tabId: TabId) => [string, string][]; getLogTabData: (tabId: TabId) => LogTabData; setLogTabData: (tabId: TabId, data: LogTabData) => void; - loadLogs: (tabId: TabId, logTabData: IComputedValue) => Promise; - reloadLogs: (tabId: TabId, logTabData: IComputedValue) => Promise; - updateTabName: (tabId: TabId) => void; + loadLogs: (tabId: TabId, pod: IComputedValue, logTabData: IComputedValue) => Promise; + reloadLogs: (tabId: TabId, pod: IComputedValue, logTabData: IComputedValue) => Promise; + renameTab: (tabId: TabId, title: string) => void; stopLoadingLogs: (tabId: TabId) => void; + getPodById: (id: string) => Pod | undefined; + getPodsByOwnerId: (id: string) => Pod[]; + areLogsPresent: (tabId: TabId) => boolean; + searchStore: SearchStore; } export class LogTabViewModel { constructor(protected readonly tabId: TabId, private readonly dependencies: LogTabViewModelDependencies) {} + get searchStore() { + return this.dependencies.searchStore; + } + + readonly isLoading = computed(() => this.dependencies.areLogsPresent(this.tabId)); readonly logs = computed(() => this.dependencies.getLogs(this.tabId)); readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId)); readonly timestampSplitLogs = computed(() => this.dependencies.getTimestampSplitLogs(this.tabId)); readonly logTabData = computed(() => this.dependencies.getLogTabData(this.tabId)); - readonly searchStore = new SearchStore(); + readonly pods = computed(() => { + const data = this.logTabData.get(); + + if (!data) { + return []; + } + + if (typeof data.owner?.uid === "string") { + return this.dependencies.getPodsByOwnerId(data.owner.uid); + } + + return [this.dependencies.getPodById(data.selectedPodId)]; + }); + readonly pod = computed(() => { + const data = this.logTabData.get(); + + if (!data) { + return undefined; + } + + return this.dependencies.getPodById(data.selectedPodId); + }); updateLogTabData = (partialData: Partial) => { this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData }); }; - loadLogs = () => this.dependencies.loadLogs(this.tabId, this.logTabData); - reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.logTabData); - updateTabName = () => this.dependencies.updateTabName(this.tabId); + loadLogs = () => this.dependencies.loadLogs(this.tabId, this.pod, this.logTabData); + reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData); + renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title); stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId); } diff --git a/src/renderer/components/dock/logs/reload-logs.injectable.ts b/src/renderer/components/dock/logs/reload-logs.injectable.ts index 9fa917bf4b..4ba55c4b58 100644 --- a/src/renderer/components/dock/logs/reload-logs.injectable.ts +++ b/src/renderer/components/dock/logs/reload-logs.injectable.ts @@ -3,11 +3,23 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import type { Pod } from "../../../../common/k8s-api/endpoints"; import logStoreInjectable from "./store.injectable"; +import type { LogTabData } from "./tab-store"; + +const reloadLogsInjectable = getInjectable({ + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return ( + tabId: string, + pod: IComputedValue, + logTabData: IComputedValue, + ): Promise => logStore.reload(tabId, pod, logTabData); + }, -const reloadLoadsInjectable = getInjectable({ - instantiate: (di) => di.inject(logStoreInjectable).reload, lifecycle: lifecycleEnum.singleton, }); -export default reloadLoadsInjectable; +export default reloadLogsInjectable; diff --git a/src/renderer/components/dock/logs/resource-selector.tsx b/src/renderer/components/dock/logs/resource-selector.tsx index bca2c30f45..459069fd61 100644 --- a/src/renderer/components/dock/logs/resource-selector.tsx +++ b/src/renderer/components/dock/logs/resource-selector.tsx @@ -5,19 +5,25 @@ import "./resource-selector.scss"; -import React, { useEffect } from "react"; +import React from "react"; import { observer } from "mobx-react"; -import { Pod } from "../../../../common/k8s-api/endpoints"; import { Badge } from "../../badge"; import { Select, SelectOption } from "../../select"; -import { podsStore } from "../../+workloads-pods/pods.store"; import type { LogTabViewModel } from "./logs-view-model"; +import type { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints"; export interface LogResourceSelectorProps { model: LogTabViewModel; } +function getSelectOptions(containers: IPodContainer[]): SelectOption[] { + return containers.map(container => ({ + value: container.name, + label: container.name, + })); +} + export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps) => { const tabData = model.logTabData.get(); @@ -25,66 +31,61 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps return null; } - const { selectedPod, selectedContainer, pods } = tabData; - const pod = new Pod(selectedPod); - const containers = pod.getContainers(); - const initContainers = pod.getInitContainers(); + const { selectedContainer, owner } = tabData; + const pods = model.pods.get(); + const pod = model.pod.get(); - const onContainerChange = (option: SelectOption) => { + if (!pod) { + return null; + } + + const onContainerChange = (option: SelectOption) => { model.updateLogTabData({ - selectedContainer: containers - .concat(initContainers) - .find(container => container.name === option.value), + selectedContainer: option.value, }); - model.reloadLogs(); }; - const onPodChange = (option: SelectOption) => { - const selectedPod = podsStore.getByName(option.value, pod.getNs()); - - model.updateLogTabData({ selectedPod }); - model.updateTabName(); - }; - - const getSelectOptions = (items: string[]) => { - return items.map(item => { - return { - value: item, - label: item, - }; + const onPodChange = ({ value }: SelectOption) => { + model.updateLogTabData({ + selectedPodId: value.getId(), + selectedContainer: value.getAllContainers()[0]?.name, }); + model.renameTab(`Pod ${value.getName()}`); + model.reloadLogs(); }; const containerSelectOptions = [ { - label: `Containers`, - options: getSelectOptions(containers.map(container => container.name)), + label: "Containers", + options: getSelectOptions(pod.getContainers()), }, { - label: `Init Containers`, - options: getSelectOptions(initContainers.map(container => container.name)), + label: "Init Containers", + options: getSelectOptions(pod.getInitContainers()), }, ]; - const podSelectOptions = [ - { - label: pod.getOwnerRefs()[0]?.name, - options: getSelectOptions(pods.map(pod => pod.metadata.name)), - }, - ]; - - useEffect(() => { - model.reloadLogs(); - }, [selectedPod]); + const podSelectOptions = pods.map(pod => ({ + label: pod.getName(), + value: pod, + })); return (
Namespace + { + owner && ( + <> + Owner + + ) + } Pod void; - toPrevOverlay: () => void; - toNextOverlay: () => void; + onSearch?: (query: string) => void; + scrollToOverlay: (lineNumber: number | undefined) => void; model: LogTabViewModel; } - -export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, model }: PodLogSearchProps) => { - const tabData = model.logTabData.get(); +export const LogSearch = observer(({ onSearch, scrollToOverlay, model: { logTabData, searchStore, ...model }}: PodLogSearchProps) => { + const tabData = logTabData.get(); if (!tabData) { return null; @@ -29,27 +27,23 @@ export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, mod const logs = tabData.showTimestamps ? model.logs.get() : model.logsWithoutTimestamps.get(); - const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = model.searchStore; + const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore; const jumpDisabled = !searchQuery || !occurrences.length; - const findCounts = ( -
- {activeFind}/{totalFinds} -
- ); const setSearch = (query: string) => { - model.searchStore.onSearch(logs, query); - onSearch(query); + searchStore.onSearch(logs, query); + onSearch?.(query); + scrollToOverlay(searchStore.activeOverlayLine); }; const onPrevOverlay = () => { setPrevOverlayActive(); - toPrevOverlay(); + scrollToOverlay(searchStore.activeOverlayLine); }; const onNextOverlay = () => { setNextOverlayActive(); - toNextOverlay(); + scrollToOverlay(searchStore.activeOverlayLine); }; const onClear = () => { @@ -58,13 +52,17 @@ export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, mod const onKeyDown = (evt: React.KeyboardEvent) => { if (evt.key === "Enter") { - onNextOverlay(); + if (evt.shiftKey) { + onPrevOverlay(); + } else { + onNextOverlay(); + } } }; useEffect(() => { // Refresh search when logs changed - model.searchStore.onSearch(logs); + searchStore.onSearch(logs); }, [logs]); return ( @@ -73,7 +71,11 @@ export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, mod value={searchQuery} onChange={setSearch} showClearIcon={true} - contentRight={totalFinds > 0 && findCounts} + contentRight={totalFinds > 0 && ( +
+ {activeFind}/{totalFinds} +
+ )} onClear={onClear} onKeyDown={onKeyDown} /> diff --git a/src/renderer/components/dock/logs/set-log-tab-data.injectable.ts b/src/renderer/components/dock/logs/set-log-tab-data.injectable.ts index ac783dfcc7..db84635bf1 100644 --- a/src/renderer/components/dock/logs/set-log-tab-data.injectable.ts +++ b/src/renderer/components/dock/logs/set-log-tab-data.injectable.ts @@ -3,10 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { LogTabData } from "./tab-store"; import logTabStoreInjectable from "./tab-store.injectable"; const setLogTabDataInjectable = getInjectable({ - instantiate: (di) => di.inject(logTabStoreInjectable).setData, + instantiate: (di) => { + const logTabStore = di.inject(logTabStoreInjectable); + + return (tabId: string, data: LogTabData): void => logTabStore.setData(tabId, data); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/stop-loading-logs.injectable.ts b/src/renderer/components/dock/logs/stop-loading-logs.injectable.ts index afe2542083..db0df464cf 100644 --- a/src/renderer/components/dock/logs/stop-loading-logs.injectable.ts +++ b/src/renderer/components/dock/logs/stop-loading-logs.injectable.ts @@ -6,7 +6,12 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import logStoreInjectable from "./store.injectable"; const stopLoadingLogsInjectable = getInjectable({ - instantiate: (di) => di.inject(logStoreInjectable).stopLoadingLogs, + instantiate: (di) => { + const logStore = di.inject(logStoreInjectable); + + return (tabId: string): void => logStore.stopLoadingLogs(tabId); + }, + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/logs/store.ts b/src/renderer/components/dock/logs/store.ts index 7c0116252f..fb0c446119 100644 --- a/src/renderer/components/dock/logs/store.ts +++ b/src/renderer/components/dock/logs/store.ts @@ -3,12 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { computed, observable, makeObservable, IComputedValue } from "mobx"; - -import { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints"; -import { autoBind, getOrInsertWith, interval, IntervalFn } from "../../../utils"; -import type { TabId } from "../dock-store/dock.store"; -import type { LogTabData } from "./tab.store"; +import { observable, IComputedValue, when } from "mobx"; +import type { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints"; +import { getOrInsertWith, interval, IntervalFn } from "../../../utils"; +import type { TabId } from "../dock/store"; +import type { LogTabData } from "./tab-store"; type PodLogLine = string; @@ -19,15 +18,12 @@ interface Dependencies { } export class LogStore { - @observable protected podLogs = observable.map(); + protected podLogs = observable.map(); protected refreshers = new Map(); - constructor(private dependencies: Dependencies) { - makeObservable(this); - autoBind(this); - } + constructor(private dependencies: Dependencies) {} - handlerError(tabId: TabId, error: any): void { + protected handlerError(tabId: TabId, error: any): void { if (error.error && !(error.message || error.reason || error.code)) { error = error.error; } @@ -47,24 +43,24 @@ export class LogStore { * Also, it handles loading errors, rewriting whole logs with error * messages */ - load = async (tabId: TabId, logTabData: IComputedValue) => { + public async load(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { try { - const logs = await this.loadLogs(logTabData, { - tailLines: this.getLinesByTabId(tabId) + logLinesToLoad, + const logs = await this.loadLogs(computedPod, logTabData, { + tailLines: this.getLogLines(tabId) + logLinesToLoad, }); - this.getRefresher(tabId, logTabData).start(); + this.getRefresher(tabId, computedPod, logTabData).start(); this.podLogs.set(tabId, logs); } catch (error) { this.handlerError(tabId, error); } - }; + } - private getRefresher(tabId: TabId, logTabData: IComputedValue): IntervalFn { + private getRefresher(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): IntervalFn { return getOrInsertWith(this.refreshers, tabId, () => ( interval(10, () => { if (this.podLogs.has(tabId)) { - this.loadMore(tabId, logTabData); + this.loadMore(tabId, computedPod, logTabData); } }) )); @@ -84,14 +80,14 @@ export class LogStore { * starting from last line received. * @param tabId */ - loadMore = async (tabId: TabId, logTabData: IComputedValue) => { + public async loadMore(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { if (!this.podLogs.get(tabId).length) { return; } try { const oldLogs = this.podLogs.get(tabId); - const logs = await this.loadLogs(logTabData, { + const logs = await this.loadLogs(computedPod, logTabData, { sinceTime: this.getLastSinceTime(tabId), }); @@ -100,7 +96,7 @@ export class LogStore { } catch (error) { this.handlerError(tabId, error); } - }; + } /** * Main logs loading function adds necessary data to payload and makes @@ -109,17 +105,19 @@ export class LogStore { * @param params request parameters described in IPodLogsQuery interface * @returns A fetch request promise */ - private async loadLogs(logTabData: IComputedValue, params: Partial): Promise { - const { selectedContainer, previous, selectedPod } = logTabData.get(); - const pod = new Pod(selectedPod); + private async loadLogs(computedPod: IComputedValue, logTabData: IComputedValue, params: Partial): Promise { + await when(() => Boolean(computedPod.get() && logTabData.get()), { timeout: 5_000 }); + + const { selectedContainer, showPrevious } = logTabData.get(); + const pod = computedPod.get(); const namespace = pod.getNs(); const name = pod.getName(); const result = await this.dependencies.callForLogs({ namespace, name }, { ...params, timestamps: true, // Always setting timestamp to separate old logs from new ones - container: selectedContainer.name, - previous, + container: selectedContainer, + previous: showPrevious, }); return result.trimEnd().split("\n"); @@ -130,26 +128,29 @@ export class LogStore { * Converts logs into a string array * @returns Length of log lines */ - @computed get lines(): number { return this.logs.length; } - public getLinesByTabId = (tabId: TabId): number => { - return this.getLogsByTabId(tabId).length; - }; + getLogLines(tabId: TabId): number{ + return this.getLogs(tabId).length; + } - public getLogsByTabId = (tabId: TabId): string[] => { + areLogsPresent(tabId: TabId): boolean { + return !this.podLogs.has(tabId); + } + + getLogs(tabId: TabId): string[]{ return this.podLogs.get(tabId) ?? []; - }; + } - public getLogsWithoutTimestampsByTabId = (tabId: TabId): string[] => { - return this.getLogsByTabId(tabId).map(this.removeTimestamps); - }; + getLogsWithoutTimestamps(tabId: TabId): string[]{ + return this.getLogs(tabId).map(this.removeTimestamps); + } - public getTimestampSplitLogsByTabId = (tabId: TabId): [string, string][] => { - return this.getLogsByTabId(tabId).map(this.splitOutTimestamp); - }; + getTimestampSplitLogs(tabId: TabId): [string, string][]{ + return this.getLogs(tabId).map(this.splitOutTimestamp); + } /** * @deprecated This now only returns the empty array @@ -173,7 +174,7 @@ export class LogStore { * (this allows to avoid getting the last stamp in the selection) * @param tabId */ - getLastSinceTime(tabId: TabId) { + getLastSinceTime(tabId: TabId): string { const logs = this.podLogs.get(tabId); const timestamps = this.getTimestamps(logs[logs.length - 1]); const stamp = new Date(timestamps ? timestamps[0] : null); @@ -183,7 +184,7 @@ export class LogStore { return stamp.toISOString(); } - splitOutTimestamp = (logs: string): [string, string] => { + splitOutTimestamp(logs: string): [string, string] { const extraction = /^(\d+\S+)(.*)/m.exec(logs); if (!extraction || extraction.length < 3) { @@ -191,23 +192,23 @@ export class LogStore { } return [extraction[1], extraction[2]]; - }; + } - getTimestamps(logs: string) { + getTimestamps(logs: string): RegExpMatchArray { return logs.match(/^\d+\S+/gm); } - removeTimestamps = (logs: string) => { + removeTimestamps(logs: string): string { return logs.replace(/^\d+.*?\s/gm, ""); - }; + } - clearLogs(tabId: TabId) { + clearLogs(tabId: TabId): void { this.podLogs.delete(tabId); } - reload = (tabId: TabId, logTabData: IComputedValue) => { + reload(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { this.clearLogs(tabId); - return this.load(tabId, logTabData); - }; + return this.load(tabId, computedPod, logTabData); + } } diff --git a/src/renderer/components/dock/logs/tab-store.injectable.ts b/src/renderer/components/dock/logs/tab-store.injectable.ts index 2ccc2067dc..61d4667f55 100644 --- a/src/renderer/components/dock/logs/tab-store.injectable.ts +++ b/src/renderer/components/dock/logs/tab-store.injectable.ts @@ -3,13 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { LogTabStore } from "./tab.store"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import { LogTabStore } from "./tab-store"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; const logTabStoreInjectable = getInjectable({ instantiate: (di) => new LogTabStore({ - dockStore: di.inject(dockStoreInjectable), createStorage: di.inject(createStorageInjectable), }), diff --git a/src/renderer/components/dock/logs/tab-store.ts b/src/renderer/components/dock/logs/tab-store.ts new file mode 100644 index 0000000000..b2649472e0 --- /dev/null +++ b/src/renderer/components/dock/logs/tab-store.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import type { StorageHelper } from "../../../utils"; +import type { TabId } from "../dock/store"; +import { logTabDataValidator } from "./log-tab-data.validator"; + +export interface LogTabOwnerRef { + /** + * The uid of the owner + */ + uid: string; + /** + * The name of the owner + */ + name: string; + /** + * The kind of the owner + */ + kind: string; +} + +export interface LogTabData { + /** + * The owning workload for this logging tab + */ + owner?: LogTabOwnerRef; + + /** + * The uid of the currently selected pod + */ + selectedPodId: string; + + /** + * The namespace of the pods/workload + */ + namespace: string; + + /** + * The name of the currently selected container within the currently selected + * pod + */ + selectedContainer: string; + + /** + * Whether to show timestamps in the logs + */ + showTimestamps: boolean; + + /** + * Whether to show the logs of the previous container instance + */ + showPrevious: boolean; +} + +interface Dependencies { + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + +export class LogTabStore extends DockTabStore { + constructor(protected dependencies: Dependencies) { + super(dependencies, { + storageKey: "pod_logs", + }); + } + + /** + * Returns true if the data for `tabId` is valid + */ + isDataValid(tabId: TabId): boolean { + if (!this.getData(tabId)) { + return true; + } + + return !logTabDataValidator.validate(this.getData(tabId)).error; + } +} + diff --git a/src/renderer/components/dock/logs/tab.store.ts b/src/renderer/components/dock/logs/tab.store.ts deleted file mode 100644 index 83310f740e..0000000000 --- a/src/renderer/components/dock/logs/tab.store.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import uniqueId from "lodash/uniqueId"; -import { reaction } from "mobx"; -import { podsStore } from "../../+workloads-pods/pods.store"; - -import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints"; -import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object"; -import logger from "../../../../common/logger"; -import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; -import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; -import type { StorageHelper } from "../../../utils"; - -export interface LogTabData { - pods: Pod[]; - selectedPod: Pod; - selectedContainer: IPodContainer - showTimestamps?: boolean - previous?: boolean -} - -interface PodLogsTabData { - selectedPod: Pod - selectedContainer: IPodContainer -} - -interface WorkloadLogsTabData { - workload: WorkloadKubeObject -} - -interface Dependencies { - dockStore: DockStore - createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> -} - -export class LogTabStore extends DockTabStore { - constructor(protected dependencies: Dependencies) { - super(dependencies, { - storageKey: "pod_logs", - }); - - reaction(() => podsStore.items.length, () => this.updateTabsData()); - } - - createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string { - const podOwner = selectedPod.getOwnerRefs()[0]; - const pods = podsStore.getPodsByOwnerId(podOwner?.uid); - const title = `Pod ${selectedPod.getName()}`; - - return this.createLogsTab(title, { - pods: pods.length ? pods : [selectedPod], - selectedPod, - selectedContainer, - }); - } - - createWorkloadTab({ workload }: WorkloadLogsTabData): void { - const pods = podsStore.getPodsByOwnerId(workload.getId()); - - if (!pods.length) return; - - const selectedPod = pods[0]; - const selectedContainer = selectedPod.getAllContainers()[0]; - const title = `${workload.kind} ${selectedPod.getName()}`; - - this.createLogsTab(title, { - pods, - selectedPod, - selectedContainer, - }); - } - - updateTabName(tabId: string) { - const { selectedPod } = this.getData(tabId); - - this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`); - } - - private createDockTab(tabParams: DockTabCreateSpecific) { - this.dependencies.dockStore.createTab({ - ...tabParams, - kind: TabKind.POD_LOGS, - }, false); - } - - private createLogsTab(title: string, data: LogTabData): string { - const id = uniqueId("log-tab-"); - - this.createDockTab({ id, title }); - this.setData(id, { - ...data, - showTimestamps: false, - previous: false, - }); - - return id; - } - - private updateTabsData() { - for (const [tabId, tabData] of this.data) { - try { - if (!tabData.selectedPod) { - tabData.selectedPod = tabData.pods[0]; - } - - const pod = new Pod(tabData.selectedPod); - const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid); - const isSelectedPodInList = pods.find(item => item.getId() == pod.getId()); - const selectedPod = isSelectedPodInList ? pod : pods[0]; - const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0]; - - if (pods.length > 0) { - this.setData(tabId, { - ...tabData, - selectedPod, - selectedContainer, - pods, - }); - - this.updateTabName(tabId); - } else { - this.closeTab(tabId); - } - } catch (error) { - logger.error(`[LOG-TAB-STORE]: failed to set data for tabId=${tabId} deleting`, error); - this.data.delete(tabId); - } - } - } - - private closeTab(tabId: string) { - this.clearData(tabId); - this.dependencies.dockStore.closeTab(tabId); - } -} - diff --git a/src/renderer/components/dock/logs/view.tsx b/src/renderer/components/dock/logs/view.tsx new file mode 100644 index 0000000000..c73933b12f --- /dev/null +++ b/src/renderer/components/dock/logs/view.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React, { createRef, useEffect } from "react"; +import { observer } from "mobx-react"; +import { InfoPanel } from "../info-panel"; +import { LogResourceSelector } from "./resource-selector"; +import { LogList } from "./list"; +import { LogSearch } from "./search"; +import { LogControls } from "./controls"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import logsViewModelInjectable from "./logs-view-model.injectable"; +import type { LogTabViewModel } from "./logs-view-model"; +import type { DockTab } from "../dock/store"; +import { cssNames, Disposer } from "../../../utils"; +import type { KubeWatchSubscribeStoreOptions } from "../../../kube-watch-api/kube-watch-api"; +import subscribeStoresInjectable from "../../../kube-watch-api/subscribe-stores.injectable"; +import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { podsStore } from "../../+workloads-pods/pods.store"; + +export interface LogsDockTabProps { + className?: string; + tab: DockTab; +} + +interface Dependencies { + model: LogTabViewModel; + subscribeStores: (stores: KubeObjectStore[], opts?: KubeWatchSubscribeStoreOptions) => Disposer; +} + +const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStores }: Dependencies & LogsDockTabProps) => { + const logListElement = createRef(); + const data = model.logTabData.get(); + + useEffect(() => { + model.reloadLogs(); + + return model.stopLoadingLogs; + }, []); + useEffect(() => subscribeStores([ + podsStore, + ], { + namespaces: data ? [data.namespace] : [], + }), [data?.namespace]); + + const scrollToOverlay = (overlayLine: number | undefined) => { + if (!logListElement.current || overlayLine === undefined) { + return; + } + + // Scroll vertically + logListElement.current.scrollToItem(overlayLine, "center"); + // Scroll horizontally in timeout since virtual list need some time to prepare its contents + setTimeout(() => { + const overlay = document.querySelector(".PodLogs .list span.active"); + + if (!overlay) return; + overlay.scrollIntoViewIfNeeded(); + }, 100); + }; + + if (!data) { + return null; + } + + return ( +
+ + + +
+ )} + showSubmitClose={false} + showButtons={false} + showStatusPanel={false} + /> + + +
+ ); +}); + + +export const LogsDockTab = withInjectables(NonInjectedLogsDockTab, { + getProps: (di, props) => ({ + model: di.inject(logsViewModelInjectable, { + tabId: props.tab.id, + }), + subscribeStores: di.inject(subscribeStoresInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/dock/terminal-store/terminal.store.ts b/src/renderer/components/dock/terminal-store/terminal.store.ts deleted file mode 100644 index 9203edb87b..0000000000 --- a/src/renderer/components/dock/terminal-store/terminal.store.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { autorun, observable, when } from "mobx"; -import { autoBind, noop } from "../../../utils"; -import type { Terminal } from "../terminal/terminal"; -import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; -import { - DockStore, - DockTab, - DockTabCreate, - TabId, - TabKind, -} from "../dock-store/dock.store"; -import { WebSocketApiState } from "../../../api/websocket-api"; -import { Notifications } from "../../notifications"; - -export interface ITerminalTab extends DockTab { - node?: string; // activate node shell mode -} - -interface Dependencies { - createTerminalTab: () => DockTabCreate - dockStore: DockStore - createTerminal: (tabId: TabId, api: TerminalApi) => Terminal -} - -export class TerminalStore { - protected terminals = new Map(); - protected connections = observable.map(); - - constructor(private dependencies: Dependencies) { - autoBind(this); - - // connect active tab - autorun(() => { - const { selectedTab, isOpen } = dependencies.dockStore; - - if (selectedTab?.kind === TabKind.TERMINAL && isOpen) { - this.connect(selectedTab.id); - } - }); - // disconnect closed tabs - autorun(() => { - const currentTabs = dependencies.dockStore.tabs.map(tab => tab.id); - - for (const [tabId] of this.connections) { - if (!currentTabs.includes(tabId)) this.disconnect(tabId); - } - }); - } - - connect(tabId: TabId) { - if (this.isConnected(tabId)) { - return; - } - const tab: ITerminalTab = this.dependencies.dockStore.getTabById(tabId); - const api = new TerminalApi({ - id: tabId, - node: tab.node, - }); - const terminal = this.dependencies.createTerminal(tabId, api); - - this.connections.set(tabId, api); - this.terminals.set(tabId, terminal); - - api.connect(); - } - - disconnect(tabId: TabId) { - if (!this.isConnected(tabId)) { - return; - } - const terminal = this.terminals.get(tabId); - const terminalApi = this.connections.get(tabId); - - terminal.destroy(); - terminalApi.destroy(); - this.connections.delete(tabId); - this.terminals.delete(tabId); - } - - reconnect(tabId: TabId) { - this.connections.get(tabId)?.connect(); - } - - isConnected(tabId: TabId) { - return Boolean(this.connections.get(tabId)); - } - - isDisconnected(tabId: TabId) { - return this.connections.get(tabId)?.readyState === WebSocketApiState.CLOSED; - } - - async sendCommand(command: string, options: { enter?: boolean; newTab?: boolean; tabId?: TabId } = {}) { - const { enter, newTab, tabId } = options; - - if (tabId) { - this.dependencies.dockStore.selectTab(tabId); - } - - if (newTab) { - const tab = this.dependencies.createTerminalTab(); - - await when(() => this.connections.has(tab.id)); - - const shellIsReady = when(() => this.connections.get(tab.id).isReady); - const notifyVeryLong = setTimeout(() => { - shellIsReady.cancel(); - Notifications.info( - "If terminal shell is not ready please check your shell init files, if applicable.", - { - timeout: 4_000, - }, - ); - }, 10_000); - - await shellIsReady.catch(noop); - clearTimeout(notifyVeryLong); - } - - const terminalApi = this.connections.get(this.dependencies.dockStore.selectedTabId); - - if (terminalApi) { - if (enter) { - command += "\r"; - } - - terminalApi.sendMessage({ - type: TerminalChannels.STDIN, - data: command, - }); - } else { - console.warn( - "The selected tab is does not have a connection. Cannot send command.", - { tabId: this.dependencies.dockStore.selectedTabId, command }, - ); - } - } - - getTerminal(tabId: TabId) { - return this.terminals.get(tabId); - } - - reset() { - [...this.connections].forEach(([tabId]) => { - this.disconnect(tabId); - }); - } -} diff --git a/src/renderer/components/dock/terminal-tab.scss b/src/renderer/components/dock/terminal-tab.scss deleted file mode 100644 index faa0f5a7f3..0000000000 --- a/src/renderer/components/dock/terminal-tab.scss +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -.TerminalTab { - -} diff --git a/src/renderer/components/dock/terminal-window.tsx b/src/renderer/components/dock/terminal-window.tsx deleted file mode 100644 index 5d76f69543..0000000000 --- a/src/renderer/components/dock/terminal-window.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./terminal-window.scss"; - -import React from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { cssNames } from "../../utils"; -import type { Terminal } from "./terminal/terminal"; -import type { TerminalStore } from "./terminal-store/terminal.store"; -import { ThemeStore } from "../../theme.store"; -import { DockTab, TabKind, TabId, DockStore } from "./dock-store/dock.store"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; -import terminalStoreInjectable from "./terminal-store/terminal-store.injectable"; - -interface Props { - tab: DockTab; -} - -interface Dependencies { - dockStore: DockStore - terminalStore: TerminalStore -} - -@observer -class NonInjectedTerminalWindow extends React.Component { - public elem: HTMLElement; - public terminal: Terminal; - - componentDidMount() { - disposeOnUnmount(this, [ - this.props.dockStore.onTabChange(({ tabId }) => this.activate(tabId), { - tabKind: TabKind.TERMINAL, - fireImmediately: true, - }), - - // refresh terminal available space (cols/rows) when resized - this.props.dockStore.onResize(() => this.terminal?.fitLazy(), { - fireImmediately: true, - }), - ]); - } - - activate(tabId: TabId) { - this.terminal?.detach(); // detach previous - this.terminal = this.props.terminalStore.getTerminal(tabId); - this.terminal.attachTo(this.elem); - } - - render() { - return ( -
this.elem = elem} - /> - ); - } -} - -export const TerminalWindow = withInjectables( - NonInjectedTerminalWindow, - - { - getProps: (di, props) => ({ - dockStore: di.inject(dockStoreInjectable), - terminalStore: di.inject(terminalStoreInjectable), - ...props, - }), - }, -); - diff --git a/src/renderer/components/dock/terminal/clear-terminal-tab-data.injectable.ts b/src/renderer/components/dock/terminal/clear-terminal-tab-data.injectable.ts new file mode 100644 index 0000000000..4396e3ee20 --- /dev/null +++ b/src/renderer/components/dock/terminal/clear-terminal-tab-data.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import terminalStoreInjectable from "./store.injectable"; + +const clearTerminalTabDataInjectable = getInjectable({ + instantiate: (di) => { + const terminalStore = di.inject(terminalStoreInjectable); + + return (tabId: TabId): void => { + terminalStore.destroy(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clearTerminalTabDataInjectable; diff --git a/src/renderer/components/dock/terminal/create-terminal-tab.injectable.ts b/src/renderer/components/dock/terminal/create-terminal-tab.injectable.ts new file mode 100644 index 0000000000..9348bd49ef --- /dev/null +++ b/src/renderer/components/dock/terminal/create-terminal-tab.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "../dock/store.injectable"; +import { DockTabCreateSpecific, TabKind } from "../dock/store"; + +const createTerminalTabInjectable = getInjectable({ + instantiate: (di) => { + const dockStore = di.inject(dockStoreInjectable); + + return (tabParams: DockTabCreateSpecific = {}) => + dockStore.createTab({ + title: `Terminal`, + ...tabParams, + kind: TabKind.TERMINAL, + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createTerminalTabInjectable; diff --git a/src/renderer/components/dock/terminal/create-terminal.injectable.ts b/src/renderer/components/dock/terminal/create-terminal.injectable.ts index b52b24d13f..2dc6f04941 100644 --- a/src/renderer/components/dock/terminal/create-terminal.injectable.ts +++ b/src/renderer/components/dock/terminal/create-terminal.injectable.ts @@ -4,20 +4,11 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { Terminal } from "./terminal"; -import type { TabId } from "../dock-store/dock.store"; +import type { TabId } from "../dock/store"; import type { TerminalApi } from "../../../api/terminal-api"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; const createTerminalInjectable = getInjectable({ - instantiate: (di) => { - const dependencies = { - dockStore: di.inject(dockStoreInjectable), - }; - - return (tabId: TabId, api: TerminalApi) => - new Terminal(dependencies, tabId, api); - }, - + instantiate: () => (tabId: TabId, api: TerminalApi) => new Terminal(tabId, api), lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/terminal-tab.tsx b/src/renderer/components/dock/terminal/dock-tab.tsx similarity index 70% rename from src/renderer/components/dock/terminal-tab.tsx rename to src/renderer/components/dock/terminal/dock-tab.tsx index a633ffb062..2fc9d436d2 100644 --- a/src/renderer/components/dock/terminal-tab.tsx +++ b/src/renderer/components/dock/terminal/dock-tab.tsx @@ -3,19 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import "./terminal-tab.scss"; - import React from "react"; import { observer } from "mobx-react"; -import { boundMethod, cssNames } from "../../utils"; -import { DockTab, DockTabProps } from "./dock-tab"; -import { Icon } from "../icon"; -import type { TerminalStore } from "./terminal-store/terminal.store"; -import type { DockStore } from "./dock-store/dock.store"; +import { boundMethod, cssNames } from "../../../utils"; +import { DockTab, DockTabProps } from "../dock-tab"; +import { Icon } from "../../icon"; +import type { TerminalStore } from "./store"; +import type { DockStore } from "../dock/store"; import { reaction } from "mobx"; import { withInjectables } from "@ogre-tools/injectable-react"; -import dockStoreInjectable from "./dock-store/dock-store.injectable"; -import terminalStoreInjectable from "./terminal-store/terminal-store.injectable"; +import dockStoreInjectable from "../dock/store.injectable"; +import terminalStoreInjectable from "./store.injectable"; interface Props extends DockTabProps { } @@ -73,15 +71,11 @@ class NonInjectedTerminalTab extends React.Component { } } -export const TerminalTab = withInjectables( - NonInjectedTerminalTab, - - { - getProps: (di, props) => ({ - dockStore: di.inject(dockStoreInjectable), - terminalStore: di.inject(terminalStoreInjectable), - ...props, - }), - }, -); +export const TerminalTab = withInjectables(NonInjectedTerminalTab, { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + terminalStore: di.inject(terminalStoreInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/dock/terminal/get-terminal-api.injectable.ts b/src/renderer/components/dock/terminal/get-terminal-api.injectable.ts new file mode 100644 index 0000000000..7724380c50 --- /dev/null +++ b/src/renderer/components/dock/terminal/get-terminal-api.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TerminalApi } from "../../../api/terminal-api"; +import type { TabId } from "../dock/store"; +import terminalStoreInjectable from "./store.injectable"; + +const getTerminalApiInjectable = getInjectable({ + instantiate: (di) => { + const terminalStore = di.inject(terminalStoreInjectable); + + return (tabId: TabId): TerminalApi => terminalStore.getTerminalApi(tabId); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default getTerminalApiInjectable; diff --git a/src/renderer/components/dock/terminal/send-command.injectable.ts b/src/renderer/components/dock/terminal/send-command.injectable.ts new file mode 100644 index 0000000000..6e6b970b21 --- /dev/null +++ b/src/renderer/components/dock/terminal/send-command.injectable.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { when } from "mobx"; +import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; +import { noop } from "../../../utils"; +import { Notifications } from "../../notifications"; +import selectDockTabInjectable from "../dock/select-dock-tab.injectable"; +import type { DockTab, TabId } from "../dock/store"; +import createTerminalTabInjectable from "./create-terminal-tab.injectable"; +import getTerminalApiInjectable from "./get-terminal-api.injectable"; + +interface Dependencies { + selectTab: (tabId: TabId) => void; + createTerminalTab: () => DockTab; + getTerminalApi: (tabId: TabId) => TerminalApi; +} + +export interface SendCommandOptions { + /** + * Emit an enter after the command + */ + enter?: boolean; + + /** + * @deprecated This option is ignored and infered to be `true` if `tabId` is not provided + */ + newTab?: any; + + /** + * Specify a specific terminal tab to send this command to + */ + tabId?: TabId; +} + +const sendCommand = ({ selectTab, createTerminalTab, getTerminalApi }: Dependencies) => async (command: string, options: SendCommandOptions = {}): Promise => { + let { tabId } = options; + + if (tabId) { + selectTab(tabId); + } else { + tabId = createTerminalTab().id; + } + + await when(() => Boolean(getTerminalApi(tabId))); + + const terminalApi = getTerminalApi(tabId); + const shellIsReady = when(() =>terminalApi.isReady); + const notifyVeryLong = setTimeout(() => { + shellIsReady.cancel(); + Notifications.info( + "If terminal shell is not ready please check your shell init files, if applicable.", + { + timeout: 4_000, + }, + ); + }, 10_000); + + await shellIsReady.catch(noop); + clearTimeout(notifyVeryLong); + + if (terminalApi) { + if (options.enter) { + command += "\r"; + } + + terminalApi.sendMessage({ + type: TerminalChannels.STDIN, + data: command, + }); + } else { + console.warn( + "The selected tab is does not have a connection. Cannot send command.", + { tabId, command }, + ); + } +}; + +const sendCommandInjectable = getInjectable({ + instantiate: (di) => sendCommand({ + createTerminalTab: di.inject(createTerminalTabInjectable), + selectTab: di.inject(selectDockTabInjectable), + getTerminalApi: di.inject(getTerminalApiInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default sendCommandInjectable; diff --git a/src/renderer/components/dock/terminal-store/terminal-store.injectable.ts b/src/renderer/components/dock/terminal/store.injectable.ts similarity index 52% rename from src/renderer/components/dock/terminal-store/terminal-store.injectable.ts rename to src/renderer/components/dock/terminal/store.injectable.ts index 6e0990b31e..06c4c4d7f4 100644 --- a/src/renderer/components/dock/terminal-store/terminal-store.injectable.ts +++ b/src/renderer/components/dock/terminal/store.injectable.ts @@ -3,15 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { TerminalStore } from "./terminal.store"; -import createTerminalTabInjectable from "../create-terminal-tab/create-terminal-tab.injectable"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; -import createTerminalInjectable from "../terminal/create-terminal.injectable"; +import { TerminalStore } from "./store"; +import createTerminalInjectable from "./create-terminal.injectable"; const terminalStoreInjectable = getInjectable({ instantiate: (di) => new TerminalStore({ - createTerminalTab: di.inject(createTerminalTabInjectable), - dockStore: di.inject(dockStoreInjectable), createTerminal: di.inject(createTerminalInjectable), }), diff --git a/src/renderer/components/dock/terminal/store.ts b/src/renderer/components/dock/terminal/store.ts new file mode 100644 index 0000000000..a54dccb820 --- /dev/null +++ b/src/renderer/components/dock/terminal/store.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { action, observable } from "mobx"; +import type { Terminal } from "./terminal"; +import { TerminalApi } from "../../../api/terminal-api"; +import type { DockTab, TabId } from "../dock/store"; +import { WebSocketApiState } from "../../../api/websocket-api"; + +export interface ITerminalTab extends DockTab { + node?: string; // activate node shell mode +} + +interface Dependencies { + createTerminal: (tabId: TabId, api: TerminalApi) => Terminal; +} + +export class TerminalStore { + protected terminals = new Map(); + protected connections = observable.map(); + + constructor(private dependencies: Dependencies) { + } + + @action + connect(tab: ITerminalTab) { + if (this.isConnected(tab.id)) { + return; + } + const api = new TerminalApi({ + id: tab.id, + node: tab.node, + }); + const terminal = this.dependencies.createTerminal(tab.id, api); + + this.connections.set(tab.id, api); + this.terminals.set(tab.id, terminal); + + api.connect(); + } + + @action + destroy(tabId: TabId) { + const terminal = this.terminals.get(tabId); + const terminalApi = this.connections.get(tabId); + + terminal?.destroy(); + terminalApi?.destroy(); + this.connections.delete(tabId); + this.terminals.delete(tabId); + } + + /** + * @deprecated use `this.destroy()` instead + */ + disconnect(tabId: TabId) { + this.destroy(tabId); + } + + reconnect(tabId: TabId) { + this.connections.get(tabId)?.connect(); + } + + isConnected(tabId: TabId) { + return Boolean(this.connections.get(tabId)); + } + + isDisconnected(tabId: TabId) { + return this.connections.get(tabId)?.readyState === WebSocketApiState.CLOSED; + } + + getTerminal(tabId: TabId) { + return this.terminals.get(tabId); + } + + getTerminalApi(tabId: TabId) { + return this.connections.get(tabId); + } + + reset() { + [...this.connections].forEach(([tabId]) => { + this.destroy(tabId); + }); + } +} diff --git a/src/renderer/components/dock/terminal-window.scss b/src/renderer/components/dock/terminal/terminal-window.scss similarity index 100% rename from src/renderer/components/dock/terminal-window.scss rename to src/renderer/components/dock/terminal/terminal-window.scss diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index 718c855b97..022f71f8aa 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -7,7 +7,7 @@ import debounce from "lodash/debounce"; import { reaction } from "mobx"; import { Terminal as XTerm } from "xterm"; import { FitAddon } from "xterm-addon-fit"; -import type { DockStore, TabId } from "../dock-store/dock.store"; +import type { TabId } from "../dock/store"; import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; import { ThemeStore } from "../../../theme.store"; import { disposer } from "../../../utils"; @@ -18,12 +18,7 @@ import { clipboard } from "electron"; import logger from "../../../../common/logger"; import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers"; -interface Dependencies { - dockStore: DockStore -} - export class Terminal { - private terminalConfig: TerminalConfig = UserStore.getInstance().terminalConfig; public static get spawningPool() { @@ -56,12 +51,6 @@ export class Terminal { return this.xterm.element.querySelector(".xterm-viewport"); } - get isActive() { - const { isOpen, selectedTabId } = this.dependencies.dockStore; - - return isOpen && selectedTabId === this.tabId; - } - attachTo(parentElem: HTMLElement) { parentElem.appendChild(this.elem); this.onActivate(); @@ -75,7 +64,7 @@ export class Terminal { } } - constructor(private dependencies: Dependencies, public tabId: TabId, protected api: TerminalApi) { + constructor(public tabId: TabId, protected api: TerminalApi) { // enable terminal addons this.xterm.loadAddon(this.fitAddon); @@ -107,7 +96,6 @@ export class Terminal { reaction(() => UserStore.getInstance().terminalConfig.fontFamily, this.setFontFamily, { fireImmediately: true, }), - dependencies.dockStore.onResize(this.onResize), () => onDataHandler.dispose(), () => this.fitAddon.dispose(), () => this.api.removeAllListeners(), @@ -126,7 +114,7 @@ export class Terminal { fit = () => { // Since this function is debounced we need to read this value as late as possible - if (!this.isActive || !this.xterm) { + if (!this.xterm) { return; } diff --git a/src/renderer/components/dock/terminal/view.tsx b/src/renderer/components/dock/terminal/view.tsx new file mode 100644 index 0000000000..f1e0279539 --- /dev/null +++ b/src/renderer/components/dock/terminal/view.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./terminal-window.scss"; + +import React from "react"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { cssNames } from "../../../utils"; +import type { Terminal } from "./terminal"; +import type { TerminalStore } from "./store"; +import { ThemeStore } from "../../../theme.store"; +import type { DockTab, DockStore } from "../dock/store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "../dock/store.injectable"; +import terminalStoreInjectable from "./store.injectable"; + +interface Props { + tab: DockTab; +} + +interface Dependencies { + dockStore: DockStore; + terminalStore: TerminalStore; +} + +@observer +class NonInjectedTerminalWindow extends React.Component { + public elem: HTMLElement; + public terminal: Terminal; + + componentDidMount() { + this.props.terminalStore.connect(this.props.tab); + this.terminal = this.props.terminalStore.getTerminal(this.props.tab.id); + this.terminal.attachTo(this.elem); + + disposeOnUnmount(this, [ + // refresh terminal available space (cols/rows) when resized + this.props.dockStore.onResize(() => this.terminal.onResize(), { + fireImmediately: true, + }), + ]); + } + + componentDidUpdate(): void { + this.terminal.detach(); + this.props.terminalStore.connect(this.props.tab); + this.terminal = this.props.terminalStore.getTerminal(this.props.tab.id); + this.terminal.attachTo(this.elem); + } + + componentWillUnmount(): void { + this.terminal.detach(); + } + + render() { + return ( +
this.elem = elem} + /> + ); + } +} + +export const TerminalWindow = withInjectables(NonInjectedTerminalWindow, { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + terminalStore: di.inject(terminalStoreInjectable), + ...props, + }), +}); + diff --git a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts deleted file mode 100644 index f6e6a2dde2..0000000000 --- a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { - action, - autorun, - computed, - IReactionDisposer, - reaction, - makeObservable, -} from "mobx"; -import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store"; -import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; -import { - getReleaseValues, - HelmRelease, -} from "../../../../common/k8s-api/endpoints/helm-releases.api"; -import { iter, StorageHelper } from "../../../utils"; -import type { IAsyncComputed } from "@ogre-tools/injectable-react"; - -export interface IChartUpgradeData { - releaseName: string; - releaseNamespace: string; -} - -interface Dependencies { - releases: IAsyncComputed - valuesStore: DockTabStore - dockStore: DockStore - createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> -} - -export class UpgradeChartStore extends DockTabStore { - private watchers = new Map(); - - @computed private get releaseNameReverseLookup(): Map { - return new Map(iter.map(this.data, ([id, { releaseName }]) => [releaseName, id])); - } - - get values() { - return this.dependencies.valuesStore; - } - - constructor(protected dependencies : Dependencies) { - super(dependencies, { - storageKey: "chart_releases", - }); - - makeObservable(this); - - autorun(() => { - const { selectedTab, isOpen } = dependencies.dockStore; - - if (selectedTab?.kind === TabKind.UPGRADE_CHART && isOpen) { - this.loadData(selectedTab.id); - } - }, { delay: 250 }); - - autorun(() => { - const objects = [...this.data.values()]; - - objects.forEach(({ releaseName }) => this.createReleaseWatcher(releaseName)); - }); - } - - private createReleaseWatcher(releaseName: string) { - if (this.watchers.get(releaseName)) { - return; - } - const dispose = reaction(() => { - const release = this.dependencies.releases.value.get().find(release => release.getName() === releaseName); - - return release?.getRevision(); // watch changes only by revision - }, - release => { - const releaseTab = this.getTabByRelease(releaseName); - - if (!releaseTab) { - return; - } - - // auto-reload values if was loaded before - if (release) { - if (this.dependencies.dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) { - this.loadValues(releaseTab.id); - } - } - // clean up watcher, close tab if release not exists / was removed - else { - dispose(); - this.watchers.delete(releaseName); - this.dependencies.dockStore.closeTab(releaseTab.id); - } - }); - - this.watchers.set(releaseName, dispose); - } - - isLoading(tabId = this.dependencies.dockStore.selectedTabId) { - const values = this.values.getData(tabId); - - return values === undefined; - } - - @action - async loadData(tabId: TabId) { - const values = this.values.getData(tabId); - - await Promise.all([ - !values && this.loadValues(tabId), - ]); - } - - @action - async loadValues(tabId: TabId) { - this.values.clearData(tabId); // reset - const { releaseName, releaseNamespace } = this.getData(tabId); - const values = await getReleaseValues(releaseName, releaseNamespace, true); - - this.values.setData(tabId, values); - } - - getTabByRelease(releaseName: string): DockTab { - return this.dependencies.dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName)); - } -} diff --git a/src/renderer/components/dock/upgrade-chart/clear-upgrade-chart-tab-data.injectable.ts b/src/renderer/components/dock/upgrade-chart/clear-upgrade-chart-tab-data.injectable.ts new file mode 100644 index 0000000000..e6515a4749 --- /dev/null +++ b/src/renderer/components/dock/upgrade-chart/clear-upgrade-chart-tab-data.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { TabId } from "../dock/store"; +import upgradeChartTabStoreInjectable from "./store.injectable"; + +const clearUpgradeChartTabDataInjectable = getInjectable({ + instantiate: (di) => { + const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable); + + return (tabId: TabId) => { + upgradeChartTabStore.clearData(tabId); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clearUpgradeChartTabDataInjectable; diff --git a/src/renderer/components/dock/upgrade-chart/create-upgrade-chart-tab.injectable.ts b/src/renderer/components/dock/upgrade-chart/create-upgrade-chart-tab.injectable.ts new file mode 100644 index 0000000000..0d3de6158a --- /dev/null +++ b/src/renderer/components/dock/upgrade-chart/create-upgrade-chart-tab.injectable.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import upgradeChartTabStoreInjectable from "./store.injectable"; +import dockStoreInjectable from "../dock/store.injectable"; +import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { DockStore, DockTabCreateSpecific, TabId, TabKind } from "../dock/store"; +import type { UpgradeChartTabStore } from "./store"; +import { runInAction } from "mobx"; + +interface Dependencies { + upgradeChartStore: UpgradeChartTabStore; + dockStore: DockStore +} + +const createUpgradeChartTab = ({ upgradeChartStore, dockStore }: Dependencies) => (release: HelmRelease, tabParams: DockTabCreateSpecific = {}): TabId => { + const tabId = upgradeChartStore.getTabIdByRelease(release.getName()); + + if (tabId) { + dockStore.open(); + dockStore.selectTab(tabId); + + return tabId; + } + + return runInAction(() => { + const tab = dockStore.createTab( + { + title: `Helm Upgrade: ${release.getName()}`, + ...tabParams, + kind: TabKind.UPGRADE_CHART, + }, + false, + ); + + upgradeChartStore.setData(tab.id, { + releaseName: release.getName(), + releaseNamespace: release.getNs(), + }); + + return tab.id; + }); +}; + +const createUpgradeChartTabInjectable = getInjectable({ + instantiate: (di) => createUpgradeChartTab({ + upgradeChartStore: di.inject(upgradeChartTabStoreInjectable), + dockStore: di.inject(dockStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createUpgradeChartTabInjectable; diff --git a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts b/src/renderer/components/dock/upgrade-chart/store.injectable.ts similarity index 55% rename from src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts rename to src/renderer/components/dock/upgrade-chart/store.injectable.ts index d0732e7fdb..13e69e9b10 100644 --- a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts +++ b/src/renderer/components/dock/upgrade-chart/store.injectable.ts @@ -3,27 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { UpgradeChartStore } from "./upgrade-chart.store"; -import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import { UpgradeChartTabStore } from "./store"; import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; -import releasesInjectable from "../../+apps-releases/releases.injectable"; -const upgradeChartStoreInjectable = getInjectable({ +const upgradeChartTabStoreInjectable = getInjectable({ instantiate: (di) => { const createDockTabStore = di.inject(createDockTabStoreInjectable); - const valuesStore = createDockTabStore(); - - return new UpgradeChartStore({ - releases: di.inject(releasesInjectable), - dockStore: di.inject(dockStoreInjectable), + return new UpgradeChartTabStore({ createStorage: di.inject(createStorageInjectable), - valuesStore, + valuesStore: createDockTabStore(), }); }, lifecycle: lifecycleEnum.singleton, }); -export default upgradeChartStoreInjectable; +export default upgradeChartTabStoreInjectable; diff --git a/src/renderer/components/dock/upgrade-chart/store.ts b/src/renderer/components/dock/upgrade-chart/store.ts new file mode 100644 index 0000000000..ddc1a78188 --- /dev/null +++ b/src/renderer/components/dock/upgrade-chart/store.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { action, computed, makeObservable } from "mobx"; +import type { TabId } from "../dock/store"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import type { StorageHelper } from "../../../utils"; + +export interface IChartUpgradeData { + releaseName: string; + releaseNamespace: string; +} + +interface Dependencies { + valuesStore: DockTabStore; + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper>; +} + +export class UpgradeChartTabStore extends DockTabStore { + @computed private get releaseNameReverseLookup(): Map { + return new Map(this.getAllData().map(([id, { releaseName }]) => [releaseName, id])); + } + + get values() { + return this.dependencies.valuesStore; + } + + constructor(protected dependencies : Dependencies) { + super(dependencies, { + storageKey: "chart_releases", + }); + + makeObservable(this); + } + + @action + async reloadValues(tabId: TabId) { + this.values.clearData(tabId); // reset + const { releaseName, releaseNamespace } = this.getData(tabId); + const values = await getReleaseValues(releaseName, releaseNamespace, true); + + this.values.setData(tabId, values); + } + + getTabIdByRelease(releaseName: string): TabId { + return this.releaseNameReverseLookup.get(releaseName); + } +} diff --git a/src/renderer/components/dock/upgrade-chart.scss b/src/renderer/components/dock/upgrade-chart/upgrade-chart.scss similarity index 100% rename from src/renderer/components/dock/upgrade-chart.scss rename to src/renderer/components/dock/upgrade-chart/upgrade-chart.scss diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart/view.tsx similarity index 64% rename from src/renderer/components/dock/upgrade-chart.tsx rename to src/renderer/components/dock/upgrade-chart/view.tsx index ce25d1815f..bff0491610 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart/view.tsx @@ -8,27 +8,20 @@ import "./upgrade-chart.scss"; import React from "react"; import { action, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { cssNames } from "../../utils"; -import type { DockTab } from "./dock-store/dock.store"; -import { InfoPanel } from "./info-panel"; -import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store"; -import { Spinner } from "../spinner"; -import { Badge } from "../badge"; -import { EditorPanel } from "./editor-panel"; -import { - helmChartStore, - IChartVersion, -} from "../+apps-helm-charts/helm-chart.store"; -import type { - HelmRelease, - IReleaseUpdateDetails, - IReleaseUpdatePayload, -} from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { Select, SelectOption } from "../select"; +import { cssNames } from "../../../utils"; +import type { DockTab } from "../dock/store"; +import { InfoPanel } from "../info-panel"; +import type { UpgradeChartTabStore } from "./store"; +import { Spinner } from "../../spinner"; +import { Badge } from "../../badge"; +import { EditorPanel } from "../editor-panel"; +import { helmChartStore, IChartVersion } from "../../+apps-helm-charts/helm-chart.store"; +import type { HelmRelease, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { Select, SelectOption } from "../../select"; import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; -import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable"; -import updateReleaseInjectable from "../+apps-releases/update-release/update-release.injectable"; -import releasesInjectable from "../+apps-releases/releases.injectable"; +import upgradeChartTabStoreInjectable from "./store.injectable"; +import updateReleaseInjectable from "../../+apps-releases/update-release/update-release.injectable"; +import releasesInjectable from "../../+apps-releases/releases.injectable"; interface Props { className?: string; @@ -36,9 +29,9 @@ interface Props { } interface Dependencies { - releases: IAsyncComputed - upgradeChartStore: UpgradeChartStore - updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise + releases: IAsyncComputed; + upgradeChartTabStore: UpgradeChartTabStore; + updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise; } @observer @@ -53,10 +46,21 @@ export class NonInjectedUpgradeChart extends React.Component this.release, () => this.loadVersions()), + reaction( + () => this.release, + release => this.reloadVersions(release), + { + fireImmediately: true, + }, + ), + reaction( + () => this.release?.getRevision(), + () => this.reloadValues(), + { + fireImmediately: true, + }, + ), ]); } @@ -65,7 +69,7 @@ export class NonInjectedUpgradeChart extends React.Component { this.error = ""; - this.props.upgradeChartStore.values.setData(this.tabId, value); + this.props.upgradeChartTabStore.values.setData(this.tabId, value); }); onError = action((error: Error | string) => { @@ -125,7 +136,7 @@ export class NonInjectedUpgradeChart extends React.Component; } const currentVersion = release.getVersion(); @@ -168,15 +179,11 @@ export class NonInjectedUpgradeChart extends React.Component( - NonInjectedUpgradeChart, - - { - getProps: (di, props) => ({ - releases: di.inject(releasesInjectable), - updateRelease: di.inject(updateReleaseInjectable), - upgradeChartStore: di.inject(upgradeChartStoreInjectable), - ...props, - }), - }, -); +export const UpgradeChart = withInjectables(NonInjectedUpgradeChart, { + getProps: (di, props) => ({ + releases: di.inject(releasesInjectable), + updateRelease: di.inject(updateReleaseInjectable), + upgradeChartTabStore: di.inject(upgradeChartTabStoreInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/input/search-input.tsx b/src/renderer/components/input/search-input.tsx index a004041e9d..36bcc135c4 100644 --- a/src/renderer/components/input/search-input.tsx +++ b/src/renderer/components/input/search-input.tsx @@ -50,13 +50,9 @@ export class SearchInput extends React.Component { @boundMethod onKeyDown(evt: React.KeyboardEvent) { - if (this.props.onKeyDown) { - this.props.onKeyDown(evt); - } - // clear on escape-key - const escapeKey = evt.nativeEvent.code === "Escape"; + this.props.onKeyDown?.(evt); - if (escapeKey) { + if (evt.nativeEvent.code === "Escape") { this.clear(); evt.stopPropagation(); } @@ -87,6 +83,7 @@ export class SearchInput extends React.Component { onKeyDown={this.onKeyDown} iconRight={rightIcon} ref={this.inputRef} + blurOnEnter={false} /> ); } diff --git a/src/renderer/components/item-object-list/content.tsx b/src/renderer/components/item-object-list/content.tsx new file mode 100644 index 0000000000..ccfdddd7d3 --- /dev/null +++ b/src/renderer/components/item-object-list/content.tsx @@ -0,0 +1,271 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./item-list-layout.scss"; + +import React, { ReactNode } from "react"; +import { computed, makeObservable } from "mobx"; +import { observer } from "mobx-react"; +import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; +import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table"; +import { boundMethod, cssNames, IClassName, isReactNode, prevDefault, stopPropagation } from "../../utils"; +import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons"; +import { NoItems } from "../no-items"; +import { Spinner } from "../spinner"; +import type { ItemObject, ItemStore } from "../../../common/item.store"; +import { Filter, pageFilters } from "./page-filters.store"; +import { ThemeStore } from "../../theme.store"; +import { MenuActions } from "../menu/menu-actions"; +import { MenuItem } from "../menu"; +import { Checkbox } from "../checkbox"; +import { UserStore } from "../../../common/user-store"; + +interface ItemListLayoutContentProps { + getFilters: () => Filter[] + tableId?: string; + className: IClassName; + getItems: () => I[]; + store: ItemStore; + getIsReady: () => boolean; // show loading indicator while not ready + isSelectable?: boolean; // show checkbox in rows for selecting items + isConfigurable?: boolean; + copyClassNameFromHeadCells?: boolean; + sortingCallbacks?: TableSortCallbacks; + tableProps?: Partial>; // low-level table configuration + renderTableHeader: TableCellProps[] | null; + renderTableContents: (item: I) => (ReactNode | TableCellProps)[]; + renderItemMenu?: (item: I, store: ItemStore) => ReactNode; + customizeTableRowProps?: (item: I) => Partial; + addRemoveButtons?: Partial; + virtual?: boolean; + + // item details view + hasDetailsView?: boolean; + detailsItem?: I; + onDetails?: (item: I) => void; + + // other + customizeRemoveDialog?: (selectedItems: I[]) => Partial; + + /** + * Message to display when a store failed to load + * + * @default "Failed to load items" + */ + failedToLoadMessage?: React.ReactNode; +} + +@observer +export class ItemListLayoutContent extends React.Component> { + constructor(props: ItemListLayoutContentProps) { + super(props); + makeObservable(this); + } + + @computed get failedToLoad() { + return this.props.store.failedLoading; + } + + @boundMethod + getRow(uid: string) { + const { + isSelectable, renderTableHeader, renderTableContents, renderItemMenu, + store, hasDetailsView, onDetails, + copyClassNameFromHeadCells, customizeTableRowProps, detailsItem, + } = this.props; + const { isSelected } = store; + const item = this.props.getItems().find(item => item.getId() == uid); + + if (!item) return null; + const itemId = item.getId(); + + return ( + onDetails(item)) : undefined} + {...customizeTableRowProps(item)} + > + {isSelectable && ( + store.toggleSelection(item))} + /> + )} + { + renderTableContents(item).map((content, index) => { + const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; + const headCell = renderTableHeader?.[index]; + + if (copyClassNameFromHeadCells && headCell) { + cellProps.className = cssNames(cellProps.className, headCell.className); + } + + if (!headCell || this.showColumn(headCell)) { + return ; + } + + return null; + }) + } + {renderItemMenu && ( + +
+ {renderItemMenu(item, store)} +
+
+ )} +
+ ); + } + + @boundMethod + removeItemsDialog() { + const { customizeRemoveDialog, store } = this.props; + const { selectedItems, removeSelectedItems } = store; + const visibleMaxNamesCount = 5; + const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", "); + const dialogCustomProps = customizeRemoveDialog ? customizeRemoveDialog(selectedItems) : {}; + const selectedCount = selectedItems.length; + const tailCount = selectedCount > visibleMaxNamesCount ? selectedCount - visibleMaxNamesCount : 0; + const tail = tailCount > 0 ? <>, and {tailCount} more : null; + const message = selectedCount <= 1 ?

Remove item {selectedNames}?

:

Remove {selectedCount} items {selectedNames}{tail}?

; + + ConfirmDialog.open({ + ok: removeSelectedItems, + labelOk: "Remove", + message, + ...dialogCustomProps, + }); + } + + renderNoItems() { + if (this.failedToLoad) { + return {this.props.failedToLoadMessage}; + } + + if (!this.props.getIsReady()) { + return ; + } + + if (this.props.getFilters().length > 0) { + return ( + + No items found. +

+ pageFilters.reset()} className="contrast"> + Reset filters? + +

+
+ ); + } + + return ; + } + + renderItems() { + if (this.props.virtual) { + return null; + } + + return this.props.getItems().map(item => this.getRow(item.getId())); + } + + renderTableHeader() { + const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props; + + if (!renderTableHeader) { + return null; + } + + const enabledItems = this.props.getItems().filter(item => !customizeTableRowProps(item).disabled); + + return ( + + {isSelectable && ( + store.toggleSelectionAll(enabledItems))} + /> + )} + {renderTableHeader.map((cellProps, index) => ( + this.showColumn(cellProps) && ( + + ) + ))} + + {isConfigurable && this.renderColumnVisibilityMenu()} + + + ); + } + + render() { + const { + store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, + detailsItem, className, tableProps = {}, tableId, + } = this.props; + const { selectedItems } = store; + const selectedItemId = detailsItem && detailsItem.getId(); + const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); + + return ( +
+ + {this.renderTableHeader()} + {this.renderItems()} +
+ +
+ ); + } + + showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { + const { tableId, isConfigurable } = this.props; + + return !isConfigurable || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn); + } + + renderColumnVisibilityMenu() { + const { renderTableHeader, tableId } = this.props; + + return ( + + {renderTableHeader.map((cellProps, index) => ( + !cellProps.showWithColumn && ( + + `} + value={this.showColumn(cellProps)} + onChange={() => UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)} + /> + + ) + ))} + + ); + } +} diff --git a/src/renderer/components/item-object-list/filters.tsx b/src/renderer/components/item-object-list/filters.tsx new file mode 100644 index 0000000000..5dd3c13dc2 --- /dev/null +++ b/src/renderer/components/item-object-list/filters.tsx @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./item-list-layout.scss"; + +import React from "react"; +import { PageFiltersList } from "./page-filters-list"; +import { observer } from "mobx-react"; +import type { Filter } from "./page-filters.store"; + +export interface ItemListLayoutFilterProps { + getIsReady: () => boolean + getFilters: () => Filter[] + getFiltersAreShown: () => boolean + hideFilters: boolean +} + +export const ItemListLayoutFilters = observer(({ getFilters, getFiltersAreShown, getIsReady, hideFilters }: ItemListLayoutFilterProps) => { + const filters = getFilters(); + + if (!getIsReady() || !filters.length || hideFilters || !getFiltersAreShown()) { + return null; + } + + return ; +}); + diff --git a/src/renderer/components/item-object-list/header.tsx b/src/renderer/components/item-object-list/header.tsx new file mode 100644 index 0000000000..ce5f19fd05 --- /dev/null +++ b/src/renderer/components/item-object-list/header.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./item-list-layout.scss"; + +import React, { ReactNode } from "react"; +import { observer } from "mobx-react"; +import { cssNames, IClassName } from "../../utils"; +import type { ItemObject, ItemStore } from "../../../common/item.store"; +import type { Filter } from "./page-filters.store"; +import type { HeaderCustomizer, HeaderPlaceholders, SearchFilter } from "./list-layout"; +import { SearchInputUrl } from "../input"; + +export interface ItemListLayoutHeaderProps { + getItems: () => I[]; + getFilters: () => Filter[]; + toggleFilters: () => void; + + store: ItemStore; + searchFilters?: SearchFilter[]; + + // header (title, filtering, searching, etc.) + showHeader?: boolean; + headerClassName?: IClassName; + renderHeaderTitle?: + | ReactNode + | ((parent: ItemListLayoutHeader) => ReactNode); + customizeHeader?: HeaderCustomizer | HeaderCustomizer[]; +} + +@observer +export class ItemListLayoutHeader extends React.Component< + ItemListLayoutHeaderProps +> { + render() { + const { + showHeader, + customizeHeader, + renderHeaderTitle, + headerClassName, + searchFilters, + getItems, + store, + getFilters, + toggleFilters, + } = this.props; + + if (!showHeader) { + return null; + } + + const renderInfo = () => { + const allItemsCount = store.getTotalCount(); + const itemsCount = getItems().length; + + if (getFilters().length > 0) { + return ( + <> + Filtered: {itemsCount} / {allItemsCount} + + ); + } + + return allItemsCount === 1 + ? `${allItemsCount} item` + : `${allItemsCount} items`; + }; + + const customizeHeaderFunctions = [customizeHeader].flat().filter(Boolean); + const renderedTitle = typeof renderHeaderTitle === "function" + ? renderHeaderTitle(this) + : renderHeaderTitle; + + const { + filters, + info, + searchProps, + title, + } = customizeHeaderFunctions.reduce( + (prevPlaceholders, customizer) => customizer(prevPlaceholders), + { + title:
{renderedTitle}
, + info: renderInfo(), + searchProps: {}, + }, + ); + + return ( +
+ {title} + { + info && ( +
+ {info} +
+ ) + } + {filters} + {searchFilters.length > 0 && searchProps && } +
+ ); + } +} diff --git a/src/renderer/components/item-object-list/index.tsx b/src/renderer/components/item-object-list/index.tsx index 6861509d03..bb05729496 100644 --- a/src/renderer/components/item-object-list/index.tsx +++ b/src/renderer/components/item-object-list/index.tsx @@ -3,4 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./item-list-layout"; +export * from "./list-layout"; diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx deleted file mode 100644 index 2d6a91fe70..0000000000 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ /dev/null @@ -1,549 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./item-list-layout.scss"; -import groupBy from "lodash/groupBy"; - -import React, { ReactNode } from "react"; -import { computed, makeObservable } from "mobx"; -import { observer } from "mobx-react"; -import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; -import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table"; -import { - boundMethod, - cssNames, - IClassName, - isReactNode, - noop, - ObservableToggleSet, - prevDefault, - stopPropagation, - StorageHelper, -} from "../../utils"; -import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons"; -import { NoItems } from "../no-items"; -import { Spinner } from "../spinner"; -import type { ItemObject, ItemStore } from "../../../common/item.store"; -import { SearchInputUrlProps, SearchInputUrl } from "../input"; -import { Filter, FilterType, pageFilters } from "./page-filters.store"; -import { PageFiltersList } from "./page-filters-list"; -import { ThemeStore } from "../../theme.store"; -import { MenuActions } from "../menu/menu-actions"; -import { MenuItem } from "../menu"; -import { Checkbox } from "../checkbox"; -import { UserStore } from "../../../common/user-store"; -import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; -import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import itemListLayoutStorageInjectable - from "./item-list-layout-storage/item-list-layout-storage.injectable"; - - -export type SearchFilter = (item: I) => string | number | (string | number)[]; -export type SearchFilters = Record>; -export type ItemsFilter = (items: I[]) => I[]; -export type ItemsFilters = Record>; - -export interface HeaderPlaceholders { - title?: ReactNode; - searchProps?: SearchInputUrlProps; - filters?: ReactNode; - info?: ReactNode; -} - -export type HeaderCustomizer = (placeholders: HeaderPlaceholders) => HeaderPlaceholders; -export interface ItemListLayoutProps { - tableId?: string; - className: IClassName; - items?: I[]; - store: ItemStore; - dependentStores?: ItemStore[]; - preloadStores?: boolean; - hideFilters?: boolean; - searchFilters?: SearchFilter[]; - /** @deprecated */ - filterItems?: ItemsFilter[]; - - // header (title, filtering, searching, etc.) - showHeader?: boolean; - headerClassName?: IClassName; - renderHeaderTitle?: ReactNode | ((parent: NonInjectedItemListLayout) => ReactNode); - customizeHeader?: HeaderCustomizer | HeaderCustomizer[]; - - // items list configuration - isReady?: boolean; // show loading indicator while not ready - isSelectable?: boolean; // show checkbox in rows for selecting items - isConfigurable?: boolean; - copyClassNameFromHeadCells?: boolean; - sortingCallbacks?: TableSortCallbacks; - tableProps?: Partial>; // low-level table configuration - renderTableHeader: TableCellProps[] | null; - renderTableContents: (item: I) => (ReactNode | TableCellProps)[]; - renderItemMenu?: (item: I, store: ItemStore) => ReactNode; - customizeTableRowProps?: (item: I) => Partial; - addRemoveButtons?: Partial; - virtual?: boolean; - - // item details view - hasDetailsView?: boolean; - detailsItem?: I; - onDetails?: (item: I) => void; - - // other - customizeRemoveDialog?: (selectedItems: I[]) => Partial; - renderFooter?: (parent: NonInjectedItemListLayout) => React.ReactNode; - - /** - * Message to display when a store failed to load - * - * @default "Failed to load items" - */ - failedToLoadMessage?: React.ReactNode; - - filterCallbacks?: ItemsFilters; -} - -const defaultProps: Partial> = { - showHeader: true, - isSelectable: true, - isConfigurable: false, - copyClassNameFromHeadCells: true, - preloadStores: true, - dependentStores: [], - searchFilters: [], - customizeHeader: [], - filterItems: [], - hasDetailsView: true, - onDetails: noop, - virtual: true, - customizeTableRowProps: () => ({}), - failedToLoadMessage: "Failed to load items", -}; - -interface Dependencies { - namespaceStore: NamespaceStore; - itemListLayoutStorage: StorageHelper<{ showFilters: boolean }>; -} - -@observer -class NonInjectedItemListLayout extends React.Component & Dependencies> { - static defaultProps = defaultProps as object; - - constructor(props: ItemListLayoutProps & Dependencies) { - super(props); - makeObservable(this); - } - - get showFilters(): boolean { - return this.props.itemListLayoutStorage.get().showFilters; - } - - set showFilters(showFilters: boolean) { - this.props.itemListLayoutStorage.merge({ showFilters }); - } - - async componentDidMount() { - const { isConfigurable, tableId, preloadStores } = this.props; - - if (isConfigurable && !tableId) { - throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); - } - - if (isConfigurable && !UserStore.getInstance().hiddenTableColumns.has(tableId)) { - UserStore.getInstance().hiddenTableColumns.set(tableId, new ObservableToggleSet()); - } - - if (preloadStores) { - this.loadStores(); - } - } - - private loadStores() { - const { store, dependentStores } = this.props; - const stores = Array.from(new Set([store, ...dependentStores])); - - stores.forEach(store => store.loadAll(this.props.namespaceStore.contextNamespaces)); - } - - private filterCallbacks: ItemsFilters = { - [FilterType.SEARCH]: items => { - const { searchFilters } = this.props; - const search = pageFilters.getValues(FilterType.SEARCH)[0] || ""; - - if (search && searchFilters.length) { - const normalizeText = (text: string) => String(text).toLowerCase(); - const searchTexts = [search].map(normalizeText); - - return items.filter(item => { - return searchFilters.some(getTexts => { - const sourceTexts: string[] = [getTexts(item)].flat().map(normalizeText); - - return sourceTexts.some(source => searchTexts.some(search => source.includes(search))); - }); - }); - } - - return items; - }, - }; - - @computed get isReady() { - return this.props.isReady ?? this.props.store.isLoaded; - } - - @computed get failedToLoad() { - return this.props.store.failedLoading; - } - - @computed get filters() { - let { activeFilters } = pageFilters; - const { searchFilters } = this.props; - - if (searchFilters.length === 0) { - activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH); - } - - return activeFilters; - } - - applyFilters(filters: ItemsFilter[], items: I[]): I[] { - if (!filters || !filters.length) return items; - - return filters.reduce((items, filter) => filter(items), items); - } - - @computed get items() { - const { filters, filterCallbacks, props } = this; - const filterGroups = groupBy(filters, ({ type }) => type); - - const filterItems: ItemsFilter[] = []; - - Object.entries(filterGroups).forEach(([type, filtersGroup]) => { - const filterCallback = filterCallbacks[type] ?? props.filterCallbacks?.[type]; - - if (filterCallback && filtersGroup.length > 0) { - filterItems.push(filterCallback); - } - }); - - const items = this.props.items ?? this.props.store.items; - - return this.applyFilters(filterItems.concat(this.props.filterItems), items); - } - - @boundMethod - getRow(uid: string) { - const { - isSelectable, renderTableHeader, renderTableContents, renderItemMenu, - store, hasDetailsView, onDetails, - copyClassNameFromHeadCells, customizeTableRowProps, detailsItem, - } = this.props; - const { isSelected } = store; - const item = this.items.find(item => item.getId() == uid); - - if (!item) return null; - const itemId = item.getId(); - - return ( - onDetails(item)) : undefined} - {...customizeTableRowProps(item)} - > - {isSelectable && ( - store.toggleSelection(item))} - /> - )} - { - renderTableContents(item).map((content, index) => { - const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; - const headCell = renderTableHeader?.[index]; - - if (copyClassNameFromHeadCells && headCell) { - cellProps.className = cssNames(cellProps.className, headCell.className); - } - - if (!headCell || this.showColumn(headCell)) { - return ; - } - - return null; - }) - } - {renderItemMenu && ( - -
- {renderItemMenu(item, store)} -
-
- )} -
- ); - } - - @boundMethod - removeItemsDialog() { - const { customizeRemoveDialog, store } = this.props; - const { selectedItems, removeSelectedItems } = store; - const visibleMaxNamesCount = 5; - const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", "); - const dialogCustomProps = customizeRemoveDialog ? customizeRemoveDialog(selectedItems) : {}; - const selectedCount = selectedItems.length; - const tailCount = selectedCount > visibleMaxNamesCount ? selectedCount - visibleMaxNamesCount : 0; - const tail = tailCount > 0 ? <>, and {tailCount} more : null; - const message = selectedCount <= 1 ?

Remove item {selectedNames}?

:

Remove {selectedCount} items {selectedNames}{tail}?

; - - ConfirmDialog.open({ - ok: removeSelectedItems, - labelOk: "Remove", - message, - ...dialogCustomProps, - }); - } - - @boundMethod - toggleFilters() { - this.showFilters = !this.showFilters; - } - - renderFilters() { - const { hideFilters } = this.props; - const { isReady, filters } = this; - - if (!isReady || !filters.length || hideFilters || !this.showFilters) { - return null; - } - - return ; - } - - renderNoItems() { - if (this.failedToLoad) { - return {this.props.failedToLoadMessage}; - } - - if (!this.isReady) { - return ; - } - - if (this.filters.length > 0) { - return ( - - No items found. -

- pageFilters.reset()} className="contrast"> - Reset filters? - -

-
- ); - } - - return ; - } - - renderItems() { - if (this.props.virtual) { - return null; - } - - return this.items.map(item => this.getRow(item.getId())); - } - - renderHeaderContent(placeholders: HeaderPlaceholders): ReactNode { - const { searchFilters } = this.props; - const { title, filters, searchProps, info } = placeholders; - - return ( - <> - {title} - { - info && ( -
- {info} -
- ) - } - {filters} - {searchFilters.length > 0 && searchProps && } - - ); - } - - renderInfo() { - const { items, filters } = this; - const allItemsCount = this.props.store.getTotalCount(); - const itemsCount = items.length; - - if (filters.length > 0) { - return ( - <>Filtered: {itemsCount} / {allItemsCount} - ); - } - - return allItemsCount === 1 ? `${allItemsCount} item` : `${allItemsCount} items`; - } - - renderHeader() { - const { showHeader, customizeHeader, renderHeaderTitle, headerClassName } = this.props; - - if (!showHeader) { - return null; - } - - const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle; - const customizeHeaders = [customizeHeader].flat().filter(Boolean); - const initialPlaceholders: HeaderPlaceholders = { - title:
{title}
, - info: this.renderInfo(), - searchProps: {}, - }; - const headerPlaceholders = customizeHeaders.reduce((prevPlaceholders, customizer) => customizer(prevPlaceholders), initialPlaceholders); - const header = this.renderHeaderContent(headerPlaceholders); - - return ( -
- {header} -
- ); - } - - renderTableHeader() { - const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props; - - if (!renderTableHeader) { - return null; - } - - const enabledItems = this.items.filter(item => !customizeTableRowProps(item).disabled); - - return ( - - {isSelectable && ( - store.toggleSelectionAll(enabledItems))} - /> - )} - {renderTableHeader.map((cellProps, index) => ( - this.showColumn(cellProps) && ( - - ) - ))} - - {isConfigurable && this.renderColumnVisibilityMenu()} - - - ); - } - - renderList() { - const { - store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, - detailsItem, className, tableProps = {}, tableId, - } = this.props; - const { removeItemsDialog, items } = this; - const { selectedItems } = store; - const selectedItemId = detailsItem && detailsItem.getId(); - const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); - - return ( -
- - {this.renderTableHeader()} - {this.renderItems()} -
- -
- ); - } - - showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { - const { tableId, isConfigurable } = this.props; - - return !isConfigurable || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn); - } - - renderColumnVisibilityMenu() { - const { renderTableHeader, tableId } = this.props; - - return ( - - {renderTableHeader.map((cellProps, index) => ( - !cellProps.showWithColumn && ( - - `} - value={this.showColumn(cellProps)} - onChange={() => UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)} - /> - - ) - ))} - - ); - } - - renderFooter() { - return this.props.renderFooter?.(this); - } - - render() { - const { className } = this.props; - - return ( -
- {this.renderHeader()} - {this.renderFilters()} - {this.renderList()} - {this.renderFooter()} -
- ); - } -} - -export function ItemListLayout( - props: ItemListLayoutProps, -) { - const InjectedItemListLayout = withInjectables< - Dependencies, - ItemListLayoutProps - >( - NonInjectedItemListLayout, - - { - getProps: (di, props) => ({ - namespaceStore: di.inject(namespaceStoreInjectable), - itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable), - ...props, - }), - }, - ); - - return ; -} diff --git a/src/renderer/components/item-object-list/list-layout.tsx b/src/renderer/components/item-object-list/list-layout.tsx new file mode 100644 index 0000000000..39211d0a6b --- /dev/null +++ b/src/renderer/components/item-object-list/list-layout.tsx @@ -0,0 +1,313 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./item-list-layout.scss"; + +import React, { ReactNode } from "react"; +import { computed, makeObservable, untracked } from "mobx"; +import type { ConfirmDialogParams } from "../confirm-dialog"; +import type { + TableCellProps, + TableProps, + TableRowProps, + TableSortCallbacks, +} from "../table"; +import { + boundMethod, + cssNames, + IClassName, + noop, + ObservableToggleSet, + StorageHelper, +} from "../../utils"; +import type { AddRemoveButtonsProps } from "../add-remove-buttons"; +import type { ItemObject, ItemStore } from "../../../common/item.store"; +import type { SearchInputUrlProps } from "../input"; +import { Filter, FilterType, pageFilters } from "./page-filters.store"; +import { PageFiltersList } from "./page-filters-list"; +import { UserStore } from "../../../common/user-store"; +import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import itemListLayoutStorageInjectable + from "./storage.injectable"; +import { ItemListLayoutContent } from "./content"; +import { ItemListLayoutHeader } from "./header"; +import groupBy from "lodash/groupBy"; +import { ItemListLayoutFilters } from "./filters"; +import { observer } from "mobx-react"; + +export type SearchFilter = (item: I) => string | number | (string | number)[]; +export type SearchFilters = Record>; +export type ItemsFilter = (items: I[]) => I[]; +export type ItemsFilters = Record>; + +export interface HeaderPlaceholders { + title?: ReactNode; + searchProps?: SearchInputUrlProps; + filters?: ReactNode; + info?: ReactNode; +} + +export type HeaderCustomizer = (placeholders: HeaderPlaceholders) => HeaderPlaceholders; +export interface ItemListLayoutProps { + tableId?: string; + className: IClassName; + items?: I[]; + getItems?: () => I[]; + store: ItemStore; + dependentStores?: ItemStore[]; + preloadStores?: boolean; + hideFilters?: boolean; + searchFilters?: SearchFilter[]; + /** @deprecated */ + filterItems?: ItemsFilter[]; + + // header (title, filtering, searching, etc.) + showHeader?: boolean; + headerClassName?: IClassName; + renderHeaderTitle?: ReactNode | ((parent: NonInjectedItemListLayout) => ReactNode); + customizeHeader?: HeaderCustomizer | HeaderCustomizer[]; + + // items list configuration + isReady?: boolean; // show loading indicator while not ready + isSelectable?: boolean; // show checkbox in rows for selecting items + isConfigurable?: boolean; + copyClassNameFromHeadCells?: boolean; + sortingCallbacks?: TableSortCallbacks; + tableProps?: Partial>; // low-level table configuration + renderTableHeader: TableCellProps[] | null; + renderTableContents: (item: I) => (ReactNode | TableCellProps)[]; + renderItemMenu?: (item: I, store: ItemStore) => ReactNode; + customizeTableRowProps?: (item: I) => Partial; + addRemoveButtons?: Partial; + virtual?: boolean; + + // item details view + hasDetailsView?: boolean; + detailsItem?: I; + onDetails?: (item: I) => void; + + // other + customizeRemoveDialog?: (selectedItems: I[]) => Partial; + renderFooter?: (parent: NonInjectedItemListLayout) => React.ReactNode; + + /** + * Message to display when a store failed to load + * + * @default "Failed to load items" + */ + failedToLoadMessage?: React.ReactNode; + + filterCallbacks?: ItemsFilters; +} + +const defaultProps: Partial> = { + showHeader: true, + isSelectable: true, + isConfigurable: false, + copyClassNameFromHeadCells: true, + preloadStores: true, + dependentStores: [], + searchFilters: [], + customizeHeader: [], + filterItems: [], + hasDetailsView: true, + onDetails: noop, + virtual: true, + customizeTableRowProps: () => ({}), + failedToLoadMessage: "Failed to load items", +}; + +interface Dependencies { + namespaceStore: NamespaceStore; + itemListLayoutStorage: StorageHelper<{ showFilters: boolean }>; +} + +@observer +class NonInjectedItemListLayout extends React.Component & Dependencies> { + static defaultProps = defaultProps as object; + + constructor(props: ItemListLayoutProps & Dependencies) { + super(props); + makeObservable(this); + } + + async componentDidMount() { + const { isConfigurable, tableId, preloadStores } = this.props; + + if (isConfigurable && !tableId) { + throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); + } + + if (isConfigurable && !UserStore.getInstance().hiddenTableColumns.has(tableId)) { + UserStore.getInstance().hiddenTableColumns.set(tableId, new ObservableToggleSet()); + } + + if (preloadStores) { + this.loadStores(); + } + } + + private loadStores() { + const { store, dependentStores } = this.props; + const stores = Array.from(new Set([store, ...dependentStores])); + + stores.forEach(store => store.loadAll(this.props.namespaceStore.contextNamespaces)); + } + + get showFilters(): boolean { + return this.props.itemListLayoutStorage.get().showFilters; + } + + set showFilters(showFilters: boolean) { + this.props.itemListLayoutStorage.merge({ showFilters }); + } + + @computed get filters() { + let { activeFilters } = pageFilters; + const { searchFilters } = this.props; + + if (searchFilters.length === 0) { + activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH); + } + + return activeFilters; + } + + @boundMethod + toggleFilters() { + this.showFilters = !this.showFilters; + } + + @computed get isReady() { + return this.props.isReady ?? this.props.store.isLoaded; + } + + renderFilters() { + const { hideFilters } = this.props; + const { isReady, filters } = this; + + if (!isReady || !filters.length || hideFilters || !this.showFilters) { + return null; + } + + return ; + } + + private filterCallbacks: ItemsFilters = { + [FilterType.SEARCH]: items => { + const { searchFilters } = this.props; + const search = pageFilters.getValues(FilterType.SEARCH)[0] || ""; + + if (search && searchFilters.length) { + const normalizeText = (text: string) => String(text).toLowerCase(); + const searchTexts = [search].map(normalizeText); + + return items.filter(item => { + return searchFilters.some(getTexts => { + const sourceTexts: string[] = [getTexts(item)].flat().map(normalizeText); + + return sourceTexts.some(source => searchTexts.some(search => source.includes(search))); + }); + }); + } + + return items; + }, + }; + + @computed get items() { + const filterGroups = groupBy(this.filters, ({ type }) => type); + + const filterItems: ItemsFilter[] = []; + + Object.entries(filterGroups).forEach(([type, filtersGroup]) => { + const filterCallback = this.filterCallbacks[type] ?? this.props.filterCallbacks?.[type]; + + if (filterCallback && filtersGroup.length > 0) { + filterItems.push(filterCallback); + } + }); + + const items = this.props.getItems ? this.props.getItems() : (this.props.items ?? this.props.store.items); + + return applyFilters(filterItems.concat(this.props.filterItems), items); + } + + render() { + return untracked(() => ( +
+ this.items} + getFilters={() => this.filters} + toggleFilters={this.toggleFilters} + store={this.props.store} + searchFilters={this.props.searchFilters} + showHeader={this.props.showHeader} + headerClassName={this.props.headerClassName} + renderHeaderTitle={this.props.renderHeaderTitle} + customizeHeader={this.props.customizeHeader} + /> + + this.isReady} + getFilters={() => this.filters} + getFiltersAreShown={() => this.showFilters} + hideFilters={this.props.hideFilters} + /> + + this.items} + getFilters={() => this.filters} + tableId={this.props.tableId} + className={this.props.className} + store={this.props.store} + getIsReady={() => this.isReady} + isSelectable={this.props.isSelectable} + isConfigurable={this.props.isConfigurable} + copyClassNameFromHeadCells={this.props.copyClassNameFromHeadCells} + sortingCallbacks={this.props.sortingCallbacks} + tableProps={this.props.tableProps} + renderTableHeader={this.props.renderTableHeader} + renderTableContents={this.props.renderTableContents} + renderItemMenu={this.props.renderItemMenu} + customizeTableRowProps={this.props.customizeTableRowProps} + addRemoveButtons={this.props.addRemoveButtons} + virtual={this.props.virtual} + hasDetailsView={this.props.hasDetailsView} + detailsItem={this.props.detailsItem} + onDetails={this.props.onDetails} + customizeRemoveDialog={this.props.customizeRemoveDialog} + failedToLoadMessage={this.props.failedToLoadMessage} + /> + + {this.props.renderFooter?.(this)} +
+ )); + } +} + +const InjectedItemListLayout = withInjectables>(NonInjectedItemListLayout, { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable), + ...props, + }), +}); + +export function ItemListLayout(props: ItemListLayoutProps) { + return ; +} + +function applyFilters(filters: ItemsFilter[], items: I[]): I[] { + if (!filters || !filters.length) { + return items; + } + + return filters.reduce((items, filter) => filter(items), items); +} diff --git a/src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts b/src/renderer/components/item-object-list/storage.injectable.ts similarity index 85% rename from src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts rename to src/renderer/components/item-object-list/storage.injectable.ts index 9033e5fdfe..a105d35472 100644 --- a/src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts +++ b/src/renderer/components/item-object-list/storage.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; +import createStorageInjectable from "../../utils/create-storage/create-storage.injectable"; const itemListLayoutStorageInjectable = getInjectable({ instantiate: (di) => { diff --git a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index 304694da0f..de1253fd65 100644 --- a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -10,7 +10,7 @@ import { computed, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames, Disposer } from "../../utils"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout"; +import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/list-layout"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectMenu } from "../kube-object-menu"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; @@ -99,14 +99,14 @@ class NonInjectedKubeObjectListLayout extends React.Compon } render() { - const { className, customizeHeader, store, items = store.contextItems, ...layoutProps } = this.props; + const { className, customizeHeader, store, items, ...layoutProps } = this.props; const placeholderString = ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind; return ( this.props.items || store.contextItems} preloadStores={false} // loading handled in kubeWatchApi.subscribeStores() detailsItem={this.selectedItem} customizeHeader={[ diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 935c4b9208..9c22de1b27 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -17,8 +17,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import clusterInjectable from "./dependencies/cluster.injectable"; import hideDetailsInjectable from "./dependencies/hide-details.injectable"; -import editResourceTabInjectable from "../dock/edit-resource-tab/edit-resource-tab.injectable"; -import { TabKind } from "../dock/dock-store/dock.store"; +import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; import kubeObjectMenuRegistryInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-registry.injectable"; import { DiRender, renderFor } from "../test-utils/renderFor"; import type { Cluster } from "../../../common/cluster/cluster"; @@ -54,12 +53,7 @@ describe("kube-object-menu", () => { di.override(hideDetailsInjectable, () => () => {}); - di.override(editResourceTabInjectable, () => () => ({ - id: "irrelevant", - kind: TabKind.TERMINAL, - pinned: false, - title: "irrelevant", - })); + di.override(createEditResourceTabInjectable, () => () => "irrelevant"); addDynamicMenuItem({ di, diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx index f4f3a81b58..6d425ab9c3 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -11,7 +11,7 @@ import identity from "lodash/identity"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; import { withInjectables } from "@ogre-tools/injectable-react"; import clusterNameInjectable from "./dependencies/cluster-name.injectable"; -import editResourceTabInjectable from "../dock/edit-resource-tab/edit-resource-tab.injectable"; +import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; import hideDetailsInjectable from "./dependencies/hide-details.injectable"; import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable"; import apiManagerInjectable from "./dependencies/api-manager.injectable"; @@ -27,7 +27,7 @@ interface Dependencies { kubeObjectMenuItems: React.ElementType[]; clusterName: string; hideDetails: () => void; - editResourceTab: (kubeObject: KubeObject) => void; + createEditResourceTab: (kubeObject: KubeObject) => void; } class NonInjectedKubeObjectMenu extends React.Component & Dependencies> { @@ -51,7 +51,7 @@ class NonInjectedKubeObjectMenu extends React.Co @boundMethod async update() { this.props.hideDetails(); - this.props.editResourceTab(this.props.object); + this.props.createEditResourceTab(this.props.object); } @boundMethod @@ -117,7 +117,7 @@ export function KubeObjectMenu( getProps: (di, props) => ({ clusterName: di.inject(clusterNameInjectable), apiManager: di.inject(apiManagerInjectable), - editResourceTab: di.inject(editResourceTabInjectable), + createEditResourceTab: di.inject(createEditResourceTabInjectable), hideDetails: di.inject(hideDetailsInjectable), kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, { diff --git a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx index 0190adcc71..9b557a2f6c 100644 --- a/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx @@ -20,15 +20,14 @@ jest.mock("../../../../common/ipc"); jest.mock("../../../ipc"); jest.mock("../../../../common/vars", () => { - const SemVer = require("semver").SemVer; - - const versionStub = new SemVer("1.0.0"); + const { SemVer } = require("semver"); return { + ...jest.requireActual<{}>("../../../../common/vars"), __esModule: true, isWindows: null, isLinux: null, - appSemVer: versionStub, + appSemVer: new SemVer("1.0.0"), }; }); diff --git a/src/renderer/components/layout/top-bar/top-bar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx index e5d491401c..db61d3e8d0 100644 --- a/src/renderer/components/layout/top-bar/top-bar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -14,15 +14,15 @@ import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import { computed } from "mobx"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import mockFs from "mock-fs"; +import isLinuxInjectable from "../../../../common/vars/is-linux.injectable"; +import isWindowsInjectable from "../../../../common/vars/is-windows.injectable"; jest.mock("../../../../common/vars", () => { - const SemVer = require("semver").SemVer; - - const versionStub = new SemVer("1.0.0"); + const { SemVer } = require("semver"); return { - isMac: true, - appSemVer: versionStub, + ...jest.requireActual<{}>("../../../../common/vars"), + appSemVer: new SemVer("1.0.0"), }; }); @@ -98,30 +98,30 @@ describe("", () => { }); it("renders home button", async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - expect(await getByTestId("home-button")).toBeInTheDocument(); + expect(await findByTestId("home-button")).toBeInTheDocument(); }); it("renders history arrows", async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - expect(await getByTestId("history-back")).toBeInTheDocument(); - expect(await getByTestId("history-forward")).toBeInTheDocument(); + expect(await findByTestId("history-back")).toBeInTheDocument(); + expect(await findByTestId("history-forward")).toBeInTheDocument(); }); it("enables arrow by ipc event", async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - expect(await getByTestId("history-back")).not.toHaveClass("disabled"); - expect(await getByTestId("history-forward")).not.toHaveClass("disabled"); + expect(await findByTestId("history-back")).not.toHaveClass("disabled"); + expect(await findByTestId("history-forward")).not.toHaveClass("disabled"); }); it("triggers browser history back and forward", async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - const prevButton = await getByTestId("history-back"); - const nextButton = await getByTestId("history-forward"); + const prevButton = await findByTestId("history-back"); + const nextButton = await findByTestId("history-forward"); fireEvent.click(prevButton); @@ -144,12 +144,15 @@ describe("", () => { }, ])); - const { getByTestId } = render(); + const { findByTestId } = render(); - expect(await getByTestId(testId)).toHaveTextContent(text); + expect(await findByTestId(testId)).toHaveTextContent(text); }); - it("doesn't show windows title buttons", () => { + it("doesn't show windows title buttons on macos", () => { + di.override(isLinuxInjectable, () => false); + di.override(isWindowsInjectable, () => false); + const { queryByTestId } = render(); expect(queryByTestId("window-menu")).not.toBeInTheDocument(); @@ -157,4 +160,16 @@ describe("", () => { expect(queryByTestId("window-maximize")).not.toBeInTheDocument(); expect(queryByTestId("window-close")).not.toBeInTheDocument(); }); + + it("does show windows title buttons on linux", () => { + di.override(isLinuxInjectable, () => true); + di.override(isWindowsInjectable, () => false); + + const { queryByTestId } = render(); + + expect(queryByTestId("window-menu")).toBeInTheDocument(); + expect(queryByTestId("window-minimize")).toBeInTheDocument(); + expect(queryByTestId("window-maximize")).toBeInTheDocument(); + expect(queryByTestId("window-close")).toBeInTheDocument(); + }); }); diff --git a/src/renderer/components/layout/top-bar/top-bar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx index dc801a5424..3030cbad57 100644 --- a/src/renderer/components/layout/top-bar/top-bar.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.tsx @@ -13,18 +13,21 @@ import { ipcRendererOn } from "../../../../common/ipc"; import { watchHistoryState } from "../../../remote-helpers/history-updater"; import { isActiveRoute, navigate } from "../../../navigation"; import { catalogRoute, catalogURL } from "../../../../common/routes"; -import { isLinux, isWindows } from "../../../../common/vars"; import { cssNames } from "../../../utils"; import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { TopBarRegistration } from "./top-bar-registration"; import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc"; import { WindowAction } from "../../../../common/ipc/window"; +import isLinuxInjectable from "../../../../common/vars/is-linux.injectable"; +import isWindowsInjectable from "../../../../common/vars/is-windows.injectable"; -interface Props extends React.HTMLAttributes {} +export interface TopBarProps extends React.HTMLAttributes {} interface Dependencies { items: IComputedValue; + isWindows: boolean; + isLinux: boolean; } const prevEnabled = observable.box(false); @@ -38,7 +41,7 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => { nextEnabled.set(state); }); -const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => { +const NonInjectedTopBar = observer(({ items, children, isWindows, isLinux, ...rest }: TopBarProps & Dependencies) => { const elem = useRef(); const openAppContextMenu = () => { @@ -161,9 +164,11 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => ( -export const TopBar = withInjectables(observer(NonInjectedTopBar), { +export const TopBar = withInjectables(NonInjectedTopBar, { getProps: (di, props) => ({ items: di.inject(topBarItemsInjectable), + isLinux: di.inject(isLinuxInjectable), + isWindows: di.inject(isWindowsInjectable), ...props, }), }); diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 901b7edbf8..c353008371 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -8,8 +8,10 @@ import { memoize } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import writeJsonFileInjectable from "../common/fs/write-json-file/write-json-file.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file/read-json-file.injectable"; +import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; +import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; +import readDirInjectable from "../common/fs/read-dir.injectable"; +import readFileInjectable from "../common/fs/read-file.injectable"; export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => { const di = createContainer(); @@ -31,6 +33,14 @@ export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverride if (doGeneralOverrides) { di.override(getValueFromRegisteredChannelInjectable, () => () => undefined); + di.override(readDirInjectable, () => () => { + throw new Error("Tried to read contents of a directory from file system without specifying explicit override."); + }); + + di.override(readFileInjectable, () => () => { + throw new Error("Tried to read a file from file system without specifying explicit override."); + }); + di.override(writeJsonFileInjectable, () => () => { throw new Error("Tried to write JSON file to file system without specifying explicit override."); }); diff --git a/src/renderer/kube-watch-api/kube-watch-api.ts b/src/renderer/kube-watch-api/kube-watch-api.ts index 4b9a802723..f38ffcec1c 100644 --- a/src/renderer/kube-watch-api/kube-watch-api.ts +++ b/src/renderer/kube-watch-api/kube-watch-api.ts @@ -27,7 +27,6 @@ class WrappedAbortController extends AbortController { interface SubscribeStoreParams { store: KubeObjectStore; parent: AbortController; - watchChanges: boolean; namespaces: string[]; onLoadFailure?: (err: any) => void; } @@ -75,7 +74,7 @@ export interface KubeWatchSubscribeStoreOptions { /** * A function that is called when listing fails. If set then blocks errors - * being rejected with + * from rejecting promises */ onLoadFailure?: (err: any) => void; } @@ -89,12 +88,16 @@ export class KubeWatchApi { constructor(private dependencies: Dependencies) {} - private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { - if (this.#watch.inc(store) > 1) { + private subscribeStore({ store, parent, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { + const isNamespaceFilterWatch = !namespaces; + + if (isNamespaceFilterWatch && this.#watch.inc(store) > 1) { // don't load or subscribe to a store more than once return () => this.#watch.dec(store); } + namespaces ??= this.dependencies.clusterFrameContext?.contextNamespaces ?? []; + let childController = new WrappedAbortController(parent); const unsubscribe = disposer(); @@ -117,7 +120,7 @@ export class KubeWatchApi { */ loadThenSubscribe(namespaces).catch(noop); - const cancelReloading = watchChanges + const cancelReloading = isNamespaceFilterWatch && store.api.isNamespaced ? reaction( // Note: must slice because reaction won't fire if it isn't there () => [this.dependencies.clusterFrameContext.contextNamespaces.slice(), this.dependencies.clusterFrameContext.hasSelectedAll] as const, @@ -141,7 +144,7 @@ export class KubeWatchApi { : noop; // don't watch namespaces if namespaces were provided return () => { - if (this.#watch.dec(store) === 0) { + if (isNamespaceFilterWatch && this.#watch.dec(store) === 0) { // only stop the subcribe if this is the last one cancelReloading(); childController.abort(); @@ -156,8 +159,7 @@ export class KubeWatchApi { ...stores.map(store => this.subscribeStore({ store, parent, - watchChanges: !namespaces && store.api.isNamespaced, - namespaces: namespaces ?? this.dependencies.clusterFrameContext?.contextNamespaces ?? [], + namespaces, onLoadFailure, })), ); diff --git a/src/renderer/kube-watch-api/subscribe-stores.injectable.ts b/src/renderer/kube-watch-api/subscribe-stores.injectable.ts new file mode 100644 index 0000000000..35eb3a722e --- /dev/null +++ b/src/renderer/kube-watch-api/subscribe-stores.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import kubeWatchApiInjectable from "./kube-watch-api.injectable"; + +const subscribeStoresInjectable = getInjectable({ + instantiate: (di) => di.inject(kubeWatchApiInjectable).subscribeStores, + lifecycle: lifecycleEnum.singleton, +}); + +export default subscribeStoresInjectable; diff --git a/src/renderer/search-store/search-store.injectable.ts b/src/renderer/search-store/search-store.injectable.ts new file mode 100644 index 0000000000..3d5d08f484 --- /dev/null +++ b/src/renderer/search-store/search-store.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { SearchStore } from "./search-store"; + +const searchStoreInjectable = getInjectable({ + instantiate: () => new SearchStore(), + lifecycle: lifecycleEnum.transient, +}); + +export default searchStoreInjectable; diff --git a/src/renderer/search-store/search-store.test.ts b/src/renderer/search-store/search-store.test.ts index fa8623274c..f3fc9a4289 100644 --- a/src/renderer/search-store/search-store.test.ts +++ b/src/renderer/search-store/search-store.test.ts @@ -9,6 +9,7 @@ import { stdout, stderr } from "process"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import searchStoreInjectable from "./search-store.injectable"; jest.mock("electron", () => ({ app: { @@ -34,7 +35,7 @@ describe("search store tests", () => { await di.runSetups(); - searchStore = new SearchStore(); + searchStore = di.inject(searchStoreInjectable); }); it("does nothing with empty search query", () => { diff --git a/src/renderer/utils/create-storage/create-storage.injectable.ts b/src/renderer/utils/create-storage/create-storage.injectable.ts index 1f665bd002..729123a3ad 100644 --- a/src/renderer/utils/create-storage/create-storage.injectable.ts +++ b/src/renderer/utils/create-storage/create-storage.injectable.ts @@ -5,8 +5,8 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import { createStorage } from "./create-storage"; -import readJsonFileInjectable from "../../../common/fs/read-json-file/read-json-file.injectable"; -import writeJsonFileInjectable from "../../../common/fs/write-json-file/write-json-file.injectable"; +import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; +import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; const createStorageInjectable = getInjectable({ instantiate: (di) => diff --git a/src/renderer/utils/save-file.injectable.ts b/src/renderer/utils/save-file.injectable.ts new file mode 100644 index 0000000000..593c6955dc --- /dev/null +++ b/src/renderer/utils/save-file.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { saveFileDialog } from "./saveFile"; + +const openSaveFileDialogInjectable = getInjectable({ + instantiate: () => saveFileDialog, + lifecycle: lifecycleEnum.singleton, +}); + +export default openSaveFileDialogInjectable; diff --git a/webpack.extensions.ts b/webpack.extensions.ts index a4b06bf9f8..330684adb4 100644 --- a/webpack.extensions.ts +++ b/webpack.extensions.ts @@ -34,6 +34,10 @@ export default function generateExtensionTypes(): webpack.Configuration { stats: "errors-warnings", module: { rules: [ + { + test: /\.node$/, + loader: "ignore-loader", + }, { test: /\.tsx?$/, loader: "ts-loader", diff --git a/yarn.lock b/yarn.lock index dd2ff2256c..226a290633 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7135,6 +7135,11 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= +ignore-loader@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.2.tgz#d81f240376d0ba4f0d778972c3ad25874117a463" + integrity sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM= + ignore-walk@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" From ac42a6565f65d5b5f844a667baf9a595bb2a3dca Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Mon, 31 Jan 2022 16:16:29 +0100 Subject: [PATCH 10/13] Replace kube object menu registry with reactive solution (#4731) Co-authored-by: Sebastian Malton --- __mocks__/monaco-editor.ts | 5 + src/extensions/common-api/registrations.ts | 2 +- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - src/renderer/bootstrap.tsx | 3 - .../service-account-menu.tsx | 22 ++++ .../+service-accounts/view.tsx | 15 --- .../+workloads-cronjobs/cron-job-menu.tsx | 63 +++++++++ .../+workloads-cronjobs/cronjobs.tsx | 57 -------- .../deployment-menu.tsx | 47 +++++++ .../+workloads-deployments/deployments.tsx | 43 +----- .../replica-set-menu.tsx | 23 ++++ .../+workloads-replicasets/replicasets.tsx | 17 --- .../stateful-set-menu.tsx | 23 ++++ .../+workloads-statefulsets/statefulsets.tsx | 17 --- .../get-kube-object-menu-items.ts | 31 +++-- .../kube-object-menu-items.injectable.ts | 4 +- .../kube-object-menu-registration.ts} | 17 +-- .../kube-object-menu-registry.injectable.ts | 13 -- .../static-kube-object-menu-items.ts | 47 +++++++ .../kube-object-menu.test.tsx | 124 ++++++++++-------- src/renderer/initializers/index.ts | 1 - .../initializers/kube-object-menu-registry.ts | 52 -------- src/renderer/initializers/registries.ts | 1 - 25 files changed, 333 insertions(+), 299 deletions(-) create mode 100644 __mocks__/monaco-editor.ts create mode 100644 src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx create mode 100644 src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx create mode 100644 src/renderer/components/+workloads-deployments/deployment-menu.tsx create mode 100644 src/renderer/components/+workloads-replicasets/replica-set-menu.tsx create mode 100644 src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx rename src/{extensions/registries/kube-object-menu-registry.ts => renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration.ts} (50%) delete mode 100644 src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registry.injectable.ts create mode 100644 src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/static-kube-object-menu-items.ts delete mode 100644 src/renderer/initializers/kube-object-menu-registry.ts diff --git a/__mocks__/monaco-editor.ts b/__mocks__/monaco-editor.ts new file mode 100644 index 0000000000..cbe02cb296 --- /dev/null +++ b/__mocks__/monaco-editor.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export default {}; diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts index 5178dd9ee4..be3e8d87cd 100644 --- a/src/extensions/common-api/registrations.ts +++ b/src/extensions/common-api/registrations.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ export type { StatusBarRegistration } from "../../renderer/components/cluster-manager/status-bar-registration"; +export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; -export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry"; diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 11ace2cbf1..e954a1f460 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -276,7 +276,6 @@ export class ExtensionLoader { const removeItems = [ registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension), registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension), - registries.KubeObjectMenuRegistry.getInstance().add(extension.kubeObjectMenuItems), registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems), registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts), registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems), diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 9104f8871b..9d69b4485a 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -19,6 +19,7 @@ import type { AppPreferenceRegistration } from "../renderer/components/+preferen import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns"; import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views"; import type { StatusBarRegistration } from "../renderer/components/cluster-manager/status-bar-registration"; +import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -29,7 +30,7 @@ export class LensRendererExtension extends LensExtension { entitySettings: registries.EntitySettingRegistration[] = []; statusBarItems: StatusBarRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; - kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; + kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; commands: CommandRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 464d19cd4e..0713e3eede 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -8,7 +8,6 @@ export * from "./page-registry"; export * from "./page-menu-registry"; export * from "./kube-object-detail-registry"; -export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; export * from "./entity-setting-registry"; export * from "./catalog-entity-detail-registry"; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 9be509ab61..5593134586 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -73,9 +73,6 @@ export async function bootstrap(di: DependencyInjectionContainer) { logger.info(`${logPrefix} initializing EntitySettingsRegistry`); initializers.initEntitySettingsRegistry(); - logger.info(`${logPrefix} initializing KubeObjectMenuRegistry`); - initializers.initKubeObjectMenuRegistry(); - logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); initializers.initKubeObjectDetailRegistry(); diff --git a/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx b/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx new file mode 100644 index 0000000000..6fc180a2e9 --- /dev/null +++ b/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; + +import type { KubeObjectMenuProps } from "../../kube-object-menu"; +import type { ServiceAccount } from "../../../../common/k8s-api/endpoints"; +import { MenuItem } from "../../menu"; +import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog"; +import { Icon } from "../../icon"; + +export function ServiceAccountMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + + return ( + openServiceAccountKubeConfig(object)}> + + Kubeconfig + + ); +} diff --git a/src/renderer/components/+user-management/+service-accounts/view.tsx b/src/renderer/components/+user-management/+service-accounts/view.tsx index b8405781b0..94c7370639 100644 --- a/src/renderer/components/+user-management/+service-accounts/view.tsx +++ b/src/renderer/components/+user-management/+service-accounts/view.tsx @@ -8,13 +8,8 @@ import "./view.scss"; import { observer } from "mobx-react"; import React from "react"; import type { RouteComponentProps } from "react-router"; -import type { ServiceAccount } from "../../../../common/k8s-api/endpoints/service-accounts.api"; -import { Icon } from "../../icon"; import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; -import type { KubeObjectMenuProps } from "../../kube-object-menu"; -import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog"; -import { MenuItem } from "../../menu"; import { CreateServiceAccountDialog } from "./create-dialog"; import { serviceAccountsStore } from "./store"; import type { ServiceAccountsRouteParams } from "../../../../common/routes"; @@ -69,13 +64,3 @@ export class ServiceAccounts extends React.Component { } } -export function ServiceAccountMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; - - return ( - openServiceAccountKubeConfig(object)}> - - Kubeconfig - - ); -} diff --git a/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx b/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx new file mode 100644 index 0000000000..b7f6184519 --- /dev/null +++ b/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx @@ -0,0 +1,63 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import type { KubeObjectMenuProps } from "../kube-object-menu"; +import { CronJob, cronJobApi } from "../../../common/k8s-api/endpoints"; +import { MenuItem } from "../menu"; +import { CronJobTriggerDialog } from "./cronjob-trigger-dialog"; +import { Icon } from "../icon"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Notifications } from "../notifications"; + +export function CronJobMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + + return ( + <> + CronJobTriggerDialog.open(object)}> + + Trigger + + + {object.isSuspend() ? + ConfirmDialog.open({ + ok: async () => { + try { + await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Resume`, + message: ( +

+ Resume CronJob {object.getName()}? +

), + })}> + + Resume +
+ + : ConfirmDialog.open({ + ok: async () => { + try { + await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Suspend`, + message: ( +

+ Suspend CronJob {object.getName()}? +

), + })}> + + Suspend +
+ } + + ); +} diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index c4605ff45f..30e476502e 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -8,18 +8,11 @@ import "./cronjobs.scss"; import React from "react"; import { observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import { CronJob, cronJobApi } from "../../../common/k8s-api/endpoints/cron-job.api"; -import { MenuItem } from "../menu"; -import { Icon } from "../icon"; import { cronJobStore } from "./cronjob.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { eventStore } from "../+events/event.store"; -import type { KubeObjectMenuProps } from "../kube-object-menu"; import { KubeObjectListLayout } from "../kube-object-list-layout"; -import { CronJobTriggerDialog } from "./cronjob-trigger-dialog"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { ConfirmDialog } from "../confirm-dialog/confirm-dialog"; -import { Notifications } from "../notifications/notifications"; import type { CronJobsRouteParams } from "../../../common/routes"; import moment from "moment"; @@ -87,53 +80,3 @@ export class CronJobs extends React.Component { } } -export function CronJobMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; - - return ( - <> - CronJobTriggerDialog.open(object)}> - - Trigger - - - {object.isSuspend() ? - ConfirmDialog.open({ - ok: async () => { - try { - await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() }); - } catch (err) { - Notifications.error(err); - } - }, - labelOk: `Resume`, - message: ( -

- Resume CronJob {object.getName()}? -

), - })}> - - Resume -
- - : ConfirmDialog.open({ - ok: async () => { - try { - await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() }); - } catch (err) { - Notifications.error(err); - } - }, - labelOk: `Suspend`, - message: ( -

- Suspend CronJob {object.getName()}? -

), - })}> - - Suspend -
- } - - ); -} diff --git a/src/renderer/components/+workloads-deployments/deployment-menu.tsx b/src/renderer/components/+workloads-deployments/deployment-menu.tsx new file mode 100644 index 0000000000..9c7544a1c4 --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployment-menu.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import type { KubeObjectMenuProps } from "../kube-object-menu"; +import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints"; +import { MenuItem } from "../menu"; +import { DeploymentScaleDialog } from "./deployment-scale-dialog"; +import { Icon } from "../icon"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Notifications } from "../notifications"; + +export function DeploymentMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + + return ( + <> + DeploymentScaleDialog.open(object)}> + + Scale + + ConfirmDialog.open({ + ok: async () => + { + try { + await deploymentApi.restart({ + namespace: object.getNs(), + name: object.getName(), + }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Restart`, + message: ( +

+ Are you sure you want to restart deployment {object.getName()}? +

+ ), + })}> + + Restart +
+ + ); +} diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 79ce4af810..3506db0486 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -8,12 +8,7 @@ import "./deployments.scss"; import React from "react"; import { observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints"; -import type { KubeObjectMenuProps } from "../kube-object-menu"; -import { MenuItem } from "../menu"; -import { Icon } from "../icon"; -import { DeploymentScaleDialog } from "./deployment-scale-dialog"; -import { ConfirmDialog } from "../confirm-dialog"; +import type { Deployment } from "../../../common/k8s-api/endpoints"; import { deploymentStore } from "./deployments.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; @@ -21,7 +16,6 @@ import { cssNames } from "../../utils"; import kebabCase from "lodash/kebabCase"; import orderBy from "lodash/orderBy"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { Notifications } from "../notifications"; import type { DeploymentsRouteParams } from "../../../common/routes"; enum columnId { @@ -95,38 +89,3 @@ export class Deployments extends React.Component { ); } } - -export function DeploymentMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; - - return ( - <> - DeploymentScaleDialog.open(object)}> - - Scale - - ConfirmDialog.open({ - ok: async () => - { - try { - await deploymentApi.restart({ - namespace: object.getNs(), - name: object.getName(), - }); - } catch (err) { - Notifications.error(err); - } - }, - labelOk: `Restart`, - message: ( -

- Are you sure you want to restart deployment {object.getName()}? -

- ), - })}> - - Restart -
- - ); -} diff --git a/src/renderer/components/+workloads-replicasets/replica-set-menu.tsx b/src/renderer/components/+workloads-replicasets/replica-set-menu.tsx new file mode 100644 index 0000000000..deab2193cd --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replica-set-menu.tsx @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import type { KubeObjectMenuProps } from "../kube-object-menu"; +import type { ReplicaSet } from "../../../common/k8s-api/endpoints"; +import { MenuItem } from "../menu"; +import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; +import { Icon } from "../icon"; + +export function ReplicaSetMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + + return ( + <> + ReplicaSetScaleDialog.open(object)}> + + Scale + + + ); +} diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index f3986125b6..be21604719 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -7,15 +7,10 @@ import "./replicasets.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { ReplicaSet } from "../../../common/k8s-api/endpoints"; -import type { KubeObjectMenuProps } from "../kube-object-menu"; import { replicaSetStore } from "./replicasets.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; -import { MenuItem } from "../menu/menu"; -import { Icon } from "../icon/icon"; -import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; import type { ReplicaSetsRouteParams } from "../../../common/routes"; import { eventStore } from "../+events/event.store"; @@ -75,15 +70,3 @@ export class ReplicaSets extends React.Component { } } -export function ReplicaSetMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; - - return ( - <> - ReplicaSetScaleDialog.open(object)}> - - Scale - - - ); -} diff --git a/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx b/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx new file mode 100644 index 0000000000..b9725f6e13 --- /dev/null +++ b/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import type { KubeObjectMenuProps } from "../kube-object-menu"; +import type { StatefulSet } from "../../../common/k8s-api/endpoints"; +import { MenuItem } from "../menu"; +import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; +import { Icon } from "../icon"; + +export function StatefulSetMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + + return ( + <> + StatefulSetScaleDialog.open(object)}> + + Scale + + + ); +} diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index c00f9456ae..d7aa9e847f 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -12,12 +12,8 @@ import type { StatefulSet } from "../../../common/k8s-api/endpoints"; import { podsStore } from "../+workloads-pods/pods.store"; import { statefulSetStore } from "./statefulset.store"; import { eventStore } from "../+events/event.store"; -import type { KubeObjectMenuProps } from "../kube-object-menu"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; -import { MenuItem } from "../menu/menu"; -import { Icon } from "../icon/icon"; import type { StatefulSetsRouteParams } from "../../../common/routes"; enum columnId { @@ -76,16 +72,3 @@ export class StatefulSets extends React.Component { ); } } - -export function StatefulSetMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; - - return ( - <> - StatefulSetScaleDialog.open(object)}> - - Scale - - - ); -} diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts index 0e8baf98e6..7091a7c242 100644 --- a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts +++ b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts @@ -2,21 +2,36 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { KubeObjectMenuRegistry } from "../../../../../extensions/registries"; +import { conforms, includes, eq } from "lodash/fp"; import type { KubeObject } from "../../../../../common/k8s-api/kube-object"; +import type { LensRendererExtension } from "../../../../../extensions/lens-renderer-extension"; +import { staticKubeObjectMenuItems as staticMenuItems } from "./static-kube-object-menu-items"; + +interface Dependencies { + extensions: LensRendererExtension[]; + kubeObject: KubeObject; +} export const getKubeObjectMenuItems = ({ - kubeObjectMenuRegistry, + extensions, kubeObject, -}: { - kubeObjectMenuRegistry: KubeObjectMenuRegistry; - kubeObject: KubeObject; -}) => { +}: Dependencies) => { if (!kubeObject) { return []; } - return kubeObjectMenuRegistry - .getItemsForKind(kubeObject.kind, kubeObject.apiVersion) + const extensionMenuItems = extensions.flatMap( + (extension) => extension.kubeObjectMenuItems, + ); + + return [...staticMenuItems, ...extensionMenuItems] + .filter( + conforms({ + kind: eq(kubeObject.kind), + apiVersions: includes(kubeObject.apiVersion), + }), + ) + .map((item) => item.components.MenuItem); }; + diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts index 3d8f16de8e..703374ab2d 100644 --- a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts +++ b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts @@ -3,15 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import kubeObjectMenuRegistryInjectable from "./kube-object-menu-registry.injectable"; import { getKubeObjectMenuItems } from "./get-kube-object-menu-items"; import type { KubeObject } from "../../../../../common/k8s-api/kube-object"; +import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable"; const kubeObjectMenuItemsInjectable = getInjectable({ instantiate: (di, { kubeObject }: { kubeObject: KubeObject }) => getKubeObjectMenuItems({ - kubeObjectMenuRegistry: di.inject(kubeObjectMenuRegistryInjectable), + extensions: di.inject(rendererExtensionsInjectable).get(), kubeObject, }), diff --git a/src/extensions/registries/kube-object-menu-registry.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration.ts similarity index 50% rename from src/extensions/registries/kube-object-menu-registry.ts rename to src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration.ts index a1db08e26d..b3a0904634 100644 --- a/src/extensions/registries/kube-object-menu-registry.ts +++ b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration.ts @@ -4,10 +4,15 @@ */ import type React from "react"; -import { BaseRegistry } from "./base-registry"; +import type { KubeObject } from "../../../../../common/k8s-api/kube-object"; + +export interface KubeObjectMenuItemProps { + object: KubeObject; + toolbar?: boolean; +} export interface KubeObjectMenuComponents { - MenuItem: React.ComponentType; + MenuItem: React.ComponentType; } export interface KubeObjectMenuRegistration { @@ -15,11 +20,3 @@ export interface KubeObjectMenuRegistration { apiVersions: string[]; components: KubeObjectMenuComponents; } - -export class KubeObjectMenuRegistry extends BaseRegistry { - getItemsForKind(kind: string, apiVersion: string) { - return this.getItems().filter((item) => { - return item.kind === kind && item.apiVersions.includes(apiVersion); - }); - } -} diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registry.injectable.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registry.injectable.ts deleted file mode 100644 index 1bbcc031ae..0000000000 --- a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registry.injectable.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { KubeObjectMenuRegistry } from "../../../../../extensions/registries"; -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; - -const kubeObjectMenuRegistryInjectable = getInjectable({ - instantiate: () => KubeObjectMenuRegistry.getInstance(), - lifecycle: lifecycleEnum.singleton, -}); - -export default kubeObjectMenuRegistryInjectable; diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/static-kube-object-menu-items.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/static-kube-object-menu-items.ts new file mode 100644 index 0000000000..0368fbb241 --- /dev/null +++ b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/static-kube-object-menu-items.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { ServiceAccountMenu } from "../../../+user-management/+service-accounts/service-account-menu"; +import { CronJobMenu } from "../../../+workloads-cronjobs/cron-job-menu"; +import { DeploymentMenu } from "../../../+workloads-deployments/deployment-menu"; +import { ReplicaSetMenu } from "../../../+workloads-replicasets/replica-set-menu"; +import { StatefulSetMenu } from "../../../+workloads-statefulsets/stateful-set-menu"; + +export const staticKubeObjectMenuItems = [ + { + kind: "ServiceAccount", + apiVersions: ["v1"], + components: { + MenuItem: ServiceAccountMenu, + }, + }, + { + kind: "CronJob", + apiVersions: ["batch/v1beta1"], + components: { + MenuItem: CronJobMenu, + }, + }, + { + kind: "Deployment", + apiVersions: ["apps/v1"], + components: { + MenuItem: DeploymentMenu, + }, + }, + { + kind: "ReplicaSet", + apiVersions: ["apps/v1"], + components: { + MenuItem: ReplicaSetMenu, + }, + }, + { + kind: "StatefulSet", + apiVersions: ["apps/v1"], + components: { + MenuItem: StatefulSetMenu, + }, + }, +]; diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 9c22de1b27..ac6b55f36f 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -2,76 +2,108 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - import React from "react"; import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import userEvent from "@testing-library/user-event"; import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; -import type { KubeObjectMenuRegistration } from "../../../extensions/registries"; -import { KubeObjectMenuRegistry } from "../../../extensions/registries"; import { ConfirmDialog } from "../confirm-dialog"; import asyncFn, { AsyncFnMock } from "@async-fn/jest"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import clusterInjectable from "./dependencies/cluster.injectable"; import hideDetailsInjectable from "./dependencies/hide-details.injectable"; -import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; -import kubeObjectMenuRegistryInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-registry.injectable"; import { DiRender, renderFor } from "../test-utils/renderFor"; import type { Cluster } from "../../../common/cluster/cluster"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; import apiManagerInjectable from "./dependencies/api-manager.injectable"; import { KubeObjectMenu } from "./index"; +import type { KubeObjectMenuRegistration } from "./dependencies/kube-object-menu-items/kube-object-menu-registration"; +import { computed } from "mobx"; +import { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; // TODO: Make tooltips free of side effects by making it deterministic jest.mock("../tooltip"); +class SomeTestExtension extends LensRendererExtension { + constructor( + kubeObjectMenuItems: KubeObjectMenuRegistration[], + ) { + super({ + id: "some-id", + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: "some-id", version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.kubeObjectMenuItems = kubeObjectMenuItems; + } +} + describe("kube-object-menu", () => { let di: ConfigurableDependencyInjectionContainer; let render: DiRender; beforeEach(async () => { + const MenuItemComponent: React.FC = () =>
  • Some menu item
  • ; + + const kubeObjectMenuItems = [ + { + apiVersions: ["some-api-version"], + kind: "some-kind", + components: { MenuItem: MenuItemComponent }, + }, + + { + apiVersions: ["some-unrelated-api-version"], + kind: "some-kind", + components: { MenuItem: MenuItemComponent }, + }, + + { + apiVersions: ["some-api-version"], + kind: "some-unrelated-kind", + components: { MenuItem: MenuItemComponent }, + }, + ]; + + const someTestExtension = new SomeTestExtension(kubeObjectMenuItems); + di = getDiForUnitTesting({ doGeneralOverrides: true }); - await di.runSetups(); - - // TODO: Remove global shared state - KubeObjectMenuRegistry.resetInstance(); - KubeObjectMenuRegistry.createInstance(); - render = renderFor(di); - di.override(clusterInjectable, () => ({ - name: "Some name", - }) as Cluster); + di.override(rendererExtensionsInjectable, () => + computed(() => [someTestExtension]), + ); - di.override(apiManagerInjectable, () => ({ - getStore: api => void api, - }) as ApiManager); + await di.runSetups(); + + di.override( + clusterInjectable, + () => + ({ + name: "Some name", + } as Cluster), + ); + + di.override( + apiManagerInjectable, + () => + ({ + getStore: (api) => void api, + } as ApiManager), + ); di.override(hideDetailsInjectable, () => () => {}); di.override(createEditResourceTabInjectable, () => () => "irrelevant"); - - addDynamicMenuItem({ - di, - apiVersions: ["some-api-version"], - kind: "some-kind", - }); - - addDynamicMenuItem({ - di, - apiVersions: ["some-unrelated-api-version"], - kind: "some-kind", - }); - - addDynamicMenuItem({ - di, - apiVersions: ["some-api-version"], - kind: "some-unrelated-kind", - }); }); it("given no cluster, does not crash", () => { @@ -242,25 +274,3 @@ describe("kube-object-menu", () => { }); }); }); - -const addDynamicMenuItem = ({ - di, - apiVersions, - kind, -}: { - di: ConfigurableDependencyInjectionContainer; - apiVersions: string[]; - kind: string; -}) => { - const MenuItemComponent: React.FC = () =>
  • Some menu item
  • ; - - const dynamicMenuItemStub: KubeObjectMenuRegistration = { - apiVersions, - kind, - components: { MenuItem: MenuItemComponent }, - }; - - const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable); - - kubeObjectMenuRegistry.add([dynamicMenuItemStub]); -}; diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 3965d45fcf..11c318090c 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -8,7 +8,6 @@ export * from "./catalog"; export * from "./entity-settings-registry"; export * from "./ipc"; export * from "./kube-object-detail-registry"; -export * from "./kube-object-menu-registry"; export * from "./registries"; export * from "./workloads-overview-detail-registry"; export * from "./catalog-category-registry"; diff --git a/src/renderer/initializers/kube-object-menu-registry.ts b/src/renderer/initializers/kube-object-menu-registry.ts deleted file mode 100644 index d25ef0aaf7..0000000000 --- a/src/renderer/initializers/kube-object-menu-registry.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { KubeObjectMenuRegistry } from "../../extensions/registries"; -import { ServiceAccountMenu } from "../components/+user-management/+service-accounts"; -import { CronJobMenu } from "../components/+workloads-cronjobs"; -import { DeploymentMenu } from "../components/+workloads-deployments"; -import { ReplicaSetMenu } from "../components/+workloads-replicasets"; -import { StatefulSetMenu } from "../components/+workloads-statefulsets"; - -export function initKubeObjectMenuRegistry() { - KubeObjectMenuRegistry.getInstance() - .add([ - { - kind: "ServiceAccount", - apiVersions: ["v1"], - components: { - MenuItem: ServiceAccountMenu, - }, - }, - { - kind: "CronJob", - apiVersions: ["batch/v1beta1"], - components: { - MenuItem: CronJobMenu, - }, - }, - { - kind: "Deployment", - apiVersions: ["apps/v1"], - components: { - MenuItem: DeploymentMenu, - }, - }, - { - kind: "ReplicaSet", - apiVersions: ["apps/v1"], - components: { - MenuItem: ReplicaSetMenu, - }, - }, - { - kind: "StatefulSet", - apiVersions: ["apps/v1"], - components: { - MenuItem: StatefulSetMenu, - }, - }, - ]); -} diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 1c9e5662d4..374e114b7c 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -12,7 +12,6 @@ export function initRegistries() { registries.EntitySettingRegistry.createInstance(); registries.GlobalPageRegistry.createInstance(); registries.KubeObjectDetailRegistry.createInstance(); - registries.KubeObjectMenuRegistry.createInstance(); registries.KubeObjectStatusRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); } From 0b1696fe161d6db09510c6c9b662db35c93124f7 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 31 Jan 2022 10:46:40 -0500 Subject: [PATCH 11/13] Fix build due to removal of old extended collections (#4779) --- src/common/user-store/preferences-helpers.ts | 6 ++--- src/common/user-store/user-store.ts | 11 +++------ src/common/utils/collection-functions.ts | 24 +++++++++++++++++++ src/common/utils/index.ts | 1 - src/common/utils/toggle-set.ts | 24 ------------------- .../+config-secrets/secret-details.tsx | 6 ++--- .../namespace-store/namespace.store.ts | 10 ++++---- .../item-object-list/list-layout.tsx | 24 +++---------------- 8 files changed, 41 insertions(+), 65 deletions(-) delete mode 100644 src/common/utils/toggle-set.ts diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 228b754387..39d1e0938e 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -6,7 +6,7 @@ import moment from "moment-timezone"; import path from "path"; import os from "os"; -import { getAppVersion, ObservableToggleSet } from "../utils"; +import { getAppVersion } from "../utils"; import type { editor } from "monaco-editor"; import merge from "lodash/merge"; import { SemVer } from "semver"; @@ -236,10 +236,10 @@ const terminalCopyOnSelect: PreferenceDescription = { }, }; -const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map>> = { +const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map>> = { fromStore(val) { return new Map( - (val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)]), + (val ?? []).map(([tableId, columnIds]) => [tableId, new Set(columnIds)]), ); }, toStore(val) { diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index a1ffe65d66..9d16b2c419 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -11,7 +11,7 @@ import migrations, { fileNameMigration } from "../../migrations/user-store"; import { getAppVersion } from "../utils/app-version"; import { kubeConfigDefaultPath } from "../kube-helpers"; import { appEventBus } from "../app-event-bus/event-bus"; -import { ObservableToggleSet, toJS } from "../../renderer/utils"; +import { getOrInsertSet, toggle, toJS } from "../../renderer/utils"; import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers"; import logger from "../../main/logger"; @@ -71,7 +71,7 @@ export class UserStore extends BaseStore /* implements UserStore * The column IDs under each configurable table ID that have been configured * to not be shown */ - hiddenTableColumns = observable.map>(); + hiddenTableColumns = observable.map>(); /** * Monaco editor configs @@ -133,16 +133,11 @@ export class UserStore extends BaseStore /* implements UserStore return columnIds.some(columnId => config.has(columnId)); } - @action /** * Toggles the hidden configuration of a table's column */ toggleTableColumnVisibility(tableId: string, columnId: string) { - if (!this.hiddenTableColumns.get(tableId)) { - this.hiddenTableColumns.set(tableId, new ObservableToggleSet()); - } - - this.hiddenTableColumns.get(tableId).toggle(columnId); + toggle(getOrInsertSet(this.hiddenTableColumns, tableId), columnId); } @action diff --git a/src/common/utils/collection-functions.ts b/src/common/utils/collection-functions.ts index 0d18a41e8a..267668ce81 100644 --- a/src/common/utils/collection-functions.ts +++ b/src/common/utils/collection-functions.ts @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import { runInAction } from "mobx"; + /** * Get the value behind `key`. If it was not present, first insert `value` * @param map The map to interact with @@ -26,6 +28,14 @@ export function getOrInsertMap(map: Map>, key: K): Map return getOrInsert(map, key, new Map()); } +/** + * Like `getOrInsert` but specifically for when `V` is `Set` so that + * the typings are inferred. + */ +export function getOrInsertSet(map: Map>, key: K): Set { + return getOrInsert(map, key, new Set()); +} + /** * Like `getOrInsert` but with delayed creation of the item */ @@ -36,3 +46,17 @@ export function getOrInsertWith(map: Map, key: K, value: () => V): V return map.get(key); } + +/** + * If `key` is in `set`, remove it otherwise add it. + * @param set The set to manipulate + * @param key The key to toggle the "is in"-ness of + */ +export function toggle(set: Set, key: K): void { + runInAction(() => { + // Returns true if value was already in Set; otherwise false. + if (!set.delete(key)) { + set.add(key); + } + }); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index d349bd3b14..d8a07f31f6 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -38,7 +38,6 @@ export * from "./singleton"; export * from "./sort-compare"; export * from "./splitArray"; export * from "./tar"; -export * from "./toggle-set"; export * from "./toJS"; export * from "./type-narrowing"; export * from "./types"; diff --git a/src/common/utils/toggle-set.ts b/src/common/utils/toggle-set.ts deleted file mode 100644 index 3b3c226f9b..0000000000 --- a/src/common/utils/toggle-set.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { ObservableSet } from "mobx"; - -export class ToggleSet extends Set { - public toggle(value: T): void { - if (!this.delete(value)) { - // Set.prototype.delete returns false if `value` was not in the set - this.add(value); - } - } -} - -export class ObservableToggleSet extends ObservableSet { - public toggle(value: T): void { - if (!this.delete(value)) { - // Set.prototype.delete returns false if `value` was not in the set - this.add(value); - } - } -} diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index 55e6d605f5..fce67d0abc 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -12,7 +12,7 @@ import { DrawerItem, DrawerTitle } from "../drawer"; import { Input } from "../input"; import { Button } from "../button"; import { Notifications } from "../notifications"; -import { base64, ObservableToggleSet } from "../../utils"; +import { base64, toggle } from "../../utils"; import { Icon } from "../icon"; import { secretsStore } from "./secrets.store"; import type { KubeObjectDetailsProps } from "../kube-object-details"; @@ -27,7 +27,7 @@ interface Props extends KubeObjectDetailsProps { export class SecretDetails extends React.Component { @observable isSaving = false; @observable data: { [name: string]: string } = {}; - revealSecret = new ObservableToggleSet(); + revealSecret = new Set(); constructor(props: Props) { super(props); @@ -99,7 +99,7 @@ export class SecretDetails extends React.Component { this.revealSecret.toggle(name)} + onClick={() => toggle(this.revealSecret, name)} /> )}
    diff --git a/src/renderer/components/+namespaces/namespace-store/namespace.store.ts b/src/renderer/components/+namespaces/namespace-store/namespace.store.ts index 00cfd6748a..4066070cc3 100644 --- a/src/renderer/components/+namespaces/namespace-store/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace-store/namespace.store.ts @@ -4,7 +4,7 @@ */ import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx"; -import { autoBind, noop, StorageHelper, ToggleSet } from "../../../utils"; +import { autoBind, noop, StorageHelper, toggle } from "../../../utils"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../../common/k8s-api/kube-object.store"; import { Namespace, namespacesApi } from "../../../../common/k8s-api/endpoints/namespaces.api"; @@ -175,10 +175,10 @@ export class NamespaceStore extends KubeObjectStore { */ @action toggleContext(namespaces: string | string[]) { - const nextState = new ToggleSet(this.contextNamespaces); + const nextState = new Set(this.contextNamespaces); for (const namespace of [namespaces].flat()) { - nextState.toggle(namespace); + toggle(nextState, namespace); } this.dependencies.storage.set([...nextState]); @@ -191,9 +191,9 @@ export class NamespaceStore extends KubeObjectStore { * @param namespace The name of a namespace */ toggleSingle(namespace: string) { - const nextState = new ToggleSet(this.contextNamespaces); + const nextState = new Set(this.contextNamespaces); - nextState.toggle(namespace); + toggle(nextState, namespace); this.dependencies.storage.set([...nextState]); } diff --git a/src/renderer/components/item-object-list/list-layout.tsx b/src/renderer/components/item-object-list/list-layout.tsx index 39211d0a6b..959f814bce 100644 --- a/src/renderer/components/item-object-list/list-layout.tsx +++ b/src/renderer/components/item-object-list/list-layout.tsx @@ -8,31 +8,17 @@ import "./item-list-layout.scss"; import React, { ReactNode } from "react"; import { computed, makeObservable, untracked } from "mobx"; import type { ConfirmDialogParams } from "../confirm-dialog"; -import type { - TableCellProps, - TableProps, - TableRowProps, - TableSortCallbacks, -} from "../table"; -import { - boundMethod, - cssNames, - IClassName, - noop, - ObservableToggleSet, - StorageHelper, -} from "../../utils"; +import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } from "../table"; +import { boundMethod, cssNames, IClassName, noop, StorageHelper } from "../../utils"; import type { AddRemoveButtonsProps } from "../add-remove-buttons"; import type { ItemObject, ItemStore } from "../../../common/item.store"; import type { SearchInputUrlProps } from "../input"; import { Filter, FilterType, pageFilters } from "./page-filters.store"; import { PageFiltersList } from "./page-filters-list"; -import { UserStore } from "../../../common/user-store"; import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import itemListLayoutStorageInjectable - from "./storage.injectable"; +import itemListLayoutStorageInjectable from "./storage.injectable"; import { ItemListLayoutContent } from "./content"; import { ItemListLayoutHeader } from "./header"; import groupBy from "lodash/groupBy"; @@ -142,10 +128,6 @@ class NonInjectedItemListLayout extends React.Component Date: Mon, 31 Jan 2022 17:59:49 +0200 Subject: [PATCH 12/13] Disable native window open (#4778) Signed-off-by: Juho Heikka --- src/main/window-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 903c9d8525..9985f39597 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -74,7 +74,7 @@ export class WindowManager extends Singleton { nodeIntegrationInSubFrames: true, webviewTag: true, contextIsolation: false, - nativeWindowOpen: true, + nativeWindowOpen: false, }, }); this.windowState.manage(this.mainWindow); From 4ec36a154d70d42e92d639fe0ef50855bf24a2cc Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Tue, 1 Feb 2022 01:05:45 -0500 Subject: [PATCH 13/13] select all on helm releases now selects all or none (toggles) (#4742) * select all on helm releases now selects all (or none if all were already selected) Signed-off-by: Jim Ehrismann * Fix isSelectedAll when no items are visibile Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton --- .../components/+apps-releases/releases.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index 34f9798783..4d38cb5a20 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -109,11 +109,23 @@ class NonInjectedHelmReleases extends Component { item.toggle(); }, - isSelectedAll: () => - releases.get().every((release) => release.isSelected), + isSelectedAll: (visibleItems: RemovableHelmRelease[]) => ( + visibleItems.length > 0 + && visibleItems.every((release) => release.isSelected) + ), - toggleSelectionAll: () => { - releases.get().forEach((release) => release.toggle()); + toggleSelectionAll: (visibleItems: RemovableHelmRelease[]) => { + let selected = false; + + if (!legacyReleaseStore.isSelectedAll(visibleItems)) { + selected = true; + } + + visibleItems.forEach((release) => { + if (release.isSelected !== selected) { + release.toggle(); + } + }); }, isSelected: (item) => item.isSelected, @@ -197,7 +209,7 @@ class NonInjectedHelmReleases extends Component { })} onDetails={this.onDetails} /> - +