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

Release v5.5.4 (#5589)

This commit is contained in:
Sebastian Malton 2022-06-10 11:05:57 -04:00 committed by GitHub
parent 2322f3f0ea
commit d908b56d3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 197 additions and 70 deletions

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.5.3", "version": "5.5.4",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",

View File

@ -10,7 +10,14 @@ import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class ClusterApi extends KubeApi<Cluster> { export class ClusterApi extends KubeApi<Cluster> {
/**
* @deprecated This field is legacy and never used.
*/
static kind = "Cluster"; static kind = "Cluster";
/**
* @deprecated This field is legacy and never used.
*/
static namespaced = true; static namespaced = true;
} }
@ -98,6 +105,7 @@ export interface Cluster {
export class Cluster extends KubeObject { export class Cluster extends KubeObject {
static kind = "Cluster"; static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
static namespaced = true;
getStatus() { getStatus() {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export enum TerminalChannels {
STDIN = "stdin",
STDOUT = "stdout",
CONNECTED = "connected",
RESIZE = "resize",
PING = "ping",
}
export type TerminalMessage = {
type: TerminalChannels.STDIN;
data: string;
} | {
type: TerminalChannels.STDOUT;
data: string;
} | {
type: TerminalChannels.CONNECTED;
} | {
type: TerminalChannels.RESIZE;
data: {
width: number;
height: number;
};
} | {
type: TerminalChannels.PING;
};

View File

@ -141,17 +141,13 @@ export class ContextHandler {
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> { protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensureServer(); await this.ensureServer();
const ca = this.dependencies.authProxyCa;
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
return { return {
target: { target: {
protocol: "https:", protocol: "https:",
host: "127.0.0.1", host: "127.0.0.1",
port: this.kubeAuthProxy.port, port: this.kubeAuthProxy.port,
path: apiPrefix, path: this.kubeAuthProxy.apiPrefix,
ca, ca: this.dependencies.authProxyCa,
}, },
changeOrigin: true, changeOrigin: true,
timeout, timeout,

View File

@ -4,7 +4,6 @@
*/ */
import fs from "fs"; import fs from "fs";
import v8 from "v8";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import type { HelmRepo } from "./helm-repo-manager"; import type { HelmRepo } from "./helm-repo-manager";
import logger from "../logger"; import logger from "../logger";
@ -13,7 +12,7 @@ import { iter, sortCharts } from "../../common/utils";
import { execHelm } from "./exec"; import { execHelm } from "./exec";
interface ChartCacheEntry { interface ChartCacheEntry {
data: Buffer; data: string; // serialized JSON
mtimeMs: number; mtimeMs: number;
} }
@ -77,7 +76,7 @@ export class HelmChartManager {
const normalized = normalizeHelmCharts(this.repo.name, data.entries); const normalized = normalizeHelmCharts(this.repo.name, data.entries);
HelmChartManager.#cache.set(this.repo.name, { HelmChartManager.#cache.set(this.repo.name, {
data: v8.serialize(normalized), data: JSON.stringify(normalized),
mtimeMs: cacheFileStats.mtimeMs, mtimeMs: cacheFileStats.mtimeMs,
}); });
} }
@ -94,7 +93,7 @@ export class HelmChartManager {
} }
} }
return v8.deserialize(HelmChartManager.#cache.get(this.repo.name).data); return JSON.parse(HelmChartManager.#cache.get(this.repo.name).data);
} }
} }

View File

@ -13,8 +13,8 @@ import { get } from "lodash";
import { Node, NodesApi } from "../../../common/k8s-api/endpoints"; import { Node, NodesApi } from "../../../common/k8s-api/endpoints";
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api"; import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
import logger from "../../logger"; import logger from "../../logger";
import { TerminalChannels } from "../../../renderer/api/terminal-api";
import type { Kubectl } from "../../kubectl/kubectl"; import type { Kubectl } from "../../kubectl/kubectl";
import { TerminalChannels } from "../../../common/terminal/channels";
export class NodeShellSession extends ShellSession { export class NodeShellSession extends ShellSession {
ShellType = "node-shell"; ShellType = "node-shell";

View File

@ -16,10 +16,8 @@ import { UserStore } from "../../common/user-store";
import * as pty from "node-pty"; import * as pty from "node-pty";
import { appEventBus } from "../../common/app-event-bus/event-bus"; import { appEventBus } from "../../common/app-event-bus/event-bus";
import logger from "../logger"; import logger from "../logger";
import type { TerminalMessage } from "../../renderer/api/terminal-api";
import { TerminalChannels } from "../../renderer/api/terminal-api";
import { deserialize, serialize } from "v8";
import { stat } from "fs/promises"; import { stat } from "fs/promises";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
export class ShellOpenError extends Error { export class ShellOpenError extends Error {
constructor(message: string, public cause: Error) { constructor(message: string, public cause: Error) {
@ -163,7 +161,7 @@ export abstract class ShellSession {
} }
protected send(message: TerminalMessage): void { protected send(message: TerminalMessage): void {
this.websocket.send(serialize(message)); this.websocket.send(JSON.stringify(message));
} }
protected async getCwd(env: Record<string, string>): Promise<string> { protected async getCwd(env: Record<string, string>): Promise<string> {
@ -234,17 +232,13 @@ export abstract class ShellSession {
}); });
this.websocket this.websocket
.on("message", (data: string | Uint8Array) => { .on("message", (data: unknown): void => {
if (!this.running) { if (!this.running) {
return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`); return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
} }
if (typeof data === "string") {
return void logger.silly(`[SHELL-SESSION]: Received message from ${this.terminalId}`, { data });
}
try { try {
const message: TerminalMessage = deserialize(data); const message: TerminalMessage = JSON.parse(String(data));
switch (message.type) { switch (message.type) {
case TerminalChannels.STDIN: case TerminalChannels.STDIN:
@ -253,6 +247,9 @@ export abstract class ShellSession {
case TerminalChannels.RESIZE: case TerminalChannels.RESIZE:
shellProcess.resize(message.data.width, message.data.height); shellProcess.resize(message.data.width, message.data.height);
break; break;
case TerminalChannels.PING:
logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
break;
default: default:
logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message); logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
break; break;

View File

@ -11,31 +11,8 @@ import url from "url";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable } from "mobx";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import logger from "../../common/logger"; import logger from "../../common/logger";
import { deserialize, serialize } from "v8";
import { once } from "lodash"; import { once } from "lodash";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
export enum TerminalChannels {
STDIN = "stdin",
STDOUT = "stdout",
CONNECTED = "connected",
RESIZE = "resize",
}
export type TerminalMessage = {
type: TerminalChannels.STDIN;
data: string;
} | {
type: TerminalChannels.STDOUT;
data: string;
} | {
type: TerminalChannels.CONNECTED;
} | {
type: TerminalChannels.RESIZE;
data: {
width: number;
height: number;
};
};
enum TerminalColor { enum TerminalColor {
RED = "\u001b[31m", RED = "\u001b[31m",
@ -126,11 +103,10 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
this.prependListener("connected", onReady); this.prependListener("connected", onReady);
super.connect(socketUrl); super.connect(socketUrl);
this.socket.binaryType = "arraybuffer";
} }
sendMessage(message: TerminalMessage) { sendMessage(message: TerminalMessage) {
return this.send(serialize(message)); return this.send(JSON.stringify(message));
} }
sendTerminalSize(cols: number, rows: number) { sendTerminalSize(cols: number, rows: number) {
@ -145,9 +121,9 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
} }
} }
protected _onMessage({ data, ...evt }: MessageEvent<ArrayBuffer>): void { protected _onMessage({ data, ...evt }: MessageEvent<string>): void {
try { try {
const message: TerminalMessage = deserialize(new Uint8Array(data)); const message = JSON.parse(data) as TerminalMessage;
switch (message.type) { switch (message.type) {
case TerminalChannels.STDOUT: case TerminalChannels.STDOUT:

View File

@ -8,6 +8,7 @@ import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { Arguments } from "typed-emitter"; import type { Arguments } from "typed-emitter";
import { isDevelopment } from "../../common/vars"; import { isDevelopment } from "../../common/vars";
import { TerminalChannels, type TerminalMessage } from "../../common/terminal/channels";
interface WebsocketApiParams { interface WebsocketApiParams {
/** /**
@ -29,9 +30,9 @@ interface WebsocketApiParams {
/** /**
* The message for pinging the websocket * The message for pinging the websocket
* *
* @default "PING" * @default "{type: \"ping\"}"
*/ */
pingMessage?: string | ArrayBufferLike | Blob | ArrayBufferView; pingMessage?: string;
/** /**
* If set to a number > 0, then the API will ping the socket on that interval. * If set to a number > 0, then the API will ping the socket on that interval.
@ -65,10 +66,10 @@ export interface WebSocketEvents {
type Defaulted<Params, DefaultParams extends keyof Params> = Required<Pick<Params, DefaultParams>> & Omit<Params, DefaultParams>; type Defaulted<Params, DefaultParams extends keyof Params> = Required<Pick<Params, DefaultParams>> & Omit<Params, DefaultParams>;
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> { export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
protected socket?: WebSocket | null; protected socket: WebSocket | null = null;
protected pendingCommands: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = []; protected pendingCommands: string[] = [];
protected reconnectTimer?: any; protected reconnectTimer?: number;
protected pingTimer?: any; protected pingTimer?: number;
protected params: Defaulted<WebsocketApiParams, keyof typeof WebSocketApi["defaultParams"]>; protected params: Defaulted<WebsocketApiParams, keyof typeof WebSocketApi["defaultParams"]>;
@observable readyState = WebSocketApiState.PENDING; @observable readyState = WebSocketApiState.PENDING;
@ -77,7 +78,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
logging: isDevelopment, logging: isDevelopment,
reconnectDelay: 10, reconnectDelay: 10,
flushOnOpen: true, flushOnOpen: true,
pingMessage: "PING", pingMessage: JSON.stringify({ type: TerminalChannels.PING } as TerminalMessage),
}; };
constructor(params: WebsocketApiParams) { constructor(params: WebsocketApiParams) {
@ -87,7 +88,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
const { pingInterval } = this.params; const { pingInterval } = this.params;
if (pingInterval) { if (pingInterval) {
this.pingTimer = setInterval(() => this.ping(), pingInterval * 1000); this.pingTimer = window.setInterval(() => this.ping(), pingInterval * 1000);
} }
} }
@ -143,7 +144,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
} }
} }
send(command: string | ArrayBufferLike | Blob | ArrayBufferView) { send(command: string) {
if (this.isConnected) { if (this.isConnected) {
this.socket.send(command); this.socket.send(command);
} else { } else {
@ -186,7 +187,7 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
this.writeLog("will reconnect in", `${reconnectDelay}s`); this.writeLog("will reconnect in", `${reconnectDelay}s`);
this.reconnectTimer = setTimeout(() => this.connect(url), reconnectDelay * 1000); this.reconnectTimer = window.setTimeout(() => this.connect(url), reconnectDelay * 1000);
this.readyState = WebSocketApiState.RECONNECTING; this.readyState = WebSocketApiState.RECONNECTING;
} }
} else { } else {

View File

@ -4,8 +4,8 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { when } from "mobx"; import { when } from "mobx";
import { TerminalChannels } from "../../../../common/terminal/channels";
import type { TerminalApi } from "../../../api/terminal-api"; import type { TerminalApi } from "../../../api/terminal-api";
import { TerminalChannels } from "../../../api/terminal-api";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import selectDockTabInjectable from "../dock/select-dock-tab.injectable"; import selectDockTabInjectable from "../dock/select-dock-tab.injectable";

View File

@ -9,7 +9,6 @@ import { Terminal as XTerm } from "xterm";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import type { TabId } from "../dock/store"; import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api"; import type { TerminalApi } from "../../../api/terminal-api";
import { TerminalChannels } from "../../../api/terminal-api";
import { ThemeStore } from "../../../theme.store"; import { ThemeStore } from "../../../theme.store";
import { disposer } from "../../../utils"; import { disposer } from "../../../utils";
import { isMac } from "../../../../common/vars"; import { isMac } from "../../../../common/vars";
@ -18,6 +17,7 @@ import { UserStore } from "../../../../common/user-store";
import { clipboard } from "electron"; import { clipboard } from "electron";
import logger from "../../../../common/logger"; import logger from "../../../../common/logger";
import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers"; import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers";
import { TerminalChannels } from "../../../../common/terminal/channels";
export class Terminal { export class Terminal {
private terminalConfig: TerminalConfig = UserStore.getInstance().terminalConfig; private terminalConfig: TerminalConfig = UserStore.getInstance().terminalConfig;

View File

@ -9,7 +9,12 @@ import { cssNames } from "../../utils";
export interface DrawerTitleProps { export interface DrawerTitleProps {
className?: string; className?: string;
children: React.ReactNode; children?: React.ReactNode;
/**
* @deprecated Prefer passing the value as `children`
*/
title?: React.ReactNode;
/** /**
* Specifies how large this title is * Specifies how large this title is

View File

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Tooltip /> does not render to DOM if not visibile 1`] = `
<body>
<div>
<div
id="my-target"
>
Target Text
</div>
</div>
</body>
`;
exports[`<Tooltip /> renders to DOM when forced to by visibile prop 1`] = `
<body>
<div>
<div
class="Tooltip visible"
role="tooltip"
>
I am a tooltip
</div>
<div
id="my-target"
>
Target Text
</div>
</div>
</body>
`;
exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
<body>
<div>
<div
class="Tooltip right"
role="tooltip"
style="left: 10px; top: 0px;"
>
I am a tooltip
</div>
<div
id="my-target"
>
Target Text
</div>
</div>
</body>
`;

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import assert from "assert";
import React from "react";
import { Tooltip } from "./tooltip";
describe("<Tooltip />", () => {
it("does not render to DOM if not visibile", () => {
const result = render((
<>
<Tooltip targetId="my-target" usePortal={false}>I am a tooltip</Tooltip>
<div id="my-target">Target Text</div>
</>
));
expect(result.baseElement).toMatchSnapshot();
});
it("renders to DOM when hovering over target", () => {
const result = render((
<>
<Tooltip
targetId="my-target"
data-testid="tooltip"
usePortal={false}
>
I am a tooltip
</Tooltip>
<div id="my-target">Target Text</div>
</>
));
const target = result.baseElement.querySelector("#my-target");
assert(target);
userEvent.hover(target);
expect(result.baseElement).toMatchSnapshot();
});
it("renders to DOM when forced to by visibile prop", () => {
const result = render((
<>
<Tooltip
targetId="my-target"
data-testid="tooltip"
visible={true}
usePortal={false}
>
I am a tooltip
</Tooltip>
<div id="my-target">Target Text</div>
</>
));
expect(result.baseElement).toMatchSnapshot();
});
});

View File

@ -53,9 +53,9 @@ const defaultProps: Partial<TooltipProps> = {
export class Tooltip extends React.Component<TooltipProps> { export class Tooltip extends React.Component<TooltipProps> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
@observable.ref elem: HTMLElement; @observable.ref elem: HTMLDivElement | null = null;
@observable activePosition: TooltipPosition; @observable activePosition?: TooltipPosition;
@observable isVisible = this.props.visible ?? false; @observable isVisible = false;
@observable isContentVisible = false; // animation manager @observable isContentVisible = false; // animation manager
constructor(props: TooltipProps) { constructor(props: TooltipProps) {
@ -219,18 +219,19 @@ export class Tooltip extends React.Component<TooltipProps> {
}; };
} }
bindRef(elem: HTMLElement) { bindRef(elem: HTMLDivElement) {
this.elem = elem; this.elem = elem;
} }
render() { render() {
if (!this.isVisible) { const { style, formatters, usePortal, children, visible = this.isVisible } = this.props;
if (!visible) {
return null; return null;
} }
const { style, formatters, usePortal, children } = this.props;
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, { const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
visible: this.isContentVisible, visible: this.isContentVisible || this.props.visible,
formatter: !!formatters, formatter: !!formatters,
}); });
const tooltip = ( const tooltip = (