Merge branch 'master' into react-table-with-resizable-columns

This commit is contained in:
Alex Andreev committed 2022-08-16 10:33:00 +03:00
commit 7d2dfaf179
63 files changed
+1844 -1132

No files matched your search

@@ -8,6 +8,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
import getRandomIdInjectable from "../../common/utils/get-random-id.injectable";
describe("clicking tray menu item originating from extension", () => {
let applicationBuilder: ApplicationBuilder;
@@ -20,6 +21,7 @@ describe("clicking tray menu item originating from extension", () => {
logErrorMock = jest.fn();
mainDi.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger);
mainDi.override(getRandomIdInjectable, () => () => "some-random-id");
});
await applicationBuilder.render();
@@ -42,7 +44,7 @@ describe("clicking tray menu item originating from extension", () => {
it("when item is clicked, triggers the click handler", () => {
applicationBuilder.tray.click(
"some-label-tray-menu-item-for-extension-some-extension-id",
"some-random-id-tray-menu-item-for-extension-some-extension-id",
);
expect(clickMock).toHaveBeenCalled();
@@ -55,13 +57,13 @@ describe("clicking tray menu item originating from extension", () => {
});
applicationBuilder.tray.click(
"some-label-tray-menu-item-for-extension-some-extension-id",
"some-random-id-tray-menu-item-for-extension-some-extension-id",
);
});
it("logs the error", () => {
expect(logErrorMock).toHaveBeenCalledWith(
'[TRAY]: Clicking of tray item "some-label" from extension "some-extension-id" failed.',
'[TRAY]: Clicking of tray item "some-random-id" from extension "some-extension-id" failed.',
expect.any(Error),
);
});
@@ -72,13 +74,13 @@ describe("clicking tray menu item originating from extension", () => {
clickMock.mockImplementation(() => Promise.reject("some-rejection"));
applicationBuilder.tray.click(
"some-label-tray-menu-item-for-extension-some-extension-id",
"some-random-id-tray-menu-item-for-extension-some-extension-id",
);
});
it("logs the error", () => {
expect(logErrorMock).toHaveBeenCalledWith(
'[TRAY]: Clicking of tray item "some-label" from extension "some-extension-id" failed.',
'[TRAY]: Clicking of tray item "some-random-id" from extension "some-extension-id" failed.',
"some-rejection",
);
});
@@ -92,7 +94,7 @@ describe("clicking tray menu item originating from extension", () => {
it("does not have the tray menu item from extension", () => {
expect(
applicationBuilder.tray.get(
"some-label-tray-menu-item-for-extension-some-extension-id",
"some-random-id-tray-menu-item-for-extension-some-extension-id",
),
).toBeNull();
});
@@ -103,7 +105,7 @@ describe("clicking tray menu item originating from extension", () => {
expect(
applicationBuilder.tray.get(
"some-label-tray-menu-item-for-extension-some-extension-id",
"some-random-id-tray-menu-item-for-extension-some-extension-id",
),
).not.toBeNull();
});
@@ -13,6 +13,7 @@ describe("preferences: extension adding tray items", () => {
let builder: ApplicationBuilder;
let someObservableForVisibility: IObservableValue<boolean>;
let someObservableForEnabled: IObservableValue<boolean>;
let someObservableLabel: IObservableValue<string>;
beforeEach(async () => {
builder = getApplicationBuilder();
@@ -25,6 +26,7 @@ describe("preferences: extension adding tray items", () => {
someObservableForVisibility = observable.box(false);
someObservableForEnabled = observable.box(false);
someObservableLabel = observable.box("Some label");
const testExtension = getExtensionFake({
id: "some-extension-id",
@@ -33,38 +35,51 @@ describe("preferences: extension adding tray items", () => {
mainOptions: {
trayMenus: [
{
id: "some-controlled-visibility",
label: "some-controlled-visibility",
click: () => {},
visible: computed(() => someObservableForVisibility.get()),
},
{
id: "some-uncontrolled-visibility",
label: "some-uncontrolled-visibility",
click: () => {},
},
{
id: "some-controlled-enabled",
label: "some-controlled-enabled",
click: () => {},
enabled: computed(() => someObservableForEnabled.get()),
},
{
id: "some-uncontrolled-enabled",
label: "some-uncontrolled-enabled",
click: () => {},
},
{
id: "some-statically-enabled",
label: "some-statically-enabled",
click: () => {},
enabled: true,
},
{
id: "some-statically-disabled",
label: "some-statically-disabled",
click: () => {},
enabled: false,
},
{
id: "some-item-with-controlled-label",
label: computed(() => someObservableLabel.get()),
click: () => {},
enabled: true,
},
],
},
});
@@ -72,6 +87,37 @@ describe("preferences: extension adding tray items", () => {
builder.extensions.enable(testExtension);
});
describe("given controlled label", () => {
it("has the label", () => {
const item = builder.tray.get(
"some-item-with-controlled-label-tray-menu-item-for-extension-some-extension",
);
expect(item?.label).toBe("Some label");
});
it("when label changes, updates the label", () => {
runInAction(() => {
someObservableLabel.set("Some new label");
});
const item = builder.tray.get(
"some-item-with-controlled-label-tray-menu-item-for-extension-some-extension",
);
expect(item?.label).toBe("Some new label");
});
});
it("given item is statically disabled, item is disabled", () => {
const item = builder.tray.get(
"some-statically-disabled-tray-menu-item-for-extension-some-extension",
);
expect(item?.enabled).toBe(false);
});
it("shows item which doesn't control the visibility", () => {
expect(
builder.tray.get(
@@ -6,7 +6,6 @@
import { forRemoteCluster, KubeApi } from "../kube-api";
import { KubeJsonApi } from "../kube-json-api";
import { KubeObject } from "../kube-object";
import AbortController from "abort-controller";
import { delay } from "../../utils/delay";
import { PassThrough } from "stream";
import { ApiManager } from "../api-manager";
@@ -58,11 +58,11 @@ export interface ResourceMetricSource {
}
export interface BaseHorizontalPodAutoscalerMetricSpec {
resource: ResourceMetricSource;
object: ObjectMetricSource;
external: ExternalMetricSource;
pods: PodsMetricSource;
containerResource: ContainerResourceMetricSource;
external: ExternalMetricSource;
object: ObjectMetricSource;
pods: PodsMetricSource;
resource: ResourceMetricSource;
}
export type HorizontalPodAutoscalerMetricSpec =
@@ -72,6 +72,55 @@ export type HorizontalPodAutoscalerMetricSpec =
| OptionVarient<HpaMetricType.Pods, BaseHorizontalPodAutoscalerMetricSpec, "pods">
| OptionVarient<HpaMetricType.ContainerResource, BaseHorizontalPodAutoscalerMetricSpec, "containerResource">;
export interface ContainerResourceMetricStatus {
container: string;
currentAverageUtilization?: number;
currentAverageValue: string;
name: string;
}
export interface ExternalMetricStatus {
currentAverageValue?: string;
currentValue: string;
metricName: string;
metricSelector?: LabelSelector;
}
export interface ObjectMetricStatus {
averageValue?: string;
currentValue?: string;
metricName: string;
selector?: LabelSelector;
target: CrossVersionObjectReference;
}
export interface PodsMetricStatus {
currentAverageValue: string;
metricName: string;
selector?: LabelSelector;
}
export interface ResourceMetricStatus {
currentAverageUtilization?: number;
currentAverageValue: string;
name: string;
}
export interface BaseHorizontalPodAutoscalerMetricStatus {
containerResource: ContainerResourceMetricStatus;
external: ExternalMetricStatus;
object: ObjectMetricStatus;
pods: PodsMetricStatus;
resource: ResourceMetricStatus;
}
export type HorizontalPodAutoscalerMetricStatus =
| OptionVarient<HpaMetricType.Resource, BaseHorizontalPodAutoscalerMetricStatus, "resource">
| OptionVarient<HpaMetricType.External, BaseHorizontalPodAutoscalerMetricStatus, "external">
| OptionVarient<HpaMetricType.Object, BaseHorizontalPodAutoscalerMetricStatus, "object">
| OptionVarient<HpaMetricType.Pods, BaseHorizontalPodAutoscalerMetricStatus, "pods">
| OptionVarient<HpaMetricType.ContainerResource, BaseHorizontalPodAutoscalerMetricStatus, "containerResource">;
export interface CrossVersionObjectReference {
kind: string;
name: string;
@@ -89,7 +138,7 @@ export interface HorizontalPodAutoscalerStatus {
conditions?: BaseKubeObjectCondition[];
currentReplicas: number;
desiredReplicas: number;
currentMetrics: HorizontalPodAutoscalerMetricSpec[];
currentMetrics?: HorizontalPodAutoscalerMetricStatus[];
}
interface MetricCurrentTarget {
@@ -142,114 +191,11 @@ export class HorizontalPodAutoscaler extends KubeObject<
return this.status?.currentMetrics ?? [];
}
protected getMetricName(metric: HorizontalPodAutoscalerMetricSpec): string {
switch (metric.type) {
case HpaMetricType.Resource:
return metric.resource.name;
case HpaMetricType.Pods:
return metric.pods.metricName;
case HpaMetricType.Object:
return metric.object.metricName;
case HpaMetricType.External:
return metric.external.metricName;
case HpaMetricType.ContainerResource:
return metric.containerResource.name;
default:
return `<unknown metric type: ${(metric as HorizontalPodAutoscalerMetricSpec).type}>`;
}
}
protected getResourceMetricValue(currentMetric: ResourceMetricSource | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.targetAverageUtilization
? `${currentMetric.targetAverageUtilization}%`
: currentMetric?.targetAverageValue
),
target: (
targetMetric?.targetAverageUtilization
? `${targetMetric.targetAverageUtilization}%`
: targetMetric?.targetAverageValue
),
};
}
protected getPodsMetricValue(currentMetric: PodsMetricSource | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget {
return {
current: currentMetric?.targetAverageValue,
target: targetMetric?.targetAverageValue,
};
}
protected getObjectMetricValue(currentMetric: ObjectMetricSource | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.targetValue
?? currentMetric?.averageValue
),
target: (
targetMetric?.targetValue
?? targetMetric?.averageValue
),
};
}
protected getExternalMetricValue(currentMetric: ExternalMetricSource | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.targetValue
?? currentMetric?.targetAverageValue
),
target: (
targetMetric?.targetValue
?? targetMetric?.targetAverageValue
),
};
}
protected getContainerResourceMetricValue(currentMetric: ContainerResourceMetricSource | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.targetAverageUtilization
? `${currentMetric.targetAverageUtilization}%`
: currentMetric?.targetAverageValue
),
target: (
targetMetric?.targetAverageUtilization
? `${targetMetric.targetAverageUtilization}%`
: targetMetric?.targetAverageValue
),
};
}
protected getMetricCurrentTarget(metric: HorizontalPodAutoscalerMetricSpec): MetricCurrentTarget {
const currentMetric = this.getMetrics()
.find(m => (
m.type === metric.type
&& this.getMetricName(m) === this.getMetricName(metric)
));
switch (metric.type) {
case HpaMetricType.Resource:
return this.getResourceMetricValue(currentMetric?.resource, metric.resource);
case HpaMetricType.Pods:
return this.getPodsMetricValue(currentMetric?.pods, metric.pods);
case HpaMetricType.Object:
return this.getObjectMetricValue(currentMetric?.object, metric.object);
case HpaMetricType.External:
return this.getExternalMetricValue(currentMetric?.external, metric.external);
case HpaMetricType.ContainerResource:
return this.getContainerResourceMetricValue(currentMetric?.containerResource, metric.containerResource);
default:
return {};
}
}
getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string {
const {
current = "unknown",
target = "unknown",
} = this.getMetricCurrentTarget(metric);
} = getMetricCurrentTarget(metric, this.getCurrentMetrics());
return `${current} / ${target}`;
}
@@ -263,3 +209,105 @@ export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler>
});
}
}
function getMetricName(metric: HorizontalPodAutoscalerMetricSpec | HorizontalPodAutoscalerMetricStatus): string | undefined {
switch (metric.type) {
case HpaMetricType.Resource:
return metric.resource.name;
case HpaMetricType.Pods:
return metric.pods.metricName;
case HpaMetricType.Object:
return metric.object.metricName;
case HpaMetricType.External:
return metric.external.metricName;
case HpaMetricType.ContainerResource:
return metric.containerResource.name;
default:
return undefined;
}
}
function getResourceMetricValue(currentMetric: ResourceMetricStatus | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget {
return {
current: (
typeof currentMetric?.currentAverageUtilization === "number"
? `${currentMetric.currentAverageUtilization}%`
: currentMetric?.currentAverageValue
),
target: (
typeof targetMetric?.targetAverageUtilization === "number"
? `${targetMetric.targetAverageUtilization}%`
: targetMetric?.targetAverageValue
),
};
}
function getPodsMetricValue(currentMetric: PodsMetricStatus | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget {
return {
current: currentMetric?.currentAverageValue,
target: targetMetric?.targetAverageValue,
};
}
function getObjectMetricValue(currentMetric: ObjectMetricStatus | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.currentValue
?? currentMetric?.averageValue
),
target: (
targetMetric?.targetValue
?? targetMetric?.averageValue
),
};
}
function getExternalMetricValue(currentMetric: ExternalMetricStatus | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget {
return {
current: (
currentMetric?.currentValue
?? currentMetric?.currentAverageValue
),
target: (
targetMetric?.targetValue
?? targetMetric?.targetAverageValue
),
};
}
function getContainerResourceMetricValue(currentMetric: ContainerResourceMetricStatus | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget {
return {
current: (
typeof currentMetric?.currentAverageUtilization === "number"
? `${currentMetric.currentAverageUtilization}%`
: currentMetric?.currentAverageValue
),
target: (
typeof targetMetric?.targetAverageUtilization === "number"
? `${targetMetric.targetAverageUtilization}%`
: targetMetric?.targetAverageValue
),
};
}
function getMetricCurrentTarget(spec: HorizontalPodAutoscalerMetricSpec, status: HorizontalPodAutoscalerMetricStatus[]): MetricCurrentTarget {
const currentMetric = status.find(m => (
m.type === spec.type
&& getMetricName(m) === getMetricName(spec)
));
switch (spec.type) {
case HpaMetricType.Resource:
return getResourceMetricValue(currentMetric?.resource, spec.resource);
case HpaMetricType.Pods:
return getPodsMetricValue(currentMetric?.pods, spec.pods);
case HpaMetricType.Object:
return getObjectMetricValue(currentMetric?.object, spec.object);
case HpaMetricType.External:
return getExternalMetricValue(currentMetric?.external, spec.external);
case HpaMetricType.ContainerResource:
return getContainerResourceMetricValue(currentMetric?.containerResource, spec.containerResource);
default:
return {};
}
}
+4 -2
View File
@@ -19,7 +19,6 @@ import { KubeJsonApi } from "./kube-json-api";
import type { Disposer } from "../utils";
import { isDefined, noop, WrappedAbortController } from "../utils";
import type { RequestInit } from "node-fetch";
import type AbortController from "abort-controller";
import type { AgentOptions } from "https";
import { Agent } from "https";
import type { Patch } from "rfc6902";
@@ -29,6 +28,9 @@ import logger from "../logger";
import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import autoRegistrationEmitterInjectable from "./api-manager/auto-registration-emitter.injectable";
// TODO: upgrade node-fetch once we are starting to use ES modules
type LegacyAbortSignal = NonNullable<RequestInit["signal"]>;
/**
* The options used for creating a `KubeApi`
*/
@@ -717,7 +719,7 @@ export class KubeApi<
const requestParams = timeout ? { query: { timeoutSeconds: timeout }} : {};
const watchUrl = this.getWatchUrl(namespace);
const responsePromise = this.request.getResponse(watchUrl, requestParams, {
signal: abortController.signal,
signal: abortController.signal as LegacyAbortSignal,
timeout: 600_000,
});
+5 -2
View File
@@ -15,13 +15,15 @@ import { ItemStore } from "../item.store";
import type { KubeApiQueryParams, KubeApi, KubeApiWatchCallback } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller";
import type { Patch } from "rfc6902";
import logger from "../logger";
import assert from "assert";
import type { PartialDeep } from "type-fest";
import { entries } from "../utils/objects";
// TODO: upgrade node-fetch once we are starting to use ES modules
type LegacyAbortSignal = NonNullable<RequestInit["signal"]>;
export type OnLoadFailure = (error: unknown) => void;
export interface KubeObjectStoreLoadingParams {
@@ -477,7 +479,8 @@ export abstract class KubeObjectStore<
callback,
});
const { signal } = abortController;
// TODO: upgrade node-fetch once we are starting to use ES modules
const signal = abortController.signal as LegacyAbortSignal;
const callback: KubeApiWatchCallback<D> = (data, error) => {
if (!this.isLoaded || error?.type === "aborted") return;
@@ -4,6 +4,11 @@
*/
import type { Injectable } from "@ogre-tools/injectable";
export interface GlobalOverride {
injectable: Injectable<any, any, any>;
overridingInstantiate: any;
}
export const getGlobalOverride = <T extends Injectable<any, any, any>>(
injectable: T,
overridingInstantiate: T["instantiate"],
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import { unionPATHs } from "../union-env-path";
describe("unionPATHs", () => {
it("return the same path if given only one with no double delimiters", () => {
expect(unionPATHs(`/bin/bar${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
});
it("return equivalent path if given only one with no double delimiters", () => {
expect(unionPATHs(`/bin/bar${path.delimiter}${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
});
it("should remove duplicate entries, appending non duplicates in order received", () => {
expect(unionPATHs(
`/bin/bar${path.delimiter}/usr/bin`,
`/bin/bar${path.delimiter}/usr/lens/bat`,
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat`);
});
it("should remove duplicate entries, appending non duplicates in order received, 3", () => {
expect(unionPATHs(
`/bin/bar${path.delimiter}/usr/bin`,
`/bin/bar${path.delimiter}/usr/lens/bat`,
`/usr/local/lens${path.delimiter}/usr/bin`,
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat${path.delimiter}/usr/local/lens`);
});
});
+5 -2
View File
@@ -2,10 +2,13 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import AbortController from "abort-controller";
/**
* This is like an `AbortController` but will also abort if the parent aborts,
* but won't make the parent abort if this aborts (single direction)
*/
export class WrappedAbortController extends AbortController {
constructor(parent?: AbortController) {
constructor(parent?: AbortController | undefined) {
super();
parent?.signal.addEventListener("abort", () => {
+29 -9
View File
@@ -3,18 +3,38 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { TypedRegEx } from "typed-regex";
// Helper to convert CPU K8S units to numbers
const thousand = 1000;
const million = thousand * thousand;
const shortBillion = thousand * million;
const unitConverters = new Map([
["n", 1000 ** -3],
["u", 1000 ** -2],
["m", 1000 ** -1], // milli
["", 1000 ** 0], // no units
["k", 1000 ** 1],
["M", 1000 ** 2],
["G", 1000 ** 3],
["P", 1000 ** 4],
["T", 1000 ** 5],
["E", 1000 ** 6],
]);
export function cpuUnitsToNumber(cpu: string) {
const cpuNum = parseInt(cpu);
const cpuUnitsRegex = TypedRegEx("^(?<digits>[+-]?[0-9.]+(e[-+]?[0-9]+)?)(?<unit>[EinumkKMGTP]*)$");
if (cpu.includes("m")) return cpuNum / thousand;
if (cpu.includes("u")) return cpuNum / million;
if (cpu.includes("n")) return cpuNum / shortBillion;
export function cpuUnitsToNumber(value: string) {
const match = cpuUnitsRegex.captures(value);
return parseFloat(cpu);
if (!match) {
return undefined;
}
const { digits = "", unit } = match;
const conversion = unitConverters.get(unit);
if (conversion === undefined) {
return undefined;
}
return parseFloat(digits) * conversion;
}
-2
View File
@@ -3,8 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AbortController } from "abort-controller";
/**
* Return a promise that will be resolved after at least `timeout` ms have
* passed. If `failFast` is provided then the promise is also resolved if it has
-1
View File
@@ -22,7 +22,6 @@ export * from "./hash-set";
export * from "./n-fircate";
export * from "./noop";
export * from "./observable-crate/impl";
export * from "./openBrowser";
export * from "./paths";
export * from "./promise-exec";
export * from "./readonly";
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import openLinkInBrowserInjectable from "./open-link-in-browser.injectable";
export default getGlobalOverride(openLinkInBrowserInjectable, () => async () => {});
@@ -0,0 +1,28 @@
/**
* 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 { shell } from "electron";
const allowedProtocols = new Set(["http:", "https:"]);
export type OpenLinkInBrowser = (url: string) => Promise<void>;
const openLinkInBrowserInjectable = getInjectable({
id: "open-link-in-browser",
instantiate: (): OpenLinkInBrowser => (
async (url) => {
const { protocol } = new URL(url);
if (!allowedProtocols.has(protocol)) {
throw new TypeError("not an http(s) URL");
}
await shell.openExternal(url);
}
),
causesSideEffects: true,
});
export default openLinkInBrowserInjectable;
-29
View File
@@ -1,29 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { shell } from "electron";
const allowedProtocols = new Set(["http:", "https:"]);
/**
* Opens a link using the program configured as the default browser
* on the target platform. Will reject URLs with a scheme other than
* http or https to prevent programs other than the default browser
* running.
*
* @param url The URL to open
*/
export function openBrowser(url: string): Promise<void> {
if (allowedProtocols.has(new URL(url).protocol)) {
return shell.openExternal(url);
}
return Promise.reject(new TypeError("not an http(s) URL"));
}
/**
* @deprecated use openBrowser
*/
export const openExternal = openBrowser;
-2
View File
@@ -3,8 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AbortSignal } from "abort-controller";
/**
* Creates a new promise that will be rejected when the signal rejects.
*
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import * as iter from "./iter";
/**
* Join all entires with a PATH env var delimated string together
* @param PATHs Any number of PATH env variables
*
* NOTE: This function does not attempt to handle any sort of escape sequences since after testing
* it was found that `zsh` (at least on `macOS`) does not when trying to find programs
*/
export function unionPATHs(...PATHs: string[]): string {
const entries = new Set(iter.filterFlatMap(PATHs, PATH => PATH.split(path.delimiter)));
return iter.join(entries.values(), path.delimiter);
}
+10 -1
View File
@@ -3,6 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export { Singleton, openExternal, openBrowser, getAppVersion } from "../../common/utils";
import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
export { Singleton, getAppVersion } from "../../common/utils";
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
export { cssNames } from "../../renderer/utils/cssNames";
/**
* @deprecated Use {@link openBrowser} instead
*/
export const openExternal = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable);
export const openBrowser = asLegacyGlobalFunctionForExtensionApi(openLinkInBrowserInjectable);
+12
View File
@@ -8,6 +8,8 @@ import configurePackages from "./common/configure-packages";
import { configure } from "mobx";
import { setImmediate } from "timers";
import { TextEncoder, TextDecoder as TextDecoderNode } from "util";
import glob from "glob";
import path from "path";
// setup default configuration for external npm-packages
configurePackages();
@@ -45,3 +47,13 @@ global.ResizeObserver = class {
jest.mock("./renderer/components/monaco-editor/monaco-editor");
jest.mock("./renderer/components/tooltip/withTooltip");
const getInjectables = (environment: "renderer" | "main", filePathGlob: string) =>
glob.sync(`./{common,extensions,${environment}}/**/${filePathGlob}`, {
cwd: __dirname,
}).map(x => path.resolve(__dirname, x));
(global as any).rendererInjectablePaths = getInjectables("renderer", "*.injectable.{ts,tsx}");
(global as any).rendererGlobalOverridePaths = getInjectables("renderer", "*.global-override-for-injectable.{ts,tsx}");
(global as any).mainInjectablePaths = getInjectables("main", "*.injectable.{ts,tsx}");
(global as any).mainGlobalOverridePaths = getInjectables("main", "*.global-override-for-injectable.{ts,tsx}");
@@ -4,8 +4,8 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
import updatingIsEnabledInjectable from "../updating-is-enabled.injectable";
import { afterApplicationIsLoadedInjectionToken } from "../../start-main-application/runnable-tokens/after-application-is-loaded-injection-token";
const startCheckingForUpdatesInjectable = getInjectable({
id: "start-checking-for-updates",
@@ -23,7 +23,7 @@ const startCheckingForUpdatesInjectable = getInjectable({
};
},
injectionToken: afterRootFrameIsReadyInjectionToken,
injectionToken: afterApplicationIsLoadedInjectionToken,
});
export default startCheckingForUpdatesInjectable;
@@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeQuitOfFrontEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopCheckingForUpdatesInjectable = getInjectable({
id: "stop-checking-for-updates",
@@ -21,7 +21,7 @@ const stopCheckingForUpdatesInjectable = getInjectable({
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopCheckingForUpdatesInjectable;
@@ -11,9 +11,8 @@ import { watch } from "chokidar";
import type { Stats } from "fs";
import fs from "fs";
import path from "path";
import type stream from "stream";
import type { Disposer } from "../../../common/utils";
import { bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils";
import { disposer, bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils";
import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
@@ -265,47 +264,35 @@ const diffChangedConfigFor = (dependencies: ComputeDiffDependencies) => ({ fileP
return noop;
}
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
const fileReader = fs.createReadStream(filePath, {
mode: fs.constants.O_RDONLY,
const controller = new AbortController();
const fileContentsP = fs.promises.readFile(filePath, {
signal: controller.signal,
});
const readStream: stream.Readable = fileReader;
const decoder = new TextDecoder("utf-8", { fatal: true });
let fileString = "";
let closed = false;
const cleanup = disposer(
() => controller.abort(),
);
const cleanup = () => {
closed = true;
fileReader.close(); // This may not close the stream.
// Artificially marking end-of-stream, as if the underlying resource had
// indicated end-of-file by itself, allows the stream to close.
// This does not cancel pending read operations, and if there is such an
// operation, the process may still not be able to exit successfully
// until it finishes.
fileReader.push(null);
fileReader.read(0);
readStream.removeAllListeners();
};
fileContentsP
.then((fileData) => {
const decoder = new TextDecoder("utf-8", { fatal: true });
readStream
.on("data", (chunk: Buffer) => {
try {
fileString += decoder.decode(chunk, { stream: true });
const fileString = decoder.decode(fileData);
computeDiff(dependencies)(fileString, source, filePath);
} catch (error) {
logger.warn(`${logPrefix} skipping ${filePath}: ${error}`);
source.clear();
cleanup();
}
})
.on("close", () => cleanup())
.on("error", error => {
cleanup();
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
})
.on("end", () => {
if (!closed) {
computeDiff(dependencies)(fileString, source, filePath);
.catch(error => {
if (controller.signal.aborted) {
return;
}
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
cleanup();
});
return cleanup;
@@ -96,8 +96,6 @@ export class DistributionDetector extends BaseClusterDetector {
}
public async getKubernetesVersion() {
if (this.cluster.version) return this.cluster.version;
const response = await this.k8sRequest("/version");
return response.gitVersion;
+7 -23
View File
@@ -3,8 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import glob from "glob";
import { kebabCase, memoize, noop, chunk } from "lodash/fp";
import { kebabCase, noop, chunk } from "lodash/fp";
import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import { createContainer } from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
@@ -101,6 +100,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
import type { HotbarStore } from "../common/hotbars/store";
import focusApplicationInjectable from "./electron-app/features/focus-application.injectable";
import type { GlobalOverride } from "../common/test-utils/get-global-override";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
@@ -113,9 +113,9 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
setLegacyGlobalDiForExtensionApi(di, Environments.main);
const filePaths = getInjectableFilePaths();
const injectables = filePaths.map(filePath => require(filePath).default);
const injectables: Injectable<any, any, any>[] = (global as any).mainInjectablePaths.map(
(filePath: string) => require(filePath).default,
);
chunk(100)(injectables).forEach(chunkInjectables => {
di.register(...chunkInjectables);
@@ -124,10 +124,8 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.preventSideEffects();
if (doGeneralOverrides) {
const globalOverrideFilePaths = getGlobalOverridePaths();
const globalOverrides = globalOverrideFilePaths.map(
(filePath) => require(filePath).default,
const globalOverrides: GlobalOverride[] = (global as any).mainGlobalOverridePaths.map(
(filePath: string) => require(filePath).default,
);
globalOverrides.forEach(globalOverride => {
@@ -215,20 +213,6 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
return di;
}
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);
const getGlobalOverridePaths = memoize(() =>
glob.sync(
"../{common,extensions,main}/**/*.global-override-for-injectable.{ts,tsx}",
{ cwd: __dirname },
),
);
// TODO: Reorganize code in Runnables to get rid of requirement for override
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
[
@@ -5,7 +5,6 @@
import { getInjectable } from "@ogre-tools/injectable";
import { docsUrl, productName, supportUrl } from "../../common/vars";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
import type { MenuItemConstructorOptions } from "electron";
import { webContents } from "electron";
import loggerInjectable from "../../common/logger.injectable";
@@ -25,6 +24,7 @@ import applicationWindowInjectable from "../start-main-application/lens-window/a
import reloadWindowInjectable from "../start-main-application/lens-window/reload-window.injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
import processCheckingForUpdatesInjectable from "../application-update/check-for-updates/process-checking-for-updates.injectable";
import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable";
function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) {
return check ? [] : menuItems;
@@ -54,6 +54,7 @@ const applicationMenuItemsInjectable = getInjectable({
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
logger.info(`[MENU]: autoUpdateEnabled=${updatingIsEnabled}`);
@@ -260,7 +261,7 @@ const applicationMenuItemsInjectable = getInjectable({
label: "Documentation",
id: "documentation",
click: async () => {
openBrowser(docsUrl).catch((error) => {
openLinkInBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
@@ -269,7 +270,7 @@ const applicationMenuItemsInjectable = getInjectable({
label: "Support",
id: "support",
click: async () => {
openBrowser(supportUrl).catch((error) => {
openLinkInBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
+3 -1
View File
@@ -8,6 +8,7 @@ import os from "os";
import { app } from "electron";
import logger from "./logger";
import { isSnap } from "../common/vars";
import { unionPATHs } from "../common/utils/union-env-path";
/**
* shellSync loads what would have been the environment if this application was
@@ -25,7 +26,8 @@ export async function shellSync() {
}
if (!isSnap) {
process.env.PATH = env.PATH;
// Prefer the synced PATH over the initial one
process.env.PATH = unionPATHs(env.PATH ?? "", process.env.PATH ?? "");
}
// The spread operator allows joining of objects. The precedence is last to first.
@@ -6,10 +6,10 @@ import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import applicationWindowStateInjectable from "./application-window-state.injectable";
import { BrowserWindow } from "electron";
import { openBrowser } from "../../../../common/utils";
import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable";
import type { ElectronWindow } from "./create-lens-window.injectable";
import type { RequireExactlyOne } from "type-fest";
import openLinkInBrowserInjectable from "../../../../common/utils/open-link-in-browser.injectable";
export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover";
@@ -46,6 +46,7 @@ const createElectronWindowInjectable = getInjectable({
instantiate: (di): CreateElectronWindow => {
const logger = di.inject(loggerInjectable);
const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable);
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
return (configuration) => {
const applicationWindowState = di.inject(
@@ -76,9 +77,7 @@ const createElectronWindowInjectable = getInjectable({
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
webviewTag: true,
contextIsolation: false,
nativeWindowOpen: false,
},
});
@@ -88,20 +87,16 @@ const createElectronWindowInjectable = getInjectable({
.on("focus", () => {
configuration.onFocus?.();
})
.on("blur", () => {
configuration.onBlur?.();
})
.on("closed", () => {
configuration.onClose();
applicationWindowState.unmanage();
})
.webContents.on("dom-ready", () => {
configuration.onDomReady?.();
})
.on("did-fail-load", (_event, code, desc) => {
logger.error(
`[CREATE-ELECTRON-WINDOW]: Failed to load window "${configuration.id}"`,
@@ -111,54 +106,13 @@ const createElectronWindowInjectable = getInjectable({
},
);
})
.on("did-finish-load", () => {
logger.info(
`[CREATE-ELECTRON-WINDOW]: Window "${configuration.id}" loaded`,
);
})
.on("will-attach-webview", (event, webPreferences, params) => {
logger.debug(
`[CREATE-ELECTRON-WINDOW]: Attaching webview to window "${configuration.id}"`,
);
// Following is security recommendations because we allow webview tag (webviewTag: true)
// suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
// and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups
if (webPreferences.preload) {
logger.warn(
"[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview",
);
delete webPreferences.preload;
}
// @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload
if (webPreferences.preloadURL) {
logger.warn(
"[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview",
);
delete webPreferences.preload;
}
if (params.allowpopups) {
logger.warn(
"[CREATE-ELECTRON-WINDOW]: We do not allow allowpopups props, stop webview from renderer",
);
// event.preventDefault() will destroy the guest page.
event.preventDefault();
return;
}
// Always disable Node.js integration for all webviews
webPreferences.nodeIntegration = false;
webPreferences.nativeWindowOpen = false;
})
.setWindowOpenHandler((details) => {
openBrowser(details.url).catch((error) => {
openLinkInBrowser(details.url).catch((error) => {
logger.error("[CREATE-ELECTRON-WINDOW]: failed to open browser", {
error,
});
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token";
import { afterApplicationIsLoadedInjectionToken } from "../../runnable-tokens/after-application-is-loaded-injection-token";
import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import ensureDirInjectable from "../../../../common/fs/ensure-dir.injectable";
import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable";
@@ -27,7 +27,7 @@ const startKubeConfigSyncInjectable = getInjectable({
causesSideEffects: true,
injectionToken: afterRootFrameIsReadyInjectionToken,
injectionToken: afterApplicationIsLoadedInjectionToken,
});
export default startKubeConfigSyncInjectable;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeQuitOfFrontEndInjectionToken } from "../../runnable-tokens/before-quit-of-front-end-injection-token";
import { beforeQuitOfBackEndInjectionToken } from "../../runnable-tokens/before-quit-of-back-end-injection-token";
import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable";
const stopKubeConfigSyncInjectable = getInjectable({
@@ -19,7 +19,7 @@ const stopKubeConfigSyncInjectable = getInjectable({
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopKubeConfigSyncInjectable;
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { kebabCase } from "lodash/fp";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
@@ -16,7 +15,7 @@ import { withErrorSuppression } from "../../../common/utils/with-error-suppressi
import type { WithErrorLoggingFor } from "../../../common/utils/with-error-logging/with-error-logging.injectable";
import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
import { isBoolean } from "../../../common/utils";
import { isBoolean, isString } from "../../../common/utils";
const trayMenuItemRegistratorInjectable = getInjectable({
id: "tray-menu-item-registrator",
@@ -38,7 +37,7 @@ export default trayMenuItemRegistratorInjectable;
const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor: WithErrorLoggingFor, getRandomId: () => string) => {
const _toItemInjectables = (parentId: string | null) => (registration: TrayMenuRegistration): Injectable<TrayMenuItem, TrayMenuItem, void>[] => {
const trayItemId = registration.id || kebabCase(registration.label || getRandomId());
const trayItemId = registration.id || getRandomId();
const id = `${trayItemId}-tray-menu-item-for-extension-${extension.sanitizedExtensionId}`;
const parentInjectable = getInjectable({
@@ -51,7 +50,18 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor:
separator: registration.type === "separator",
label: computed(() => registration.label || ""),
label: computed(() => {
if (!registration.label) {
return "";
}
if (isString(registration.label)) {
return registration.label;
}
return registration.label.get();
}),
tooltip: registration.toolTip,
click: () => {
+1 -1
View File
@@ -6,7 +6,7 @@
import type { IComputedValue } from "mobx";
export interface TrayMenuRegistration {
label?: string;
label?: string | IComputedValue<string>;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
type?: "normal" | "separator" | "submenu";
+1 -1
View File
@@ -6,7 +6,7 @@
import shellEnvironment from "shell-env";
import logger from "../logger";
export type EnvironmentVariables = Record<string, string>;
export type EnvironmentVariables = Partial<Record<string, string>>;
let shellSyncFailed = false;
@@ -63,27 +63,13 @@ class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependenci
case HpaMetricType.Resource: {
const metricSpec = metric.resource ?? metric.containerResource;
const addition = metricSpec.targetAverageUtilization
? "(as a percentage of request)"
? " (as a percentage of request)"
: "";
return (
<>
Resource
{metricSpec.name}
{" "}
on Pods
{addition}
</>
);
return `Resource ${metricSpec.name} on Pods${addition}`;
}
case HpaMetricType.Pods:
return (
<>
{metric.pods.metricName}
{" "}
on Pods
</>
);
return `${metric.pods.metricName} on Pods`;
case HpaMetricType.Object: {
return (
@@ -95,15 +81,7 @@ class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependenci
);
}
case HpaMetricType.External:
return (
<>
{metric.external.metricName}
{" "}
on
{" "}
{JSON.stringify(metric.external.metricSelector)}
</>
);
return `${metric.external.metricName} on ${JSON.stringify(metric.external.metricSelector)}`;
}
};
@@ -19,7 +19,7 @@ import logger from "../../../common/logger";
export interface ResourceQuotaDetailsProps extends KubeObjectDetailsProps<ResourceQuota> {
}
function transformUnit(name: string, value: string): number {
function transformUnit(name: string, value: string): number | undefined {
if (name.includes("memory") || name.includes("storage")) {
return unitsToBytes(value);
}
@@ -36,23 +36,38 @@ function renderQuotas(quota: ResourceQuota): JSX.Element[] {
return object.entries(hard)
.filter(hasDefinedTupleValue)
.map(([name, value]) => {
const current = transformUnit(name, value);
const max = transformUnit(name, value);
const usage = max === 0 ? 100 : Math.ceil(current / max * 100); // special case 0 max as always 100% usage
.map(([name, rawMax]) => {
const rawCurrent = used[name] ?? "0";
const current = transformUnit(name, rawCurrent);
const max = transformUnit(name, rawMax);
if (current === undefined || max === undefined) {
return (
<div key={name} className={cssNames("param", kebabCase(name))}>
<span className="title">{name}</span>
<span className="value">
{`${rawCurrent} / ${rawMax}`}
</span>
</div>
);
}
const usage = max === 0
? 100 // special case 0 max as always 100% usage
: current / max * 100;
return (
<div key={name} className={cssNames("param", kebabCase(name))}>
<span className="title">{name}</span>
<span className="value">
{`${used[name]} / ${value}`}
{`${rawCurrent} / ${rawMax}`}
</span>
<LineProgress
max={max}
value={current}
tooltip={(
<p>
{`Set: ${value}. Usage: ${usage}%`}
{`Set: ${rawMax}. Usage: ${+usage.toFixed(2)}%`}
</p>
)}
/>
@@ -6,7 +6,6 @@
import React from "react";
import { autoBind, cssNames } from "../../utils";
import type { PortForwardItem, PortForwardStore } from "../../port-forward";
import { openPortForward } from "../../port-forward";
import type { MenuActionsProps } from "../menu/menu-actions";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
@@ -15,6 +14,8 @@ import { Notifications } from "../notifications";
import { withInjectables } from "@ogre-tools/injectable-react";
import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
import type { OpenPortForward } from "../../port-forward/open-port-forward.injectable";
import openPortForwardInjectable from "../../port-forward/open-port-forward.injectable";
export interface PortForwardMenuProps extends MenuActionsProps {
portForward: PortForwardItem;
@@ -24,6 +25,7 @@ export interface PortForwardMenuProps extends MenuActionsProps {
interface Dependencies {
portForwardStore: PortForwardStore;
openPortForwardDialog: (item: PortForwardItem) => void;
openPortForward: OpenPortForward;
}
class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependencies> extends React.Component<Props> {
@@ -94,7 +96,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
return (
<>
{ portForward.status === "Active" && (
<MenuItem onClick={() => openPortForward(portForward)}>
<MenuItem onClick={() => this.props.openPortForward(portForward)}>
<Icon
material="open_in_browser"
interactive={toolbar}
@@ -139,6 +141,7 @@ export const PortForwardMenu = withInjectables<Dependencies, PortForwardMenuProp
getProps: (di, props) => ({
portForwardStore: di.inject(portForwardStoreInjectable),
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
openPortForward: di.inject(openPortForwardInjectable),
...props,
}),
},
@@ -13,7 +13,7 @@ import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Button } from "../button";
import type { ForwardedPort, PortForwardStore } from "../../port-forward";
import { openPortForward, predictProtocol } from "../../port-forward";
import { predictProtocol } from "../../port-forward";
import { Spinner } from "../spinner";
import { withInjectables } from "@ogre-tools/injectable-react";
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
@@ -21,6 +21,8 @@ import portForwardDialogModelInjectable from "../../port-forward/port-forward-di
import logger from "../../../common/logger";
import aboutPortForwardingInjectable from "../../port-forward/about-port-forwarding.injectable";
import notifyErrorPortForwardingInjectable from "../../port-forward/notify-error-port-forwarding.injectable";
import type { OpenPortForward } from "../../port-forward/open-port-forward.injectable";
import openPortForwardInjectable from "../../port-forward/open-port-forward.injectable";
export interface ServicePortComponentProps {
service: Service;
@@ -32,6 +34,7 @@ interface Dependencies {
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean; onClose: () => void }) => void;
aboutPortForwarding: () => void;
notifyErrorPortForwarding: (message: string) => void;
openPortForward: OpenPortForward;
}
@observer
@@ -88,7 +91,7 @@ class NonInjectedServicePortComponent extends React.Component<ServicePortCompone
@action
async portForward() {
const { service, port } = this.props;
const { service, port, openPortForward } = this.props;
let portForward: ForwardedPort = {
kind: "service",
name: service.getName(),
@@ -180,7 +183,7 @@ class NonInjectedServicePortComponent extends React.Component<ServicePortCompone
<span title="Open in a browser" onClick={() => this.portForward()}>
{port.toString()}
</span>
<Button primary onClick={portForwardAction}>
<Button primary onClick={portForwardAction}>
{" "}
{this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."}
{" "}
@@ -202,6 +205,7 @@ export const ServicePortComponent = withInjectables<Dependencies, ServicePortCom
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
aboutPortForwarding: di.inject(aboutPortForwardingInjectable),
notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable),
openPortForward: di.inject(openPortForwardInjectable),
...props,
}),
},
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { workloadInjectionToken } from "../workload-injection-token";
import { ResourceNames } from "../../../../utils/rbac";
import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable";
import navigateToCronJobsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable";
import namespaceStoreInjectable from "../../../+namespaces/store.injectable";
import cronJobsStoreInjectable from "../../../+workloads-cronjobs/store.injectable";
import { computed } from "mobx";
@@ -14,7 +14,7 @@ const cronJobsWorkloadInjectable = getInjectable({
id: "cron-jobs-workload",
instantiate: (di) => {
const navigate = di.inject(navigateToPodsInjectable);
const navigate = di.inject(navigateToCronJobsInjectable);
const namespaceStore = di.inject(namespaceStoreInjectable);
const store = di.inject(cronJobsStoreInjectable);
@@ -5,16 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import { workloadInjectionToken } from "../workload-injection-token";
import { ResourceNames } from "../../../../utils/rbac";
import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable";
import namespaceStoreInjectable from "../../../+namespaces/store.injectable";
import replicasetsStoreInjectable from "../../../+workloads-replicasets/store.injectable";
import { computed } from "mobx";
import navigateToReplicasetsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable";
const replicasetsWorkloadInjectable = getInjectable({
id: "replicasets-workload",
instantiate: (di) => {
const navigate = di.inject(navigateToPodsInjectable);
const navigate = di.inject(navigateToReplicasetsInjectable);
const namespaceStore = di.inject(namespaceStoreInjectable);
const store = di.inject(replicasetsStoreInjectable);
@@ -5,16 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import { workloadInjectionToken } from "../workload-injection-token";
import { ResourceNames } from "../../../../utils/rbac";
import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable";
import namespaceStoreInjectable from "../../../+namespaces/store.injectable";
import statefulsetsStoreInjectable from "../../../+workloads-statefulsets/store.injectable";
import { computed } from "mobx";
import navigateToStatefulsetsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable";
const statefulsetsWorkloadInjectable = getInjectable({
id: "statefulsets-workload",
instantiate: (di) => {
const navigate = di.inject(navigateToPodsInjectable);
const navigate = di.inject(navigateToStatefulsetsInjectable);
const namespaceStore = di.inject(namespaceStoreInjectable);
const store = di.inject(statefulsetsStoreInjectable);
@@ -13,7 +13,7 @@ import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Button } from "../button";
import type { ForwardedPort, PortForwardStore } from "../../port-forward";
import { openPortForward, predictProtocol } from "../../port-forward";
import { predictProtocol } from "../../port-forward";
import { Spinner } from "../spinner";
import { withInjectables } from "@ogre-tools/injectable-react";
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
@@ -21,6 +21,8 @@ import portForwardDialogModelInjectable from "../../port-forward/port-forward-di
import logger from "../../../common/logger";
import aboutPortForwardingInjectable from "../../port-forward/about-port-forwarding.injectable";
import notifyErrorPortForwardingInjectable from "../../port-forward/notify-error-port-forwarding.injectable";
import type { OpenPortForward } from "../../port-forward/open-port-forward.injectable";
import openPortForwardInjectable from "../../port-forward/open-port-forward.injectable";
export interface PodContainerPortProps {
pod: Pod;
@@ -32,6 +34,7 @@ interface Dependencies {
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean; onClose: () => void }) => void;
aboutPortForwarding: () => void;
notifyErrorPortForwarding: (message: string) => void;
openPortForward: OpenPortForward;
}
@observer
@@ -86,7 +89,7 @@ class NonInjectedPodContainerPort extends React.Component<PodContainerPortProps
@action
async portForward() {
const { pod, port } = this.props;
const { pod, port, openPortForward } = this.props;
let portForward: ForwardedPort = {
kind: "pod",
name: pod.getName(),
@@ -178,7 +181,7 @@ class NonInjectedPodContainerPort extends React.Component<PodContainerPortProps
<span title="Open in a browser" onClick={() => this.portForward()}>
{text}
</span>
<Button primary onClick={portForwardAction}>
<Button primary onClick={portForwardAction}>
{" "}
{this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."}
{" "}
@@ -200,6 +203,7 @@ export const PodContainerPort = withInjectables<Dependencies, PodContainerPortPr
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
aboutPortForwarding: di.inject(aboutPortForwardingInjectable),
notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable),
openPortForward: di.inject(openPortForwardInjectable),
...props,
}),
},
@@ -82,7 +82,7 @@ export class PodStore extends KubeObjectStore<Pod, PodApi> {
}
return {
cpu: total.cpu + cpuUnitsToNumber(cpu),
cpu: total.cpu + (cpuUnitsToNumber(cpu) ?? 0),
memory: total.memory + unitsToBytes(memory),
};
}, empty);
@@ -25,7 +25,7 @@
cursor: pointer;
}
.badge:not(.isExpanded) {
.badge:not(.scrollable):not(.isExpanded) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -115,6 +115,7 @@ export class ClusterFrameHandler {
(view: LensView) => {
logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`);
view.frame.classList.remove("hidden");
view.frame.focus();
ipcRenderer.send(clusterVisibilityHandler, clusterId);
},
);
@@ -11,6 +11,8 @@ import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable"
import terminalConfigInjectable from "../../../../common/user-store/terminal-config.injectable";
import terminalCopyOnSelectInjectable from "../../../../common/user-store/terminal-copy-on-select.injectable";
import themeStoreInjectable from "../../../themes/store.injectable";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import openLinkInBrowserInjectable from "../../../../common/utils/open-link-in-browser.injectable";
export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal;
@@ -22,6 +24,8 @@ const createTerminalInjectable = getInjectable({
terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
themeStore: di.inject(themeStoreInjectable),
isMac: di.inject(isMacInjectable),
openLinkInBrowser: di.inject(openLinkInBrowserInjectable),
};
return (tabId, api) => new Terminal(dependencies, { tabId, api });
@@ -12,19 +12,22 @@ import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api";
import type { ThemeStore } from "../../../themes/store";
import { disposer } from "../../../utils";
import { isMac } from "../../../../common/vars";
import { once } from "lodash";
import { clipboard } from "electron";
import logger from "../../../../common/logger";
import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers";
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";
export interface TerminalDependencies {
readonly spawningPool: HTMLElement;
readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly themeStore: ThemeStore;
readonly isMac: boolean;
openLinkInBrowser: OpenLinkInBrowser;
}
export interface TerminalArguments {
@@ -93,7 +96,6 @@ export class Terminal {
this.xterm.loadAddon(this.fitAddon);
this.xterm.open(this.dependencies.spawningPool);
this.xterm.registerLinkMatcher(/https?:\/\/[^\s]+/i, this.onClickLink);
this.xterm.attachCustomKeyEventHandler(this.keyHandler);
this.xterm.onSelectionChange(this.onSelectionChange);
@@ -108,7 +110,16 @@ export class Terminal {
this.api.on("data", this.onApiData);
window.addEventListener("resize", this.onResize);
const linkProvider = new LinkProvider(
this.xterm,
/https?:\/\/[^\s]+/i,
(event, link) => this.dependencies.openLinkInBrowser(link),
undefined,
0,
);
this.disposer.push(
this.xterm.registerLinkProvider(linkProvider),
reaction(() => this.theme, colors => this.xterm.setOption("theme", colors), {
fireImmediately: true,
}),
@@ -169,10 +180,6 @@ export class Terminal {
this.viewport.scrollTop = this.scrollPos; // restore last scroll position
};
onClickLink = (evt: MouseEvent, link: string) => {
window.open(link, "_blank");
};
onContextMenu = () => {
if (
// don't paste if user hasn't turned on the feature
@@ -229,7 +236,7 @@ export class Terminal {
}
//Ctrl+K: clear the entire buffer, making the prompt line the new first line on mac os
if (isMac && metaKey) {
if (this.dependencies.isMac && metaKey) {
switch (code) {
case "KeyK":
this.onClear();
+4 -4
View File
@@ -54,11 +54,10 @@
position: absolute;
right: 0;
bottom: 0;
width: 0;
height: $unit * 0.5;
transition: width 150ms;
height: 3px;
transition: opacity 150ms;
background: currentColor;
color: var(--halfGray)
opacity: 0;
}
&:focus {
@@ -76,6 +75,7 @@
left: 0;
right: auto;
color: var(--line-color-active);
opacity: 1;
}
}
}
+7 -23
View File
@@ -3,8 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import glob from "glob";
import { memoize, noop, chunk } from "lodash/fp";
import { noop, chunk } from "lodash/fp";
import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import {
createContainer,
@@ -73,6 +72,7 @@ import forceUpdateModalRootFrameComponentInjectable from "./application-update/f
import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable";
import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable";
import storageSaveDelayInjectable from "./utils/create-storage/storage-save-delay.injectable";
import type { GlobalOverride } from "../common/test-utils/get-global-override";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@@ -85,9 +85,9 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
setLegacyGlobalDiForExtensionApi(di, Environments.renderer);
const filePaths = getInjectableFilePaths();
const injectables = filePaths.map(filePath => require(filePath).default);
const injectables: Injectable<any, any, any>[] = (global as any).rendererInjectablePaths.map(
(filePath: string) => require(filePath).default,
);
chunk(100)(injectables).forEach(chunkInjectables => {
di.register(...chunkInjectables);
@@ -96,10 +96,8 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
di.preventSideEffects();
if (doGeneralOverrides) {
const globalOverrideFilePaths = getGlobalOverridePaths();
const globalOverrides = globalOverrideFilePaths.map(
(filePath) => require(filePath).default,
const globalOverrides: GlobalOverride[] = (global as any).rendererGlobalOverridePaths.map(
(filePath: string) => require(filePath).default,
);
globalOverrides.forEach(globalOverride => {
@@ -232,20 +230,6 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
return di;
};
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);
const getGlobalOverridePaths = memoize(() =>
glob.sync(
"../{common,extensions,renderer}/**/*.global-override-for-injectable.{ts,tsx}",
{ cwd: __dirname },
),
);
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
injectables.forEach(injectable => {
di.override(injectable, () => () => {
@@ -5,11 +5,14 @@
import { comparer, reaction } from "mobx";
import type { Disposer } from "../../common/utils";
import { disposer, getOrInsert, noop, WrappedAbortController } from "../../common/utils";
import AbortController from "abort-controller";
import { once } from "lodash";
import type { ClusterFrameContext } from "../cluster-frame-context/cluster-frame-context";
import logger from "../../common/logger";
import type { KubeObjectStoreLoadAllParams, KubeObjectStoreSubscribeParams } from "../../common/k8s-api/kube-object.store";
import type { RequestInit } from "node-fetch";
// TODO: upgrade node-fetch once we are starting to use ES modules
type LegacyAbortSignal = NonNullable<RequestInit["signal"]>;
// Kubernetes watch-api client
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
@@ -103,7 +106,7 @@ export class KubeWatchApi {
const loadThenSubscribe = async (namespaces: string[] | undefined) => {
try {
await store.loadAll({ namespaces, reqInit: { signal: childController.signal }, onLoadFailure });
await store.loadAll({ namespaces, reqInit: { signal: childController.signal as LegacyAbortSignal }, onLoadFailure });
unsubscribe.push(store.subscribe({ onLoadFailure, abortController: childController }));
} catch (error) {
if (!(error instanceof DOMException)) {
@@ -0,0 +1,38 @@
/**
* 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 loggerInjectable from "../../common/logger.injectable";
import openLinkInBrowserInjectable from "../../common/utils/open-link-in-browser.injectable";
import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable";
import type { ForwardedPort } from "./port-forward-item";
import { portForwardAddress } from "./port-forward-utils";
export type OpenPortForward = (portForward: ForwardedPort) => void;
const openPortForwardInjectable = getInjectable({
id: "open-port-forward",
instantiate: (di): OpenPortForward => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const logger = di.inject(loggerInjectable);
return (portForward) => {
const browseTo = portForwardAddress(portForward);
openLinkInBrowser(browseTo)
.catch(error => {
logger.error(`failed to open in browser: ${error}`, {
port: portForward.port,
kind: portForward.kind,
namespace: portForward.namespace,
name: portForward.name,
});
showErrorNotification(`Failed to open ${browseTo} in browser`);
});
};
},
});
export default openPortForwardInjectable;
@@ -14,7 +14,6 @@ import { Wizard, WizardStep } from "../components/wizard";
import { Input } from "../components/input";
import { cssNames } from "../utils";
import type { PortForwardStore } from "./port-forward-store/port-forward-store";
import { openPortForward } from "./port-forward-utils";
import { Checkbox } from "../components/checkbox";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { PortForwardDialogData, PortForwardDialogModel } from "./port-forward-dialog-model/port-forward-dialog-model";
@@ -23,6 +22,8 @@ import logger from "../../common/logger";
import portForwardStoreInjectable from "./port-forward-store/port-forward-store.injectable";
import aboutPortForwardingInjectable from "./about-port-forwarding.injectable";
import notifyErrorPortForwardingInjectable from "./notify-error-port-forwarding.injectable";
import type { OpenPortForward } from "./open-port-forward.injectable";
import openPortForwardInjectable from "./open-port-forward.injectable";
export interface PortForwardDialogProps extends Partial<DialogProps> {}
@@ -31,6 +32,7 @@ interface Dependencies {
model: PortForwardDialogModel;
aboutPortForwarding: () => void;
notifyErrorPortForwarding: (message: string) => void;
openPortForward: OpenPortForward;
}
@observer
@@ -89,7 +91,7 @@ class NonInjectedPortForwardDialog extends Component<PortForwardDialogProps & De
}
if (portForward.status === "Active" && data.openInBrowser) {
openPortForward(portForward);
this.props.openPortForward(portForward);
}
} catch (error) {
logger.error(`[PORT-FORWARD-DIALOG]: ${error}`, portForward);
@@ -177,6 +179,7 @@ export const PortForwardDialog = withInjectables<Dependencies, PortForwardDialog
model: di.inject(portForwardDialogModelInjectable),
aboutPortForwarding: di.inject(aboutPortForwardingInjectable),
notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable),
openPortForward: di.inject(openPortForwardInjectable),
...props,
}),
},
@@ -3,34 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { openBrowser } from "../utils";
import { Notifications } from "../components/notifications";
import type { ForwardedPort } from "./port-forward-item";
import logger from "../../common/logger";
export function portForwardAddress(portForward: ForwardedPort) {
return `${portForward.protocol ?? "http"}://localhost:${portForward.forwardPort}`;
}
export function openPortForward(portForward: ForwardedPort) {
const browseTo = portForwardAddress(portForward);
openBrowser(browseTo)
.catch(error => {
logger.error(`failed to open in browser: ${error}`, {
port: portForward.port,
kind: portForward.kind,
namespace: portForward.namespace,
name: portForward.name,
});
Notifications.error(`Failed to open ${browseTo} in browser`);
},
);
}
export function predictProtocol(name: string | undefined) {
return name === "https" ? "https" : "http";
}