Add enough code to get first couple terminal tests to pass

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-01-10 15:17:50 -05:00
1 parent 87dc14cbfb
commit dba4f59644
54 files changed
+1338 -378

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
@@ -4,7 +4,7 @@
*/
import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel";
export type AddHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>;
@@ -4,7 +4,7 @@
*/
import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel";
export type GetActiveHelmRepositoriesChannel = RequestChannel<void, AsyncResult<HelmRepo[]>>;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel";
import type { HelmRepo } from "./helm-repo";
export type RemoveHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>;
+5 -5
View File
@@ -5,9 +5,9 @@
export interface Logger {
info: (message: string, ...args: any) => void;
error: (message: string, ...args: any) => void;
debug: (message: string, ...args: any) => void;
warn: (message: string, ...args: any) => void;
silly: (message: string, ...args: any) => void;
info: (message: string, ...args: any[]) => void;
error: (message: string, ...args: any[]) => void;
debug: (message: string, ...args: any[]) => void;
warn: (message: string, ...args: any[]) => void;
silly: (message: string, ...args: any[]) => void;
}
+4 -6
View File
@@ -11,15 +11,15 @@ import { getApplicationBuilder } from "../../../renderer/components/test-utils/g
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { MessageChannel } from "./message-channel-listener-injection-token";
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
import type { RequestFromChannel } from "./request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
import type { RequestChannel } from "./request-channel-listener-injection-token";
import type { RequestChannel } from "./request-channel";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "../../test-utils/get-promise-status";
import { runInAction } from "mobx";
import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens";
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens";
import type { RequestFromChannel } from "../../../renderer/utils/channel/request-from-channel.injectable";
import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable";
type TestMessageChannel = MessageChannel<string>;
type TestRequestChannel = RequestChannel<string, string>;
@@ -164,9 +164,7 @@ describe("channel", () => {
const windowDi = applicationBuilder.applicationWindow.only.di;
requestFromChannel = windowDi.inject(
requestFromChannelInjectionToken,
);
requestFromChannel = windowDi.inject(requestFromChannelInjectable);
});
describe("when requesting from channel", () => {
@@ -8,3 +8,11 @@ export interface RequestChannel<Request, Response> {
_requestSignature?: Request; // used only to mark `Request` as "used"
_responseSignature?: Response; // used only to mark `Response` as "used"
}
export type ChannelRequest<Channel> = Channel extends RequestChannel<infer Request, any>
? Request
: never;
export type ChannelResponse<Channel> = Channel extends RequestChannel<any, infer Response>
? Response
: never;
@@ -1,15 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { RequestChannel } from "./request-channel-listener-injection-token";
export interface RequestFromChannel {
<Request, Response>(channel: RequestChannel<Request, Response>, request: Request): Promise<Response>;
<Response>(channel: RequestChannel<void, Response>): Promise<Response>;
}
export const requestFromChannelInjectionToken = getInjectionToken<RequestFromChannel>({
id: "request-from-request-channel",
});
@@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../channel/request-channel";
export type ResolveSystemProxyChannel = RequestChannel<string, string>;
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "../channel/message-channel-listener-injection-token";
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../channel/request-channel";
export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
export type ClearClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
export type DeleteClusterChannel = RequestChannel<ClusterId, void>;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
export type SetClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
@@ -5,16 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import { getActiveHelmRepositoriesChannel } from "../../../../../common/helm/get-active-helm-repositories-channel";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import showErrorNotificationInjectable from "../../../../../renderer/components/notifications/show-error-notification.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import { runInAction } from "mobx";
import requestFromChannelInjectable from "../../../../../renderer/utils/channel/request-from-channel.injectable";
const activeHelmRepositoriesInjectable = getInjectable({
id: "active-helm-repositories",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const requestFromChannel = di.inject(requestFromChannelInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const helmRepositoriesErrorState = di.inject(
helmRepositoriesErrorStateInjectable,
@@ -4,17 +4,17 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../../../../../../renderer/components/notifications/show-error-notification.injectable";
import showSuccessNotificationInjectable from "../../../../../../../renderer/components/notifications/show-success-notification.injectable";
import { addHelmRepositoryChannel } from "../../../../../../../common/helm/add-helm-repository-channel";
import requestFromChannelInjectable from "../../../../../../utils/channel/request-from-channel.injectable";
const addHelmRepositoryInjectable = getInjectable({
id: "add-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const requestFromChannel = di.inject(requestFromChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
@@ -4,15 +4,15 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import { removeHelmRepositoryChannel } from "../../../../../common/helm/remove-helm-repository-channel";
import requestFromChannelInjectable from "../../../../utils/channel/request-from-channel.injectable";
const removePublicHelmRepositoryInjectable = getInjectable({
id: "remove-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const requestFromChannel = di.inject(requestFromChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
@@ -0,0 +1,584 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test for opening terminal tab within cluster frame when new terminal tab is opened renders 1`] = `
<body>
<div>
<div
id="terminal-init"
/>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-workloads"
>
<a
aria-current="page"
class="navItem active"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Workloads
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span>
Network
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Helm
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span>
Custom Resources
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-overview"
role="tab"
tabindex="0"
>
<div
class="label"
>
Overview
</div>
</div>
</div>
<main>
<div
class="WorkloadsOverview flex column gaps"
data-testid="page-for-workloads-overview"
>
<div
class="header flex gaps align-center"
>
<h5
class="box grow"
>
Overview
</h5>
<div
class="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
>
<div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-overview-namespace-select-filter-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container Select__value-container--is-multi css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-overview-namespace-select-filter-input-placeholder"
>
All namespaces
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="overview-namespace-select-filter-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="OverviewStatuses"
>
<div
class="workloads"
/>
</div>
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock isOpen"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-for-terminal"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-terminal"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-terminal"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu-actions-for-dock"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
</i>
<div>
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
<div>
Minimize
</div>
</div>
</div>
<div
class="tab-content terminal"
data-testid="dock-tab-content-for-terminal"
style="flex-basis: 300px;"
>
<div
class="TerminalWindow dark"
>
<div
class="terminal xterm"
dir="ltr"
tabindex="0"
>
<div
class="xterm-viewport"
style="background-color: rgb(0, 0, 0);"
>
<div
class="xterm-scroll-area"
/>
</div>
<div
class="xterm-screen"
>
<div
class="xterm-helpers"
>
<textarea
aria-label="Terminal input"
aria-multiline="false"
autocapitalize="off"
autocorrect="off"
class="xterm-helper-textarea"
spellcheck="false"
tabindex="0"
/>
<span
aria-hidden="true"
class="xterm-char-measure-element"
style="font-size: 42px;"
>
W
</span>
<div
class="composition-view"
/>
</div>
<canvas
class="xterm-text-layer"
style="z-index: 0;"
/>
<canvas
class="xterm-selection-layer"
style="z-index: 1;"
/>
<canvas
class="xterm-link-layer"
style="z-index: 2;"
/>
<canvas
class="xterm-cursor-layer"
style="z-index: 3;"
/>
<div
class="xterm-decoration-container"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestChannel } from "../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../common/utils/channel/request-channel";
export interface ShellApiAuthArgs {
clusterId: string;
@@ -0,0 +1,63 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import assert from "assert";
import { TypedRegEx } from "typed-regex";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
const pngBase64Matcher = TypedRegEx("data:image/png;base64,(?<ENCODED>.+)");
function convertCanvasToPngBuffer(canvas: { toDataURL: () => string }): Buffer {
const content = canvas.toDataURL();
const match = pngBase64Matcher.captures(content);
assert(match);
return Buffer.from(match.ENCODED, "base64");
}
describe("test for opening terminal tab within cluster frame", () => {
let builder: ApplicationBuilder;
let result: RenderResult;
let textCanvas: HTMLCanvasElement;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
result = await builder.render();
});
describe("when new terminal tab is opened", () => {
beforeEach(() => {
result.getByTestId("dock-tab-for-terminal").click();
textCanvas = result.baseElement.querySelector("canvas.xterm-text-layer") as HTMLCanvasElement;
});
it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
});
it("opens tab", () => {
const terminalTabContents = result.queryByTestId("dock-tab-content-for-terminal");
expect(terminalTabContents).toBeInTheDocument();
});
it("shows connecting message", async () => {
expect(convertCanvasToPngBuffer(textCanvas)).toMatchImageSnapshot();
});
it.skip("connects websocket to main", () => {
});
it.skip("displays the values on screen", () => {
});
});
});
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
import requestFromChannelInjectable from "../../../renderer/utils/channel/request-from-channel.injectable";
import { shellApiAuthChannel } from "../common/shell-api-auth-channel";
export type RequestShellApiToken = (tabId: string) => Promise<Uint8Array>;
const requestShellApiTokenInjectable = getInjectable({
id: "request-shell-api-token",
instantiate: (di): RequestShellApiToken => {
const hostedClusterId = di.inject(hostedClusterIdInjectable);
const requestChannel = di.inject(requestFromChannelInjectable);
return (tabId) => {
assert(hostedClusterId, "Can only request shell access within a cluster frame");
return requestChannel(shellApiAuthChannel, {
clusterId: hostedClusterId,
tabId,
});
};
},
});
export default requestShellApiTokenInjectable;
+3
View File
@@ -4,3 +4,6 @@
*/
import "@testing-library/jest-dom";
import { toMatchImageSnapshot } from "jest-image-snapshot";
expect.extend({ toMatchImageSnapshot });
+20
View File
@@ -9,6 +9,7 @@ import { TextEncoder, TextDecoder as TextDecoderNode } from "util";
import glob from "glob";
import path from "path";
import { enableMapSet, setAutoFreeze } from "immer";
import { WebSocket } from "ws";
declare global {
interface InjectablePaths {
@@ -52,11 +53,30 @@ global.ResizeObserver = class {
disconnect = () => {};
};
global.WebSocket = WebSocket as any;
jest.mock("./renderer/components/monaco-editor/monaco-editor");
jest.mock("./renderer/components/tooltip/withTooltip");
jest.mock("monaco-editor");
/**
* This is the official workaround https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
*/
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
const getInjectables = (environment: "renderer" | "main", filePathGlob: string) => [
...glob.sync(`./{common,extensions,${environment}}/**/${filePathGlob}`, {
cwd: __dirname,
@@ -0,0 +1,48 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IncomingMessage } from "http";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import { getClusterIdFromHost } from "../../common/utils";
import { apiKubePrefix } from "../../common/vars";
export type GetClusterForRequest = (req: IncomingMessage) => Cluster | undefined;
const getClusterForRequestInjectable = getInjectable({
id: "get-cluster-for-request",
instantiate: (di): GetClusterForRequest => {
const getClusterById = di.inject(getClusterByIdInjectable);
return (req) => {
if (!req.headers.host) {
return undefined;
}
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = getClusterById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
const clusterId = getClusterIdFromHost(req.headers.host);
if (!clusterId) {
return undefined;
}
return getClusterById(clusterId);
};
},
});
export default getClusterForRequestInjectable;
+2
View File
@@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
import loggerInjectable from "../../common/logger.injectable";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
import { ClusterManager } from "./manager";
@@ -19,6 +20,7 @@ const clusterManagerInjectable = getInjectable({
clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable),
visibleCluster: di.inject(visibleClusterInjectable),
logger: di.inject(loggerInjectable),
getClusterById: di.inject(getClusterByIdInjectable),
}),
});
+3 -1
View File
@@ -16,12 +16,14 @@ import type { ClusterStore } from "../../common/cluster-store/cluster-store";
import type { ClusterId } from "../../common/cluster-types";
import type { CatalogEntityRegistry } from "../catalog";
import type { Logger } from "../../common/logger";
import type { GetClusterById } from "../../common/cluster-store/get-by-id.injectable";
const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
interface Dependencies {
getClusterById: GetClusterById;
readonly store: ClusterStore;
readonly catalogEntityRegistry: CatalogEntityRegistry;
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
@@ -181,7 +183,7 @@ export class ClusterManager {
@action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = this.dependencies.store.getById(entity.getId());
const cluster = this.dependencies.getClusterById(entity.getId());
if (!cluster) {
const model = {
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import showErrorPopupInjectable from "./show-error-popup.injectable";
export default getGlobalOverride(showErrorPopupInjectable, () => () => {
throw new Error("Tried to show an error popup without an override");
});
@@ -5,15 +5,15 @@
import { getInjectable } from "@ogre-tools/injectable";
import electronDialogInjectable from "./electron-dialog.injectable";
export type ShowErrorPopup = (heading: string, message: string) => void;
const showErrorPopupInjectable = getInjectable({
id: "show-error-popup",
instantiate: (di) => {
instantiate: (di): ShowErrorPopup => {
const dialog = di.inject(electronDialogInjectable);
return (heading: string, message: string) => {
dialog.showErrorBox(heading, message);
};
return (heading, message) => dialog.showErrorBox(heading, message);
},
});
-2
View File
@@ -10,7 +10,6 @@ import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as
import spawnInjectable from "./child-process/spawn.injectable";
import initializeExtensionsInjectable from "./start-main-application/runnables/initialize-extensions.injectable";
import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable";
import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable";
import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable";
import setupDeepLinkingInjectable from "./electron-app/runnables/setup-deep-linking.injectable";
import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/runnables/setup-main-window-visibility-after-activation.injectable";
@@ -103,7 +102,6 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
initializeExtensionsInjectable,
initializeClusterManagerInjectable,
setupIpcMainHandlersInjectable,
setupLensProxyInjectable,
setupSyncingOfWeblinksInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({
+1 -3
View File
@@ -10,7 +10,6 @@ import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { Router } from "../router/router";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import type { Cluster } from "../../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions";
import { getBoolean } from "../utils/parse-query";
import assert from "assert";
@@ -18,8 +17,8 @@ import type { SetRequired } from "type-fest";
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
import type { Logger } from "../../common/logger";
import type { SelfSignedCert } from "selfsigned";
import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable";
export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
export type LensProxyApiRequest = (args: ProxyApiRequestArgs) => void | Promise<void>;
@@ -115,7 +114,6 @@ export class LensProxy {
const { address, port } = this.proxyServer.address() as net.AddressInfo;
this.dependencies.lensProxyPort.set(port);
this.dependencies.logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`);
this.proxyServer.on("error", (error) => {
@@ -82,7 +82,7 @@ const setupLensProxyInjectable = getInjectable({
};
},
causesSideEffects: true,
// causesSideEffects: true,
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
@@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { IpcMainInvokeEvent } from "electron";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import type { Disposer } from "../../../../common/utils";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
import type { RequestChannelListener } from "./listener-tokens";
export type EnlistRequestChannelListener = <TChannel extends RequestChannel<unknown, unknown>>(listener: RequestChannelListener<TChannel>) => Disposer;
@@ -8,7 +8,7 @@ import type { IpcMain, IpcMainInvokeEvent } from "electron";
import { getPromiseStatus } from "../../../../common/test-utils/get-promise-status";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
import type { EnlistRequestChannelListener } from "./enlist-request-channel-listener.injectable";
import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable";
import type { RequestChannelHandler } from "./listener-tokens";
@@ -5,7 +5,7 @@
import type { DiContainerForInjection } from "@ogre-tools/injectable";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
export type RequestChannelHandler<Channel> = Channel extends RequestChannel<infer Request, infer Response>
? (req: Request) => Promise<Response> | Response
@@ -4,8 +4,8 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { disposer } from "../../../../common/utils";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel";
import enlistRequestChannelListenerInjectable from "./enlist-request-channel-listener.injectable";
import { requestChannelListenerInjectionToken } from "./listener-tokens";
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../../../common/cluster/cluster";
import catalogEntityRegistryInjectable from "./registry.injectable";
@@ -12,10 +12,18 @@ export type GetActiveClusterEntity = () => Cluster | undefined;
const getActiveClusterEntityInjectable = getInjectable({
id: "get-active-cluster-entity",
instantiate: (di): GetActiveClusterEntity => {
const store = di.inject(clusterStoreInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
return () => store.getById(entityRegistry.activeEntity?.getId());
return () => {
const clusterId = entityRegistry.activeEntity?.getId();
if (!clusterId) {
return undefined;
}
return getClusterById(clusterId);
};
},
});
@@ -3,10 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import loggerInjectable from "../../common/logger.injectable";
import hostedClusterIdInjectable from "../cluster-frame-context/hosted-cluster-id.injectable";
import defaultWebsocketApiParamsInjectable from "./default-websocket-api-params.injectable";
import requestShellApiTokenInjectable from "../../features/terminal/renderer/request-shell-api-token.injectable";
import currentLocationInjectable from "./current-location.injectable";
import defaultWebsocketApiParamsInjectable from "./default-websocket-params.injectable";
import type { TerminalApiDependencies, TerminalApiQuery } from "./terminal-api";
import { TerminalApi } from "./terminal-api";
@@ -15,20 +15,14 @@ export type CreateTerminalApi = (query: TerminalApiQuery) => TerminalApi;
const createTerminalApiInjectable = getInjectable({
id: "create-terminal-api",
instantiate: (di): CreateTerminalApi => {
const hostedClusterId = di.inject(hostedClusterIdInjectable);
const deps: Omit<TerminalApiDependencies, "hostedClusterId"> = {
logger: di.inject(loggerInjectable),
const deps: TerminalApiDependencies = {
requestShellApiToken: di.inject(requestShellApiTokenInjectable),
defaultParams: di.inject(defaultWebsocketApiParamsInjectable),
logger: di.inject(loggerInjectable),
currentLocation: di.inject(currentLocationInjectable),
};
return (query) => {
assert(hostedClusterId, "Can only create terminal APIs within a cluster frame");
return new TerminalApi({
hostedClusterId,
...deps,
}, query);
};
return (query) => new TerminalApi(deps, query);
},
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
export interface CurrentLocation {
hostname: string;
port: string;
protocol: string;
}
const currentLocationInjectable = getInjectable({
id: "current-location",
instantiate: (): CurrentLocation => ({
hostname: location.hostname,
port: location.port,
protocol: location.protocol,
}),
causesSideEffects: true,
});
export default currentLocationInjectable;
+11 -11
View File
@@ -8,10 +8,11 @@ import { WebSocketApi } from "./websocket-api";
import isEqual from "lodash/isEqual";
import url from "url";
import { makeObservable, observable } from "mobx";
import { ipcRenderer } from "electron";
import type { Logger } from "../../common/logger";
import { once } from "lodash";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
import type { RequestShellApiToken } from "../../features/terminal/renderer/request-shell-api-token.injectable";
import type { CurrentLocation } from "./current-location.injectable";
enum TerminalColor {
RED = "\u001b[31m",
@@ -38,8 +39,13 @@ export interface TerminalEvents extends WebSocketEvents {
}
export interface TerminalApiDependencies extends WebSocketApiDependencies {
readonly hostedClusterId: string;
readonly logger: Logger;
readonly currentLocation: CurrentLocation;
requestShellApiToken: RequestShellApiToken;
}
export interface ConnectOpts {
signal: AbortSignal;
}
export class TerminalApi extends WebSocketApi<TerminalEvents> {
@@ -68,13 +74,8 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
this.emitStatus("Connecting ...");
}
const authTokenArray = await ipcRenderer.invoke("cluster:shell-api", this.dependencies.hostedClusterId, this.query.id);
if (!(authTokenArray instanceof Uint8Array)) {
throw new TypeError("ShellApi token is not a Uint8Array");
}
const { hostname, protocol, port } = location;
const authTokenArray = await this.dependencies.requestShellApiToken(this.query.id);
const { hostname, protocol, port } = this.dependencies.currentLocation;
const socketUrl = url.format({
protocol: protocol.includes("https") ? "wss" : "ws",
hostname,
@@ -110,8 +111,7 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
this.prependListener("data", onReady);
this.prependListener("connected", onReady);
super.connect(socketUrl);
this.connectTo(socketUrl);
}
sendMessage(message: TerminalMessage) {
+8 -6
View File
@@ -8,6 +8,7 @@ import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter";
import type { Defaulted } from "../utils";
import type { DefaultWebsocketApiParams } from "./default-websocket-params.injectable";
import type { Logger } from "../../common/logger";
interface WebsocketApiParams {
/**
@@ -64,6 +65,7 @@ export interface WebSocketEvents {
export interface WebSocketApiDependencies {
readonly defaultParams: DefaultWebsocketApiParams;
readonly logger: Logger;
}
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
@@ -93,7 +95,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
return this.socket?.readyState === WebSocket.OPEN;
}
connect(url: string) {
connectTo(url: string) {
// close previous connection first
this.socket?.close();
@@ -114,10 +116,10 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
reconnect(): void {
if (!this.socket) {
return void console.error("[WEBSOCKET-API]: cannot reconnect to a socket that is not connected");
return this.dependencies.logger.error("[WEBSOCKET-API]: cannot reconnect to a socket that is not connected");
}
this.connect(this.socket.url);
this.connectTo(this.socket.url);
}
destroy() {
@@ -182,7 +184,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
this.writeLog("will reconnect in", `${reconnectDelay}s`);
this.reconnectTimer = window.setTimeout(() => this.connect(url), reconnectDelay * 1000);
this.reconnectTimer = window.setTimeout(() => this.connectTo(url), reconnectDelay * 1000);
this.readyState = WebSocketApiState.RECONNECTING;
}
} else {
@@ -192,9 +194,9 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
this.writeLog("%cCLOSE", `color:${error ? "red" : "black"};font-weight:bold;`, evt);
}
protected writeLog(...data: any[]) {
protected writeLog(message: string, ...data: any[]) {
if (this.params.logging) {
console.debug(...data);
this.dependencies.logger.debug(message, ...data);
}
}
}
@@ -4,16 +4,20 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import hostedClusterIdInjectable from "./hosted-cluster-id.injectable";
import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
const hostedClusterInjectable = getInjectable({
id: "hosted-cluster",
instantiate: (di) => {
const hostedClusterId = di.inject(hostedClusterIdInjectable);
const store = di.inject(clusterStoreInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
return store.getById(hostedClusterId);
if (!hostedClusterId) {
return undefined;
}
return getClusterById(hostedClusterId);
},
});
@@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ITerminalOptions } from "xterm";
import { Terminal } from "xterm";
export type CreateTerminalRenderer = (opts: ITerminalOptions) => Terminal;
const createTerminalRendererInjectable = getInjectable({
id: "create-terminal-renderer",
instantiate: (): CreateTerminalRenderer => (params) => new Terminal(params),
});
export default createTerminalRendererInjectable;
@@ -13,6 +13,7 @@ import terminalCopyOnSelectInjectable from "../../../../common/user-store/termin
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import openLinkInBrowserInjectable from "../../../../common/utils/open-link-in-browser.injectable";
import xtermColorThemeInjectable from "../../../themes/terminal-colors.injectable";
import createTerminalRendererInjectable from "./create-renderer.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal;
@@ -24,10 +25,11 @@ const createTerminalInjectable = getInjectable({
spawningPool: di.inject(terminalSpawningPoolInjectable),
terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
isMac: di.inject(isMacInjectable),
openLinkInBrowser: di.inject(openLinkInBrowserInjectable),
xtermColorTheme: di.inject(xtermColorThemeInjectable),
isMac: di.inject(isMacInjectable),
logger: di.inject(loggerInjectable),
openLinkInBrowser: di.inject(openLinkInBrowserInjectable),
createTerminalRenderer: di.inject(createTerminalRendererInjectable),
};
return (tabId, api) => new Terminal(dependencies, { tabId, api });
+38 -36
View File
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, observable } from "mobx";
import { action, runInAction } from "mobx";
import type { Terminal } from "./terminal";
import type { TerminalApi } from "../../../api/terminal-api";
import type { DockTab, TabId } from "../dock/store";
@@ -20,69 +20,71 @@ interface Dependencies {
createTerminalApi: CreateTerminalApi;
}
export interface TerminalConnection {
terminal: Terminal;
api: TerminalApi;
}
export interface TerminalConnectOptions {
signal: AbortSignal;
}
export class TerminalStore {
protected terminals = new Map<TabId, Terminal>();
protected connections = observable.map<TabId, TerminalApi>();
private readonly connections = new Map<TabId, TerminalConnection>();
constructor(private dependencies: Dependencies) {
}
constructor(private readonly dependencies: Dependencies) {}
@action
connect(tab: ITerminalTab) {
if (this.isConnected(tab.id)) {
return;
connect(tab: ITerminalTab): TerminalConnection & { connectionPromise?: Promise<void> } {
{
const connection = this.connections.get(tab.id);
if (connection) {
return connection;
}
}
const api = this.dependencies.createTerminalApi({
id: tab.id,
node: tab.node,
});
const terminal = this.dependencies.createTerminal(tab.id, api);
this.connections.set(tab.id, api);
this.terminals.set(tab.id, terminal);
runInAction(() => {
this.connections.set(tab.id, { api, terminal });
});
api.connect();
const connectionPromise = api.connect();
return { terminal, api, connectionPromise };
}
@action
destroy(tabId: TabId) {
const terminal = this.terminals.get(tabId);
const terminalApi = this.connections.get(tabId);
const { terminal, api } = this.connections.get(tabId) ?? {};
terminal?.destroy();
terminalApi?.destroy();
api?.destroy();
this.connections.delete(tabId);
this.terminals.delete(tabId);
}
/**
* @deprecated use `this.destroy()` instead
*/
disconnect(tabId: TabId) {
this.destroy(tabId);
async reconnect(tabId: TabId): Promise<void> {
await this.connections.get(tabId)?.api.connect();
}
reconnect(tabId: TabId) {
this.connections.get(tabId)?.connect();
isDisconnected(tabId: TabId): boolean {
return this.connections.get(tabId)?.api.readyState === WebSocketApiState.CLOSED;
}
isConnected(tabId: TabId) {
return Boolean(this.connections.get(tabId));
getTerminal(tabId: TabId): Terminal | undefined {
return this.connections.get(tabId)?.terminal;
}
isDisconnected(tabId: TabId) {
return this.connections.get(tabId)?.readyState === WebSocketApiState.CLOSED;
getTerminalApi(tabId: TabId): TerminalApi | undefined {
return this.connections.get(tabId)?.api;
}
getTerminal(tabId: TabId) {
return this.terminals.get(tabId);
}
getTerminalApi(tabId: TabId) {
return this.connections.get(tabId);
}
reset() {
reset(): void {
[...this.connections].forEach(([tabId]) => {
this.destroy(tabId);
});
@@ -14,7 +14,6 @@ const terminalSpawningPoolInjectable = getInjectable({
return pool;
},
causesSideEffects: true,
});
export default terminalSpawningPoolInjectable;
@@ -6,7 +6,7 @@
import debounce from "lodash/debounce";
import type { IComputedValue } from "mobx";
import { reaction } from "mobx";
import { Terminal as XTerm } from "xterm";
import type { Terminal as XTerminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api";
@@ -19,6 +19,7 @@ import assert from "assert";
import { TerminalChannels } from "../../../../common/terminal/channels";
import { LinkProvider } from "xterm-link-provider";
import type { OpenLinkInBrowser } from "../../../../common/utils/open-link-in-browser.injectable";
import type { CreateTerminalRenderer } from "./create-renderer.injectable";
export interface TerminalDependencies {
readonly spawningPool: HTMLElement;
@@ -28,6 +29,7 @@ export interface TerminalDependencies {
readonly xtermColorTheme: IComputedValue<Record<string, string>>;
readonly logger: Logger;
openLinkInBrowser: OpenLinkInBrowser;
createTerminalRenderer: CreateTerminalRenderer;
}
export interface TerminalArguments {
@@ -36,7 +38,7 @@ export interface TerminalArguments {
}
export class Terminal {
private readonly xterm: XTerm;
private readonly xterm: XTerminal;
private readonly fitAddon = new FitAddon();
private scrollPos = 0;
private readonly disposer = disposer();
@@ -44,13 +46,19 @@ export class Terminal {
protected readonly api: TerminalApi;
private get elem() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.xterm.element!;
const { element } = this.xterm;
assert(element, "Terminal element must be mounted");
return element;
}
private get viewport() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.elem.querySelector(".xterm-viewport")!;
const viewport = this.elem.querySelector(".xterm-viewport");
assert(viewport, 'Terminal element must have a descendant with a className of ".xterm-viewport"');
return viewport;
}
attachTo(parentElem: HTMLElement) {
@@ -67,11 +75,11 @@ export class Terminal {
}
}
get fontFamily() {
private get fontFamily() {
return this.dependencies.terminalConfig.get().fontFamily;
}
get fontSize() {
private get fontSize() {
return this.dependencies.terminalConfig.get().fontSize;
}
@@ -82,11 +90,12 @@ export class Terminal {
this.tabId = tabId;
this.api = api;
this.xterm = new XTerm({
this.xterm = this.dependencies.createTerminalRenderer({
cursorBlink: true,
cursorStyle: "bar",
fontSize: this.fontSize,
fontFamily: this.fontFamily,
theme: this.dependencies.xtermColorTheme.get(),
});
// enable terminal addons
this.xterm.loadAddon(this.fitAddon);
@@ -96,7 +105,6 @@ export class Terminal {
this.xterm.onSelectionChange(this.onSelectionChange);
// bind events
const onDataHandler = this.xterm.onData(this.onData);
const clearOnce = once(this.onClear);
this.viewport.addEventListener("scroll", this.onScroll);
@@ -117,15 +125,13 @@ export class Terminal {
this.disposer.push(
this.xterm.registerLinkProvider(linkProvider),
reaction(() => this.dependencies.xtermColorTheme.get(),
reaction(
() => this.dependencies.xtermColorTheme.get(),
colors => this.xterm.options.theme = colors,
{
fireImmediately: true,
},
),
reaction(() => this.fontSize, this.setFontSize, { fireImmediately: true }),
reaction(() => this.fontFamily, this.setFontFamily, { fireImmediately: true }),
() => onDataHandler.dispose(),
reaction(() => this.fontSize, this.setFontSize),
reaction(() => this.fontFamily, this.setFontFamily),
this.xterm.onData(this.onData),
() => this.fitAddon.dispose(),
() => this.api.removeAllListeners(),
() => window.removeEventListener("resize", this.onResize),
+30 -43
View File
@@ -5,8 +5,7 @@
import "./terminal-window.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import React, { useEffect, useRef, useState } from "react";
import { cssNames } from "../../../utils";
import type { Terminal } from "./terminal";
import type { TerminalStore } from "./store";
@@ -15,7 +14,6 @@ import type { DockTab, DockStore } from "../dock/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "../dock/store.injectable";
import terminalStoreInjectable from "./store.injectable";
import assert from "assert";
import activeThemeInjectable from "../../../themes/active.injectable";
import type { IComputedValue } from "mobx";
@@ -29,52 +27,41 @@ interface Dependencies {
activeTheme: IComputedValue<LensTheme>;
}
@observer
class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & Dependencies> {
public elem: HTMLElement | null = null;
public terminal!: Terminal;
const NonInjectedTerminalWindow = (props: TerminalWindowProps & Dependencies) => {
const {
activeTheme,
dockStore,
tab,
terminalStore,
} = props;
componentDidMount() {
this.props.terminalStore.connect(this.props.tab);
const terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
const [terminal, setTerminal] = useState<Terminal>();
const element = useRef<HTMLDivElement | null>(null);
assert(terminal, "Terminal must be created for tab before mounting");
this.terminal = terminal;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.terminal.attachTo(this.elem!);
useEffect(() => {
const { terminal } = terminalStore.connect(tab);
disposeOnUnmount(this, [
// refresh terminal available space (cols/rows) when <Dock/> resized
this.props.dockStore.onResize(() => this.terminal.onResize(), {
fireImmediately: true,
}),
]);
}
setTerminal(terminal);
componentDidUpdate(): void {
this.terminal.detach();
this.props.terminalStore.connect(this.props.tab);
const terminal = this.props.terminalStore.getTerminal(this.props.tab.id);
if (element.current) {
terminal.attachTo(element.current);
}
}, [tab.id]);
assert(terminal, "Terminal must be created for tab before mounting");
this.terminal = terminal;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.terminal.attachTo(this.elem!);
}
useEffect(() => dockStore.onResize(
() => {
terminal?.onResize();
}, {
fireImmediately: true,
}), []);
componentWillUnmount(): void {
this.terminal.detach();
}
render() {
return (
<div
className={cssNames("TerminalWindow", this.props.activeTheme.get().type)}
ref={elem => this.elem = elem}
/>
);
}
}
return (
<div
className={cssNames("TerminalWindow", activeTheme.get().type)}
ref={element}
/>
);
};
export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>(NonInjectedTerminalWindow, {
getProps: (di, props) => ({
@@ -21,7 +21,6 @@ import { filter, first, join, last, map, matches } from "lodash/fp";
import navigateToPreferencesInjectable from "../../../features/preferences/common/navigate-to-preferences.injectable";
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable";
import startFrameInjectable from "../../start-frame/start-frame.injectable";
@@ -35,7 +34,6 @@ import { overrideChannels } from "../../../test-utils/channel-fakes/override-cha
import assert from "assert";
import { openMenu } from "react-select-event";
import userEvent from "@testing-library/user-event";
import lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable";
import type { Route } from "../../../common/front-end-routing/front-end-route-injection-token";
import type { NavigateToRouteOptions } from "../../../common/front-end-routing/navigate-to-route-injection-token";
import { navigateToRouteInjectionToken } from "../../../common/front-end-routing/navigate-to-route-injection-token";
@@ -46,8 +44,6 @@ import { renderFor } from "./renderFor";
import { RootFrame } from "../../frames/root-frame/root-frame";
import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
import activeKubernetesClusterInjectable from "../../cluster-frame-context/active-kubernetes-cluster.injectable";
import { catalogEntityFromCluster } from "../../../main/cluster/manager";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import createApplicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-application-window.injectable";
import type { CreateElectronWindow } from "../../../main/start-main-application/lens-window/application-window/create-electron-window.injectable";
@@ -69,6 +65,12 @@ import fsInjectable from "../../../common/fs/fs.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import homeDirectoryPathInjectable from "../../../common/os/home-directory-path.injectable";
import { testUsingFakeTime } from "../../../common/test-utils/use-fake-time";
import type { ClusterId } from "../../../common/cluster-types";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../main/start-main-application/runnable-tokens/on-load-of-application-injection-token";
import currentLocationInjectable from "../../api/current-location.injectable";
import lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable";
type Callback = (di: DiContainer) => void | Promise<void>;
@@ -265,11 +267,14 @@ export const getApplicationBuilder = () => {
const render = renderFor(windowDi);
rendered = render(
<Router history={history}>
<environment.RootComponent />
</Router>,
);
rendered = render((
<>
<div id="terminal-init" />
<Router history={history}>
<environment.RootComponent />
</Router>
</>
));
},
send: (arg) => {
@@ -289,11 +294,11 @@ export const getApplicationBuilder = () => {
const namespaces = observable.set<string>();
const namespaceItems = observable.array<Namespace>();
const selectedNamespaces = observable.set<string>();
const clusters = observable.map<ClusterId, Cluster>();
const clusterId = "some-cluster-id";
const startMainApplication = mainDi.inject(startMainApplicationInjectable);
const startApplication = async ({ shouldStartHidden }: { shouldStartHidden: boolean }) => {
mainDi.inject(lensProxyPortInjectable).set(42);
for (const callback of beforeApplicationStartCallbacks) {
await callback(mainDi);
}
@@ -308,6 +313,41 @@ export const getApplicationBuilder = () => {
applicationHasStarted = true;
};
mainDi.override(getClusterByIdInjectable, () => (id) => clusters.get(id));
beforeWindowStartCallbacks.push((windowDi) => windowDi.override(getClusterByIdInjectable, () => (id) => clusters.get(id)));
beforeWindowStartCallbacks.push((windowDi) => windowDi.override(currentLocationInjectable, () => {
const port = mainDi.inject(lensProxyPortInjectable);
return {
hostname: "localhost",
port: `${port.get()}`,
protocol: "http",
};
}));
runInAction(() => {
mainDi.register(getInjectable({
id: "create-fake-cluster",
instantiate: (di) => ({
id: "create-fake-cluster",
run: () => {
const createCluster = di.inject(createClusterInjectable);
const cluster = createCluster({
contextName: "some-context-name",
id: clusterId,
kubeConfigPath: "/some-kube-config-path",
}, {
clusterServerUrl: "https://some-url.com:8797",
});
clusters.set(clusterId, cluster);
},
}),
injectionToken: onLoadOfApplicationInjectionToken,
}));
});
const builder: ApplicationBuilder = {
mainDi,
applicationWindow: {
@@ -501,18 +541,7 @@ export const getApplicationBuilder = () => {
environment = environments.clusterFrame;
builder.beforeWindowStart((windowDi) => {
const clusterStub = {
id: "some-cluster-id",
accessibleNamespaces: observable.array(),
shouldShowResource: (kind) => allowedResourcesState.has(formatKubeApiResource(kind)),
} as Partial<Cluster> as Cluster;
windowDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(clusterStub)),
);
windowDi.override(hostedClusterIdInjectable, () => clusterStub.id);
windowDi.override(hostedClusterInjectable, () => clusterStub);
windowDi.override(hostedClusterIdInjectable, () => clusterId);
// TODO: Figure out a way to remove this stub.
windowDi.override(namespaceStoreInjectable, () => ({
-2
View File
@@ -9,7 +9,6 @@ import { createContainer, isInjectable, getInjectable } from "@ogre-tools/inject
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable";
import { getOverrideFsWithFakes } from "../test-utils/override-fs-with-fakes";
import terminalSpawningPoolInjectable from "./components/dock/terminal/terminal-spawning-pool.injectable";
import hostedClusterIdInjectable from "./cluster-frame-context/hosted-cluster-id.injectable";
import { runInAction } from "mobx";
import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable";
@@ -70,7 +69,6 @@ export const getDiForUnitTesting = (
}));
});
di.override(terminalSpawningPoolInjectable, () => document.createElement("div"));
di.override(hostedClusterIdInjectable, () => undefined);
di.override(legacyOnChannelListenInjectable, () => () => noop);
@@ -3,9 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ChannelRequest, ChannelResponse, RequestChannel } from "../../../common/utils/channel/request-channel";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
export interface RequestFromChannel {
<Channel extends RequestChannel<void, unknown>>(channel: Channel): Promise<ChannelResponse<Channel>>;
<Channel extends RequestChannel<unknown, unknown>>(channel: Channel, request: ChannelRequest<Channel>): Promise<ChannelResponse<Channel>>;
}
const requestFromChannelInjectable = getInjectable({
id: "request-from-channel",
@@ -15,8 +19,6 @@ const requestFromChannelInjectable = getInjectable({
return ((channel, request) => ipcRenderer.invoke(channel.id, request)) as RequestFromChannel;
},
injectionToken: requestFromChannelInjectionToken,
});
export default requestFromChannelInjectable;
@@ -6,10 +6,10 @@ import { getInjectable } from "@ogre-tools/injectable";
import { beforeFrameStartsSecondInjectionToken } from "../../before-frame-starts/tokens";
import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels";
import createSyncBoxStateInjectable from "../../../common/utils/sync-box/sync-box-state.injectable";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
import { runInAction } from "mobx";
import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token";
import assert from "assert";
import requestFromChannelInjectable from "../channel/request-from-channel.injectable";
const provideInitialValuesForSyncBoxesInjectable = getInjectable({
id: "provide-initial-values-for-sync-boxes",
@@ -17,7 +17,7 @@ const provideInitialValuesForSyncBoxesInjectable = getInjectable({
instantiate: (di) => ({
id: "provide-initial-values-for-sync-boxes",
run: async () => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const requestFromChannel = di.inject(requestFromChannelInjectable);
const syncBoxes = di.injectMany(syncBoxInjectionToken);
const initialValues = await requestFromChannel(syncBoxInitialValueChannel);
@@ -25,7 +25,7 @@ const provideInitialValuesForSyncBoxesInjectable = getInjectable({
for (const { id, value } of initialValues) {
const syncBox = syncBoxes.find((box) => box.id === id);
assert(syncBox);
assert(syncBox, `Missing synx box with id="${id}"`);
di.inject(createSyncBoxStateInjectable, syncBox.id).set(value);
}
});
@@ -4,10 +4,10 @@
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { deserialize, serialize } from "v8";
import type { RequestChannel } from "../../common/utils/channel/request-channel-listener-injection-token";
import type { RequestFromChannel } from "../../common/utils/channel/request-from-channel-injection-token";
import type { RequestChannel } from "../../common/utils/channel/request-channel";
import enlistRequestChannelListenerInjectableInMain from "../../main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable";
import type { RequestChannelListener } from "../../main/utils/channel/channel-listeners/listener-tokens";
import type { RequestFromChannel, RequestFromChannel } from "../../renderer/utils/channel/request-from-channel.injectable";
import requestFromChannelInjectable from "../../renderer/utils/channel/request-from-channel.injectable";
export const overrideRequestingFromWindowToMain = (mainDi: DiContainer) => {