mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' of github.com:lensapp/lens into issue-3472
This commit is contained in:
commit
f5f1128c42
2
.yarnrc
2
.yarnrc
@ -1,3 +1,3 @@
|
||||
disturl "https://atom.io/download/electron"
|
||||
target "13.6.1"
|
||||
target "14.2.4"
|
||||
runtime "electron"
|
||||
|
||||
@ -57,6 +57,14 @@ Will register a new view for the KubernetesCluster category, and because the pri
|
||||
|
||||
The default list view has a priority of 50 and and custom views with priority (defaulting to 50) >= 50 will be displayed afterwards.
|
||||
|
||||
#### Styling Custom Views
|
||||
|
||||
By default, custom view blocks are styled with [Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). Some details comes from this.
|
||||
|
||||
- To set fixed height of a custom block, use `max-height` css rule.
|
||||
- To set flexible height, use `height`.
|
||||
- Otherwise, custom view will have height of it's contents.
|
||||
|
||||
## Entities
|
||||
|
||||
An entity is the data within the catalog.
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
*/
|
||||
import type { ElectronApplication, Page } from "playwright";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { isWindows } from "../../src/common/vars";
|
||||
|
||||
describe("preferences page tests", () => {
|
||||
let window: Page, cleanup: () => Promise<void>;
|
||||
@ -33,7 +34,8 @@ describe("preferences page tests", () => {
|
||||
await cleanup();
|
||||
}, 10*60*1000);
|
||||
|
||||
it('shows "preferences" and can navigate through the tabs', async () => {
|
||||
// skip on windows due to suspected playwright issue with Electron 14
|
||||
utils.itIf(!isWindows)('shows "preferences" and can navigate through the tabs', async () => {
|
||||
const pages = [
|
||||
{
|
||||
id: "application",
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import type { ElectronApplication, Page } from "playwright";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { isWindows } from "../../src/common/vars";
|
||||
|
||||
describe("Lens command palette", () => {
|
||||
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication;
|
||||
@ -19,7 +20,8 @@ describe("Lens command palette", () => {
|
||||
}, 10*60*1000);
|
||||
|
||||
describe("menu", () => {
|
||||
it("opens command dialog from menu", async () => {
|
||||
// skip on windows due to suspected playwright issue with Electron 14
|
||||
utils.itIf(!isWindows)("opens command dialog from menu", async () => {
|
||||
await app.evaluate(async ({ app }) => {
|
||||
await app.applicationMenu
|
||||
.getMenuItemById("view")
|
||||
|
||||
@ -40,7 +40,7 @@ async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promis
|
||||
throw new Error(`Lens did not open the main window within ${timeout}ms`);
|
||||
}
|
||||
|
||||
export async function start() {
|
||||
async function attemptStart() {
|
||||
const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4());
|
||||
|
||||
// Make sure that the directory is clear
|
||||
@ -77,6 +77,19 @@ export async function start() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function start() {
|
||||
// this is an attempted workaround for an issue with playwright not always getting the main window when using Electron 14.2.4 (observed on windows)
|
||||
for (let i = 0; ; i++) {
|
||||
try {
|
||||
return await attemptStart();
|
||||
} catch (error) {
|
||||
if (i === 4) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function clickWelcomeButton(window: Page) {
|
||||
await window.click("[data-testid=welcome-menu-container] li a");
|
||||
}
|
||||
|
||||
@ -191,7 +191,6 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^1.2.2",
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.16.1",
|
||||
@ -334,7 +333,7 @@
|
||||
"css-loader": "^5.2.7",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.4",
|
||||
"electron": "^13.6.1",
|
||||
"electron": "^14.2.4",
|
||||
"electron-builder": "^22.14.5",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"esbuild": "^0.13.15",
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { broadcastMessage, requestMain } from "../ipc";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
|
||||
|
||||
export interface KubernetesClusterPrometheusMetrics {
|
||||
address?: {
|
||||
@ -69,7 +69,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
||||
if (app) {
|
||||
await ClusterStore.getInstance().getById(this.metadata.uid)?.activate();
|
||||
} else {
|
||||
await requestMain(clusterActivateHandler, this.metadata.uid, false);
|
||||
await requestClusterActivation(this.metadata.uid, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
||||
if (app) {
|
||||
ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect();
|
||||
} else {
|
||||
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
|
||||
await requestClusterDisconnection(this.metadata.uid, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
||||
context.menuItems.push({
|
||||
title: "Disconnect",
|
||||
icon: "link_off",
|
||||
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid),
|
||||
onClick: () => requestClusterDisconnection(this.metadata.uid),
|
||||
});
|
||||
break;
|
||||
case LensKubernetesClusterStatus.DISCONNECTED:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { ipcMain, ipcRenderer, webFrame } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
|
||||
@ -11,16 +11,16 @@ import { Cluster } from "../cluster/cluster";
|
||||
import migrations from "../../migrations/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { ipcMainHandle, requestMain } from "../ipc";
|
||||
import { ipcMainHandle } from "../ipc";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
||||
import { requestInitialClusterStates } from "../../renderer/ipc";
|
||||
import { clusterStates } from "../ipc/cluster";
|
||||
|
||||
export interface ClusterStoreModel {
|
||||
clusters?: ClusterModel[];
|
||||
}
|
||||
|
||||
const initialStates = "cluster:states";
|
||||
|
||||
interface Dependencies {
|
||||
createCluster: (model: ClusterModel) => Cluster
|
||||
}
|
||||
@ -49,18 +49,18 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
async loadInitialOnRenderer() {
|
||||
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||
|
||||
for (const { id, state } of await requestMain(initialStates)) {
|
||||
for (const { id, state } of await requestInitialClusterStates()) {
|
||||
this.getById(id)?.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
provideInitialFromMain() {
|
||||
ipcMainHandle(initialStates, () => {
|
||||
return this.clustersList.map(cluster => ({
|
||||
ipcMainHandle(clusterStates, () => (
|
||||
this.clustersList.map(cluster => ({
|
||||
id: cluster.id,
|
||||
state: cluster.getState(),
|
||||
}));
|
||||
});
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
protected pushStateToViewsAutomatically() {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../ipc";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { ContextHandler } from "../../main/context-handler/context-handler";
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||
@ -20,6 +20,7 @@ import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, C
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForKubeConfigs: string,
|
||||
@ -645,7 +646,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
|
||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
||||
broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
|
||||
return namespaceList;
|
||||
|
||||
@ -10,8 +10,9 @@ import { toJS } from "./utils";
|
||||
import { CatalogEntity } from "./catalog";
|
||||
import { catalogEntity } from "../main/catalog-sources/general";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
|
||||
import { broadcastMessage } from "./ipc";
|
||||
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
|
||||
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
@ -182,7 +183,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
if (emptyCellIndex != -1) {
|
||||
hotbar.items[emptyCellIndex] = newItem;
|
||||
} else {
|
||||
broadcastMessage(HotbarTooManyItems);
|
||||
broadcastMessage(hotbarTooManyItemsChannel);
|
||||
}
|
||||
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
|
||||
hotbar.items[cellIndex] = newItem;
|
||||
|
||||
@ -3,14 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export enum CatalogIpcEvents {
|
||||
/**
|
||||
* This is broadcast on whenever there is an update to any catalog item
|
||||
*/
|
||||
ITEMS = "catalog:items",
|
||||
/**
|
||||
* This is used to activate a specific entity in the renderer main frame
|
||||
*/
|
||||
export const catalogEntityRunListener = "catalog-entity:run";
|
||||
|
||||
/**
|
||||
* This can be sent from renderer to main to initialize a broadcast of ITEMS
|
||||
*/
|
||||
INIT = "catalog:init",
|
||||
}
|
||||
/**
|
||||
* This is broadcast on whenever there is an update to any catalog item
|
||||
*/
|
||||
export const catalogItemsChannel = "catalog:items";
|
||||
|
||||
/**
|
||||
* This can be sent from renderer to main to initialize a broadcast of ITEMS
|
||||
*/
|
||||
export const catalogInitChannel = "catalog:init";
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -13,3 +13,16 @@ export const clusterSetDeletingHandler = "cluster:deleting:set";
|
||||
export const clusterClearDeletingHandler = "cluster:deleting:clear";
|
||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
||||
export const clusterStates = "cluster:states";
|
||||
|
||||
/**
|
||||
* This channel is broadcast on whenever the cluster fails to list namespaces
|
||||
* during a refresh and no `accessibleNamespaces` have been set.
|
||||
*/
|
||||
export const clusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
|
||||
|
||||
export type ListNamespaceForbiddenArgs = [clusterId: string];
|
||||
|
||||
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
|
||||
return args.length === 1 && typeof args[0] === "string";
|
||||
}
|
||||
@ -3,6 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import * as dialog from "./dialog";
|
||||
|
||||
export { dialog };
|
||||
export const openFilePickingDialogChannel = "dialog:open:file-picking";
|
||||
9
src/common/ipc/extension-handling.ts
Normal file
9
src/common/ipc/extension-handling.ts
Normal 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";
|
||||
@ -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";
|
||||
@ -3,4 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export const HotbarTooManyItems = "hotbar:too-many-items";
|
||||
export const hotbarTooManyItemsChannel = "hotbar:too-many-items";
|
||||
|
||||
@ -3,13 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
||||
export const catalogEntityRunListener = "catalog-entity:run";
|
||||
|
||||
export * from "./ipc";
|
||||
export * from "./invalid-kubeconfig";
|
||||
export * from "./update-available.ipc";
|
||||
export * from "./cluster.ipc";
|
||||
export * from "./update-available";
|
||||
export * from "./type-enforced-ipc";
|
||||
export * from "./hotbar";
|
||||
export * from "./extension-loader.ipc";
|
||||
|
||||
@ -12,25 +12,8 @@ import { toJS } from "../utils/toJS";
|
||||
import logger from "../../main/logger";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||
import type { Disposer } from "../utils";
|
||||
import type remote from "@electron/remote";
|
||||
|
||||
const electronRemote = (() => {
|
||||
if (ipcRenderer) {
|
||||
try {
|
||||
return require("@electron/remote");
|
||||
} catch {
|
||||
// ignore temp
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const subFramesChannel = "ipc:get-sub-frames";
|
||||
|
||||
export async function requestMain(channel: string, ...args: any[]) {
|
||||
return ipcRenderer.invoke(channel, ...args.map(sanitizePayload));
|
||||
}
|
||||
export const broadcastMainChannel = "ipc:broadcast-main";
|
||||
|
||||
export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
|
||||
ipcMain.handle(channel, async (event, ...args) => {
|
||||
@ -42,51 +25,55 @@ function getSubFrames(): ClusterFrameInfo[] {
|
||||
return Array.from(clusterFrameMap.values());
|
||||
}
|
||||
|
||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||
const subFramesP = ipcRenderer
|
||||
? requestMain(subFramesChannel)
|
||||
: Promise.resolve(getSubFrames());
|
||||
export async function broadcastMessage(channel: string, ...args: any[]): Promise<void> {
|
||||
if (ipcRenderer) {
|
||||
return ipcRenderer.invoke(broadcastMainChannel, channel, ...args.map(sanitizePayload));
|
||||
}
|
||||
|
||||
subFramesP
|
||||
.then(subFrames => {
|
||||
const views: undefined | ReturnType<typeof webContents.getAllWebContents> | ReturnType<typeof remote.webContents.getAllWebContents> = (webContents || electronRemote?.webContents)?.getAllWebContents();
|
||||
if (!webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!views || !Array.isArray(views) || views.length === 0) return;
|
||||
args = args.map(sanitizePayload);
|
||||
ipcMain.listeners(channel).forEach((func) => func({
|
||||
processId: undefined, frameId: undefined, sender: undefined, senderFrame: undefined,
|
||||
}, ...args));
|
||||
|
||||
ipcRenderer?.send(channel, ...args);
|
||||
ipcMain?.emit(channel, ...args);
|
||||
const subFrames = getSubFrames();
|
||||
const views = webContents.getAllWebContents();
|
||||
|
||||
for (const view of views) {
|
||||
let viewType = "unknown";
|
||||
if (!views || !Array.isArray(views) || views.length === 0) return;
|
||||
|
||||
// There will be a uncaught exception if the view is destroyed.
|
||||
try {
|
||||
viewType = view.getType();
|
||||
} catch {
|
||||
// We can ignore the view destroyed exception as viewType is only used for logging.
|
||||
}
|
||||
args = args.map(sanitizePayload);
|
||||
|
||||
// Send message to views.
|
||||
try {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
|
||||
view.send(channel, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error: String(error) });
|
||||
}
|
||||
for (const view of views) {
|
||||
let viewType = "unknown";
|
||||
|
||||
// Send message to subFrames of views.
|
||||
for (const frameInfo of subFrames) {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
|
||||
// There will be a uncaught exception if the view is destroyed.
|
||||
try {
|
||||
viewType = view.getType();
|
||||
} catch {
|
||||
// We can ignore the view destroyed exception as viewType is only used for logging.
|
||||
}
|
||||
|
||||
try {
|
||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) });
|
||||
}
|
||||
}
|
||||
// Send message to views.
|
||||
try {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
|
||||
view.send(channel, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error });
|
||||
}
|
||||
|
||||
// Send message to subFrames of views.
|
||||
for (const frameInfo of subFrames) {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
|
||||
|
||||
try {
|
||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ipcMainOn(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): Disposer {
|
||||
@ -101,10 +88,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen
|
||||
return () => ipcRenderer.off(channel, listener);
|
||||
}
|
||||
|
||||
export function bindBroadcastHandlers() {
|
||||
ipcMainHandle(subFramesChannel, () => getSubFrames());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizing data for IPC-messaging before send.
|
||||
* Removes possible observable values to avoid exceptions like "can't clone object".
|
||||
|
||||
39
src/common/ipc/window.ts
Normal file
39
src/common/ipc/window.ts
Normal 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",
|
||||
}
|
||||
@ -9,11 +9,10 @@ import { ResourceApplier } from "../../main/resource-applier";
|
||||
import type { KubernetesCluster } from "../catalog-entities";
|
||||
import logger from "../../main/logger";
|
||||
import { app } from "electron";
|
||||
import { requestMain } from "../ipc";
|
||||
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import yaml from "js-yaml";
|
||||
import { productName } from "../vars";
|
||||
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
|
||||
|
||||
export class ResourceStack {
|
||||
constructor(protected cluster: KubernetesCluster, protected name: string) {}
|
||||
@ -41,7 +40,7 @@ export class ResourceStack {
|
||||
}
|
||||
|
||||
protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
|
||||
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
|
||||
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
|
||||
|
||||
if (!clusterModel) {
|
||||
throw new Error(`cluster not found`);
|
||||
@ -54,7 +53,7 @@ export class ResourceStack {
|
||||
if (app) {
|
||||
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
|
||||
} else {
|
||||
const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
|
||||
const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
|
||||
|
||||
if (response.stderr) {
|
||||
throw new Error(response.stderr);
|
||||
@ -65,7 +64,7 @@ export class ResourceStack {
|
||||
}
|
||||
|
||||
protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
|
||||
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
|
||||
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
|
||||
|
||||
if (!clusterModel) {
|
||||
throw new Error(`cluster not found`);
|
||||
@ -78,7 +77,7 @@ export class ResourceStack {
|
||||
if (app) {
|
||||
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
|
||||
} else {
|
||||
const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
|
||||
const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);
|
||||
|
||||
if (response.stderr) {
|
||||
throw new Error(response.stderr);
|
||||
|
||||
@ -25,3 +25,14 @@ export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
||||
return getOrInsert(map, key, new Map<MK, MV>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but with delayed creation of the item
|
||||
*/
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, value: () => V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, value());
|
||||
}
|
||||
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
@ -8,8 +8,7 @@ import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||
import { runInAction } from "mobx";
|
||||
import updateExtensionsStateInjectable
|
||||
from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
|
||||
import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
|
||||
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
@ -24,7 +23,7 @@ jest.mock(
|
||||
() => ({
|
||||
ipcRenderer: {
|
||||
invoke: jest.fn(async (channel: string) => {
|
||||
if (channel === "extensions:main") {
|
||||
if (channel === "extension-loader:main:state") {
|
||||
return [
|
||||
[
|
||||
manifestPath,
|
||||
@ -61,7 +60,7 @@ jest.mock(
|
||||
}),
|
||||
on: jest.fn(
|
||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
if (channel === "extensions:main") {
|
||||
if (channel === "extension-loader:main:state") {
|
||||
// First initialize with extensions 1 and 2
|
||||
// and then broadcast event to remove extension 2 and add extension number 3
|
||||
setTimeout(() => {
|
||||
|
||||
@ -10,12 +10,7 @@ import fse from "fs-extra";
|
||||
import { makeObservable, observable, reaction, when } from "mobx";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import {
|
||||
broadcastMessage,
|
||||
ipcMainHandle,
|
||||
ipcRendererOn,
|
||||
requestMain,
|
||||
} from "../../common/ipc";
|
||||
import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc";
|
||||
import { toJS } from "../../common/utils";
|
||||
import logger from "../../main/logger";
|
||||
import type { ExtensionsStore } from "../extensions-store/extensions-store";
|
||||
@ -24,6 +19,8 @@ import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
|
||||
import { isProduction } from "../../common/vars";
|
||||
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
||||
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
@ -96,9 +93,6 @@ export class ExtensionDiscovery {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
|
||||
// IPC channel to broadcast changes to extension-discovery from main
|
||||
protected static readonly extensionDiscoveryChannel = "extension-discovery:main";
|
||||
|
||||
public events = new EventEmitter();
|
||||
|
||||
constructor(protected dependencies : Dependencies) {
|
||||
@ -141,14 +135,14 @@ export class ExtensionDiscovery {
|
||||
this.isLoaded = isLoaded;
|
||||
};
|
||||
|
||||
requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage);
|
||||
ipcRendererOn(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
|
||||
requestInitialExtensionDiscovery().then(onMessage);
|
||||
ipcRendererOn(extensionDiscoveryStateChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
|
||||
onMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
async initMain(): Promise<void> {
|
||||
ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
|
||||
ipcMainHandle(extensionDiscoveryStateChannel, () => this.toJSON());
|
||||
|
||||
reaction(() => this.toJSON(), () => {
|
||||
this.broadcast();
|
||||
@ -492,6 +486,6 @@ export class ExtensionDiscovery {
|
||||
}
|
||||
|
||||
broadcast(): void {
|
||||
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
|
||||
broadcastMessage(extensionDiscoveryStateChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import { EventEmitter } from "events";
|
||||
import { isEqual } from "lodash";
|
||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||
import path from "path";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
||||
import { Disposer, toJS } from "../../common/utils";
|
||||
import logger from "../../main/logger";
|
||||
import type { KubernetesCluster } from "../common-api/catalog";
|
||||
@ -17,6 +17,8 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ".
|
||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||
import * as registries from "../registries";
|
||||
import type { LensExtensionState } from "../extensions-store/extensions-store";
|
||||
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
||||
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
||||
|
||||
const logModule = "[EXTENSIONS-LOADER]";
|
||||
|
||||
@ -49,12 +51,6 @@ export class ExtensionLoader {
|
||||
*/
|
||||
protected instancesByName = observable.map<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
|
||||
private events = new EventEmitter();
|
||||
|
||||
@ -196,11 +192,11 @@ export class ExtensionLoader {
|
||||
this.isLoaded = true;
|
||||
this.loadOnMain();
|
||||
|
||||
ipcMainHandle(ExtensionLoader.extensionsMainChannel, () => {
|
||||
ipcMainHandle(extensionLoaderFromMainChannel, () => {
|
||||
return Array.from(this.toJSON());
|
||||
});
|
||||
|
||||
ipcMainOn(ExtensionLoader.extensionsRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.syncExtensions(extensions);
|
||||
});
|
||||
}
|
||||
@ -220,16 +216,16 @@ export class ExtensionLoader {
|
||||
});
|
||||
};
|
||||
|
||||
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
|
||||
ipcRendererOn(ExtensionLoader.extensionsMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
requestExtensionLoaderInitialState().then(extensionListHandler);
|
||||
ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
extensionListHandler(extensions);
|
||||
});
|
||||
}
|
||||
|
||||
broadcastExtensions() {
|
||||
const channel = ipcRenderer
|
||||
? ExtensionLoader.extensionsRendererChannel
|
||||
: ExtensionLoader.extensionsMainChannel;
|
||||
? extensionLoaderFromRendererChannel
|
||||
: extensionLoaderFromMainChannel;
|
||||
|
||||
broadcastMessage(channel, Array.from(this.extensions));
|
||||
}
|
||||
|
||||
@ -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 { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints/pods.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";
|
||||
|
||||
@ -6,7 +6,7 @@ import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-
|
||||
import createTerminalTabInjectable from "../../renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable";
|
||||
import terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable";
|
||||
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import logTabStoreInjectable from "../../renderer/components/dock/log-tab-store/log-tab-store.injectable";
|
||||
import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable";
|
||||
import { asLegacyGlobalSingletonForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api";
|
||||
import { TerminalStore as TerminalStoreClass } from "../../renderer/components/dock/terminal-store/terminal.store";
|
||||
|
||||
|
||||
@ -46,6 +46,8 @@ export type { ILocalKubeApiConfig, IRemoteKubeApiConfig, IKubeApiCluster } from
|
||||
export type { IPodContainer, IPodContainerStatus } from "../../common/k8s-api/endpoints";
|
||||
export type { ISecretRef } from "../../common/k8s-api/endpoints";
|
||||
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
|
||||
export type { EventStore } from "../../renderer/components/+events/event.store";
|
||||
|
||||
@ -70,8 +70,7 @@ describe("create clusters", () => {
|
||||
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
clusters: [{
|
||||
@ -93,9 +92,7 @@ describe("create clusters", () => {
|
||||
kind: "Config",
|
||||
preferences: {},
|
||||
}),
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
|
||||
@ -10,15 +10,15 @@ import "../common/catalog-entities/kubernetes-cluster";
|
||||
import { disposer, toJS } from "../common/utils";
|
||||
import { debounce } from "lodash";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
import { CatalogIpcEvents } from "../common/ipc/catalog";
|
||||
import { catalogInitChannel, catalogItemsChannel } from "../common/ipc/catalog";
|
||||
|
||||
const broadcaster = debounce((items: CatalogEntity[]) => {
|
||||
broadcastMessage(CatalogIpcEvents.ITEMS, items);
|
||||
broadcastMessage(catalogItemsChannel, items);
|
||||
}, 1_000, { leading: true, trailing: true });
|
||||
|
||||
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
|
||||
return disposer(
|
||||
ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))),
|
||||
ipcMainOn(catalogInitChannel, () => broadcaster(toJS(catalog.items))),
|
||||
reaction(() => toJS(catalog.items), (items) => {
|
||||
broadcaster(items);
|
||||
}, {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "../common/cluster-ipc";
|
||||
import "../common/ipc/cluster";
|
||||
import type http from "http";
|
||||
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||
import { Cluster } from "../common/cluster/cluster";
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
// Main process
|
||||
|
||||
import { injectSystemCAs } from "../common/system-ca";
|
||||
import { initialize as initializeRemote } from "@electron/remote/main";
|
||||
import * as Mobx from "mobx";
|
||||
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
||||
import * as LensExtensionsMainApi from "../extensions/main-api";
|
||||
@ -24,7 +23,7 @@ import type { InstalledExtension } from "../extensions/extension-discovery/exten
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { installDeveloperTools } from "./developer-tools";
|
||||
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { pushCatalogToRenderer } from "./catalog-pusher";
|
||||
@ -81,9 +80,6 @@ di.runSetups().then(() => {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
logger.debug("[APP-MAIN] initializing remote");
|
||||
initializeRemote();
|
||||
|
||||
logger.debug("[APP-MAIN] configuring packages");
|
||||
configurePackages();
|
||||
|
||||
@ -131,8 +127,6 @@ di.runSetups().then(() => {
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
await shellSync();
|
||||
|
||||
bindBroadcastHandlers();
|
||||
|
||||
powerMonitor.on("shutdown", () => app.exit());
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
@ -3,23 +3,27 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { BrowserWindow, dialog, IpcMainInvokeEvent, Menu } from "electron";
|
||||
import { BrowserWindow, IpcMainInvokeEvent, Menu } from "electron";
|
||||
import { clusterFrameMap } from "../../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/cluster-ipc";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { appEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
|
||||
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
|
||||
import { catalogEntityRegistry } from "../../catalog";
|
||||
import { pushCatalogToRenderer } from "../../catalog-pusher";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { IpcMainWindowEvents, WindowManager } from "../../window-manager";
|
||||
import { WindowManager } from "../../window-manager";
|
||||
import path from "path";
|
||||
import { remove } from "fs-extra";
|
||||
import { getAppMenu } from "../../menu/menu";
|
||||
import type { MenuRegistration } from "../../menu/menu-registration";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { onLocationChange, handleWindowAction } from "../../ipc/window";
|
||||
import { openFilePickingDialogChannel } from "../../../common/ipc/dialog";
|
||||
import { showOpenDialog } from "../../ipc/dialog";
|
||||
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window";
|
||||
|
||||
interface Dependencies {
|
||||
electronMenuItems: IComputedValue<MenuRegistration[]>,
|
||||
@ -136,21 +140,22 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => {
|
||||
await WindowManager.getInstance().ensureMainWindow();
|
||||
ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action));
|
||||
|
||||
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts);
|
||||
});
|
||||
ipcMainOn(windowLocationChangedChannel, () => onLocationChange());
|
||||
|
||||
ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => {
|
||||
ipcMainHandle(openFilePickingDialogChannel, (event, opts) => showOpenDialog(opts));
|
||||
|
||||
ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args));
|
||||
|
||||
ipcMainOn(windowOpenAppMenuAsContextMenuChannel, async (event) => {
|
||||
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
|
||||
const options = {
|
||||
|
||||
menu.popup({
|
||||
...BrowserWindow.fromWebContents(event.sender),
|
||||
// Center of the topbar menu icon
|
||||
x: 20,
|
||||
y: 20,
|
||||
} as Electron.PopupOptions;
|
||||
|
||||
menu.popup(options);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
12
src/main/ipc/dialog.ts
Normal file
12
src/main/ipc/dialog.ts
Normal 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
71
src/main/ipc/window.ts
Normal 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);
|
||||
}
|
||||
@ -8,17 +8,14 @@ import { makeObservable, observable } from "mobx";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { delay, iter, Singleton } from "../common/utils";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import logger from "./logger";
|
||||
import { isMac, productName } from "../common/vars";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
|
||||
export const enum IpcMainWindowEvents {
|
||||
OPEN_CONTEXT_MENU = "window:open-context-menu",
|
||||
}
|
||||
import { bundledExtensionsLoaded } from "../common/ipc/extension-handling";
|
||||
|
||||
function isHideable(window: BrowserWindow | null): boolean {
|
||||
return Boolean(window && !window.isDestroyed());
|
||||
@ -75,9 +72,9 @@ export class WindowManager extends Singleton {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
webviewTag: true,
|
||||
contextIsolation: false,
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainWindow);
|
||||
@ -135,7 +132,8 @@ export class WindowManager extends Singleton {
|
||||
|
||||
// Always disable Node.js integration for all webviews
|
||||
webPreferences.nodeIntegration = false;
|
||||
}).setWindowOpenHandler((details) => {
|
||||
})
|
||||
.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
|
||||
return { action: "deny" };
|
||||
@ -165,7 +163,7 @@ export class WindowManager extends Singleton {
|
||||
|
||||
if (!this.mainWindow) {
|
||||
viewHasLoaded = new Promise<void>(resolve => {
|
||||
ipcMain.once(BundledExtensionsLoaded, () => resolve());
|
||||
ipcMain.once(bundledExtensionsLoaded, () => resolve());
|
||||
});
|
||||
await this.initMainWindow(showSplash);
|
||||
}
|
||||
@ -249,9 +247,9 @@ export class WindowManager extends Singleton {
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
});
|
||||
await this.splashWindow.loadURL("static://splash.html");
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { computed, observable, makeObservable, action } from "mobx";
|
||||
import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc";
|
||||
import { ipcRendererOn } from "../../common/ipc";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||
import "../../common/catalog-entities";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
@ -14,7 +14,7 @@ import { once } from "lodash";
|
||||
import logger from "../../common/logger";
|
||||
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
||||
import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../common/ipc/catalog";
|
||||
import { navigate } from "../navigation";
|
||||
import { isMainFrame } from "process";
|
||||
|
||||
@ -79,12 +79,12 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
|
||||
init() {
|
||||
ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
ipcRendererOn(catalogItemsChannel, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
this.updateItems(items);
|
||||
});
|
||||
|
||||
// Make sure that we get items ASAP and not the next time one of them changes
|
||||
ipcRenderer.send(CatalogIpcEvents.INIT);
|
||||
ipcRenderer.send(catalogInitChannel);
|
||||
|
||||
if (isMainFrame) {
|
||||
ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
|
||||
|
||||
@ -4,14 +4,11 @@
|
||||
*/
|
||||
|
||||
.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 {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.entityName {
|
||||
@ -128,3 +125,10 @@
|
||||
.catalogAvatar {
|
||||
font-size: 1.2ch;
|
||||
}
|
||||
|
||||
.views {
|
||||
padding: calc(var(--padding) * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
@ -282,7 +282,7 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
||||
|
||||
return (
|
||||
<MainLayout sidebar={this.renderNavigation()}>
|
||||
<div className="p-6 h-full">
|
||||
<div className={styles.views}>
|
||||
{this.renderViews()}
|
||||
</div>
|
||||
{
|
||||
|
||||
@ -27,12 +27,11 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
|
||||
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog/install-from-select-file-dialog.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
interface Dependencies {
|
||||
@ -107,22 +106,15 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const Extensions = withInjectables<Dependencies>(
|
||||
NonInjectedExtensions,
|
||||
{
|
||||
getProps: (di) => ({
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installFromInput: di.inject(installFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
|
||||
installFromSelectFileDialog: di.inject(
|
||||
installFromSelectFileDialogInjectable,
|
||||
),
|
||||
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
getProps: (di) => ({
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installFromInput: di.inject(installFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -19,7 +19,7 @@ import { SubTitle } from "../layout/sub-title";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
||||
import { dialog } from "../../remote-helpers";
|
||||
import { requestOpenFilePickingDialog } from "../../ipc";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
onAddRepo: Function
|
||||
@ -73,7 +73,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
const { canceled, filePaths } = await requestOpenFilePickingDialog({
|
||||
defaultPath: this.getFilePath(type),
|
||||
properties: ["openFile", "showHiddenFiles"],
|
||||
message: `Select file`,
|
||||
|
||||
@ -10,8 +10,7 @@ import type { IToleration } from "../../../../common/k8s-api/workload-kube-objec
|
||||
import { PodTolerations } from "../pod-tolerations";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForLensLocalStorageInjectable
|
||||
from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
|
||||
@ -7,7 +7,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { catalogEntityRunListener } from "../../../common/ipc/catalog";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
|
||||
@ -8,8 +8,7 @@ import styles from "./cluster-status.module.scss";
|
||||
import { computed, observable, makeObservable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import { ipcRendererOn, requestMain } from "../../../common/ipc";
|
||||
import { ipcRendererOn } from "../../../common/ipc";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
@ -19,6 +18,7 @@ import { navigate } from "../../navigation";
|
||||
import { entitySettingsURL } from "../../../common/routes";
|
||||
import type { KubeAuthUpdate } from "../../../common/cluster-types";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { requestClusterActivation } from "../../ipc";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
@ -60,7 +60,7 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
this.isReconnecting = true;
|
||||
|
||||
try {
|
||||
await requestMain(clusterActivateHandler, this.cluster.id, true);
|
||||
await requestClusterActivation(this.cluster.id, true);
|
||||
} catch (error) {
|
||||
this.authOutput.push({
|
||||
message: error.toString(),
|
||||
|
||||
@ -12,11 +12,10 @@ import { ClusterStatus } from "./cluster-status";
|
||||
import { ClusterFrameHandler } from "./lens-views";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { navigate } from "../../navigation";
|
||||
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
||||
import { requestClusterActivation } from "../../ipc";
|
||||
|
||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
||||
}
|
||||
@ -58,7 +57,7 @@ export class ClusterView extends React.Component<Props> {
|
||||
reaction(() => this.clusterId, async (clusterId) => {
|
||||
ClusterFrameHandler.getInstance().setVisibleCluster(clusterId);
|
||||
ClusterFrameHandler.getInstance().initView(clusterId);
|
||||
requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main
|
||||
requestClusterActivation(clusterId, false); // activate and fetch cluster's state from main
|
||||
catalogEntityRegistry.activeEntity = clusterId;
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { action, IReactionDisposer, makeObservable, observable, when } from "mobx";
|
||||
import logger from "../../../main/logger";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
||||
import { clusterVisibilityHandler } from "../../../common/ipc/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { getClusterFrameUrl, Singleton } from "../../utils";
|
||||
|
||||
@ -12,8 +12,6 @@ import { Button } from "../button";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { saveKubeconfig } from "./save-config";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc";
|
||||
import { Notifications } from "../notifications";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
@ -21,6 +19,7 @@ import { Dialog } from "../dialog";
|
||||
import { Icon } from "../icon";
|
||||
import { Select } from "../select";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
|
||||
|
||||
type DialogState = {
|
||||
isOpen: boolean,
|
||||
@ -87,18 +86,18 @@ export class DeleteClusterDialog extends React.Component {
|
||||
async onDelete() {
|
||||
const { cluster, config } = dialogState;
|
||||
|
||||
await requestMain(clusterSetDeletingHandler, cluster.id);
|
||||
await requestSetClusterAsDeleting(cluster.id);
|
||||
this.removeContext();
|
||||
this.changeCurrentContext();
|
||||
|
||||
try {
|
||||
await saveKubeconfig(config, cluster.kubeConfigPath);
|
||||
HotbarStore.getInstance().removeAllHotbarItems(cluster.id);
|
||||
await requestMain(clusterDeleteHandler, cluster.id);
|
||||
await requestDeleteCluster(cluster.id);
|
||||
} catch(error) {
|
||||
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
|
||||
} finally {
|
||||
await requestMain(clusterClearDeletingHandler, cluster.id);
|
||||
await requestClearClusterAsDeleting(cluster.id);
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import * as selectEvent from "react-select-event";
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { LogResourceSelector } from "../log-resource-selector";
|
||||
import type { LogTabData } from "../log-tab-store/log-tab.store";
|
||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import mockFs from "mock-fs";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import callForLogsInjectable from "../log-store/call-for-logs/call-for-logs.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const getComponent = (tabData: LogTabData) => {
|
||||
return (
|
||||
<LogResourceSelector
|
||||
tabId="tabId"
|
||||
tabData={tabData}
|
||||
save={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getOnePodTabData = (): LogTabData => {
|
||||
const selectedPod = new Pod(dockerPod);
|
||||
|
||||
return {
|
||||
pods: [] as Pod[],
|
||||
selectedPod,
|
||||
selectedContainer: selectedPod.getContainers()[0],
|
||||
};
|
||||
};
|
||||
|
||||
const getFewPodsTabData = (): LogTabData => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const anotherPod = new Pod(dockerPod);
|
||||
|
||||
return {
|
||||
pods: [anotherPod],
|
||||
selectedPod,
|
||||
selectedContainer: selectedPod.getContainers()[0],
|
||||
};
|
||||
};
|
||||
|
||||
describe("<LogResourceSelector />", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const tabData = getOnePodTabData();
|
||||
const { container } = render(getComponent(tabData));
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("renders proper namespace", () => {
|
||||
const tabData = getOnePodTabData();
|
||||
const { getByTestId } = render(getComponent(tabData));
|
||||
const ns = getByTestId("namespace-badge");
|
||||
|
||||
expect(ns).toHaveTextContent("default");
|
||||
});
|
||||
|
||||
it("renders proper selected items within dropdowns", () => {
|
||||
const tabData = getOnePodTabData();
|
||||
const { getByText } = render(getComponent(tabData));
|
||||
|
||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
||||
expect(getByText("docker-exporter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders sibling pods in dropdown", () => {
|
||||
const tabData = getFewPodsTabData();
|
||||
const { container, getByText } = render(getComponent(tabData));
|
||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||
|
||||
selectEvent.openMenu(podSelector);
|
||||
|
||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
||||
expect(getByText("deploymentPod1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders sibling containers in dropdown", () => {
|
||||
const tabData = getFewPodsTabData();
|
||||
const { getByText, container } = render(getComponent(tabData));
|
||||
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
||||
|
||||
selectEvent.openMenu(containerSelector);
|
||||
|
||||
expect(getByText("node-exporter-1")).toBeInTheDocument();
|
||||
expect(getByText("init-node-exporter")).toBeInTheDocument();
|
||||
expect(getByText("init-node-exporter-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders pod owner as dropdown title", () => {
|
||||
const tabData = getFewPodsTabData();
|
||||
const { getByText, container } = render(getComponent(tabData));
|
||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||
|
||||
selectEvent.openMenu(podSelector);
|
||||
|
||||
expect(getByText("super-deployment")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -18,7 +18,7 @@ import { DockTabs } from "./dock-tabs";
|
||||
import { DockStore, DockTab, TabKind } from "./dock-store/dock.store";
|
||||
import { EditResource } from "./edit-resource";
|
||||
import { InstallChart } from "./install-chart";
|
||||
import { Logs } from "./logs";
|
||||
import { LogsDockTab } from "./logs/dock-tab";
|
||||
import { TerminalWindow } from "./terminal-window";
|
||||
import { UpgradeChart } from "./upgrade-chart";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
@ -85,7 +85,7 @@ class NonInjectedDock extends React.Component<Props & Dependencies> {
|
||||
case TabKind.UPGRADE_CHART:
|
||||
return <UpgradeChart tab={tab} />;
|
||||
case TabKind.POD_LOGS:
|
||||
return <Logs />;
|
||||
return <LogsDockTab tab={tab} />;
|
||||
case TabKind.TERMINAL:
|
||||
return <TerminalWindow tab={tab} />;
|
||||
}
|
||||
|
||||
@ -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 logStoreInjectable from "./log-store.injectable";
|
||||
|
||||
const reloadedLogStoreInjectable = getInjectable({
|
||||
instantiate: async (di) => {
|
||||
const nonReloadedStore = di.inject(logStoreInjectable);
|
||||
|
||||
await nonReloadedStore.reload();
|
||||
|
||||
return nonReloadedStore;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.transient,
|
||||
});
|
||||
|
||||
export default reloadedLogStoreInjectable;
|
||||
@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { LogResourceSelector } from "./log-resource-selector";
|
||||
import { LogList, NonInjectedLogList } from "./log-list";
|
||||
import { LogSearch } from "./log-search";
|
||||
import { LogControls } from "./log-controls";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { SearchStore } from "../../search-store/search-store";
|
||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
||||
import { Spinner } from "../spinner";
|
||||
import logsViewModelInjectable from "./logs/logs-view-model/logs-view-model.injectable";
|
||||
import type { LogsViewModel } from "./logs/logs-view-model/logs-view-model";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
searchStore: SearchStore
|
||||
model: LogsViewModel
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedLogs extends React.Component<Props & Dependencies> {
|
||||
private logListElement = React.createRef<NonInjectedLogList>(); // A reference for VirtualList component
|
||||
|
||||
get model() {
|
||||
return this.props.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolling to active overlay (search word highlight)
|
||||
*/
|
||||
@boundMethod
|
||||
scrollToOverlay() {
|
||||
const { activeOverlayLine } = this.props.searchStore;
|
||||
|
||||
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||
// Scroll vertically
|
||||
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
|
||||
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
||||
setTimeout(() => {
|
||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||
|
||||
if (!overlay) return;
|
||||
overlay.scrollIntoViewIfNeeded();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
renderResourceSelector() {
|
||||
const { tabs, logs, logsWithoutTimestamps, saveTab, tabId } = this.model;
|
||||
|
||||
if (!tabs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchLogs = tabs.showTimestamps ? logs : logsWithoutTimestamps;
|
||||
|
||||
const controls = (
|
||||
<div className="flex gaps">
|
||||
<LogResourceSelector
|
||||
tabId={tabId}
|
||||
tabData={tabs}
|
||||
save={saveTab}
|
||||
/>
|
||||
|
||||
<LogSearch
|
||||
onSearch={this.scrollToOverlay}
|
||||
logs={searchLogs}
|
||||
toPrevOverlay={this.scrollToOverlay}
|
||||
toNextOverlay={this.scrollToOverlay}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<InfoPanel
|
||||
tabId={this.model.tabId}
|
||||
controls={controls}
|
||||
showSubmitClose={false}
|
||||
showButtons={false}
|
||||
showStatusPanel={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { logs, tabs, tabId, saveTab } = this.model;
|
||||
|
||||
return (
|
||||
<div className="PodLogs flex column">
|
||||
{this.renderResourceSelector()}
|
||||
|
||||
<LogList
|
||||
logs={logs}
|
||||
id={tabId}
|
||||
ref={this.logListElement}
|
||||
/>
|
||||
|
||||
<LogControls
|
||||
logs={logs}
|
||||
tabData={tabs}
|
||||
save={saveTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Logs = withInjectables<Dependencies, Props>(
|
||||
NonInjectedLogs,
|
||||
|
||||
{
|
||||
|
||||
getPlaceholder: () => (
|
||||
<div className="flex box grow align-center justify-center">
|
||||
<Spinner center />
|
||||
</div>
|
||||
),
|
||||
|
||||
getProps: async (di, props) => ({
|
||||
searchStore: di.inject(searchStoreInjectable),
|
||||
model: await di.inject(logsViewModelInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import * as selectEvent from "react-select-event";
|
||||
import { Pod } from "../../../../../common/k8s-api/endpoints";
|
||||
import { LogResourceSelector } from "../resource-selector";
|
||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||
import { ThemeStore } from "../../../../theme.store";
|
||||
import { UserStore } from "../../../../../common/user-store";
|
||||
import mockFs from "mock-fs";
|
||||
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../../test-utils/renderFor";
|
||||
import { renderFor } from "../../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import callForLogsInjectable from "../call-for-logs.injectable";
|
||||
import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model";
|
||||
import type { TabId } from "../../dock-store/dock.store";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const getComponent = (model: LogTabViewModel) => (
|
||||
<LogResourceSelector model={model} />
|
||||
);
|
||||
|
||||
function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies>): LogTabViewModel {
|
||||
return new LogTabViewModel(tabId, {
|
||||
getLogs: jest.fn(),
|
||||
getLogsWithoutTimestamps: jest.fn(),
|
||||
getTimestampSplitLogs: jest.fn(),
|
||||
getLogTabData: jest.fn(),
|
||||
setLogTabData: jest.fn(),
|
||||
loadLogs: jest.fn(),
|
||||
reloadLogs: jest.fn(),
|
||||
updateTabName: jest.fn(),
|
||||
stopLoadingLogs: jest.fn(),
|
||||
...deps,
|
||||
});
|
||||
}
|
||||
|
||||
const getOnePodViewModel = (tabId: TabId): LogTabViewModel => {
|
||||
const selectedPod = new Pod(dockerPod);
|
||||
|
||||
return mockLogTabViewModel(tabId, {
|
||||
getLogTabData: () => ({
|
||||
pods: [selectedPod],
|
||||
selectedPod,
|
||||
selectedContainer: selectedPod.getContainers()[0],
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const getFewPodsTabData = (tabId: TabId): LogTabViewModel => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const anotherPod = new Pod(dockerPod);
|
||||
|
||||
return mockLogTabViewModel(tabId, {
|
||||
getLogTabData: () => ({
|
||||
pods: [selectedPod, anotherPod],
|
||||
selectedPod,
|
||||
selectedContainer: selectedPod.getContainers()[0],
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
describe("<LogResourceSelector />", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const model = getOnePodViewModel("foobar");
|
||||
const { container } = render(getComponent(model));
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("renders proper namespace", async () => {
|
||||
const model = getOnePodViewModel("foobar");
|
||||
const { findByTestId } = render(getComponent(model));
|
||||
const ns = await findByTestId("namespace-badge");
|
||||
|
||||
expect(ns).toHaveTextContent("default");
|
||||
});
|
||||
|
||||
it("renders proper selected items within dropdowns", async () => {
|
||||
const model = getOnePodViewModel("foobar");
|
||||
const { findByText } = render(getComponent(model));
|
||||
|
||||
expect(await findByText("dockerExporter")).toBeInTheDocument();
|
||||
expect(await findByText("docker-exporter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders sibling pods in dropdown", async () => {
|
||||
const model = getFewPodsTabData("foobar");
|
||||
const { container, findByText } = render(getComponent(model));
|
||||
|
||||
selectEvent.openMenu(container.querySelector(".pod-selector"));
|
||||
|
||||
expect(await findByText("dockerExporter", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
|
||||
expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders sibling containers in dropdown", async () => {
|
||||
const model = getFewPodsTabData("foobar");
|
||||
const { findByText, container } = render(getComponent(model));
|
||||
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
||||
|
||||
selectEvent.openMenu(containerSelector);
|
||||
|
||||
expect(await findByText("node-exporter-1")).toBeInTheDocument();
|
||||
expect(await findByText("init-node-exporter")).toBeInTheDocument();
|
||||
expect(await findByText("init-node-exporter-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders pod owner as dropdown title", async () => {
|
||||
const model = getFewPodsTabData("foobar");
|
||||
const { findByText, container } = render(getComponent(model));
|
||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||
|
||||
selectEvent.openMenu(podSelector);
|
||||
|
||||
expect(await findByText("super-deployment")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -3,19 +3,19 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { podsStore } from "../../../+workloads-pods/pods.store";
|
||||
import { UserStore } from "../../../../../common/user-store";
|
||||
import { Pod } from "../../../../../common/k8s-api/endpoints";
|
||||
import { ThemeStore } from "../../../../theme.store";
|
||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import type { DockStore } from "../dock-store/dock.store";
|
||||
import { mockWindow } from "../../../../../../__mocks__/windowMock";
|
||||
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||
import logTabStoreInjectable from "../tab-store.injectable";
|
||||
import type { LogTabStore } from "../tab.store";
|
||||
import dockStoreInjectable from "../../dock-store/dock-store.injectable";
|
||||
import type { DockStore } from "../../dock-store/dock.store";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
mockWindow();
|
||||
@ -6,7 +6,7 @@ import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { ToBottom } from "../to-bottom";
|
||||
import { noop } from "../../../utils";
|
||||
import { noop } from "../../../../utils";
|
||||
|
||||
describe("<ToBottom/>", () => {
|
||||
it("renders w/o errors", () => {
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { podsApi } from "../../../../../common/k8s-api/endpoints";
|
||||
import { podsApi } from "../../../../common/k8s-api/endpoints";
|
||||
|
||||
const callForLogsInjectable = getInjectable({
|
||||
instantiate: () => podsApi.getLogs,
|
||||
@ -3,53 +3,47 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./log-controls.scss";
|
||||
import "./controls.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
||||
import { cssNames, saveFileDialog } from "../../utils";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { Icon } from "../icon";
|
||||
import type { LogTabData } from "./log-tab-store/log-tab.store";
|
||||
import type { LogStore } from "./log-store/log.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { cssNames, saveFileDialog } from "../../../utils";
|
||||
import { Checkbox } from "../../checkbox";
|
||||
import { Icon } from "../../icon";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
|
||||
interface Props {
|
||||
tabData?: LogTabData
|
||||
logs: string[]
|
||||
save: (data: Partial<LogTabData>) => void
|
||||
export interface LogControlsProps {
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
logStore: LogStore
|
||||
}
|
||||
|
||||
const NonInjectedLogControls = observer((props: Props & Dependencies) => {
|
||||
const { tabData, save, logs, logStore } = props;
|
||||
export const LogControls = observer(({ model }: LogControlsProps) => {
|
||||
const tabData = model.logTabData.get();
|
||||
|
||||
if (!tabData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logs = model.timestampSplitLogs.get();
|
||||
const { showTimestamps, previous } = tabData;
|
||||
const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
|
||||
const since = logs.length ? logs[0][0] : null;
|
||||
const pod = new Pod(tabData.selectedPod);
|
||||
|
||||
const toggleTimestamps = () => {
|
||||
save({ showTimestamps: !showTimestamps });
|
||||
model.updateLogTabData({ showTimestamps: !showTimestamps });
|
||||
};
|
||||
|
||||
const togglePrevious = () => {
|
||||
save({ previous: !previous });
|
||||
logStore.reload();
|
||||
model.updateLogTabData({ previous: !previous });
|
||||
model.reloadLogs();
|
||||
};
|
||||
|
||||
const downloadLogs = () => {
|
||||
const fileName = pod.getName();
|
||||
const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
||||
const logsToDownload: string[] = showTimestamps
|
||||
? model.logs.get()
|
||||
: model.logsWithoutTimestamps.get();
|
||||
|
||||
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
||||
};
|
||||
@ -87,15 +81,3 @@ const NonInjectedLogControls = observer((props: Props & Dependencies) => {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LogControls = withInjectables<Dependencies, Props>(
|
||||
NonInjectedLogControls,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
logStore: di.inject(logStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
101
src/renderer/components/dock/logs/dock-tab.tsx
Normal file
101
src/renderer/components/dock/logs/dock-tab.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod } from "../../../utils";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import { LogResourceSelector } from "./resource-selector";
|
||||
import { LogList } from "./list";
|
||||
import { LogSearch } from "./search";
|
||||
import { LogControls } from "./controls";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import logsViewModelInjectable from "./logs-view-model.injectable";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
import type { DockTab } from "../dock-store/dock.store";
|
||||
|
||||
export interface LogsDockTabProps {
|
||||
className?: string;
|
||||
tab: DockTab;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedLogsDockTab extends React.Component<LogsDockTabProps & Dependencies> {
|
||||
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.model.reloadLogs();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.model.stopLoadingLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolling to active overlay (search word highlight)
|
||||
*/
|
||||
@boundMethod
|
||||
scrollToOverlay() {
|
||||
const { activeOverlayLine } = this.props.model.searchStore;
|
||||
|
||||
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||
// Scroll vertically
|
||||
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
|
||||
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
||||
setTimeout(() => {
|
||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||
|
||||
if (!overlay) return;
|
||||
overlay.scrollIntoViewIfNeeded();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { model, tab } = this.props;
|
||||
const { logTabData } = model;
|
||||
const data = logTabData.get();
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PodLogs flex column">
|
||||
<InfoPanel
|
||||
tabId={tab.id}
|
||||
controls={(
|
||||
<div className="flex gaps">
|
||||
<LogResourceSelector model={model} />
|
||||
<LogSearch
|
||||
onSearch={this.scrollToOverlay}
|
||||
model={model}
|
||||
toPrevOverlay={this.scrollToOverlay}
|
||||
toNextOverlay={this.scrollToOverlay}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
showSubmitClose={false}
|
||||
showButtons={false}
|
||||
showStatusPanel={false}
|
||||
/>
|
||||
<LogList model={model} ref={this.logListElement} />
|
||||
<LogControls model={model} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const LogsDockTab = withInjectables<Dependencies, LogsDockTabProps>(NonInjectedLogsDockTab, {
|
||||
getProps: (di, props) => ({
|
||||
model: di.inject(logsViewModelInjectable, {
|
||||
tabId: props.tab.id,
|
||||
}),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logTabStoreInjectable from "./tab-store.injectable";
|
||||
|
||||
const getLogTabDataInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logTabStoreInjectable).getData,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getLogTabDataInjectable;
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const getLogsWithoutTimestampsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).getLogsWithoutTimestampsByTabId,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getLogsWithoutTimestampsInjectable;
|
||||
13
src/renderer/components/dock/logs/get-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/get-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const getLogsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).getLogsByTabId,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getLogsInjectable;
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const getTimestampSplitLogsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).getTimestampSplitLogsByTabId,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getTimestampSplitLogsInjectable;
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./log-list.scss";
|
||||
import "./list.scss";
|
||||
|
||||
import React from "react";
|
||||
import AnsiUp from "ansi_up";
|
||||
@ -13,33 +13,21 @@ import { action, computed, observable, makeObservable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import moment from "moment-timezone";
|
||||
import type { Align, ListOnScrollProps } from "react-window";
|
||||
import { SearchStore } from "../../search-store/search-store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { array, boundMethod, cssNames } from "../../utils";
|
||||
import { VirtualList } from "../virtual-list";
|
||||
import type { LogStore } from "./log-store/log.store";
|
||||
import type { LogTabStore } from "./log-tab-store/log-tab.store";
|
||||
import { SearchStore } from "../../../search-store/search-store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { array, boundMethod, cssNames } from "../../../utils";
|
||||
import { VirtualList } from "../../virtual-list";
|
||||
import { ToBottom } from "./to-bottom";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
|
||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
||||
import type { LogTabViewModel } from "../logs/logs-view-model";
|
||||
|
||||
interface Props {
|
||||
logs: string[]
|
||||
id: string
|
||||
export interface LogListProps {
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
const colorConverter = new AnsiUp();
|
||||
|
||||
interface Dependencies {
|
||||
logTabStore: LogTabStore
|
||||
logStore: LogStore
|
||||
searchStore: SearchStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
export class LogList extends React.Component<LogListProps> {
|
||||
@observable isJumpButtonVisible = false;
|
||||
@observable isLastLineVisible = true;
|
||||
|
||||
@ -47,16 +35,18 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
||||
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
|
||||
|
||||
constructor(props: Props & Dependencies) {
|
||||
constructor(props: LogListProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.logs, this.onLogsInitialLoad),
|
||||
reaction(() => this.props.logs, this.onLogsUpdate),
|
||||
reaction(() => this.props.logs, this.onUserScrolledUp),
|
||||
reaction(() => this.props.model.logs.get(), (logs, prevLogs) => {
|
||||
this.onLogsInitialLoad(logs, prevLogs);
|
||||
this.onLogsUpdate();
|
||||
this.onUserScrolledUp(logs, prevLogs);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -85,7 +75,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
|
||||
if (newLogsAdded && scrolledToBeginning) {
|
||||
const firstLineContents = prevLogs[0];
|
||||
const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents);
|
||||
const lineToScroll = logs.findIndex((value) => value == firstLineContents);
|
||||
|
||||
if (lineToScroll !== -1) {
|
||||
this.scrollToItem(lineToScroll, "start");
|
||||
@ -97,15 +87,15 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
* Returns logs with or without timestamps regarding to showTimestamps prop
|
||||
*/
|
||||
@computed
|
||||
get logs() {
|
||||
const showTimestamps = this.props.logTabStore.getData(this.props.id)?.showTimestamps;
|
||||
get logs(): string[] {
|
||||
const { showTimestamps } = this.props.model.logTabData.get();
|
||||
|
||||
if (!showTimestamps) {
|
||||
return this.props.logStore.logsWithoutTimestamps;
|
||||
return this.props.model.logsWithoutTimestamps.get();
|
||||
}
|
||||
|
||||
return this.props.logs
|
||||
.map(log => this.props.logStore.splitOutTimestamp(log))
|
||||
return this.props.model.timestampSplitLogs
|
||||
.get()
|
||||
.map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
|
||||
}
|
||||
|
||||
@ -146,7 +136,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
const { scrollOffset } = props;
|
||||
|
||||
if (scrollOffset === 0) {
|
||||
this.props.logStore.load();
|
||||
this.props.model.loadLogs();
|
||||
}
|
||||
};
|
||||
|
||||
@ -177,7 +167,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
* @returns A react element with a row itself
|
||||
*/
|
||||
getLogRow = (rowIndex: number) => {
|
||||
const { searchQuery, isActiveOverlay } = this.props.searchStore;
|
||||
const { searchQuery, isActiveOverlay } = this.props.model.searchStore;
|
||||
const item = this.logs[rowIndex];
|
||||
const contents: React.ReactElement[] = [];
|
||||
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
||||
@ -250,17 +240,3 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const LogList = withInjectables<Dependencies, Props>(
|
||||
NonInjectedLogList,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
logTabStore: di.inject(logTabStoreInjectable),
|
||||
logStore: di.inject(logStoreInjectable),
|
||||
searchStore: di.inject(searchStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
13
src/renderer/components/dock/logs/load-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/load-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const loadLogsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).load,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default loadLogsInjectable;
|
||||
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { LogTabViewModel } from "./logs-view-model";
|
||||
import type { TabId } from "../dock-store/dock.store";
|
||||
import getLogsInjectable from "./get-logs.injectable";
|
||||
import getLogsWithoutTimestampsInjectable from "./get-logs-without-timestamps.injectable";
|
||||
import getTimestampSplitLogsInjectable from "./get-timestamp-split-logs.injectable";
|
||||
import reloadLoadsInjectable from "./reload-logs.injectable";
|
||||
import getLogTabDataInjectable from "./get-log-tab-data.injectable";
|
||||
import loadLogsInjectable from "./load-logs.injectable";
|
||||
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
|
||||
import updateTabNameInjectable from "./update-tab-name.injectable";
|
||||
import stopLoadingLogsInjectable from "./stop-loading-logs.injectable";
|
||||
|
||||
export interface InstantiateArgs {
|
||||
tabId: TabId;
|
||||
}
|
||||
|
||||
const logsViewModelInjectable = getInjectable({
|
||||
instantiate: (di, { tabId }: InstantiateArgs) => new LogTabViewModel(tabId, {
|
||||
getLogs: di.inject(getLogsInjectable),
|
||||
getLogsWithoutTimestamps: di.inject(getLogsWithoutTimestampsInjectable),
|
||||
getTimestampSplitLogs: di.inject(getTimestampSplitLogsInjectable),
|
||||
reloadLogs: di.inject(reloadLoadsInjectable),
|
||||
getLogTabData: di.inject(getLogTabDataInjectable),
|
||||
setLogTabData: di.inject(setLogTabDataInjectable),
|
||||
loadLogs: di.inject(loadLogsInjectable),
|
||||
updateTabName: di.inject(updateTabNameInjectable),
|
||||
stopLoadingLogs: di.inject(stopLoadingLogsInjectable),
|
||||
}),
|
||||
lifecycle: lifecycleEnum.transient,
|
||||
});
|
||||
|
||||
export default logsViewModelInjectable;
|
||||
39
src/renderer/components/dock/logs/logs-view-model.ts
Normal file
39
src/renderer/components/dock/logs/logs-view-model.ts
Normal 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 type { LogTabData } from "./tab.store";
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import type { TabId } from "../dock-store/dock.store";
|
||||
import { SearchStore } from "../../../search-store/search-store";
|
||||
|
||||
export interface LogTabViewModelDependencies {
|
||||
getLogs: (tabId: TabId) => string[];
|
||||
getLogsWithoutTimestamps: (tabId: TabId) => string[];
|
||||
getTimestampSplitLogs: (tabId: TabId) => [string, string][];
|
||||
getLogTabData: (tabId: TabId) => LogTabData;
|
||||
setLogTabData: (tabId: TabId, data: LogTabData) => void;
|
||||
loadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||
reloadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||
updateTabName: (tabId: TabId) => void;
|
||||
stopLoadingLogs: (tabId: TabId) => void;
|
||||
}
|
||||
|
||||
export class LogTabViewModel {
|
||||
constructor(protected readonly tabId: TabId, private readonly dependencies: LogTabViewModelDependencies) {}
|
||||
|
||||
readonly logs = computed(() => this.dependencies.getLogs(this.tabId));
|
||||
readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId));
|
||||
readonly timestampSplitLogs = computed(() => this.dependencies.getTimestampSplitLogs(this.tabId));
|
||||
readonly logTabData = computed(() => this.dependencies.getLogTabData(this.tabId));
|
||||
readonly searchStore = new SearchStore();
|
||||
|
||||
updateLogTabData = (partialData: Partial<LogTabData>) => {
|
||||
this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData });
|
||||
};
|
||||
|
||||
loadLogs = () => this.dependencies.loadLogs(this.tabId, this.logTabData);
|
||||
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.logTabData);
|
||||
updateTabName = () => this.dependencies.updateTabName(this.tabId);
|
||||
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../../dock-store/dock-store.injectable";
|
||||
import logTabStoreInjectable from "../../log-tab-store/log-tab-store.injectable";
|
||||
import reloadedLogStoreInjectable from "../../log-store/reloaded-log-store.injectable";
|
||||
import { LogsViewModel } from "./logs-view-model";
|
||||
|
||||
const logsViewModelInjectable = getInjectable({
|
||||
instantiate: async (di) => new LogsViewModel({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
logTabStore: di.inject(logTabStoreInjectable),
|
||||
logStore: await di.inject(reloadedLogStoreInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default logsViewModelInjectable;
|
||||
@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LogTabData, LogTabStore } from "../../log-tab-store/log-tab.store";
|
||||
import type { LogStore } from "../../log-store/log.store";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: { selectedTabId: string },
|
||||
logTabStore: LogTabStore
|
||||
logStore: LogStore
|
||||
}
|
||||
|
||||
export class LogsViewModel {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
makeObservable(this, {
|
||||
logs: computed,
|
||||
logsWithoutTimestamps: computed,
|
||||
tabs: computed,
|
||||
tabId: computed,
|
||||
});
|
||||
}
|
||||
|
||||
get logs() {
|
||||
return this.dependencies.logStore.logs;
|
||||
}
|
||||
|
||||
get logsWithoutTimestamps() {
|
||||
return this.dependencies.logStore.logsWithoutTimestamps;
|
||||
}
|
||||
|
||||
get tabs() {
|
||||
return this.dependencies.logTabStore.tabs;
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.dependencies.dockStore.selectedTabId;
|
||||
}
|
||||
|
||||
saveTab = (newTabs: LogTabData) => {
|
||||
this.dependencies.logTabStore.setData(this.tabId, { ...this.tabs, ...newTabs });
|
||||
};
|
||||
}
|
||||
13
src/renderer/components/dock/logs/reload-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/reload-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const reloadLoadsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).reload,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default reloadLoadsInjectable;
|
||||
@ -3,55 +3,48 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./log-resource-selector.scss";
|
||||
import "./resource-selector.scss";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
||||
import { Badge } from "../badge";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import type { TabId } from "./dock-store/dock.store";
|
||||
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { Badge } from "../../badge";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
|
||||
interface Props {
|
||||
tabId: TabId
|
||||
tabData: LogTabData
|
||||
save: (data: Partial<LogTabData>) => void
|
||||
export interface LogResourceSelectorProps {
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
logTabStore: LogTabStore
|
||||
reloadLogs: () => Promise<void>
|
||||
}
|
||||
export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps) => {
|
||||
const tabData = model.logTabData.get();
|
||||
|
||||
if (!tabData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => {
|
||||
const { tabData, save, tabId, logTabStore, reloadLogs } = props;
|
||||
const { selectedPod, selectedContainer, pods } = tabData;
|
||||
const pod = new Pod(selectedPod);
|
||||
const containers = pod.getContainers();
|
||||
const initContainers = pod.getInitContainers();
|
||||
|
||||
const onContainerChange = (option: SelectOption) => {
|
||||
save({
|
||||
model.updateLogTabData({
|
||||
selectedContainer: containers
|
||||
.concat(initContainers)
|
||||
.find(container => container.name === option.value),
|
||||
});
|
||||
|
||||
reloadLogs();
|
||||
model.reloadLogs();
|
||||
};
|
||||
|
||||
const onPodChange = (option: SelectOption) => {
|
||||
const selectedPod = podsStore.getByName(option.value, pod.getNs());
|
||||
|
||||
save({ selectedPod });
|
||||
|
||||
logTabStore.renameTab(tabId);
|
||||
model.updateLogTabData({ selectedPod });
|
||||
model.updateTabName();
|
||||
};
|
||||
|
||||
const getSelectOptions = (items: string[]) => {
|
||||
@ -82,7 +75,7 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
reloadLogs();
|
||||
model.reloadLogs();
|
||||
}, [selectedPod]);
|
||||
|
||||
return (
|
||||
@ -95,6 +88,7 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
||||
onChange={onPodChange}
|
||||
autoConvertOptions={false}
|
||||
className="pod-selector"
|
||||
menuClass="pod-selector-menu"
|
||||
/>
|
||||
<span>Container</span>
|
||||
<Select
|
||||
@ -103,20 +97,9 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
||||
onChange={onContainerChange}
|
||||
autoConvertOptions={false}
|
||||
className="container-selector"
|
||||
menuClass="container-selector-menu"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LogResourceSelector = withInjectables<Dependencies, Props>(
|
||||
NonInjectedLogResourceSelector,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
logTabStore: di.inject(logTabStoreInjectable),
|
||||
reloadLogs: di.inject(logStoreInjectable).reload,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -3,33 +3,33 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./log-search.scss";
|
||||
import "./search.scss";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SearchInput } from "../input";
|
||||
import type { SearchStore } from "../../search-store/search-store";
|
||||
import { Icon } from "../icon";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
||||
import { SearchInput } from "../../input";
|
||||
import { Icon } from "../../icon";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
|
||||
export interface PodLogSearchProps {
|
||||
onSearch: (query: string) => void
|
||||
toPrevOverlay: () => void
|
||||
toNextOverlay: () => void
|
||||
onSearch: (query: string) => void;
|
||||
toPrevOverlay: () => void;
|
||||
toNextOverlay: () => void;
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
interface Props extends PodLogSearchProps {
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
searchStore: SearchStore
|
||||
}
|
||||
export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, model }: PodLogSearchProps) => {
|
||||
const tabData = model.logTabData.get();
|
||||
|
||||
const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
||||
const { logs, onSearch, toPrevOverlay, toNextOverlay, searchStore } = props;
|
||||
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
|
||||
if (!tabData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logs = tabData.showTimestamps
|
||||
? model.logs.get()
|
||||
: model.logsWithoutTimestamps.get();
|
||||
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = model.searchStore;
|
||||
const jumpDisabled = !searchQuery || !occurrences.length;
|
||||
const findCounts = (
|
||||
<div className="find-count">
|
||||
@ -38,7 +38,7 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
||||
);
|
||||
|
||||
const setSearch = (query: string) => {
|
||||
searchStore.onSearch(logs, query);
|
||||
model.searchStore.onSearch(logs, query);
|
||||
onSearch(query);
|
||||
};
|
||||
|
||||
@ -64,7 +64,7 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh search when logs changed
|
||||
searchStore.onSearch(logs);
|
||||
model.searchStore.onSearch(logs);
|
||||
}, [logs]);
|
||||
|
||||
return (
|
||||
@ -92,14 +92,3 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LogSearch = withInjectables<Dependencies, Props>(
|
||||
NonInjectedLogSearch,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
searchStore: di.inject(searchStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logTabStoreInjectable from "./tab-store.injectable";
|
||||
|
||||
const setLogTabDataInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logTabStoreInjectable).setData,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default setLogTabDataInjectable;
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logStoreInjectable from "./store.injectable";
|
||||
|
||||
const stopLoadingLogsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logStoreInjectable).stopLoadingLogs,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default stopLoadingLogsInjectable;
|
||||
@ -3,15 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { LogStore } from "./log.store";
|
||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import callForLogsInjectable from "./call-for-logs/call-for-logs.injectable";
|
||||
import { LogStore } from "./store";
|
||||
import callForLogsInjectable from "./call-for-logs.injectable";
|
||||
|
||||
const logStoreInjectable = getInjectable({
|
||||
instantiate: (di) => new LogStore({
|
||||
logTabStore: di.inject(logTabStoreInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
callForLogs: di.inject(callForLogsInjectable),
|
||||
}),
|
||||
|
||||
@ -3,46 +3,28 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autorun, computed, observable, makeObservable } from "mobx";
|
||||
import { computed, observable, makeObservable, IComputedValue } from "mobx";
|
||||
|
||||
import { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { autoBind, interval } from "../../../utils";
|
||||
import { DockStore, TabId, TabKind } from "../dock-store/dock.store";
|
||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
||||
import { autoBind, getOrInsertWith, interval, IntervalFn } from "../../../utils";
|
||||
import type { TabId } from "../dock-store/dock.store";
|
||||
import type { LogTabData } from "./tab.store";
|
||||
|
||||
type PodLogLine = string;
|
||||
|
||||
const logLinesToLoad = 500;
|
||||
|
||||
interface Dependencies {
|
||||
logTabStore: LogTabStore
|
||||
dockStore: DockStore
|
||||
callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string>
|
||||
}
|
||||
|
||||
export class LogStore {
|
||||
private refresher = interval(10, () => {
|
||||
const id = this.dependencies.dockStore.selectedTabId;
|
||||
|
||||
if (!this.podLogs.get(id)) return;
|
||||
this.loadMore(id);
|
||||
});
|
||||
|
||||
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
||||
@observable protected podLogs = observable.map<TabId, PodLogLine[]>();
|
||||
protected refreshers = new Map<TabId, IntervalFn>();
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = this.dependencies.dockStore;
|
||||
|
||||
if (selectedTab?.kind === TabKind.POD_LOGS && isOpen) {
|
||||
this.refresher.start();
|
||||
} else {
|
||||
this.refresher.stop();
|
||||
}
|
||||
}, { delay: 500 });
|
||||
}
|
||||
|
||||
handlerError(tabId: TabId, error: any): void {
|
||||
@ -55,7 +37,7 @@ export class LogStore {
|
||||
`Reason: ${error.reason} (${error.code})`,
|
||||
];
|
||||
|
||||
this.refresher.stop();
|
||||
this.stopLoadingLogs(tabId);
|
||||
this.podLogs.set(tabId, message);
|
||||
}
|
||||
|
||||
@ -65,35 +47,51 @@ export class LogStore {
|
||||
* Also, it handles loading errors, rewriting whole logs with error
|
||||
* messages
|
||||
*/
|
||||
load = async () => {
|
||||
const tabId = this.dependencies.dockStore.selectedTabId;
|
||||
|
||||
load = async (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||
try {
|
||||
const logs = await this.loadLogs(tabId, {
|
||||
tailLines: this.lines + logLinesToLoad,
|
||||
const logs = await this.loadLogs(logTabData, {
|
||||
tailLines: this.getLinesByTabId(tabId) + logLinesToLoad,
|
||||
});
|
||||
|
||||
this.refresher.start();
|
||||
this.getRefresher(tabId, logTabData).start();
|
||||
this.podLogs.set(tabId, logs);
|
||||
} catch (error) {
|
||||
this.handlerError(tabId, error);
|
||||
}
|
||||
};
|
||||
|
||||
private getRefresher(tabId: TabId, logTabData: IComputedValue<LogTabData>): IntervalFn {
|
||||
return getOrInsertWith(this.refreshers, tabId, () => (
|
||||
interval(10, () => {
|
||||
if (this.podLogs.has(tabId)) {
|
||||
this.loadMore(tabId, logTabData);
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop loading more logs for a given tab
|
||||
* @param tabId The ID of the logs tab to stop loading more logs for
|
||||
*/
|
||||
public stopLoadingLogs(tabId: TabId): void {
|
||||
this.refreshers.get(tabId)?.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function is used to refresher/stream-like requests.
|
||||
* It changes 'sinceTime' param each time allowing to fetch logs
|
||||
* starting from last line received.
|
||||
* @param tabId
|
||||
*/
|
||||
loadMore = async (tabId: TabId) => {
|
||||
loadMore = async (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||
if (!this.podLogs.get(tabId).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldLogs = this.podLogs.get(tabId);
|
||||
const logs = await this.loadLogs(tabId, {
|
||||
const logs = await this.loadLogs(logTabData, {
|
||||
sinceTime: this.getLastSinceTime(tabId),
|
||||
});
|
||||
|
||||
@ -111,11 +109,9 @@ export class LogStore {
|
||||
* @param params request parameters described in IPodLogsQuery interface
|
||||
* @returns A fetch request promise
|
||||
*/
|
||||
async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||
const data = this.dependencies.logTabStore.getData(tabId);
|
||||
|
||||
const { selectedContainer, previous } = data;
|
||||
const pod = new Pod(data.selectedPod);
|
||||
private async loadLogs(logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||
const { selectedContainer, previous, selectedPod } = logTabData.get();
|
||||
const pod = new Pod(selectedPod);
|
||||
const namespace = pod.getNs();
|
||||
const name = pod.getName();
|
||||
|
||||
@ -130,6 +126,7 @@ export class LogStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This depends on dockStore, which should be removed
|
||||
* Converts logs into a string array
|
||||
* @returns Length of log lines
|
||||
*/
|
||||
@ -138,21 +135,36 @@ export class LogStore {
|
||||
return this.logs.length;
|
||||
}
|
||||
|
||||
public getLinesByTabId = (tabId: TabId): number => {
|
||||
return this.getLogsByTabId(tabId).length;
|
||||
};
|
||||
|
||||
public getLogsByTabId = (tabId: TabId): string[] => {
|
||||
return this.podLogs.get(tabId) ?? [];
|
||||
};
|
||||
|
||||
public getLogsWithoutTimestampsByTabId = (tabId: TabId): string[] => {
|
||||
return this.getLogsByTabId(tabId).map(this.removeTimestamps);
|
||||
};
|
||||
|
||||
public getTimestampSplitLogsByTabId = (tabId: TabId): [string, string][] => {
|
||||
return this.getLogsByTabId(tabId).map(this.splitOutTimestamp);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated This now only returns the empty array
|
||||
* Returns logs with timestamps for selected tab
|
||||
*/
|
||||
@computed
|
||||
get logs() {
|
||||
return this.podLogs.get(this.dependencies.dockStore.selectedTabId) ?? [];
|
||||
get logs(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This now only returns the empty array
|
||||
* Removes timestamps from each log line and returns changed logs
|
||||
* @returns Logs without timestamps
|
||||
*/
|
||||
@computed
|
||||
get logsWithoutTimestamps() {
|
||||
get logsWithoutTimestamps(): string[] {
|
||||
return this.logs.map(item => this.removeTimestamps(item));
|
||||
}
|
||||
|
||||
@ -171,7 +183,7 @@ export class LogStore {
|
||||
return stamp.toISOString();
|
||||
}
|
||||
|
||||
splitOutTimestamp(logs: string): [string, string] {
|
||||
splitOutTimestamp = (logs: string): [string, string] => {
|
||||
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
|
||||
|
||||
if (!extraction || extraction.length < 3) {
|
||||
@ -179,23 +191,23 @@ export class LogStore {
|
||||
}
|
||||
|
||||
return [extraction[1], extraction[2]];
|
||||
}
|
||||
};
|
||||
|
||||
getTimestamps(logs: string) {
|
||||
return logs.match(/^\d+\S+/gm);
|
||||
}
|
||||
|
||||
removeTimestamps(logs: string) {
|
||||
removeTimestamps = (logs: string) => {
|
||||
return logs.replace(/^\d+.*?\s/gm, "");
|
||||
}
|
||||
};
|
||||
|
||||
clearLogs(tabId: TabId) {
|
||||
this.podLogs.delete(tabId);
|
||||
}
|
||||
|
||||
reload = async () => {
|
||||
this.clearLogs(this.dependencies.dockStore.selectedTabId);
|
||||
reload = (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||
this.clearLogs(tabId);
|
||||
|
||||
await this.load();
|
||||
return this.load(tabId, logTabData);
|
||||
};
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { LogTabStore } from "./log-tab.store";
|
||||
import { LogTabStore } from "./tab.store";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { computed, makeObservable, reaction } from "mobx";
|
||||
import { reaction } from "mobx";
|
||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||
|
||||
import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
|
||||
@ -43,14 +43,6 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
});
|
||||
|
||||
reaction(() => podsStore.items.length, () => this.updateTabsData());
|
||||
|
||||
makeObservable(this, {
|
||||
tabs: computed,
|
||||
});
|
||||
}
|
||||
|
||||
get tabs() {
|
||||
return this.data.get(this.dependencies.dockStore.selectedTabId);
|
||||
}
|
||||
|
||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
|
||||
@ -81,7 +73,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
});
|
||||
}
|
||||
|
||||
renameTab(tabId: string) {
|
||||
updateTabName(tabId: string) {
|
||||
const { selectedPod } = this.getData(tabId);
|
||||
|
||||
this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
|
||||
@ -128,7 +120,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
pods,
|
||||
});
|
||||
|
||||
this.renameTab(tabId);
|
||||
this.updateTabName(tabId);
|
||||
} else {
|
||||
this.closeTab(tabId);
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
export function ToBottom({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import logTabStoreInjectable from "./tab-store.injectable";
|
||||
|
||||
const updateTabNameInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(logTabStoreInjectable).updateTabName,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default updateTabNameInjectable;
|
||||
@ -353,6 +353,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
dirty: _dirty, // excluded from passing to input-element
|
||||
defaultValue,
|
||||
trim,
|
||||
blurOnEnter,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
const { focused, dirty, valid, validating, errors } = this.state;
|
||||
|
||||
@ -104,7 +104,6 @@ class NonInjectedKubeObjectListLayout<K extends KubeObject> extends React.Compon
|
||||
|
||||
return (
|
||||
<ItemListLayout
|
||||
{...layoutProps}
|
||||
className={cssNames("KubeObjectListLayout", className)}
|
||||
store={store}
|
||||
items={items}
|
||||
@ -133,6 +132,7 @@ class NonInjectedKubeObjectListLayout<K extends KubeObject> extends React.Compon
|
||||
...[customizeHeader].flat(),
|
||||
]}
|
||||
renderItemMenu={item => <KubeObjectMenu object={item} />}
|
||||
{...layoutProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
.contents {
|
||||
grid-area: contents;
|
||||
overflow: auto;
|
||||
height: calc(100vh - var(--bottom-bar-height) - var(--main-layout-header));
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@ -7,18 +7,17 @@ import React from "react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { TopBar } from "./top-bar";
|
||||
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||
import { broadcastMessage } from "../../../../common/ipc";
|
||||
import * as vars from "../../../../common/vars";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
|
||||
|
||||
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
|
||||
|
||||
jest.mock("../../../../common/ipc");
|
||||
jest.mock("../../../ipc");
|
||||
|
||||
jest.mock("../../../../common/vars", () => {
|
||||
const SemVer = require("semver").SemVer;
|
||||
@ -33,23 +32,6 @@ jest.mock("../../../../common/vars", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const mockMinimize = jest.fn();
|
||||
const mockMaximize = jest.fn();
|
||||
const mockUnmaximize = jest.fn();
|
||||
const mockClose = jest.fn();
|
||||
|
||||
jest.mock("@electron/remote", () => {
|
||||
return {
|
||||
getCurrentWindow: () => ({
|
||||
minimize: () => mockMinimize(),
|
||||
maximize: () => mockMaximize(),
|
||||
unmaximize: () => mockUnmaximize(),
|
||||
close: () => mockClose(),
|
||||
isMaximized: () => false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<TopBar/> in Windows and Linux", () => {
|
||||
let render: DiRender;
|
||||
|
||||
@ -104,15 +86,15 @@ describe("<TopBar/> in Windows and Linux", () => {
|
||||
const close = getByTestId("window-close");
|
||||
|
||||
fireEvent.click(menu);
|
||||
expect(broadcastMessage).toHaveBeenCalledWith(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
|
||||
expect(emitOpenAppMenuAsContextMenu).toHaveBeenCalledWith();
|
||||
|
||||
fireEvent.click(minimize);
|
||||
expect(mockMinimize).toHaveBeenCalled();
|
||||
expect(requestWindowAction).toHaveBeenCalledWith("minimize");
|
||||
|
||||
fireEvent.click(maximize);
|
||||
expect(mockMaximize).toHaveBeenCalled();
|
||||
expect(requestWindowAction).toHaveBeenCalledWith("toggle-maximize");
|
||||
|
||||
fireEvent.click(close);
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(requestWindowAction).toHaveBeenCalledWith("close");
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,8 +12,7 @@ import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injec
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||
import { computed } from "mobx";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.mock("../../../../common/vars", () => {
|
||||
@ -27,6 +26,9 @@ jest.mock("../../../../common/vars", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const goBack = jest.fn();
|
||||
const goForward = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
"electron",
|
||||
() => ({
|
||||
@ -42,6 +44,25 @@ jest.mock(
|
||||
}
|
||||
},
|
||||
),
|
||||
invoke: jest.fn(
|
||||
(channel: string, action: string) => {
|
||||
console.log("channel", channel, action);
|
||||
|
||||
if (channel !== "window:window-action") return;
|
||||
|
||||
switch(action) {
|
||||
case "back": {
|
||||
goBack();
|
||||
break;
|
||||
}
|
||||
|
||||
case "forward": {
|
||||
goForward();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@ -50,24 +71,6 @@ jest.mock("../../+catalog", () => ({
|
||||
previousActiveTab: jest.fn(),
|
||||
}));
|
||||
|
||||
const goBack = jest.fn();
|
||||
const goForward = jest.fn();
|
||||
|
||||
jest.mock("@electron/remote", () => {
|
||||
return {
|
||||
webContents: {
|
||||
getAllWebContents: () => {
|
||||
return [{
|
||||
getType: () => "window",
|
||||
goBack,
|
||||
goForward,
|
||||
}];
|
||||
},
|
||||
},
|
||||
getCurrentWindow: () => jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<TopBar/>", () => {
|
||||
let di: ConfigurableDependencyInjectionContainer;
|
||||
let render: DiRender;
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
*/
|
||||
|
||||
import styles from "./top-bar.module.scss";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { Icon } from "../../icon";
|
||||
import { webContents, getCurrentWindow } from "@electron/remote";
|
||||
import { observable } from "mobx";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc";
|
||||
import { ipcRendererOn } from "../../../../common/ipc";
|
||||
import { watchHistoryState } from "../../../remote-helpers/history-updater";
|
||||
import { isActiveRoute, navigate } from "../../../navigation";
|
||||
import { catalogRoute, catalogURL } from "../../../../common/routes";
|
||||
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||
import { isLinux, isWindows } from "../../../../common/vars";
|
||||
import { cssNames } from "../../../utils";
|
||||
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { TopBarRegistration } from "./top-bar-registration";
|
||||
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
|
||||
import { WindowAction } from "../../../../common/ipc/window";
|
||||
|
||||
interface Props extends React.HTMLAttributes<any> {}
|
||||
|
||||
@ -40,10 +40,9 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
|
||||
|
||||
const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
|
||||
const elem = useRef<HTMLDivElement>();
|
||||
const window = useMemo(() => getCurrentWindow(), []);
|
||||
|
||||
const openContextMenu = () => {
|
||||
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
|
||||
const openAppContextMenu = () => {
|
||||
emitOpenAppMenuAsContextMenu();
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
@ -51,11 +50,11 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goBack();
|
||||
requestWindowAction(WindowAction.GO_BACK);
|
||||
};
|
||||
|
||||
const goForward = () => {
|
||||
webContents.getAllWebContents().find((webContent) => webContent.getType() === "window")?.goForward();
|
||||
requestWindowAction(WindowAction.GO_FORWARD);
|
||||
};
|
||||
|
||||
const windowSizeToggle = (evt: React.MouseEvent) => {
|
||||
@ -68,34 +67,30 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
|
||||
};
|
||||
|
||||
const minimizeWindow = () => {
|
||||
window.minimize();
|
||||
requestWindowAction(WindowAction.MINIMIZE);
|
||||
};
|
||||
|
||||
const toggleMaximize = () => {
|
||||
if (window.isMaximized()) {
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window.maximize();
|
||||
}
|
||||
requestWindowAction(WindowAction.TOGGLE_MAXIMIZE);
|
||||
};
|
||||
|
||||
const closeWindow = () => {
|
||||
window.close();
|
||||
requestWindowAction(WindowAction.CLOSE);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = watchHistoryState();
|
||||
|
||||
return () => disposer();
|
||||
}, []);
|
||||
useEffect(() => watchHistoryState(), []);
|
||||
|
||||
return (
|
||||
<div className={styles.topBar} onDoubleClick={windowSizeToggle} ref={elem} {...rest}>
|
||||
<div className={styles.tools}>
|
||||
{(isWindows || isLinux) && (
|
||||
<div className={styles.winMenu}>
|
||||
<div onClick={openContextMenu} 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -127,12 +122,19 @@ const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies)
|
||||
{(isWindows || isLinux) && (
|
||||
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
|
||||
<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}>
|
||||
<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 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>
|
||||
)}
|
||||
@ -162,7 +164,6 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => (
|
||||
export const TopBar = withInjectables(observer(NonInjectedTopBar), {
|
||||
getProps: (di, props) => ({
|
||||
items: di.inject(topBarItemsInjectable),
|
||||
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* 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 {
|
||||
(value: string): void;
|
||||
@ -10,9 +10,9 @@ export interface MonacoValidator {
|
||||
|
||||
export function yamlValidator(value: string) {
|
||||
try {
|
||||
yaml.load(value);
|
||||
yaml.loadAll(value);
|
||||
} catch (error) {
|
||||
throw String(error as YAMLException);
|
||||
throw String(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { FileFilter, OpenDialogOptions, remote } from "electron";
|
||||
import type { FileFilter, OpenDialogOptions } from "electron";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { requestOpenFilePickingDialog } from "../../ipc";
|
||||
|
||||
export interface PathPickOpts {
|
||||
label: string;
|
||||
@ -29,8 +30,8 @@ export interface PathPickerProps extends PathPickOpts {
|
||||
export class PathPicker extends React.Component<PathPickerProps> {
|
||||
static async pick(opts: PathPickOpts) {
|
||||
const { onPick, onCancel, label, ...dialogOptions } = opts;
|
||||
const { dialog, BrowserWindow } = remote;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||
|
||||
const { canceled, filePaths } = await requestOpenFilePickingDialog({
|
||||
message: label,
|
||||
...dialogOptions,
|
||||
});
|
||||
|
||||
@ -71,7 +71,6 @@ class NonInjectedClusterFrame extends React.Component<Dependencies> {
|
||||
this.props.subscribeStores([
|
||||
this.props.namespaceStore,
|
||||
]),
|
||||
|
||||
watchHistoryState(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -6,8 +6,6 @@ import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Terminal } from "../../../components/dock/terminal/terminal";
|
||||
import { requestMain } from "../../../../common/ipc";
|
||||
import { clusterSetFrameIdHandler } from "../../../../common/cluster-ipc";
|
||||
import type { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||
import { Notifications } from "../../../components/notifications";
|
||||
import type { AppEvent } from "../../../../common/app-event-bus/event-bus";
|
||||
@ -16,6 +14,7 @@ import { when } from "mobx";
|
||||
import { unmountComponentAtNode } from "react-dom";
|
||||
import type { ClusterFrameContext } from "../../../cluster-frame-context/cluster-frame-context";
|
||||
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
import { requestSetClusterFrameId } from "../../../ipc";
|
||||
|
||||
interface Dependencies {
|
||||
hostedCluster: Cluster;
|
||||
@ -42,7 +41,7 @@ export const initClusterFrame =
|
||||
);
|
||||
|
||||
await Terminal.preloadFonts();
|
||||
await requestMain(clusterSetFrameIdHandler, hostedCluster.id);
|
||||
await requestSetClusterFrameId(hostedCluster.id);
|
||||
await hostedCluster.whenReady; // cluster.activate() is done at this point
|
||||
|
||||
catalogEntityRegistry.activeEntity = hostedCluster.id;
|
||||
|
||||
@ -3,12 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { delay } from "../../../../common/utils";
|
||||
import { broadcastMessage, BundledExtensionsLoaded } from "../../../../common/ipc";
|
||||
import { registerIpcListeners } from "../../../ipc";
|
||||
import { broadcastMessage } from "../../../../common/ipc";
|
||||
import { registerIpcListeners } from "../../../ipc/register-listeners";
|
||||
import logger from "../../../../common/logger";
|
||||
import { unmountComponentAtNode } from "react-dom";
|
||||
import type { ExtensionLoading } from "../../../../extensions/extension-loader";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
|
||||
|
||||
interface Dependencies {
|
||||
loadExtensions: () => Promise<ExtensionLoading[]>;
|
||||
@ -50,7 +51,7 @@ export const initRootFrame =
|
||||
|
||||
await Promise.race([bundledExtensionsFinished, timeout]);
|
||||
} finally {
|
||||
ipcRenderer.send(BundledExtensionsLoaded);
|
||||
ipcRenderer.send(bundledExtensionsLoaded);
|
||||
}
|
||||
|
||||
lensProtocolRouterRenderer.init();
|
||||
|
||||
@ -8,3 +8,4 @@
|
||||
export * from "./useOnUnmount";
|
||||
export * from "./useInterval";
|
||||
export * from "./useMutationObserver";
|
||||
export * from "./use-toggle";
|
||||
|
||||
11
src/renderer/hooks/use-toggle.ts
Normal file
11
src/renderer/hooks/use-toggle.ts
Normal 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)];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user