diff --git a/src/renderer/utils/autobind.ts b/src/common/utils/autobind.ts similarity index 100% rename from src/renderer/utils/autobind.ts rename to src/common/utils/autobind.ts diff --git a/src/common/utils/debouncePromise.ts b/src/common/utils/debouncePromise.ts new file mode 100755 index 0000000000..22ffd5217f --- /dev/null +++ b/src/common/utils/debouncePromise.ts @@ -0,0 +1,9 @@ +// Debouncing promise evaluation + +export function debouncePromise(func: (...args: F) => T | Promise, timeout = 0): (...args: F) => Promise { + let timer: NodeJS.Timeout; + return (...params: any[]) => new Promise((resolve, reject) => { + clearTimeout(timer); + timer = setTimeout(() => resolve(func.apply(this, params)), timeout); + }); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 580a8f15c2..db46a37d97 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,7 +1,14 @@ -// Common utils (main/renderer) +// Common utils (main OR renderer) +export * from "./app-version" +export * from "./autobind" export * from "./base64" export * from "./camelCase" -export * from "./splitArray" -export * from "./getRandId" +export * from "./cloneJson" +export * from "./debouncePromise" +export * from "./defineGlobal" +export * from "./getRandId" +export * from "./splitArray" +export * from "./saveToAppFiles" +export * from "./singleton" export * from "./cloneJson" diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 48f430b7c5..e7bee67c07 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -32,7 +32,7 @@ import { KubeAuthProxy } from "../kube-auth-proxy" import { getFreePort } from "../port" import { broadcastIpc } from "../../common/ipc" import { ChildProcess, spawn, SpawnOptions } from "child_process" -import { Kubectl } from "../kubectl" +import { bundledKubectlPath, Kubectl } from "../kubectl" import { mock, MockProxy } from 'jest-mock-extended'; import { waitUntilUsed } from 'tcp-port-used'; import { Readable } from "stream" @@ -81,7 +81,7 @@ describe("kube auth proxy tests", () => { return mockedCP.stdout }) mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => { - expect(command).toBe(Kubectl.bundledKubectlPath) + expect(command).toBe(bundledKubectlPath()) return mockedCP }) mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()) diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 33521fdcf5..7192425466 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; import { broadcastIpc } from "../common/ipc"; import type { Cluster } from "./cluster" -import { bundledKubectl, Kubectl } from "./kubectl" +import { Kubectl } from "./kubectl" import logger from "./logger" export interface KubeAuthProxyLog { @@ -23,7 +23,7 @@ export class KubeAuthProxy { this.env = env this.port = port this.cluster = cluster - this.kubectl = bundledKubectl + this.kubectl = Kubectl.bundled() } public async run(): Promise { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 672d93cf65..6b2f51476d 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -36,15 +36,21 @@ const packageMirrors: Map = new Map([ let bundledPath: string const initScriptVersionString = "# lens-initscript v3\n" -if (isDevelopment || isTestEnv) { - const platformName = isWindows ? "windows" : process.platform - bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl") -} else { - bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") -} +export function bundledKubectlPath(): string { + if (bundledPath) { return bundledPath } -if (isWindows) { - bundledPath = `${bundledPath}.exe` + if (isDevelopment || isTestEnv) { + const platformName = isWindows ? "windows" : process.platform + bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl") + } else { + bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") + } + + if (isWindows) { + bundledPath = `${bundledPath}.exe` + } + + return bundledPath } export class Kubectl { @@ -58,7 +64,6 @@ export class Kubectl { return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl") } - public static readonly bundledKubectlPath = bundledPath public static readonly bundledKubectlVersion: string = bundledVersion public static invalidBundle = false private static bundledInstance: Kubectl; @@ -102,7 +107,7 @@ export class Kubectl { } public getBundledPath() { - return Kubectl.bundledKubectlPath + return bundledKubectlPath() } public getPathFromPreferences() { @@ -125,19 +130,19 @@ export class Kubectl { // return binary name if bundled path is not functional if (!await this.checkBinary(this.getBundledPath(), false)) { Kubectl.invalidBundle = true - return path.basename(bundledPath) + return path.basename(this.getBundledPath()) } try { if (!await this.ensureKubectl()) { logger.error("Failed to ensure kubectl, fallback to the bundled version") - return Kubectl.bundledKubectlPath + return this.getBundledPath() } return this.path } catch (err) { logger.error("Failed to ensure kubectl, fallback to the bundled version") logger.error(err) - return Kubectl.bundledKubectlPath + return this.getBundledPath() } } @@ -183,7 +188,7 @@ export class Kubectl { try { const exist = await pathExists(this.path) if (!exist) { - await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path) + await fs.promises.copyFile(this.getBundledPath(), this.path) await fs.promises.chmod(this.path, 0o755) } return true @@ -332,6 +337,3 @@ export class Kubectl { return packageMirrors.get("default") // MacOS packages are only available from default } } - -const bundledKubectl = Kubectl.bundled() -export { bundledKubectl } diff --git a/src/main/kubectl_spec.ts b/src/main/kubectl_spec.ts index 4e5cdbf986..ade999c082 100644 --- a/src/main/kubectl_spec.ts +++ b/src/main/kubectl_spec.ts @@ -1,20 +1,20 @@ import packageInfo from "../../package.json" import path from "path" -import { bundledKubectl, Kubectl } from "../../src/main/kubectl"; +import { Kubectl } from "../../src/main/kubectl"; import { isWindows } from "../common/vars"; jest.mock("../common/user-store"); describe("kubectlVersion", () => { it("returns bundled version if exactly same version used", async () => { - const kubectl = new Kubectl(bundledKubectl.kubectlVersion) - expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) + const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion) + expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion) }) it("returns bundled version if same major.minor version is used", async () => { const { bundledKubectlVersion } = packageInfo.config; const kubectl = new Kubectl(bundledKubectlVersion); - expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion) + expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion) }) }) diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 3f1a855902..7ed79aa936 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -1,7 +1,7 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" import { spawn, ChildProcessWithoutNullStreams } from "child_process" -import { bundledKubectl } from "../kubectl" +import { Kubectl } from "../kubectl" import { getFreePort } from "../port" import { shell } from "electron" import * as tcpPortUsed from "tcp-port-used" @@ -37,7 +37,7 @@ class PortForward { public async start() { this.localPort = await getFreePort() - const kubectlBin = await bundledKubectl.getPath() + const kubectlBin = await Kubectl.bundled().getPath() const args = [ "--kubeconfig", this.kubeConfig, "port-forward", diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 962074c803..2367bb63c0 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -38,7 +38,7 @@ export class ShellSession extends EventEmitter { public async open() { this.kubectlBinDir = await this.kubectl.binDir() - const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath + const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath() this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences) this.helmBinDir = helmCli.getBinaryDir() const env = await this.getCachedShellEnv() diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 22735c3c09..e4a5432965 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -3,7 +3,7 @@ import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuP import type React from "react"; import { observable } from "mobx"; -import { autobind } from "../utils/autobind"; +import { autobind } from "../utils"; import { KubeApi } from "./kube-api"; export interface ApiComponents { diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index e1545f09c7..e1c91f6e36 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -42,12 +42,14 @@ export interface IPodMetrics { networkTransmit: T; } +// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core export interface IPodLogsQuery { container?: string; tailLines?: number; timestamps?: boolean; sinceTime?: string; // Date.toISOString()-format follow?: boolean; + previous?: boolean; } export enum PodStatus { diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index 06c4f3ec83..002ddfdcc3 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -6,7 +6,7 @@ import { Input } from '../input'; import { SubTitle } from '../layout/sub-title'; import { UserPreferences, userStore } from '../../../common/user-store'; import { observer } from 'mobx-react'; -import { Kubectl } from '../../../main/kubectl'; +import { bundledKubectlPath } from '../../../main/kubectl'; import { SelectOption, Select } from '../select'; export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { @@ -58,7 +58,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre { initContainers: pod.getInitContainers(), selectedContainer: container, showTimestamps: false, + previous: false, tailLines: 1000 }); } diff --git a/src/renderer/components/dock/pod-logs.store.ts b/src/renderer/components/dock/pod-logs.store.ts index bf2bf9eb5c..fa1da38c68 100644 --- a/src/renderer/components/dock/pod-logs.store.ts +++ b/src/renderer/components/dock/pod-logs.store.ts @@ -13,6 +13,7 @@ export interface IPodLogsData { initContainers: IPodContainer[] showTimestamps: boolean tailLines: number + previous: boolean } type TabId = string; @@ -48,7 +49,7 @@ export class PodLogsStore extends DockTabStore { } const data = this.getData(tabId); const { oldLogs, newLogs } = this.logs.get(tabId); - const { selectedContainer, tailLines } = data; + const { selectedContainer, tailLines, previous } = data; const pod = new Pod(data.pod); try { // if logs already loaded, check the latest timestamp for getting updates only from this point @@ -64,14 +65,15 @@ export class PodLogsStore extends DockTabStore { sinceTime: lastLogDate.toISOString(), timestamps: true, // Always setting timestampt to separate old logs from new ones container: selectedContainer.name, - tailLines: tailLines, + tailLines, + previous }); if (!oldLogs) { this.logs.set(tabId, { oldLogs: loadedLogs, newLogs }); } else { this.logs.set(tabId, { oldLogs, newLogs: loadedLogs }); } - } catch (error) { + } catch ({error}) { this.logs.set(tabId, { oldLogs: [ _i18n._(t`Failed to load logs: ${error.message}`), diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index 6f97163bde..06643bc711 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -94,6 +94,11 @@ export class PodLogs extends React.Component { this.save({ showTimestamps: !this.tabData.showTimestamps }); } + togglePrevious = () => { + this.save({ previous: !this.tabData.previous }); + this.reload(); + } + onScroll = (evt: React.UIEvent) => { const logsArea = evt.currentTarget; const { scrollHeight, clientHeight, scrollTop } = logsArea; @@ -148,7 +153,7 @@ export class PodLogs extends React.Component { renderControls() { if (!this.ready) return null; - const { selectedContainer, showTimestamps, tailLines } = this.tabData; + const { selectedContainer, showTimestamps, tailLines, previous } = this.tabData; const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs); return (
@@ -181,6 +186,12 @@ export class PodLogs extends React.Component { className={cssNames("timestamps-icon", { active: showTimestamps })} tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)} /> + type === "url", message: () => _i18n._(t`Wrong url format`), - validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), + validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), }; export const isPath: Validator = { diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 5245d2a728..539be6651d 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -1,5 +1,5 @@ import { computed, observable, reaction } from "mobx"; -import { autobind } from "./utils/autobind"; +import { autobind } from "./utils"; import { userStore } from "../common/user-store"; import logger from "../main/logger"; diff --git a/src/renderer/utils/debouncePromise.ts b/src/renderer/utils/debouncePromise.ts deleted file mode 100755 index 4ce949a944..0000000000 --- a/src/renderer/utils/debouncePromise.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Debouncing promise evaluation - -export const debouncePromise = function (promisedFunc: Function, timeout = 0) { - let timer: number; - return (...params: any[]) => new Promise((resolve, reject) => { - clearTimeout(timer); - timer = window.setTimeout(() => resolve(promisedFunc.apply(this, params)), timeout); - }); -}; diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 578ec5c355..4ae9b068a7 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -3,21 +3,18 @@ export const noop: any = Function(); export const isElectron = !!navigator.userAgent.match(/Electron/); -export * from '../../common/utils/camelCase' -export * from '../../common/utils/base64' +export * from "../../common/utils" -export * from './autobind' -export * from './cssVar' -export * from './cssNames' -export * from './eventEmitter' -export * from './downloadFile' -export * from './prevDefault' -export * from './createStorage' -export * from './interval' -export * from './debouncePromise' -export * from './copyToClipboard' -export * from './formatDuration' -export * from './isReactNode' -export * from './convertMemory' -export * from './convertCpu' -export * from './metricUnitsToNumber' +export * from "./cssVar" +export * from "./cssNames" +export * from "./eventEmitter" +export * from "./downloadFile" +export * from "./prevDefault" +export * from "./createStorage" +export * from "./interval" +export * from "./copyToClipboard" +export * from "./formatDuration" +export * from "./isReactNode" +export * from "./convertMemory" +export * from "./convertCpu" +export * from "./metricUnitsToNumber"