1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge remote-tracking branch 'origin/master' into webpack5-upgrade-attempt

# Conflicts:
#	src/common/catalog-entities/kubernetes-cluster.ts
This commit is contained in:
Roman 2022-01-28 14:51:27 +02:00
commit 398af8cc07
78 changed files with 657 additions and 590 deletions

View File

@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron" disturl "https://atom.io/download/electron"
target "13.6.1" target "14.2.4"
runtime "electron" runtime "electron"

View File

@ -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. 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 ## Entities
An entity is the data within the catalog. An entity is the data within the catalog.

View File

@ -11,6 +11,7 @@
*/ */
import type { ElectronApplication, Page } from "playwright"; import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("preferences page tests", () => { describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>; let window: Page, cleanup: () => Promise<void>;
@ -33,7 +34,8 @@ describe("preferences page tests", () => {
await cleanup(); await cleanup();
}, 10*60*1000); }, 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 = [ const pages = [
{ {
id: "application", id: "application",

View File

@ -5,6 +5,7 @@
import type { ElectronApplication, Page } from "playwright"; import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("Lens command palette", () => { describe("Lens command palette", () => {
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication; let window: Page, cleanup: () => Promise<void>, app: ElectronApplication;
@ -19,7 +20,8 @@ describe("Lens command palette", () => {
}, 10*60*1000); }, 10*60*1000);
describe("menu", () => { 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.evaluate(async ({ app }) => {
await app.applicationMenu await app.applicationMenu
.getMenuItemById("view") .getMenuItemById("view")

View File

@ -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`); 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()); const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4());
// Make sure that the directory is clear // 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) { export async function clickWelcomeButton(window: Page) {
await window.click("[data-testid=welcome-menu-container] li a"); await window.click("[data-testid=welcome-menu-container] li a");
} }

View File

@ -191,7 +191,6 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^1.2.2",
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1", "@kubernetes/client-node": "^0.16.1",
@ -332,7 +331,7 @@
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.3.4", "dompurify": "^2.3.4",
"electron": "^13.6.1", "electron": "^14.2.4",
"electron-builder": "^22.14.5", "electron-builder": "^22.14.5",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild": "^0.13.15", "esbuild": "^0.13.15",

View File

@ -4,45 +4,61 @@
*/ */
import { anyObject } from "jest-mock-extended"; import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { HotbarStore } from "../hotbar-store"; import { HotbarStore } from "../hotbar-store";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import directoryForUserDataInjectable import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({ jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: { catalogEntityRegistry: {
items: [ items: [
{ getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: { metadata: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c", uid: "1dfa26e2ebab15780a3547e9c7fa785c",
name: "mycluster", name: "mycluster",
source: "local", source: "local",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
}, },
},
{
metadata: { metadata: {
uid: "55b42c3c7ba3b04193416cda405269a5", uid: "55b42c3c7ba3b04193416cda405269a5",
name: "my_shiny_cluster", name: "my_shiny_cluster",
source: "remote", source: "remote",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
}, },
},
{
metadata: { metadata: {
uid: "catalog-entity", uid: "catalog-entity",
name: "Catalog", name: "Catalog",
source: "app", source: "app",
labels: {},
}, },
}, }),
], ],
}, },
})); }));
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity { function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return merge(data, { return {
getName: jest.fn(() => data.metadata?.name), getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid), getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"), getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
@ -52,7 +68,8 @@ function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKi
metadata: {}, metadata: {},
spec: {}, spec: {},
status: {}, status: {},
}) as CatalogEntity; ...data,
} as CatalogEntity;
} }
const testCluster = getMockCatalogEntity({ const testCluster = getMockCatalogEntity({

View File

@ -5,12 +5,12 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store/cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage, requestMain } from "../ipc"; import { broadcastMessage } from "../ipc";
import { app } from "electron"; import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
import KubeClusterCategoryIcon from "./icons/kubernetes.svg?raw"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg?raw";
export interface KubernetesClusterPrometheusMetrics { export interface KubernetesClusterPrometheusMetrics {
@ -68,22 +68,22 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
async connect(): Promise<void> { async connect(): Promise<void> {
if (app) { if (app) {
await ClusterStore.getInstance().getById(this.metadata.uid)?.activate(); await ClusterStore.getInstance().getById(this.getId())?.activate();
} else { } else {
await requestMain(clusterActivateHandler, this.metadata.uid, false); await requestClusterActivation(this.getId(), false);
} }
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (app) { if (app) {
ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect(); ClusterStore.getInstance().getById(this.getId())?.disconnect();
} else { } else {
await requestMain(clusterDisconnectHandler, this.metadata.uid, false); await requestClusterDisconnection(this.getId(), false);
} }
} }
async onRun(context: CatalogEntityActionContext) { async onRun(context: CatalogEntityActionContext) {
context.navigate(`/cluster/${this.metadata.uid}`); context.navigate(`/cluster/${this.getId()}`);
} }
onDetailsOpen(): void { onDetailsOpen(): void {
@ -101,7 +101,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
icon: "settings", icon: "settings",
onClick: () => broadcastMessage( onClick: () => broadcastMessage(
IpcRendererNavigationEvents.NAVIGATE_IN_APP, IpcRendererNavigationEvents.NAVIGATE_IN_APP,
`/entity/${this.metadata.uid}/settings`, `/entity/${this.getId()}/settings`,
), ),
}); });
} }
@ -112,14 +112,14 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
context.menuItems.push({ context.menuItems.push({
title: "Disconnect", title: "Disconnect",
icon: "link_off", icon: "link_off",
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid), onClick: () => requestClusterDisconnection(this.getId()),
}); });
break; break;
case LensKubernetesClusterStatus.DISCONNECTED: case LensKubernetesClusterStatus.DISCONNECTED:
context.menuItems.push({ context.menuItems.push({
title: "Connect", title: "Connect",
icon: "link", icon: "link",
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`), onClick: () => context.navigate(`/cluster/${this.getId()}`),
}); });
break; break;
} }

View File

@ -38,9 +38,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",
icon: "delete", icon: "delete",
onClick: async () => WeblinkStore.getInstance().removeById(this.metadata.uid), onClick: async () => WeblinkStore.getInstance().removeById(this.getId()),
confirm: { confirm: {
message: `Remove Web Link "${this.metadata.name}" from ${productName}?`, message: `Remove Web Link "${this.getName()}" from ${productName}?`,
}, },
}); });
} }

View File

@ -315,11 +315,24 @@ export abstract class CatalogEntity<
@observable status: Status; @observable status: Status;
@observable spec: Spec; @observable spec: Spec;
constructor(data: CatalogEntityData<Metadata, Status, Spec>) { constructor({ metadata, status, spec }: CatalogEntityData<Metadata, Status, Spec>) {
makeObservable(this); makeObservable(this);
this.metadata = data.metadata;
this.status = data.status; if (!metadata || typeof metadata !== "object") {
this.spec = data.spec; 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;
} }
/** /**

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { ipcMain, ipcRenderer, webFrame } from "electron"; import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; 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 migrations from "../../migrations/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle, requestMain } from "../ipc"; import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";
export interface ClusterStoreModel { export interface ClusterStoreModel {
clusters?: ClusterModel[]; clusters?: ClusterModel[];
} }
const initialStates = "cluster:states";
interface Dependencies { interface Dependencies {
createCluster: (model: ClusterModel) => Cluster createCluster: (model: ClusterModel) => Cluster
} }
@ -49,18 +49,18 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
async loadInitialOnRenderer() { async loadInitialOnRenderer() {
logger.info("[CLUSTER-STORE] requesting initial state sync"); 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); this.getById(id)?.setState(state);
} }
} }
provideInitialFromMain() { provideInitialFromMain() {
ipcMainHandle(initialStates, () => { ipcMainHandle(clusterStates, () => (
return this.clustersList.map(cluster => ({ this.clustersList.map(cluster => ({
id: cluster.id, id: cluster.id,
state: cluster.getState(), state: cluster.getState(),
})); }))
}); ));
} }
protected pushStateToViewsAutomatically() { protected pushStateToViewsAutomatically() {

View File

@ -5,7 +5,7 @@
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; 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 type { ContextHandler } from "../../main/context-handler/context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl"; 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 { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { Response } from "request"; import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
interface Dependencies { interface Dependencies {
directoryForKubeConfigs: string, directoryForKubeConfigs: string,
@ -641,7 +642,7 @@ export class Cluster implements ClusterModel, ClusterState {
const { response } = error as HttpError & { response: Response }; const { response } = error as HttpError & { response: Response };
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
} }
return namespaceList; return namespaceList;

View File

@ -10,8 +10,9 @@ import { toJS } from "./utils";
import { CatalogEntity } from "./catalog"; import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general"; import { catalogEntity } from "../main/catalog-sources/general";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, HotbarTooManyItems } from "./ipc"; import { broadcastMessage } from "./ipc";
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
export interface HotbarStoreModel { export interface HotbarStoreModel {
hotbars: Hotbar[]; hotbars: Hotbar[];
@ -153,28 +154,28 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
@action @action
addToHotbar(item: CatalogEntity, cellIndex?: number) { addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive(); const hotbar = this.getActive();
const uid = item.metadata?.uid; const uid = item.getId();
const name = item.metadata?.name; const name = item.getName();
if (typeof uid !== "string") { 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") { 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)) { if (this.isAddedToActive(item)) {
return; return;
} }
const entity = {
uid,
name,
source: item.metadata.source,
};
const newItem = { entity };
if (cellIndex === undefined) { if (cellIndex === undefined) {
// Add item to empty cell // Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null); const emptyCellIndex = hotbar.items.indexOf(null);
@ -182,7 +183,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
if (emptyCellIndex != -1) { if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem; hotbar.items[emptyCellIndex] = newItem;
} else { } else {
broadcastMessage(HotbarTooManyItems); broadcastMessage(hotbarTooManyItemsChannel);
} }
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) { } else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem; hotbar.items[cellIndex] = newItem;
@ -277,11 +278,14 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
} }
/** /**
* Checks if entity already pinned to hotbar * Checks if entity already pinned to the active hotbar
* @returns boolean
*/ */
isAddedToActive(entity: CatalogEntity) { isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); if (!entity) {
return false;
}
return this.getActive().items.findIndex(item => item?.entity.uid === entity.getId()) >= 0;
} }
getDisplayLabel(hotbar: Hotbar): string { getDisplayLabel(hotbar: Hotbar): string {

View File

@ -3,14 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export enum CatalogIpcEvents { /**
/** * This is used to activate a specific entity in the renderer main frame
* This is broadcast on whenever there is an update to any catalog item */
*/ export const catalogEntityRunListener = "catalog-entity:run";
ITEMS = "catalog:items",
/** /**
* This can be sent from renderer to main to initialize a broadcast of ITEMS * This is broadcast on whenever there is an update to any catalog item
*/ */
INIT = "catalog:init", export const catalogItemsChannel = "catalog:items";
}
/**
* This can be sent from renderer to main to initialize a broadcast of ITEMS
*/
export const catalogInitChannel = "catalog:init";

View File

@ -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";
}

View File

@ -13,3 +13,16 @@ export const clusterSetDeletingHandler = "cluster:deleting:set";
export const clusterClearDeletingHandler = "cluster:deleting:clear"; export const clusterClearDeletingHandler = "cluster:deleting:clear";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-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";
}

View File

@ -3,6 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import * as dialog from "./dialog"; export const openFilePickingDialogChannel = "dialog:open:file-picking";
export { dialog };

View File

@ -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";

View File

@ -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";

View File

@ -3,4 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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";

View File

@ -3,13 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 "./ipc";
export * from "./invalid-kubeconfig"; export * from "./invalid-kubeconfig";
export * from "./update-available.ipc"; export * from "./update-available";
export * from "./cluster.ipc";
export * from "./type-enforced-ipc"; export * from "./type-enforced-ipc";
export * from "./hotbar";
export * from "./extension-loader.ipc";

View File

@ -12,25 +12,8 @@ import { toJS } from "../utils/toJS";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
import type remote from "@electron/remote";
const electronRemote = (() => { export const broadcastMainChannel = "ipc:broadcast-main";
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 function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(channel, async (event, ...args) => { ipcMain.handle(channel, async (event, ...args) => {
@ -42,51 +25,55 @@ function getSubFrames(): ClusterFrameInfo[] {
return Array.from(clusterFrameMap.values()); return Array.from(clusterFrameMap.values());
} }
export function broadcastMessage(channel: string, ...args: any[]) { export async function broadcastMessage(channel: string, ...args: any[]): Promise<void> {
const subFramesP = ipcRenderer if (ipcRenderer) {
? requestMain(subFramesChannel) return ipcRenderer.invoke(broadcastMainChannel, channel, ...args.map(sanitizePayload));
: Promise.resolve(getSubFrames()); }
subFramesP if (!webContents) {
.then(subFrames => { return;
const views: undefined | ReturnType<typeof webContents.getAllWebContents> | ReturnType<typeof remote.webContents.getAllWebContents> = (webContents || electronRemote?.webContents)?.getAllWebContents(); }
if (!views || !Array.isArray(views) || views.length === 0) return; ipcMain.listeners(channel).forEach((func) => func({
args = args.map(sanitizePayload); processId: undefined, frameId: undefined, sender: undefined, senderFrame: undefined,
}, ...args));
ipcRenderer?.send(channel, ...args); const subFrames = getSubFrames();
ipcMain?.emit(channel, ...args); const views = webContents.getAllWebContents();
for (const view of views) { if (!views || !Array.isArray(views) || views.length === 0) return;
let viewType = "unknown";
// There will be a uncaught exception if the view is destroyed. args = args.map(sanitizePayload);
try {
viewType = view.getType();
} catch {
// We can ignore the view destroyed exception as viewType is only used for logging.
}
// Send message to views. for (const view of views) {
try { let viewType = "unknown";
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) });
}
// Send message to subFrames of views. // There will be a uncaught exception if the view is destroyed.
for (const frameInfo of subFrames) { try {
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args }); viewType = view.getType();
} catch {
// We can ignore the view destroyed exception as viewType is only used for logging.
}
try { // Send message to views.
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); try {
} catch (error) { logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
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) }); 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 { 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); return () => ipcRenderer.off(channel, listener);
} }
export function bindBroadcastHandlers() {
ipcMainHandle(subFramesChannel, () => getSubFrames());
}
/** /**
* Sanitizing data for IPC-messaging before send. * Sanitizing data for IPC-messaging before send.
* Removes possible observable values to avoid exceptions like "can't clone object". * Removes possible observable values to avoid exceptions like "can't clone object".

39
src/common/ipc/window.ts Normal file
View File

@ -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",
}

View File

@ -9,11 +9,10 @@ import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities"; import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { app } from "electron"; import { app } from "electron";
import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store/cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { productName } from "../vars"; import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
export class ResourceStack { export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {} constructor(protected cluster: KubernetesCluster, protected name: string) {}
@ -41,7 +40,7 @@ export class ResourceStack {
} }
protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> { protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
if (!clusterModel) { if (!clusterModel) {
throw new Error(`cluster not found`); throw new Error(`cluster not found`);
@ -54,7 +53,7 @@ export class ResourceStack {
if (app) { if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs); return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
} else { } else {
const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
if (response.stderr) { if (response.stderr) {
throw new Error(response.stderr); throw new Error(response.stderr);
@ -65,7 +64,7 @@ export class ResourceStack {
} }
protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> { protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid); const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
if (!clusterModel) { if (!clusterModel) {
throw new Error(`cluster not found`); throw new Error(`cluster not found`);
@ -78,7 +77,7 @@ export class ResourceStack {
if (app) { if (app) {
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs); return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
} else { } else {
const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs); const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);
if (response.stderr) { if (response.stderr) {
throw new Error(response.stderr); throw new Error(response.stderr);

View File

@ -8,8 +8,7 @@ import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import updateExtensionsStateInjectable import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
@ -24,7 +23,7 @@ jest.mock(
() => ({ () => ({
ipcRenderer: { ipcRenderer: {
invoke: jest.fn(async (channel: string) => { invoke: jest.fn(async (channel: string) => {
if (channel === "extensions:main") { if (channel === "extension-loader:main:state") {
return [ return [
[ [
manifestPath, manifestPath,
@ -61,7 +60,7 @@ jest.mock(
}), }),
on: jest.fn( on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => { (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 // First initialize with extensions 1 and 2
// and then broadcast event to remove extension 2 and add extension number 3 // and then broadcast event to remove extension 2 and add extension number 3
setTimeout(() => { setTimeout(() => {

View File

@ -10,12 +10,7 @@ import fse from "fs-extra";
import { makeObservable, observable, reaction, when } from "mobx"; import { makeObservable, observable, reaction, when } from "mobx";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc";
broadcastMessage,
ipcMainHandle,
ipcRendererOn,
requestMain,
} from "../../common/ipc";
import { toJS } from "../../common/utils"; import { toJS } from "../../common/utils";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type { ExtensionsStore } from "../extensions-store/extensions-store"; 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 { isProduction } from "../../common/vars";
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
import type { PackageJson } from "type-fest"; import type { PackageJson } from "type-fest";
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
@ -96,9 +93,6 @@ export class ExtensionDiscovery {
return when(() => this.isLoaded); 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(); public events = new EventEmitter();
constructor(protected dependencies : Dependencies) { constructor(protected dependencies : Dependencies) {
@ -141,14 +135,14 @@ export class ExtensionDiscovery {
this.isLoaded = isLoaded; this.isLoaded = isLoaded;
}; };
requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage); requestInitialExtensionDiscovery().then(onMessage);
ipcRendererOn(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => { ipcRendererOn(extensionDiscoveryStateChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
onMessage(message); onMessage(message);
}); });
} }
async initMain(): Promise<void> { async initMain(): Promise<void> {
ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); ipcMainHandle(extensionDiscoveryStateChannel, () => this.toJSON());
reaction(() => this.toJSON(), () => { reaction(() => this.toJSON(), () => {
this.broadcast(); this.broadcast();
@ -492,6 +486,6 @@ export class ExtensionDiscovery {
} }
broadcast(): void { broadcast(): void {
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON()); broadcastMessage(extensionDiscoveryStateChannel, this.toJSON());
} }
} }

View File

@ -8,7 +8,7 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path"; 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 { Disposer, toJS } from "../../common/utils";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type { KubernetesCluster } from "../common-api/catalog"; 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 type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries"; import * as registries from "../registries";
import type { LensExtensionState } from "../extensions-store/extensions-store"; 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]"; const logModule = "[EXTENSIONS-LOADER]";
@ -49,12 +51,6 @@ export class ExtensionLoader {
*/ */
protected instancesByName = observable.map<string, LensExtension>(); protected instancesByName = observable.map<string, LensExtension>();
// 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 // emits event "remove" of type LensExtension when the extension is removed
private events = new EventEmitter(); private events = new EventEmitter();
@ -196,11 +192,11 @@ export class ExtensionLoader {
this.isLoaded = true; this.isLoaded = true;
this.loadOnMain(); this.loadOnMain();
ipcMainHandle(ExtensionLoader.extensionsMainChannel, () => { ipcMainHandle(extensionLoaderFromMainChannel, () => {
return Array.from(this.toJSON()); return Array.from(this.toJSON());
}); });
ipcMainOn(ExtensionLoader.extensionsRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
this.syncExtensions(extensions); this.syncExtensions(extensions);
}); });
} }
@ -220,16 +216,16 @@ export class ExtensionLoader {
}); });
}; };
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler); requestExtensionLoaderInitialState().then(extensionListHandler);
ipcRendererOn(ExtensionLoader.extensionsMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions); extensionListHandler(extensions);
}); });
} }
broadcastExtensions() { broadcastExtensions() {
const channel = ipcRenderer const channel = ipcRenderer
? ExtensionLoader.extensionsRendererChannel ? extensionLoaderFromRendererChannel
: ExtensionLoader.extensionsMainChannel; : extensionLoaderFromMainChannel;
broadcastMessage(channel, Array.from(this.extensions)); broadcastMessage(channel, Array.from(this.extensions));
} }

View File

@ -43,3 +43,5 @@ export { CustomResourceDefinition, crdApi } from "../../common/k8s-api/endpoints
export type { ILocalKubeApiConfig, IRemoteKubeApiConfig, IKubeApiCluster } from "../../common/k8s-api/kube-api"; export type { ILocalKubeApiConfig, IRemoteKubeApiConfig, IKubeApiCluster } from "../../common/k8s-api/kube-api";
export type { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints/pods.api"; export type { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints/pods.api";
export type { ISecretRef } from "../../common/k8s-api/endpoints/secret.api"; export type { ISecretRef } from "../../common/k8s-api/endpoints/secret.api";
export type { KubeObjectMetadata, KubeStatusData } from "../../common/k8s-api/kube-object";
export type { KubeObjectStoreLoadAllParams, KubeObjectStoreLoadingParams, KubeObjectStoreSubscribeParams } from "../../common/k8s-api/kube-object.store";

View File

@ -46,6 +46,8 @@ export type { ILocalKubeApiConfig, IRemoteKubeApiConfig, IKubeApiCluster } from
export type { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints"; export type { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints";
export type { ISecretRef } from "../../common/k8s-api/endpoints"; export type { ISecretRef } from "../../common/k8s-api/endpoints";
export type { KubeObjectStatus } from "./kube-object-status"; export type { KubeObjectStatus } from "./kube-object-status";
export type { KubeObjectMetadata, KubeStatusData } from "../../common/k8s-api/kube-object";
export type { KubeObjectStoreLoadAllParams, KubeObjectStoreLoadingParams, KubeObjectStoreSubscribeParams } from "../../common/k8s-api/kube-object.store";
// stores // stores
export type { EventStore } from "../../renderer/components/+events/event.store"; export type { EventStore } from "../../renderer/components/+events/event.store";

View File

@ -51,8 +51,7 @@ describe("create clusters", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
const mockOpts = {
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
apiVersion: "v1", apiVersion: "v1",
clusters: [{ clusters: [{
@ -74,9 +73,7 @@ describe("create clusters", () => {
kind: "Config", kind: "Config",
preferences: {}, preferences: {},
}), }),
}; });
mockFs(mockOpts);
await di.runSetups(); await di.runSetups();

View File

@ -10,15 +10,15 @@ import "../common/catalog-entities/kubernetes-cluster";
import { disposer, toJS } from "../common/utils"; import { disposer, toJS } from "../common/utils";
import { debounce } from "lodash"; import { debounce } from "lodash";
import type { CatalogEntity } from "../common/catalog"; import type { CatalogEntity } from "../common/catalog";
import { CatalogIpcEvents } from "../common/ipc/catalog"; import { catalogInitChannel, catalogItemsChannel } from "../common/ipc/catalog";
const broadcaster = debounce((items: CatalogEntity[]) => { const broadcaster = debounce((items: CatalogEntity[]) => {
broadcastMessage(CatalogIpcEvents.ITEMS, items); broadcastMessage(catalogItemsChannel, items);
}, 1_000, { leading: true, trailing: true }); }, 1_000, { leading: true, trailing: true });
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) { export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return disposer( return disposer(
ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))), ipcMainOn(catalogInitChannel, () => broadcaster(toJS(catalog.items))),
reaction(() => toJS(catalog.items), (items) => { reaction(() => toJS(catalog.items), (items) => {
broadcaster(items); broadcaster(items);
}, { }, {

View File

@ -36,7 +36,7 @@ export class CatalogEntityRegistry {
} }
getById<T extends CatalogEntity>(id: string): T | undefined { getById<T extends CatalogEntity>(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<T extends CatalogEntity>(apiVersion: string, kind: string): T[] { getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 type http from "http";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import { Cluster } from "../common/cluster/cluster"; import { Cluster } from "../common/cluster/cluster";
@ -85,7 +85,7 @@ export class ClusterManager extends Singleton {
} }
protected updateEntityFromCluster(cluster: Cluster) { 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) { if (index === -1) {
return; return;
@ -169,11 +169,11 @@ export class ClusterManager extends Singleton {
@action @action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) { protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) { for (const entity of entities) {
const cluster = this.store.getById(entity.metadata.uid); const cluster = this.store.getById(entity.getId());
if (!cluster) { if (!cluster) {
const model = { const model = {
id: entity.metadata.uid, id: entity.getId(),
kubeConfigPath: entity.spec.kubeconfigPath, kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext, contextName: entity.spec.kubeconfigContext,
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [], accessibleNamespaces: entity.spec.accessibleNamespaces ?? [],

View File

@ -6,7 +6,6 @@
// Main process // Main process
import { injectSystemCAs } from "../common/system-ca"; import { injectSystemCAs } from "../common/system-ca";
import { initialize as initializeRemote } from "@electron/remote/main";
import * as Mobx from "mobx"; import * as Mobx from "mobx";
import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-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 type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc"; import { ipcMainOn } from "../common/ipc";
import { startUpdateChecking } from "./app-updater"; import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { pushCatalogToRenderer } from "./catalog-pusher"; import { pushCatalogToRenderer } from "./catalog-pusher";
@ -81,9 +80,6 @@ di.runSetups().then(() => {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
logger.debug("[APP-MAIN] initializing remote");
initializeRemote();
logger.debug("[APP-MAIN] configuring packages"); logger.debug("[APP-MAIN] configuring packages");
configurePackages(); configurePackages();
@ -131,8 +127,6 @@ di.runSetups().then(() => {
logger.info("🐚 Syncing shell environment"); logger.info("🐚 Syncing shell environment");
await shellSync(); await shellSync();
bindBroadcastHandlers();
powerMonitor.on("shutdown", () => app.exit()); powerMonitor.on("shutdown", () => app.exit());
registerFileProtocol("static", __static); registerFileProtocol("static", __static);

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { 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 type { ClusterId } from "../../../common/cluster-types";
import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../common/app-event-bus/event-bus"; 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 { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher"; import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager"; import { ClusterManager } from "../../cluster-manager";
import { ResourceApplier } from "../../resource-applier"; import { ResourceApplier } from "../../resource-applier";
import { IpcMainWindowEvents, WindowManager } from "../../window-manager"; import { WindowManager } from "../../window-manager";
import path from "path"; import path from "path";
import { remove } from "fs-extra"; import { remove } from "fs-extra";
import { getAppMenu } from "../../menu/menu"; import { getAppMenu } from "../../menu/menu";
import type { MenuRegistration } from "../../menu/menu-registration"; import type { MenuRegistration } from "../../menu/menu-registration";
import type { IComputedValue } from "mobx"; 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 { interface Dependencies {
electronMenuItems: IComputedValue<MenuRegistration[]>, electronMenuItems: IComputedValue<MenuRegistration[]>,
@ -136,21 +140,22 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt
} }
}); });
ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => { ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action));
await WindowManager.getInstance().ensureMainWindow();
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 menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
const options = {
menu.popup({
...BrowserWindow.fromWebContents(event.sender), ...BrowserWindow.fromWebContents(event.sender),
// Center of the topbar menu icon // Center of the topbar menu icon
x: 20, x: 20,
y: 20, y: 20,
} as Electron.PopupOptions; });
menu.popup(options);
}); });
}; };

12
src/main/ipc/dialog.ts Normal file
View File

@ -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 };
}

71
src/main/ipc/window.ts Normal file
View File

@ -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);
}

View File

@ -8,17 +8,14 @@ import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"; import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state"; import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/app-event-bus/event-bus"; 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 { delay, iter, Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger"; import logger from "./logger";
import { isMac, productName } from "../common/vars"; import { isMac, productName } from "../common/vars";
import { LensProxy } from "./lens-proxy"; import { LensProxy } from "./lens-proxy";
import { bundledExtensionsLoaded } from "../common/ipc/extension-handling";
export const enum IpcMainWindowEvents {
OPEN_CONTEXT_MENU = "window:open-context-menu",
}
function isHideable(window: BrowserWindow | null): boolean { function isHideable(window: BrowserWindow | null): boolean {
return Boolean(window && !window.isDestroyed()); return Boolean(window && !window.isDestroyed());
@ -75,9 +72,9 @@ export class WindowManager extends Singleton {
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
nodeIntegrationInSubFrames: true, nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
webviewTag: true, webviewTag: true,
contextIsolation: false, contextIsolation: false,
nativeWindowOpen: true,
}, },
}); });
this.windowState.manage(this.mainWindow); this.windowState.manage(this.mainWindow);
@ -135,7 +132,8 @@ export class WindowManager extends Singleton {
// Always disable Node.js integration for all webviews // Always disable Node.js integration for all webviews
webPreferences.nodeIntegration = false; webPreferences.nodeIntegration = false;
}).setWindowOpenHandler((details) => { })
.setWindowOpenHandler((details) => {
shell.openExternal(details.url); shell.openExternal(details.url);
return { action: "deny" }; return { action: "deny" };
@ -165,7 +163,7 @@ export class WindowManager extends Singleton {
if (!this.mainWindow) { if (!this.mainWindow) {
viewHasLoaded = new Promise<void>(resolve => { viewHasLoaded = new Promise<void>(resolve => {
ipcMain.once(BundledExtensionsLoaded, () => resolve()); ipcMain.once(bundledExtensionsLoaded, () => resolve());
}); });
await this.initMainWindow(showSplash); await this.initMainWindow(showSplash);
} }
@ -249,9 +247,9 @@ export class WindowManager extends Singleton {
show: false, show: false,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false, contextIsolation: false,
nodeIntegrationInSubFrames: true, nodeIntegrationInSubFrames: true,
nativeWindowOpen: true,
}, },
}); });
await this.splashWindow.loadURL("static://splash.html"); await this.splashWindow.loadURL("static://splash.html");

View File

@ -16,7 +16,7 @@ export default {
for (const hotbar of hotbars) { for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) { for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i]; 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) { if (!entity) {
// Clear disabled item // Clear disabled item

View File

@ -4,7 +4,7 @@
*/ */
import { computed, observable, makeObservable, action } from "mobx"; 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 { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities"; import "../../common/catalog-entities";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
@ -14,7 +14,7 @@ import { once } from "lodash";
import logger from "../../common/logger"; import logger from "../../common/logger";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event"; import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { CatalogIpcEvents } from "../../common/ipc/catalog"; import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../common/ipc/catalog";
import { navigate } from "../navigation"; import { navigate } from "../navigation";
import { isMainFrame } from "process"; import { isMainFrame } from "process";
@ -79,12 +79,12 @@ export class CatalogEntityRegistry {
} }
init() { init() {
ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => { ipcRendererOn(catalogItemsChannel, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
this.updateItems(items); this.updateItems(items);
}); });
// Make sure that we get items ASAP and not the next time one of them changes // 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) { if (isMainFrame) {
ipcRendererOn(catalogEntityRunListener, (event, id: string) => { ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
@ -120,7 +120,7 @@ export class CatalogEntityRegistry {
const entity = this.categoryRegistry.getEntityForData(item); const entity = this.categoryRegistry.getEntityForData(item);
if (entity) { if (entity) {
this._entities.set(entity.metadata.uid, entity); this._entities.set(entity.getId(), entity);
} else { } else {
this.rawEntities.push(item); this.rawEntities.push(item);
} }

View File

@ -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<T extends CatalogEntity> 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<any>) {
return this.labels
.map(label => (
<Badge
scrollable
className={styles.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
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);
}
}

View File

@ -4,11 +4,6 @@
*/ */
.Catalog { .Catalog {
:global(.TableHead) .entityName {
/* offset for icon to align texts in the column */
padding-left: calc(21px + var(--padding) * 2.5);
}
:global(.TableRow):hover .pinIcon { :global(.TableRow):hover .pinIcon {
opacity: 1; opacity: 1;
} }

View File

@ -81,14 +81,14 @@ export class EntitySettings extends React.Component<Props> {
<> <>
<div className="flex items-center pb-8"> <div className="flex items-center pb-8">
<Avatar <Avatar
title={this.entity.metadata.name} title={this.entity.getName()}
colorHash={`${this.entity.metadata.name}-${this.entity.metadata.source}`} colorHash={`${this.entity.getName()}-${this.entity.metadata.source}`}
src={this.entity.spec.icon?.src} src={this.entity.spec.icon?.src}
className={styles.settingsAvatar} className={styles.settingsAvatar}
size={40} size={40}
/> />
<div className={styles.entityName}> <div className={styles.entityName}>
{this.entity.metadata.name} {this.entity.getName()}
</div> </div>
</div> </div>
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}> <Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>

View File

@ -27,12 +27,11 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable"; import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
import installFromInputInjectable from "./install-from-input/install-from-input.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 type { LensExtensionId } from "../../../extensions/lens-extension";
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
import { supportedExtensionFormats } from "./supported-extension-formats"; import { supportedExtensionFormats } from "./supported-extension-formats";
import extensionInstallationStateStoreInjectable import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies { interface Dependencies {
@ -107,22 +106,15 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
} }
} }
export const Extensions = withInjectables<Dependencies>( export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
NonInjectedExtensions, getProps: (di) => ({
{ userExtensions: di.inject(userExtensionsInjectable),
getProps: (di) => ({ enableExtension: di.inject(enableExtensionInjectable),
userExtensions: di.inject(userExtensionsInjectable), disableExtension: di.inject(disableExtensionInjectable),
enableExtension: di.inject(enableExtensionInjectable), confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable), installFromInput: di.inject(installFromInputInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), installOnDrop: di.inject(installOnDropInjectable),
installFromInput: di.inject(installFromInputInjectable), installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
installOnDrop: di.inject(installOnDropInjectable), extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}),
installFromSelectFileDialog: di.inject( });
installFromSelectFileDialogInjectable,
),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}),
},
);

View File

@ -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<void>
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;

View File

@ -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;

View File

@ -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<void>
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);
}
};

View File

@ -19,7 +19,7 @@ import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager"; import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { dialog } from "../../remote-helpers"; import { requestOpenFilePickingDialog } from "../../ipc";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
onAddRepo: Function onAddRepo: Function
@ -73,7 +73,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
} }
async selectFileDialog(type: FileType, fileFilter: FileFilter) { async selectFileDialog(type: FileType, fileFilter: FileFilter) {
const { canceled, filePaths } = await dialog.showOpenDialog({ const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: this.getFilePath(type), defaultPath: this.getFilePath(type),
properties: ["openFile", "showHiddenFiles"], properties: ["openFile", "showHiddenFiles"],
message: `Select file`, message: `Select file`,

View File

@ -10,8 +10,7 @@ import type { IToleration } from "../../../../common/k8s-api/workload-kube-objec
import { PodTolerations } from "../pod-tolerations"; import { PodTolerations } from "../pod-tolerations";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor"; import { DiRender, renderFor } from "../../test-utils/renderFor";
import directoryForLensLocalStorageInjectable import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {

View File

@ -7,7 +7,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import { computed, IComputedValue } from "mobx"; import { computed, IComputedValue } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "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 type { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";

View File

@ -8,8 +8,7 @@ import styles from "./cluster-status.module.scss";
import { computed, observable, makeObservable } from "mobx"; import { computed, observable, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import React from "react"; import React from "react";
import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { ipcRendererOn } from "../../../common/ipc";
import { ipcRendererOn, requestMain } from "../../../common/ipc";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
@ -19,6 +18,7 @@ import { navigate } from "../../navigation";
import { entitySettingsURL } from "../../../common/routes"; import { entitySettingsURL } from "../../../common/routes";
import type { KubeAuthUpdate } from "../../../common/cluster-types"; import type { KubeAuthUpdate } from "../../../common/cluster-types";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { requestClusterActivation } from "../../ipc";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -60,7 +60,7 @@ export class ClusterStatus extends React.Component<Props> {
this.isReconnecting = true; this.isReconnecting = true;
try { try {
await requestMain(clusterActivateHandler, this.cluster.id, true); await requestClusterActivation(this.cluster.id, true);
} catch (error) { } catch (error) {
this.authOutput.push({ this.authOutput.push({
message: error.toString(), message: error.toString(),

View File

@ -12,11 +12,10 @@ import { ClusterStatus } from "./cluster-status";
import { ClusterFrameHandler } from "./lens-views"; import { ClusterFrameHandler } from "./lens-views";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { ClusterStore } from "../../../common/cluster-store/cluster-store"; 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 { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes"; import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
import { requestClusterActivation } from "../../ipc";
interface Props extends RouteComponentProps<ClusterViewRouteParams> { interface Props extends RouteComponentProps<ClusterViewRouteParams> {
} }
@ -58,7 +57,7 @@ export class ClusterView extends React.Component<Props> {
reaction(() => this.clusterId, async (clusterId) => { reaction(() => this.clusterId, async (clusterId) => {
ClusterFrameHandler.getInstance().setVisibleCluster(clusterId); ClusterFrameHandler.getInstance().setVisibleCluster(clusterId);
ClusterFrameHandler.getInstance().initView(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; catalogEntityRegistry.activeEntity = clusterId;
}, { }, {
fireImmediately: true, fireImmediately: true,

View File

@ -5,7 +5,7 @@
import { action, IReactionDisposer, makeObservable, observable, when } from "mobx"; import { action, IReactionDisposer, makeObservable, observable, when } from "mobx";
import logger from "../../../main/logger"; 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 { ClusterStore } from "../../../common/cluster-store/cluster-store";
import type { ClusterId } from "../../../common/cluster-types"; import type { ClusterId } from "../../../common/cluster-types";
import { getClusterFrameUrl, Singleton } from "../../utils"; import { getClusterFrameUrl, Singleton } from "../../utils";

View File

@ -11,7 +11,7 @@ import type { CatalogEntity } from "../../api/catalog-entity";
import * as components from "./components"; import * as components from "./components";
function getClusterForEntity(entity: CatalogEntity) { function getClusterForEntity(entity: CatalogEntity) {
return ClusterStore.getInstance().getById(entity.metadata.uid); return ClusterStore.getInstance().getById(entity.getId());
} }
export function GeneralSettings({ entity }: EntitySettingViewProps) { export function GeneralSettings({ entity }: EntitySettingViewProps) {

View File

@ -75,8 +75,8 @@ export class ClusterIconSetting extends React.Component<Props> {
accept="image/*" accept="image/*"
label={ label={
<Avatar <Avatar
colorHash={`${entity.metadata.name}-${entity.metadata.source}`} colorHash={`${entity.getName()}-${entity.metadata.source}`}
title={entity.metadata.name} title={entity.getName()}
src={entity.spec.icon?.src} src={entity.spec.icon?.src}
size={53} size={53}
/> />

View File

@ -29,7 +29,7 @@ export class ClusterNameSetting extends React.Component<Props> {
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, disposeOnUnmount(this,
autorun(() => { autorun(() => {
this.name = this.props.cluster.preferences.clusterName || this.props.entity.metadata.name; this.name = this.props.cluster.preferences.clusterName || this.props.entity.getName();
}), }),
); );
} }

View File

@ -12,8 +12,6 @@ import { Button } from "../button";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { saveKubeconfig } from "./save-config"; import { saveKubeconfig } from "./save-config";
import { requestMain } from "../../../common/ipc";
import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
@ -21,6 +19,7 @@ import { Dialog } from "../dialog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Select } from "../select"; import { Select } from "../select";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
type DialogState = { type DialogState = {
isOpen: boolean, isOpen: boolean,
@ -87,18 +86,18 @@ export class DeleteClusterDialog extends React.Component {
async onDelete() { async onDelete() {
const { cluster, config } = dialogState; const { cluster, config } = dialogState;
await requestMain(clusterSetDeletingHandler, cluster.id); await requestSetClusterAsDeleting(cluster.id);
this.removeContext(); this.removeContext();
this.changeCurrentContext(); this.changeCurrentContext();
try { try {
await saveKubeconfig(config, cluster.kubeConfigPath); await saveKubeconfig(config, cluster.kubeConfigPath);
HotbarStore.getInstance().removeAllHotbarItems(cluster.id); HotbarStore.getInstance().removeAllHotbarItems(cluster.id);
await requestMain(clusterDeleteHandler, cluster.id); await requestDeleteCluster(cluster.id);
} catch(error) { } catch(error) {
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`); Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
} finally { } finally {
await requestMain(clusterClearDeletingHandler, cluster.id); await requestClearClusterAsDeleting(cluster.id);
} }
this.onClose(); this.onClose();

View File

@ -73,7 +73,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
menuItems.unshift({ menuItems.unshift({
title: "Remove from Hotbar", 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; this.contextMenu.menuItems = menuItems;
@ -90,8 +90,8 @@ export class HotbarEntityIcon extends React.Component<Props> {
return ( return (
<HotbarIcon <HotbarIcon
uid={entity.metadata.uid} uid={entity.getId()}
title={entity.metadata.name} title={entity.getName()}
source={entity.metadata.source} source={entity.metadata.source}
src={entity.spec.icon?.src} src={entity.spec.icon?.src}
material={entity.spec.icon?.material} material={entity.spec.icon?.material}
@ -101,7 +101,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
onMenuOpen={() => this.onMenuOpen()} onMenuOpen={() => this.onMenuOpen()}
disabled={!entity} disabled={!entity}
menuItems={this.contextMenu.menuItems} menuItems={this.contextMenu.menuItems}
tooltip={`${entity.metadata.name} (${entity.metadata.source})`} tooltip={`${entity.getName()} (${entity.metadata.source})`}
{...elemProps} {...elemProps}
> >
{ this.ledIcon } { this.ledIcon }

View File

@ -353,6 +353,7 @@ export class Input extends React.Component<InputProps, State> {
dirty: _dirty, // excluded from passing to input-element dirty: _dirty, // excluded from passing to input-element
defaultValue, defaultValue,
trim, trim,
blurOnEnter,
...inputProps ...inputProps
} = this.props; } = this.props;
const { focused, dirty, valid, validating, errors } = this.state; const { focused, dirty, valid, validating, errors } = this.state;

View File

@ -104,7 +104,6 @@ class NonInjectedKubeObjectListLayout<K extends KubeObject> extends React.Compon
return ( return (
<ItemListLayout <ItemListLayout
{...layoutProps}
className={cssNames("KubeObjectListLayout", className)} className={cssNames("KubeObjectListLayout", className)}
store={store} store={store}
items={items} items={items}
@ -133,6 +132,7 @@ class NonInjectedKubeObjectListLayout<K extends KubeObject> extends React.Compon
...[customizeHeader].flat(), ...[customizeHeader].flat(),
]} ]}
renderItemMenu={item => <KubeObjectMenu object={item} />} renderItemMenu={item => <KubeObjectMenu object={item} />}
{...layoutProps}
/> />
); );
} }

View File

@ -73,7 +73,7 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity
? "Remove from Hotbar" ? "Remove from Hotbar"
: "Add to Hotbar"; : "Add to Hotbar";
const onClick = isAddedToActive const onClick = isAddedToActive
? () => hotbarStore.removeFromHotbar(metadata.uid) ? () => hotbarStore.removeFromHotbar(clusterEntity.getId())
: () => hotbarStore.addToHotbar(clusterEntity); : () => hotbarStore.addToHotbar(clusterEntity);
contextMenu.menuItems = [{ title, onClick }]; contextMenu.menuItems = [{ title, onClick }];
@ -92,8 +92,7 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity
setOpened(!opened); setOpened(!opened);
}; };
const { metadata, spec } = clusterEntity; const id = `cluster-${clusterEntity.getId()}`;
const id = `cluster-${metadata.uid}`;
const tooltipId = `tooltip-${id}`; const tooltipId = `tooltip-${id}`;
return ( return (
@ -106,17 +105,17 @@ export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity
data-testid="sidebar-cluster-dropdown" data-testid="sidebar-cluster-dropdown"
> >
<Avatar <Avatar
title={metadata.name} title={clusterEntity.getName()}
colorHash={`${metadata.name}-${metadata.source}`} colorHash={`${clusterEntity.getName()}-${clusterEntity.metadata.source}`}
size={40} size={40}
src={spec.icon?.src} src={clusterEntity.spec.icon?.src}
className={styles.avatar} className={styles.avatar}
/> />
<div className={styles.clusterName} id={tooltipId}> <div className={styles.clusterName} id={tooltipId}>
{metadata.name} {clusterEntity.getName()}
</div> </div>
<Tooltip targetId={tooltipId}> <Tooltip targetId={tooltipId}>
{metadata.name} {clusterEntity.getName()}
</Tooltip> </Tooltip>
<Icon material="arrow_drop_down" className={styles.dropdown}/> <Icon material="arrow_drop_down" className={styles.dropdown}/>
<Menu <Menu

View File

@ -7,18 +7,17 @@ import React from "react";
import { fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "./top-bar"; import { TopBar } from "./top-bar";
import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { broadcastMessage } from "../../../../common/ipc";
import * as vars from "../../../../common/vars"; import * as vars from "../../../../common/vars";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor"; import { DiRender, renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
const mockConfig = vars as { isWindows: boolean; isLinux: boolean }; const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
jest.mock("../../../../common/ipc"); jest.mock("../../../../common/ipc");
jest.mock("../../../ipc");
jest.mock("../../../../common/vars", () => { jest.mock("../../../../common/vars", () => {
const SemVer = require("semver").SemVer; 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("<TopBar/> in Windows and Linux", () => { describe("<TopBar/> in Windows and Linux", () => {
let render: DiRender; let render: DiRender;
@ -104,15 +86,15 @@ describe("<TopBar/> in Windows and Linux", () => {
const close = getByTestId("window-close"); const close = getByTestId("window-close");
fireEvent.click(menu); fireEvent.click(menu);
expect(broadcastMessage).toHaveBeenCalledWith(IpcMainWindowEvents.OPEN_CONTEXT_MENU); expect(emitOpenAppMenuAsContextMenu).toHaveBeenCalledWith();
fireEvent.click(minimize); fireEvent.click(minimize);
expect(mockMinimize).toHaveBeenCalled(); expect(requestWindowAction).toHaveBeenCalledWith("minimize");
fireEvent.click(maximize); fireEvent.click(maximize);
expect(mockMaximize).toHaveBeenCalled(); expect(requestWindowAction).toHaveBeenCalledWith("toggle-maximize");
fireEvent.click(close); fireEvent.click(close);
expect(mockClose).toHaveBeenCalled(); expect(requestWindowAction).toHaveBeenCalledWith("close");
}); });
}); });

View File

@ -12,8 +12,7 @@ import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injec
import { DiRender, renderFor } from "../../test-utils/renderFor"; import { DiRender, renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { computed } from "mobx"; import { computed } from "mobx";
import directoryForUserDataInjectable import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
jest.mock("../../../../common/vars", () => { jest.mock("../../../../common/vars", () => {
@ -27,6 +26,9 @@ jest.mock("../../../../common/vars", () => {
}; };
}); });
const goBack = jest.fn();
const goForward = jest.fn();
jest.mock( jest.mock(
"electron", "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(), 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("<TopBar/>", () => { describe("<TopBar/>", () => {
let di: ConfigurableDependencyInjectionContainer; let di: ConfigurableDependencyInjectionContainer;
let render: DiRender; let render: DiRender;

View File

@ -4,22 +4,22 @@
*/ */
import styles from "./top-bar.module.scss"; 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 { observer } from "mobx-react";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
import { webContents, getCurrentWindow } from "@electron/remote";
import { observable } from "mobx"; import { observable } from "mobx";
import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc"; import { ipcRendererOn } from "../../../../common/ipc";
import { watchHistoryState } from "../../../remote-helpers/history-updater"; import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { isActiveRoute, navigate } from "../../../navigation"; import { isActiveRoute, navigate } from "../../../navigation";
import { catalogRoute, catalogURL } from "../../../../common/routes"; import { catalogRoute, catalogURL } from "../../../../common/routes";
import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { isLinux, isWindows } from "../../../../common/vars"; import { isLinux, isWindows } from "../../../../common/vars";
import { cssNames } from "../../../utils"; import { cssNames } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration"; import type { TopBarRegistration } from "./top-bar-registration";
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
import { WindowAction } from "../../../../common/ipc/window";
interface Props extends React.HTMLAttributes<any> {} interface Props extends React.HTMLAttributes<any> {}
@ -40,10 +40,9 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => { const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
const elem = useRef<HTMLDivElement>(); const elem = useRef<HTMLDivElement>();
const window = useMemo(() => getCurrentWindow(), []);
const openContextMenu = () => { const openAppContextMenu = () => {
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU); emitOpenAppMenuAsContextMenu();
}; };
const goHome = () => { const goHome = () => {
@ -51,11 +50,11 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
}; };
const goBack = () => { const goBack = () => {
webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goBack(); requestWindowAction(WindowAction.GO_BACK);
}; };
const goForward = () => { const goForward = () => {
webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goForward(); requestWindowAction(WindowAction.GO_FORWARD);
}; };
const windowSizeToggle = (evt: React.MouseEvent) => { const windowSizeToggle = (evt: React.MouseEvent) => {
@ -68,34 +67,30 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
}; };
const minimizeWindow = () => { const minimizeWindow = () => {
window.minimize(); requestWindowAction(WindowAction.MINIMIZE);
}; };
const toggleMaximize = () => { const toggleMaximize = () => {
if (window.isMaximized()) { requestWindowAction(WindowAction.TOGGLE_MAXIMIZE);
window.unmaximize();
} else {
window.maximize();
}
}; };
const closeWindow = () => { const closeWindow = () => {
window.close(); requestWindowAction(WindowAction.CLOSE);
}; };
useEffect(() => { useEffect(() => watchHistoryState(), []);
const disposer = watchHistoryState();
return () => disposer();
}, []);
return ( return (
<div className={styles.topBar} onDoubleClick={windowSizeToggle} ref={elem} {...rest}> <div className={styles.topBar} onDoubleClick={windowSizeToggle} ref={elem} {...rest}>
<div className={styles.tools}> <div className={styles.tools}>
{(isWindows || isLinux) && ( {(isWindows || isLinux) && (
<div className={styles.winMenu}> <div className={styles.winMenu}>
<div onClick={openContextMenu} data-testid="window-menu"> <div onClick={openAppContextMenu} data-testid="window-menu">
<svg width="12" height="12" viewBox="0 0 12 12" shapeRendering="crispEdges"><path fill="currentColor" d="M0,8.5h12v1H0V8.5z"/><path fill="currentColor" d="M0,5.5h12v1H0V5.5z"/><path fill="currentColor" d="M0,2.5h12v1H0V2.5z"/></svg> <svg width="12" height="12" viewBox="0 0 12 12" shapeRendering="crispEdges">
<path fill="currentColor" d="M0,8.5h12v1H0V8.5z"/>
<path fill="currentColor" d="M0,5.5h12v1H0V5.5z"/>
<path fill="currentColor" d="M0,2.5h12v1H0V2.5z"/>
</svg>
</div> </div>
</div> </div>
)} )}
@ -127,12 +122,19 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
{(isWindows || isLinux) && ( {(isWindows || isLinux) && (
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}> <div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
<div className={styles.minimize} data-testid="window-minimize" onClick={minimizeWindow}> <div className={styles.minimize} data-testid="window-minimize" onClick={minimizeWindow}>
<svg shapeRendering="crispEdges" viewBox="0 0 12 12"><rect fill="currentColor" width="10" height="1" x="1" y="9"></rect></svg></div> <svg shapeRendering="crispEdges" viewBox="0 0 12 12">
<rect fill="currentColor" width="10" height="1" x="1" y="9" />
</svg>
</div>
<div className={styles.maximize} data-testid="window-maximize" onClick={toggleMaximize}> <div className={styles.maximize} data-testid="window-maximize" onClick={toggleMaximize}>
<svg shapeRendering="crispEdges" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor"></rect></svg> <svg shapeRendering="crispEdges" viewBox="0 0 12 12">
<rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor" />
</svg>
</div> </div>
<div className={styles.close} data-testid="window-close" onClick={closeWindow}> <div className={styles.close} data-testid="window-close" onClick={closeWindow}>
<svg shapeRendering="crispEdges" viewBox="0 0 12 12"><polygon fill="currentColor" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"></polygon></svg> <svg shapeRendering="crispEdges" viewBox="0 0 12 12">
<polygon fill="currentColor" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" />
</svg>
</div> </div>
</div> </div>
)} )}
@ -162,7 +164,6 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => (
export const TopBar = withInjectables(observer(NonInjectedTopBar), { export const TopBar = withInjectables(observer(NonInjectedTopBar), {
getProps: (di, props) => ({ getProps: (di, props) => ({
items: di.inject(topBarItemsInjectable), items: di.inject(topBarItemsInjectable),
...props, ...props,
}), }),
}); });

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import yaml, { YAMLException } from "js-yaml"; import yaml from "js-yaml";
export interface MonacoValidator { export interface MonacoValidator {
(value: string): void; (value: string): void;
@ -10,9 +10,9 @@ export interface MonacoValidator {
export function yamlValidator(value: string) { export function yamlValidator(value: string) {
try { try {
yaml.load(value); yaml.loadAll(value);
} catch (error) { } catch (error) {
throw String(error as YAMLException); throw String(error);
} }
} }

View File

@ -3,11 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { observer } from "mobx-react";
import React from "react"; import React from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
import { requestOpenFilePickingDialog } from "../../ipc";
export interface PathPickOpts { export interface PathPickOpts {
label: string; label: string;
@ -29,8 +30,8 @@ export interface PathPickerProps extends PathPickOpts {
export class PathPicker extends React.Component<PathPickerProps> { export class PathPicker extends React.Component<PathPickerProps> {
static async pick(opts: PathPickOpts) { static async pick(opts: PathPickOpts) {
const { onPick, onCancel, label, ...dialogOptions } = opts; 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, message: label,
...dialogOptions, ...dialogOptions,
}); });

View File

@ -71,7 +71,6 @@ class NonInjectedClusterFrame extends React.Component<Dependencies> {
this.props.subscribeStores([ this.props.subscribeStores([
this.props.namespaceStore, this.props.namespaceStore,
]), ]),
watchHistoryState(), watchHistoryState(),
]); ]);
} }

View File

@ -6,8 +6,6 @@ import type { Cluster } from "../../../../common/cluster/cluster";
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
import logger from "../../../../main/logger"; import logger from "../../../../main/logger";
import { Terminal } from "../../../components/dock/terminal/terminal"; 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 type { KubernetesCluster } from "../../../../common/catalog-entities";
import { Notifications } from "../../../components/notifications"; import { Notifications } from "../../../components/notifications";
import type { AppEvent } from "../../../../common/app-event-bus/event-bus"; import type { AppEvent } from "../../../../common/app-event-bus/event-bus";
@ -16,6 +14,7 @@ import { when } from "mobx";
import { unmountComponentAtNode } from "react-dom"; import { unmountComponentAtNode } from "react-dom";
import type { ClusterFrameContext } from "../../../cluster-frame-context/cluster-frame-context"; import type { ClusterFrameContext } from "../../../cluster-frame-context/cluster-frame-context";
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import { requestSetClusterFrameId } from "../../../ipc";
interface Dependencies { interface Dependencies {
hostedCluster: Cluster; hostedCluster: Cluster;
@ -42,7 +41,7 @@ export const initClusterFrame =
); );
await Terminal.preloadFonts(); await Terminal.preloadFonts();
await requestMain(clusterSetFrameIdHandler, hostedCluster.id); await requestSetClusterFrameId(hostedCluster.id);
await hostedCluster.whenReady; // cluster.activate() is done at this point await hostedCluster.whenReady; // cluster.activate() is done at this point
catalogEntityRegistry.activeEntity = hostedCluster.id; catalogEntityRegistry.activeEntity = hostedCluster.id;

View File

@ -3,12 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { delay } from "../../../../common/utils"; import { delay } from "../../../../common/utils";
import { broadcastMessage, BundledExtensionsLoaded } from "../../../../common/ipc"; import { broadcastMessage } from "../../../../common/ipc";
import { registerIpcListeners } from "../../../ipc"; import { registerIpcListeners } from "../../../ipc/register-listeners";
import logger from "../../../../common/logger"; import logger from "../../../../common/logger";
import { unmountComponentAtNode } from "react-dom"; import { unmountComponentAtNode } from "react-dom";
import type { ExtensionLoading } from "../../../../extensions/extension-loader"; import type { ExtensionLoading } from "../../../../extensions/extension-loader";
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
interface Dependencies { interface Dependencies {
loadExtensions: () => Promise<ExtensionLoading[]>; loadExtensions: () => Promise<ExtensionLoading[]>;
@ -50,7 +51,7 @@ export const initRootFrame =
await Promise.race([bundledExtensionsFinished, timeout]); await Promise.race([bundledExtensionsFinished, timeout]);
} finally { } finally {
ipcRenderer.send(BundledExtensionsLoaded); ipcRenderer.send(bundledExtensionsLoaded);
} }
lensProtocolRouterRenderer.init(); lensProtocolRouterRenderer.init();

View File

@ -8,3 +8,4 @@
export * from "./useOnUnmount"; export * from "./useOnUnmount";
export * from "./useInterval"; export * from "./useInterval";
export * from "./useMutationObserver"; export * from "./useMutationObserver";
export * from "./use-toggle";

View File

@ -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)];
}

View File

@ -50,7 +50,7 @@ export function initCatalog({ openCommandDialog }: Dependencies) {
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",
icon: "delete", icon: "delete",
onClick: () => onClusterDelete(entity.metadata.uid), onClick: () => onClusterDelete(entity.getId()),
}); });
} }
}); });

83
src/renderer/ipc/index.ts Normal file
View File

@ -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<void> {
return requestMain(windowActionHandleChannel, type);
}
export function requestOpenFilePickingDialog(opts: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[]; }> {
return requestMain(openFilePickingDialogChannel, opts);
}
export function requestSetClusterFrameId(clusterId: ClusterId): Promise<void> {
return requestMain(clusterSetFrameIdHandler, clusterId);
}
export function requestClusterActivation(clusterId: ClusterId, force?: boolean): Promise<void> {
return requestMain(clusterActivateHandler, clusterId, force);
}
export function requestClusterDisconnection(clusterId: ClusterId, force?: boolean): Promise<void> {
return requestMain(clusterDisconnectHandler, clusterId, force);
}
export function requestSetClusterAsDeleting(clusterId: ClusterId): Promise<void> {
return requestMain(clusterSetDeletingHandler, clusterId);
}
export function requestClearClusterAsDeleting(clusterId: ClusterId): Promise<void> {
return requestMain(clusterClearDeletingHandler, clusterId);
}
export function requestDeleteCluster(clusterId: ClusterId): Promise<void> {
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);
}

View File

@ -5,7 +5,7 @@
import React from "react"; import React from "react";
import { ipcRenderer, IpcRendererEvent } from "electron"; 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 { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button"; import { Button } from "../components/button";
import { isMac } from "../../common/vars"; import { isMac } from "../../common/vars";
@ -13,6 +13,8 @@ import { ClusterStore } from "../../common/cluster-store/cluster-store";
import { navigate } from "../navigation"; import { navigate } from "../navigation";
import { entitySettingsURL } from "../../common/routes"; import { entitySettingsURL } from "../../common/routes";
import { defaultHotbarCells } from "../../common/hotbar-types"; 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 { function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
notificationsStore.remove(notificationId); notificationsStore.remove(notificationId);
@ -127,13 +129,13 @@ export function registerIpcListeners() {
}); });
onCorrect({ onCorrect({
source: ipcRenderer, source: ipcRenderer,
channel: ClusterListNamespaceForbiddenChannel, channel: clusterListNamespaceForbiddenChannel,
listener: ListNamespacesForbiddenHandler, listener: ListNamespacesForbiddenHandler,
verifier: isListNamespaceForbiddenArgs, verifier: isListNamespaceForbiddenArgs,
}); });
onCorrect({ onCorrect({
source: ipcRenderer, source: ipcRenderer,
channel: HotbarTooManyItems, channel: hotbarTooManyItemsChannel,
listener: HotbarTooManyItemsHandler, listener: HotbarTooManyItemsHandler,
verifier: (args: unknown[]): args is [] => args.length === 0, verifier: (args: unknown[]): args is [] => args.length === 0,
}); });

View File

@ -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<Electron.OpenDialogReturnValue> {
return requestMain(dialogShowOpenDialogHandler, options);
}

View File

@ -3,32 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { webContents } from "@electron/remote";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { broadcastMessage } from "../../common/ipc"; import { emitWindowLocationChanged } from "../ipc";
import { navigation } from "../navigation"; import { navigation } from "../navigation";
export function watchHistoryState() { export function watchHistoryState() {
return reaction(() => navigation.location, () => { return reaction(() => navigation.location, emitWindowLocationChanged);
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);
});
} }

View File

@ -413,11 +413,6 @@
global-agent "^2.0.2" global-agent "^2.0.2"
global-tunnel-ng "^2.7.1" 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": "@electron/universal@1.0.5":
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.0.5.tgz#b812340e4ef21da2b3ee77b2b4d35c9b86defe37" resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.0.5.tgz#b812340e4ef21da2b3ee77b2b4d35c9b86defe37"
@ -4895,10 +4890,10 @@ electron-window-state@^5.0.3:
jsonfile "^4.0.0" jsonfile "^4.0.0"
mkdirp "^0.5.1" mkdirp "^0.5.1"
electron@^13.6.1: electron@^14.2.4:
version "13.6.1" version "14.2.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-13.6.1.tgz#f61c4f269b57c7dc27e0d5476216a988caa9c752" resolved "https://registry.yarnpkg.com/electron/-/electron-14.2.4.tgz#243c71a16a85a4f70086d003b3437cd30b541da0"
integrity sha512-rZ6Y7RberigruefQpWOiI4bA9ppyT88GQF8htY6N1MrAgal5RrBc+Mh92CcGU7zT9QO+XO3DarSgZafNTepffQ== integrity sha512-uskCIp+fpohqVYtM2Q28rbXLqGjZ6sWYylXcX6N+K8jR8kR2eHuDMIkO8DzWrTsqA6t4UNAzn+bJnA3VfIIjQw==
dependencies: dependencies:
"@electron/get" "^1.0.1" "@electron/get" "^1.0.1"
"@types/node" "^14.6.2" "@types/node" "^14.6.2"