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

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

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

6
.adr.json Normal file
View File

@ -0,0 +1,6 @@
{
"language": "en",
"path": "docs/architecture/decisions/",
"prefix": "",
"digits": 4
}

View File

@ -23,8 +23,8 @@ jobs:
- name: Generate Extensions API Reference using typedocs
run: |
yarn install
yarn typedocs-extensions-api
yarn install
yarn typedocs-extensions-api
- name: Verify that the markdown is valid
run: |

View File

@ -19,7 +19,7 @@ jobs:
- uses: doyensec/electronegativity-action@v1.1
with:
input: src/
electron-version: "15.5.7"
electron-version: "19.0.4"
severity: medium
- name: Upload sarif

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: "3.x"
- name: Install dependencies
run: |
@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: '${{ github.event.inputs.version }}'
ref: "${{ github.event.inputs.version }}"
- name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
@ -43,8 +43,8 @@ jobs:
- name: Checkout master branch from lens
uses: actions/checkout@v2
with:
path: 'master'
ref: 'master'
path: "master"
ref: "master"
- name: Bring in latest mkdocs.yml from master
run: |

View File

@ -23,11 +23,11 @@ jobs:
- name: Generate NPM package
run: |
make build-npm
make build-npm
- name: publish new release
if: contains(github.ref, 'refs/tags/v')
run: |
make publish-npm
make publish-npm
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron"
target "15.5.0"
disturl "https://electronjs.org/headers"
target "19.0.4"
runtime "electron"

View File

@ -13,7 +13,6 @@ import { promisify } from "util";
import { pipeline as _pipeline, Transform, Writable } from "stream";
import type { SingleBar } from "cli-progress";
import { MultiBar } from "cli-progress";
import AbortController from "abort-controller";
import { extract } from "tar-stream";
import gunzip from "gunzip-maybe";
import { getBinaryName, normalizedPlatform } from "../src/common/vars";

View File

@ -18,7 +18,7 @@ exports.default = async function notarizing(context) {
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: "io.kontena.lens-app",
appBundleId: process.env.APPBUNDLEID || "io.kontena.lens-app",
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,

View File

@ -0,0 +1,2 @@
# Architecture Decision Records

View File

@ -55,7 +55,7 @@ Some of the most-important fields include:
"license": "MIT",
"homepage": "https://github.com/lensapp/lens-extension-samples",
"engines": {
"node": "^14.18.12",
"node": "^16.14.2",
"lens": "5.4"
},
"main": "dist/main.js",
@ -72,7 +72,7 @@ Some of the most-important fields include:
"ts-loader": "^8.0.4",
"typescript": "^4.5.5",
"@types/react": "^17.0.44",
"@types/node": "^14.18.12",
"@types/node": "^16.14.2",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.11"
}

View File

@ -12,6 +12,10 @@
"email": "info@k8slens.dev"
},
"scripts": {
"adr:create": "echo \"What is the title?\"; read title; adr new \"$title\"",
"adr:change-status": "echo \"Decision number?:\"; read decision; adr status $decision",
"adr:update-readme": "adr update",
"adr:list": "adr list",
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
"dev-build": "concurrently yarn:compile:*",
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
@ -210,16 +214,15 @@
"@kubernetes/client-node": "^0.17.0",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "9.0.1",
"@ogre-tools/injectable": "9.0.1",
"@ogre-tools/injectable-extension-for-auto-registration": "9.0.1",
"@ogre-tools/injectable-extension-for-mobx": "9.0.1",
"@ogre-tools/injectable-react": "9.0.1",
"@ogre-tools/injectable": "9.0.2",
"@ogre-tools/injectable-extension-for-auto-registration": "9.0.2",
"@ogre-tools/injectable-extension-for-mobx": "9.0.2",
"@ogre-tools/injectable-react": "9.0.2",
"@sentry/electron": "^3.0.7",
"@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.0.1",
"@tanstack/react-table": "^8.5.5",
"@types/circular-dependency-plugin": "5.0.5",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0",
"await-lock": "^2.2.2",
"byline": "^5.0.0",
@ -249,7 +252,7 @@
"mobx-observable-history": "^2.0.3",
"mobx-react": "^7.5.2",
"mobx-utils": "^6.0.4",
"mock-fs": "^5.1.2",
"mock-fs": "^5.1.4",
"moment": "^2.29.4",
"moment-timezone": "^0.5.34",
"monaco-editor": "^0.29.1",
@ -284,7 +287,8 @@
"winston": "^3.8.1",
"winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5",
"ws": "^8.8.0"
"ws": "^8.8.1",
"xterm-link-provider": "^1.3.1"
},
"devDependencies": {
"@async-fn/jest": "1.6.4",
@ -293,7 +297,7 @@
"@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@sentry/types": "^6.19.7",
"@swc/core": "^1.2.218",
"@swc/core": "^1.2.223",
"@swc/jest": "^0.2.22",
"@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.4",
@ -315,7 +319,7 @@
"@types/hapi__subtext": "^7.0.0",
"@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.9",
"@types/jest": "^28.1.3",
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14",
"@types/lodash": "^4.14.181",
@ -323,7 +327,7 @@
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^2.4.0",
"@types/mock-fs": "^4.13.1",
"@types/node": "^16.11.45",
"@types/node": "^16.11.47",
"@types/node-fetch": "^2.6.2",
"@types/npm": "^2.0.32",
"@types/proper-lockfile": "^4.1.2",
@ -352,8 +356,9 @@
"@types/webpack-dev-server": "^4.7.2",
"@types/webpack-env": "^1.17.0",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.31.0",
"adr": "^1.4.1",
"ansi_up": "^5.1.0",
"chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2",
@ -364,12 +369,12 @@
"css-loader": "^6.7.1",
"deepdash": "^5.3.9",
"dompurify": "^2.3.10",
"electron": "^15.5.7",
"electron-builder": "^23.1.0",
"electron": "^19.0.4",
"electron-builder": "^23.3.3",
"electron-notarize": "^0.3.0",
"esbuild": "^0.14.49",
"esbuild": "^0.14.53",
"esbuild-loader": "^2.19.0",
"eslint": "^8.20.0",
"eslint": "^8.21.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.30.1",
@ -382,7 +387,7 @@
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "^28.1.2",
"jest": "^28.1.3",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.3",
"jest-fetch-mock": "^3.0.3",
@ -393,7 +398,7 @@
"node-gyp": "^8.3.0",
"node-loader": "^2.0.0",
"nodemon": "^2.0.19",
"playwright": "^1.24.1",
"playwright": "^1.24.2",
"postcss": "^8.4.14",
"postcss-loader": "^6.2.1",
"randomcolor": "^0.6.2",
@ -402,9 +407,10 @@
"react-refresh-typescript": "^2.0.7",
"react-router-dom": "^5.3.3",
"react-select": "^5.4.0",
"react-select-event": "^5.5.0",
"react-select-event": "^5.5.1",
"react-table": "^7.8.0",
"react-window": "^1.8.7",
"sass": "^1.53.0",
"sass": "^1.54.2",
"sass-loader": "^12.6.0",
"sharp": "^0.30.7",
"style-loader": "^3.3.1",

View File

@ -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();
});

View File

@ -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(

View File

@ -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";

View File

@ -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 {};
}
}

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,
});

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;

View File

@ -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"],

View File

@ -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`);
});
});

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", () => {

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;
}

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

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";

View File

@ -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 () => {});

View File

@ -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;

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;

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.
*

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);
}

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);

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}");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

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) => {
[

View File

@ -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 });
});
},

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.

View File

@ -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,
});

View File

@ -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;

View File

@ -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;

View File

@ -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: () => {

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";

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;

View File

@ -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)}`;
}
};

View File

@ -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>
)}
/>

View File

@ -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,
}),
},

View File

@ -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,
}),
},

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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,
}),
},

View File

@ -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);

View File

@ -25,7 +25,7 @@
cursor: pointer;
}
.badge:not(.isExpanded) {
.badge:not(.scrollable):not(.isExpanded) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -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);
},
);

View File

@ -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 });

View File

@ -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();

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;
}
}
}

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, () => () => {

View File

@ -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)) {

View File

@ -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;

View File

@ -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,
}),
},

View File

@ -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";
}

1933
yarn.lock

File diff suppressed because it is too large Load Diff