mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getConvertedParts } from "../name-parts";
|
||||
|
||||
describe("getConvertedParts", () => {
|
||||
it.each([
|
||||
["hello", ["hello"]],
|
||||
["hello.goodbye", ["hello", "goodbye"]],
|
||||
["hello.1", ["hello", 1]],
|
||||
["3-hello.1", [3, "hello", 1]],
|
||||
["3_hello.1", [3, "hello", 1]],
|
||||
["3_hello.1/foobar", [3, "hello", 1, "foobar"]],
|
||||
["3_hello.1/foobar\\new", [3, "hello", 1, "foobar", "new"]],
|
||||
])("Splits '%s' as into %j", (input, output) => {
|
||||
expect(getConvertedParts(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { convertKubectlJsonPathToNodeJsonPath, safeJSONPathValue } from "../jsonPath";
|
||||
|
||||
describe("convertKubectlJsonPathToNodeJsonPath", () => {
|
||||
it("should convert \\. to use indexed notation", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\.io/alias-name");
|
||||
|
||||
expect(res).toBe("$.metadata.labels['kubesphere.io/alias-name']");
|
||||
});
|
||||
|
||||
it("should convert keys with escaped characters to use indexed notation", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\\"io/alias-name");
|
||||
|
||||
expect(res).toBe("$.metadata.labels['kubesphere\"io/alias-name']");
|
||||
});
|
||||
|
||||
it("should convert '-' to use indexed notation", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.alias-name");
|
||||
|
||||
expect(res).toBe("$.metadata.labels['alias-name']");
|
||||
});
|
||||
|
||||
it("should drop leading dot if first group is converted to index notation", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata\\.labels.alias-name");
|
||||
|
||||
expect(res).toBe("$['metadata.labels']['alias-name']");
|
||||
});
|
||||
|
||||
it("should handle scenario when both \\. and indexed notation are present", () => {
|
||||
const rest = convertKubectlJsonPathToNodeJsonPath(".metadata.labels\\.serving['some.other.item']");
|
||||
|
||||
expect(rest).toBe("$.metadata['labels.serving']['some.other.item']");
|
||||
});
|
||||
|
||||
it("should not touch given jsonPath if no invalid characters present", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".status.conditions[?(@.type=='Ready')].status");
|
||||
|
||||
expect(res).toBe("$.status.conditions[?(@.type=='Ready')].status");
|
||||
});
|
||||
|
||||
it("strips '\\' away from the result", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']");
|
||||
|
||||
expect(res).toBe("$.metadata.labels['serving.knative.dev/configuration']");
|
||||
});
|
||||
|
||||
it("converts all [] to [0]", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[].foo[]");
|
||||
|
||||
expect(res).toBe("$.metadata.labels[0].foo[0]");
|
||||
});
|
||||
|
||||
it("removes trailing ..", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[]..");
|
||||
|
||||
expect(res).toBe("$.metadata.labels[0]");
|
||||
});
|
||||
|
||||
it("converts ending ...name to ..name", () => {
|
||||
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[]...name");
|
||||
|
||||
expect(res).toBe("$.metadata.labels[0]..name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeJSONPathValue", () => {
|
||||
let oldWarn: typeof console["warn"];
|
||||
|
||||
beforeEach(() => {
|
||||
oldWarn = console.warn;
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.warn = oldWarn;
|
||||
});
|
||||
|
||||
it("should convert boolean values to strings", () => {
|
||||
const res = safeJSONPathValue({ bar: false }, ".bar");
|
||||
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it("should convert number values to strings", () => {
|
||||
const res = safeJSONPathValue({ bar: 0 }, ".bar");
|
||||
|
||||
expect(res).toBe(0);
|
||||
});
|
||||
|
||||
it("should join sliced entries with commas only", () => {
|
||||
const res = safeJSONPathValue({
|
||||
bar: [
|
||||
{
|
||||
foo: 1,
|
||||
},
|
||||
{
|
||||
foo: "hello",
|
||||
},
|
||||
],
|
||||
}, ".bar[].foo");
|
||||
|
||||
expect(res).toBe(1);
|
||||
});
|
||||
|
||||
it("should join an array of values using JSON.stringify", () => {
|
||||
const res = safeJSONPathValue({
|
||||
bar: [
|
||||
"world",
|
||||
"hello",
|
||||
],
|
||||
}, ".bar");
|
||||
|
||||
expect(res).toEqual(["world", "hello"]);
|
||||
});
|
||||
|
||||
it("should stringify an object value", () => {
|
||||
const res = safeJSONPathValue({
|
||||
foo: { bar: "bat" },
|
||||
}, ".foo");
|
||||
|
||||
expect(res).toEqual({ "bar":"bat" });
|
||||
});
|
||||
|
||||
it("should use convertKubectlJsonPathToNodeJsonPath", () => {
|
||||
const res = safeJSONPathValue({
|
||||
foo: { "hello.world": "bat" },
|
||||
}, ".foo.hello\\.world");
|
||||
|
||||
expect(res).toBe("bat");
|
||||
});
|
||||
|
||||
it("should not throw when given '.spec.metrics[*].external.highWatermark..'", () => {
|
||||
const obj = {
|
||||
spec: {
|
||||
metrics: [
|
||||
{
|
||||
external: {
|
||||
metricName: "cpu",
|
||||
highWatermark: "100",
|
||||
},
|
||||
},
|
||||
{
|
||||
external: {
|
||||
metricName: "memory",
|
||||
highWatermark: "100",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const res = safeJSONPathValue(obj, ".spec.metrics[*].external.highWatermark..");
|
||||
|
||||
expect(res).toEqual(["100", "100"]);
|
||||
});
|
||||
|
||||
it("should not throw if path is invalid jsonpath", () => {
|
||||
const res = safeJSONPathValue({
|
||||
foo: { "hello.world": "bat" },
|
||||
}, "asd[");
|
||||
|
||||
expect(res).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should retrive value with '/' in jsonpath", () => {
|
||||
const res = safeJSONPathValue({
|
||||
foo: {
|
||||
"hello/world": "bat",
|
||||
},
|
||||
}, ".foo.hello/world");
|
||||
|
||||
expect(res).toBe("bat");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { metricUnitsToNumber } from "../metricUnitsToNumber";
|
||||
|
||||
describe("metricUnitsToNumber tests", () => {
|
||||
test("plain number", () => {
|
||||
expect(metricUnitsToNumber("124")).toStrictEqual(124);
|
||||
});
|
||||
|
||||
test("with k suffix", () => {
|
||||
expect(metricUnitsToNumber("124k")).toStrictEqual(124000);
|
||||
});
|
||||
|
||||
test("with m suffix", () => {
|
||||
expect(metricUnitsToNumber("124m")).toStrictEqual(124000000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { observable, reaction } from "mobx";
|
||||
import type { StorageHelper } from "../storage-helper";
|
||||
import { toJS } from "../../../common/utils";
|
||||
import type { CreateStorageHelper } from "../create-storage-helper.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import createStorageHelperInjectable from "../create-storage-helper.injectable";
|
||||
|
||||
interface StorageModel {
|
||||
[prop: string]: any /*json-serializable*/;
|
||||
message?: string;
|
||||
description?: any;
|
||||
}
|
||||
|
||||
describe("renderer/utils/StorageHelper", () => {
|
||||
let createStorageHelper: CreateStorageHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||
});
|
||||
|
||||
describe("Using custom StorageAdapter", () => {
|
||||
const storageKey = "ui-settings";
|
||||
const remoteStorageMock = observable.map<string, StorageModel>();
|
||||
let storageHelper: StorageHelper<StorageModel>;
|
||||
|
||||
beforeEach(() => {
|
||||
remoteStorageMock.set(storageKey, {
|
||||
message: "saved-before", // pretending as previously saved data
|
||||
});
|
||||
|
||||
storageHelper = createStorageHelper<StorageModel>(storageKey, {
|
||||
defaultValue: {
|
||||
message: "blabla",
|
||||
description: "default",
|
||||
},
|
||||
storage: {
|
||||
getItem(key: string): StorageModel {
|
||||
return Object.assign(
|
||||
storageHelper.defaultValue,
|
||||
remoteStorageMock.get(key),
|
||||
);
|
||||
},
|
||||
setItem(key: string, value: StorageModel) {
|
||||
remoteStorageMock.set(key, value);
|
||||
},
|
||||
removeItem(key: string) {
|
||||
remoteStorageMock.delete(key);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("initialized with default value", async () => {
|
||||
expect(storageHelper.key).toBe(storageKey);
|
||||
expect(storageHelper.get()).toEqual(storageHelper.defaultValue);
|
||||
});
|
||||
|
||||
it("set() fully replaces data in storage", () => {
|
||||
storageHelper.set({ message: "msg" });
|
||||
storageHelper.get().description = "desc";
|
||||
expect(storageHelper.get().message).toBe("msg");
|
||||
expect(storageHelper.get().description).toBe("desc");
|
||||
expect(remoteStorageMock.get(storageKey)).toEqual({
|
||||
message: "msg",
|
||||
description: "desc",
|
||||
} as StorageModel);
|
||||
});
|
||||
|
||||
it("merge() does partial data tree updates", () => {
|
||||
storageHelper.merge({ message: "updated" });
|
||||
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||
expect(remoteStorageMock.get(storageKey)).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||
|
||||
// `draft` modified inside, returning `void` is expected
|
||||
storageHelper.merge(draft => {
|
||||
draft.message = "updated2";
|
||||
});
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated2" });
|
||||
|
||||
// returning object modifies `draft` as well
|
||||
storageHelper.merge(draft => ({
|
||||
message: draft.message?.replace("2", "3"),
|
||||
}));
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated3" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("data in storage-helper is observable (mobx)", () => {
|
||||
let storageHelper: StorageHelper<any>;
|
||||
const defaultValue: any = { firstName: "Joe" };
|
||||
const observedChanges: any[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
observedChanges.length = 0;
|
||||
|
||||
storageHelper = createStorageHelper<typeof defaultValue>("some-key", {
|
||||
defaultValue,
|
||||
storage: {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("storage.get() is observable", () => {
|
||||
expect(storageHelper.get()).toEqual(defaultValue);
|
||||
|
||||
reaction(() => toJS(storageHelper.get()), change => {
|
||||
observedChanges.push(change);
|
||||
});
|
||||
|
||||
storageHelper.merge({ lastName: "Black" });
|
||||
storageHelper.set("whatever");
|
||||
expect(observedChanges).toEqual([{ ...defaultValue, lastName: "Black" }, "whatever"]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import ipcRendererInjectable from "../ipc-renderer.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IpcRendererEvent } from "electron";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
|
||||
const enlistMessageChannelListenerInjectable = getInjectable({
|
||||
id: "enlist-message-channel-listener-for-renderer",
|
||||
|
||||
instantiate: (di) => {
|
||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||
|
||||
return ({ channel, handler }) => {
|
||||
const nativeCallback = (_: IpcRendererEvent, message: unknown) => {
|
||||
handler(message);
|
||||
};
|
||||
|
||||
ipcRenderer.on(channel.id, nativeCallback);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.off(channel.id, nativeCallback);
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
injectionToken: enlistMessageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
export default enlistMessageChannelListenerInjectable;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { EnlistMessageChannelListener } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
|
||||
import type { IpcRendererEvent, IpcRenderer } from "electron";
|
||||
import ipcRendererInjectable from "../ipc-renderer.injectable";
|
||||
|
||||
describe("enlist message channel listener in renderer", () => {
|
||||
let enlistMessageChannelListener: EnlistMessageChannelListener;
|
||||
let ipcRendererStub: IpcRenderer;
|
||||
let onMock: jest.Mock;
|
||||
let offMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
onMock = jest.fn();
|
||||
offMock = jest.fn();
|
||||
|
||||
ipcRendererStub = {
|
||||
on: onMock,
|
||||
off: offMock,
|
||||
} as unknown as IpcRenderer;
|
||||
|
||||
di.override(ipcRendererInjectable, () => ipcRendererStub);
|
||||
|
||||
enlistMessageChannelListener = di.inject(
|
||||
enlistMessageChannelListenerInjectionToken,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let handlerMock: jest.Mock;
|
||||
let disposer: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
handlerMock = jest.fn();
|
||||
|
||||
disposer = enlistMessageChannelListener({
|
||||
channel: { id: "some-channel-id" },
|
||||
handler: handlerMock,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call handler yet", () => {
|
||||
expect(handlerMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registers the listener", () => {
|
||||
expect(onMock).toHaveBeenCalledWith(
|
||||
"some-channel-id",
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not de-register the listener yet", () => {
|
||||
expect(offMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when message arrives", () => {
|
||||
beforeEach(() => {
|
||||
onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message");
|
||||
});
|
||||
|
||||
it("calls the handler with the message", () => {
|
||||
expect(handlerMock).toHaveBeenCalledWith("some-message");
|
||||
});
|
||||
|
||||
it("when disposing the listener, de-registers the listener", () => {
|
||||
disposer();
|
||||
|
||||
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
it("given number as message, when message arrives, calls the handler with the message", () => {
|
||||
onMock.mock.calls[0][1]({} as IpcRendererEvent, 42);
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it("given boolean as message, when message arrives, calls the handler with the message", () => {
|
||||
onMock.mock.calls[0][1]({} as IpcRendererEvent, true);
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("given object as message, when message arrives, calls the handler with the message", () => {
|
||||
onMock.mock.calls[0][1]({} as IpcRendererEvent, { some: "object" });
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
|
||||
});
|
||||
});
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 { beforeFrameStartsSecondInjectionToken } from "../../../before-frame-starts/tokens";
|
||||
import listeningOnMessageChannelsInjectable from "../../../../common/utils/channel/listening-on-message-channels.injectable";
|
||||
|
||||
const startListeningOfChannelsInjectable = getInjectable({
|
||||
id: "start-listening-of-channels-renderer",
|
||||
|
||||
instantiate: (di) => ({
|
||||
id: "start-listening-of-channels-renderer",
|
||||
run: () => {
|
||||
const listeningOfChannels = di.inject(listeningOnMessageChannelsInjectable);
|
||||
|
||||
listeningOfChannels.start();
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: beforeFrameStartsSecondInjectionToken,
|
||||
});
|
||||
|
||||
export default startListeningOfChannelsInjectable;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { IpcRenderer } from "electron";
|
||||
import ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
|
||||
|
||||
export default getGlobalOverride(ipcRendererInjectable, () => ({
|
||||
invoke: () => {},
|
||||
on: () => {},
|
||||
}) as unknown as IpcRenderer);
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { ipcRenderer } from "electron";
|
||||
|
||||
const ipcRendererInjectable = getInjectable({
|
||||
id: "ipc-renderer",
|
||||
instantiate: () => ipcRenderer,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default ipcRendererInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { SendMessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token";
|
||||
import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
|
||||
import sendToMainInjectable from "./send-to-main.injectable";
|
||||
|
||||
const messageToChannelInjectable = getInjectable({
|
||||
id: "message-to-channel",
|
||||
|
||||
instantiate: (di) => {
|
||||
const sendToMain = di.inject(sendToMainInjectable);
|
||||
|
||||
return ((channel, message) => {
|
||||
sendToMain(channel.id, message);
|
||||
}) as SendMessageToChannel;
|
||||
},
|
||||
|
||||
injectionToken: sendMessageToChannelInjectionToken,
|
||||
});
|
||||
|
||||
export default messageToChannelInjectable;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||
import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token";
|
||||
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
|
||||
|
||||
const requestFromChannelInjectable = getInjectable({
|
||||
id: "request-from-channel",
|
||||
|
||||
instantiate: (di) => {
|
||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||
|
||||
return ((channel, request) => ipcRenderer.invoke(channel.id, request)) as RequestFromChannel;
|
||||
},
|
||||
|
||||
injectionToken: requestFromChannelInjectionToken,
|
||||
});
|
||||
|
||||
export default requestFromChannelInjectable;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 ipcRendererInjectable from "./ipc-renderer.injectable";
|
||||
|
||||
const sendToMainInjectable = getInjectable({
|
||||
id: "send-to-main",
|
||||
|
||||
instantiate: (di) => {
|
||||
const ipcRenderer = di.inject(ipcRendererInjectable);
|
||||
|
||||
return (channelId: string, message: any) => {
|
||||
ipcRenderer.send(channelId, message);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default sendToMainInjectable;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import type { StorageHelperDependencies, StorageHelperOptions } from "./storage-helper";
|
||||
import { StorageHelper } from "./storage-helper";
|
||||
|
||||
export type CreateStorageHelper = <T>(key: string, options: StorageHelperOptions<T>) => StorageHelper<T>;
|
||||
|
||||
const createStorageHelperInjectable = getInjectable({
|
||||
id: "create-storage-helper",
|
||||
instantiate: (di): CreateStorageHelper => {
|
||||
const deps: StorageHelperDependencies = {
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
|
||||
return (key, options) => new StorageHelper(deps, key, options);
|
||||
},
|
||||
});
|
||||
|
||||
export default createStorageHelperInjectable;
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { action } from "mobx";
|
||||
import lensLocalStorageStateInjectable from "./state.injectable";
|
||||
import createStorageHelperInjectable from "../create-storage-helper.injectable";
|
||||
import type { StorageLayer } from "../storage-helper";
|
||||
|
||||
export type CreateStorage = <T>(key: string, defaultValue: T) => StorageLayer<T>;
|
||||
|
||||
const createStorageInjectable = getInjectable({
|
||||
id: "create-storage",
|
||||
|
||||
instantiate: (di): CreateStorage => {
|
||||
const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable);
|
||||
const createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||
|
||||
return <T>(key: string, defaultValue: T) => createStorageHelper<T>(key, {
|
||||
defaultValue,
|
||||
storage: {
|
||||
getItem: (key) => lensLocalStorageState[key] as T,
|
||||
setItem: action((key, value) => lensLocalStorageState[key] = value),
|
||||
removeItem: action((key) => delete lensLocalStorageState[key]),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default createStorageInjectable;
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 AwaitLock from "await-lock";
|
||||
import { comparer, reaction, runInAction, toJS } from "mobx";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import setupAppPathsInjectable from "../../app-paths/setup-app-paths.injectable";
|
||||
import { beforeFrameStartsFirstInjectionToken } from "../../before-frame-starts/tokens";
|
||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import { storageHelperLogPrefix } from "../storage-helper";
|
||||
import lensLocalStorageStateInjectable from "./state.injectable";
|
||||
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
|
||||
|
||||
const initializeStateInjectable = getInjectable({
|
||||
id: "initialize-lens-local-storage-state",
|
||||
instantiate: (di) => ({
|
||||
id: "initialize-lens-local-storage-state",
|
||||
run: async () => {
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
|
||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||
const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable);
|
||||
const readJsonFile = di.inject(readJsonFileInjectable);
|
||||
const writeJsonFile = di.inject(writeJsonFileInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const storageSaveDelay = di.inject(storageSaveDelayInjectable);
|
||||
const lock = new AwaitLock();
|
||||
|
||||
const filePath = joinPaths(directoryForLensLocalStorage, `${hostedClusterId || "app"}.json`);
|
||||
|
||||
try {
|
||||
const localFile = await readJsonFile(filePath);
|
||||
|
||||
if (typeof localFile === "object") {
|
||||
runInAction(() => {
|
||||
Object.assign(lensLocalStorageState, localFile);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
} finally {
|
||||
logger.info(`${storageHelperLogPrefix} loading finished for ${filePath}`);
|
||||
}
|
||||
|
||||
reaction(() => toJS(lensLocalStorageState), saveFile, {
|
||||
delay: storageSaveDelay, // lazy, avoid excessive writes to fs
|
||||
equals: comparer.structural, // save only when something really changed
|
||||
});
|
||||
|
||||
async function saveFile(state: Record<string, unknown>) {
|
||||
try {
|
||||
await lock.acquireAsync();
|
||||
logger.info(`${storageHelperLogPrefix} saving ${filePath}`);
|
||||
await writeJsonFile(filePath, state);
|
||||
} catch (error) {
|
||||
logger.error(`${storageHelperLogPrefix} saving failed: ${error}`, {
|
||||
json: state, jsonFilePath: filePath,
|
||||
});
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
},
|
||||
runAfter: di.inject(setupAppPathsInjectable),
|
||||
}),
|
||||
injectionToken: beforeFrameStartsFirstInjectionToken,
|
||||
});
|
||||
|
||||
export default initializeStateInjectable;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { observable } from "mobx";
|
||||
|
||||
const lensLocalStorageStateInjectable = getInjectable({
|
||||
id: "lens-local-storage-state",
|
||||
instantiate: () => observable.object({} as Record<string, unknown>),
|
||||
});
|
||||
|
||||
export default lensLocalStorageStateInjectable;
|
||||
+9
@@ -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 "../../../common/test-utils/get-global-override";
|
||||
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
|
||||
|
||||
export default getGlobalOverride(storageSaveDelayInjectable, () => 0);
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
const storageSaveDelayInjectable = getInjectable({
|
||||
id: "storage-save-delay",
|
||||
instantiate: () => 250,
|
||||
});
|
||||
|
||||
export default storageSaveDelayInjectable;
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { iter } from "../../common/utils";
|
||||
|
||||
export type IgnoredClassNames = number | symbol | Function;
|
||||
|
||||
export type IClassName = string | string[] | IClassNameMap | undefined | null | false | IgnoredClassNames;
|
||||
export type IClassNameMap = object;
|
||||
|
||||
export function cssNames(...classNames: IClassName[]): string {
|
||||
const classNamesEnabled = new Map<string, boolean>();
|
||||
|
||||
for (const className of classNames) {
|
||||
if (typeof className === "string") {
|
||||
classNamesEnabled.set(className, true);
|
||||
} else if (Array.isArray(className)) {
|
||||
for (const name of className) {
|
||||
classNamesEnabled.set(name, true);
|
||||
}
|
||||
} else if (className && typeof className === "object") {
|
||||
for (const [name, value] of Object.entries(className)) {
|
||||
classNamesEnabled.set(name, Boolean(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return iter.chain(classNamesEnabled.entries())
|
||||
.filter(([, isActive]) => !!isActive)
|
||||
.filterMap(([className]) => className.trim())
|
||||
.join(" ");
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Helper for getting/setting css-variables
|
||||
|
||||
export function cssVar(elem: HTMLElement) {
|
||||
return {
|
||||
get(name: string) {
|
||||
const value = window.getComputedStyle(elem).getPropertyValue(name).trim();
|
||||
|
||||
return {
|
||||
toString: () => value,
|
||||
valueOf: () => parseFloat(value),
|
||||
};
|
||||
},
|
||||
set(name: string, value: number | string) {
|
||||
if (typeof value === "number") value = `${value}px`;
|
||||
elem.style.setProperty(name, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format `mode` in octal notation
|
||||
*/
|
||||
export function displayMode(mode: number): string {
|
||||
return `0o${mode.toString(8)}`;
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Common usage utils & helpers
|
||||
export * from "../../common/utils";
|
||||
export * from "../../common/event-emitter";
|
||||
|
||||
export * from "./cssNames";
|
||||
export * from "./cssVar";
|
||||
export * from "./display-mode";
|
||||
export * from "./interval";
|
||||
export * from "./isMiddleClick";
|
||||
export * from "./isReactNode";
|
||||
export * from "./metricUnitsToNumber";
|
||||
export * from "./name-parts";
|
||||
export * from "./prevDefault";
|
||||
export * from "./saveFile";
|
||||
export * from "./storage-helper";
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Helper for working with time updates / data-polling callbacks
|
||||
|
||||
export interface IntervalFn {
|
||||
start(runImmediately?: boolean): void;
|
||||
stop(): void;
|
||||
restart(runImmediately?: boolean): void;
|
||||
readonly isRunning: boolean;
|
||||
}
|
||||
|
||||
export function interval(timeSec = 1, callback: (count: number) => void, autoRun = false): IntervalFn {
|
||||
let count = 0;
|
||||
let timer = -1;
|
||||
let isRunning = false;
|
||||
const intervalManager = {
|
||||
start(runImmediately = false) {
|
||||
if (isRunning) return;
|
||||
const tick = () => callback(++count);
|
||||
|
||||
isRunning = true;
|
||||
timer = window.setInterval(tick, 1000 * timeSec);
|
||||
if (runImmediately) tick();
|
||||
},
|
||||
stop() {
|
||||
count = 0;
|
||||
isRunning = false;
|
||||
clearInterval(timer);
|
||||
},
|
||||
restart(runImmediately = false) {
|
||||
this.stop();
|
||||
this.start(runImmediately);
|
||||
},
|
||||
get isRunning() {
|
||||
return isRunning;
|
||||
},
|
||||
};
|
||||
|
||||
if (autoRun) intervalManager.start();
|
||||
|
||||
return intervalManager;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
|
||||
/**
|
||||
* Returns `true` if `node` is a falsy value
|
||||
*/
|
||||
export function isNodeFalsy(node: React.ReactNode): boolean {
|
||||
return !isNodeRenderable(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if React would render this
|
||||
*/
|
||||
export function isNodeRenderable(node: React.ReactNode): boolean {
|
||||
return Boolean(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first react node provided that is would be rendered by react
|
||||
*/
|
||||
export function foldNodes(...nodes: React.ReactNode[]): React.ReactNode {
|
||||
return nodes.find(isNodeRenderable);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
|
||||
// Helper for inlining middleClick checks
|
||||
// <form onAuxClick={isMiddleClick(() => console.log('do some action'))}>
|
||||
// <input name="text"/>
|
||||
// <button type="submit">Action</button>
|
||||
// </form>
|
||||
|
||||
export function isMiddleClick<E extends React.MouseEvent>(callback: (evt: E) => any) {
|
||||
return function (evt: E) {
|
||||
if(evt.button === 1) {
|
||||
return callback(evt);
|
||||
}
|
||||
};
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Type guard for checking valid react node to use in render
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import { isObject } from "../../common/utils";
|
||||
|
||||
export function isReactNode(node: unknown): node is ReactNode {
|
||||
return (isObject(node) && React.isValidElement(node))
|
||||
|| Array.isArray(node) && node.every(isReactNode)
|
||||
|| node == null
|
||||
|| typeof node !== "object";
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { JSONPath } from "@astronautlabs/jsonpath";
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
|
||||
const slashDashSearch = /[/\\-]/g;
|
||||
const pathByBareDots = /(?<=\w)\./;
|
||||
const textBeforeFirstSquare = /^.*(?=\[)/g;
|
||||
const backSlash = /\\/g;
|
||||
const kubectlOptionPrefix = TypedRegEx("^\\$?\\.?(?<pathExpression>.*)");
|
||||
const sliceVersion = /\[]/g;
|
||||
const tripleDotName = /\.\.\.(?<trailing>.)/g;
|
||||
const trailingDotDot = /\.\.$/;
|
||||
|
||||
/**
|
||||
* The GO package that kubectl and kubernetes uses for its JSONpath implementation has some
|
||||
* shorthand conveniences that are not part of the official spec. This function tries to convert
|
||||
* those shorthands to the official spec.
|
||||
*
|
||||
* Known shorthands:
|
||||
* - Leading `$` is optional (but implied)
|
||||
* - The string `/` can be used without a leading `\` escapement
|
||||
* - The string `\.` is used to denote the "value of '.'" and not "next key"
|
||||
* - The string `-` can be used while not in quotes
|
||||
* - `[]` as shorthand for `[0]`
|
||||
* - Remove `..` at the end of a path, we will just format it slightly differently
|
||||
* - Allow `...foo` as well as `..foo`
|
||||
*/
|
||||
export function convertKubectlJsonPathToNodeJsonPath(jsonPath: string) {
|
||||
const captures = kubectlOptionPrefix.captures(jsonPath);
|
||||
let start = "$";
|
||||
|
||||
if (!captures) {
|
||||
return start;
|
||||
}
|
||||
|
||||
let { pathExpression } = captures;
|
||||
|
||||
if (pathExpression.match(slashDashSearch)) {
|
||||
const [first, ...rest] = pathExpression.split(pathByBareDots);
|
||||
|
||||
pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`;
|
||||
}
|
||||
|
||||
pathExpression = pathExpression.replace(trailingDotDot, "");
|
||||
pathExpression = pathExpression.replace(tripleDotName, "..$<trailing>");
|
||||
|
||||
if (!pathExpression.startsWith("[")) {
|
||||
start += ".";
|
||||
}
|
||||
|
||||
// strip '\' characters from the result
|
||||
return `${start}${pathExpression.replace(backSlash, "").replace(sliceVersion, "[0]")}`;
|
||||
}
|
||||
|
||||
function convertToIndexNotation(key: string, firstItem = false) {
|
||||
if (key.match(slashDashSearch)) {
|
||||
if (key.includes("[")) { // handle cases where key contains [...]
|
||||
const keyToConvert = key.match(textBeforeFirstSquare); // get the text from the key before '['
|
||||
|
||||
if (keyToConvert && keyToConvert[0].match(slashDashSearch)) { // check if that part contains illegal characters
|
||||
return key.replace(keyToConvert[0], `['${keyToConvert[0]}']`); // surround key with '[' and ']'
|
||||
} else {
|
||||
return `.${key}`; // otherwise return as is with leading '.'
|
||||
}
|
||||
}
|
||||
|
||||
return `['${key}']`;
|
||||
} else { // no illegal characters found, do not touch
|
||||
const prefix = firstItem ? "" : ".";
|
||||
|
||||
return `${prefix}${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatJSONValue(value: unknown): string {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(formatJSONValue).join(", ");
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a safer version of `JSONPath.value(obj, path)` with untrusted jsonpath strings
|
||||
*
|
||||
* This function will also stringify the value retreived from the object
|
||||
*/
|
||||
export function safeJSONPathValue(obj: object, path: string): unknown {
|
||||
try {
|
||||
const parsedPath = JSONPath.parse(convertKubectlJsonPathToNodeJsonPath(path));
|
||||
const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || exp.expression.type === "wildcard");
|
||||
const value = JSONPath.query(obj, JSONPath.stringify(parsedPath), isSlice ? Infinity : 1);
|
||||
|
||||
if (isSlice) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value[0];
|
||||
} catch (error) {
|
||||
// something failed
|
||||
console.warn("[JSON-PATH]: failed to parse jsonpath", error);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
const base = 1000;
|
||||
const suffixes = ["k", "m", "g", "t", "q"];
|
||||
|
||||
export function metricUnitsToNumber(value: string): number {
|
||||
const suffix = value.toLowerCase().slice(-1);
|
||||
const index = suffixes.indexOf(suffix);
|
||||
|
||||
return parseInt(
|
||||
(parseFloat(value) * Math.pow(base, index + 1)).toFixed(1),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Split `name` into the parts seperated by one or more of (-, _, or .) and
|
||||
* the sections can be converted to numbers will be converted
|
||||
* @param name A kube object name
|
||||
* @returns The converted parts of the name
|
||||
*/
|
||||
export function getConvertedParts(name: string): (string | number)[] {
|
||||
return name
|
||||
.split(/[-_./\\]+/)
|
||||
.map(part => {
|
||||
const converted = +part;
|
||||
|
||||
return isNaN(converted) ? part : converted;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { WindowEventListener } from "../window/event-listener.injectable";
|
||||
|
||||
function parseKeyDownDescriptor(descriptor: string): (event: KeyboardEvent) => boolean {
|
||||
const parts = new Set((
|
||||
descriptor
|
||||
.split("+")
|
||||
.filter(Boolean)
|
||||
.map(part => part.toLowerCase())
|
||||
));
|
||||
|
||||
if (parts.size === 0) {
|
||||
return () => true;
|
||||
}
|
||||
|
||||
const hasShift = parts.delete("shift");
|
||||
const hasAlt = parts.delete("alt");
|
||||
|
||||
const rawHasCtrl = parts.delete("ctrl");
|
||||
const rawHasControl = parts.delete("control");
|
||||
const hasCtrl = rawHasCtrl || rawHasControl;
|
||||
|
||||
const rawHasMeta = parts.delete("meta");
|
||||
const rawHasCmd = parts.delete("cmd");
|
||||
const hasMeta = rawHasCmd || rawHasMeta; // This means either matches
|
||||
|
||||
const [key, ...rest] = [...parts];
|
||||
|
||||
if (rest.length !== 0) {
|
||||
throw new Error("only single key combinations are currently supported");
|
||||
}
|
||||
|
||||
return (event) => {
|
||||
return event.altKey === hasAlt
|
||||
&& event.shiftKey === hasShift
|
||||
&& event.ctrlKey === hasCtrl
|
||||
&& event.metaKey === hasMeta
|
||||
&& event.key.toLowerCase() === key.toLowerCase();
|
||||
};
|
||||
}
|
||||
|
||||
export function onKeyboardShortcut(descriptor: string, action: () => void): WindowEventListener<"keydown"> {
|
||||
const isMatchingEvent = parseKeyDownDescriptor(descriptor);
|
||||
|
||||
return (event) => {
|
||||
if (isMatchingEvent(event)) {
|
||||
action();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
|
||||
// Helper for preventing default event action and performing custom callback
|
||||
// 1)
|
||||
// <form onSubmit={prevDefault(() => console.log('do some action'))}>
|
||||
// <input name="text"/>
|
||||
// <button type="submit">Action</button>
|
||||
// </form>
|
||||
// 2)
|
||||
// <a href="/some-page-url">
|
||||
// <span>Link text</span>
|
||||
// <Icon onClick={prevDefault(() => console.log('stay on the page and open dialog'))}/>
|
||||
// </a>
|
||||
|
||||
export function prevDefault<E extends React.SyntheticEvent | Event, R>(callback: (evt: E) => R): (event: E) => R {
|
||||
return function (evt: E) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
return callback(evt);
|
||||
};
|
||||
}
|
||||
|
||||
export function stopPropagation(evt: Event | React.SyntheticEvent) {
|
||||
evt.stopPropagation();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeResource } from "../../common/rbac";
|
||||
import { apiResourceRecord } from "../../common/rbac";
|
||||
import { object } from "../../common/utils";
|
||||
|
||||
export const ResourceNames: Record<KubeResource, string> = {
|
||||
"namespaces": "Namespaces",
|
||||
"nodes": "Nodes",
|
||||
"events": "Events",
|
||||
"leases": "Leases",
|
||||
"resourcequotas": "Resource Quotas",
|
||||
"services": "Services",
|
||||
"secrets": "Secrets",
|
||||
"configmaps": "Config Maps",
|
||||
"ingresses": "Ingresses",
|
||||
"ingressclasses": "Ingress Classes",
|
||||
"networkpolicies": "Network Policies",
|
||||
"persistentvolumeclaims": "Persistent Volume Claims",
|
||||
"persistentvolumes": "Persistent Volumes",
|
||||
"storageclasses": "Storage Classes",
|
||||
"pods": "Pods",
|
||||
"daemonsets": "Daemon Sets",
|
||||
"deployments": "Deployments",
|
||||
"statefulsets": "Stateful Sets",
|
||||
"replicasets": "Replica Sets",
|
||||
"jobs": "Jobs",
|
||||
"cronjobs": "Cron Jobs",
|
||||
"endpoints": "Endpoints",
|
||||
"customresourcedefinitions": "Custom Resource Definitions",
|
||||
"horizontalpodautoscalers": "Horizontal Pod Autoscalers",
|
||||
"podsecuritypolicies": "Pod Security Policies",
|
||||
"poddisruptionbudgets": "Pod Disruption Budgets",
|
||||
"priorityclasses": "Priority Classes",
|
||||
"runtimeclasses": "Runtime Classes",
|
||||
"limitranges": "Limit Ranges",
|
||||
"roles": "Roles",
|
||||
"rolebindings": "Role Bindings",
|
||||
"clusterrolebindings": "Cluster Role Bindings",
|
||||
"clusterroles": "Cluster Roles",
|
||||
"serviceaccounts": "Service Accounts",
|
||||
};
|
||||
|
||||
export const ResourceKindMap = object.fromEntries(
|
||||
object.entries(apiResourceRecord)
|
||||
.map(([resource, { kind }]) => [kind, resource]),
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { resolveSystemProxyInjectionToken } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token";
|
||||
import requestFromChannelInjectable from "../channel/request-from-channel.injectable";
|
||||
import { resolveSystemProxyChannel } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel";
|
||||
|
||||
const resolveSystemProxyInjectable = getInjectable({
|
||||
id: "resolve-system-proxy-for-renderer",
|
||||
|
||||
instantiate: (di) => {
|
||||
const requestFromChannel = di.inject(requestFromChannelInjectable);
|
||||
|
||||
return async (url) => requestFromChannel(resolveSystemProxyChannel, url);
|
||||
},
|
||||
|
||||
injectionToken: resolveSystemProxyInjectionToken,
|
||||
});
|
||||
|
||||
export default resolveSystemProxyInjectable;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 { saveFileDialog } from "./saveFile";
|
||||
|
||||
const openSaveFileDialogInjectable = getInjectable({
|
||||
id: "open-save-file-dialog",
|
||||
instantiate: () => saveFileDialog,
|
||||
});
|
||||
|
||||
export default openSaveFileDialogInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request default save-file dialog in browser.
|
||||
* @param filename Name of file to be saved locally
|
||||
* @param contents String or Buffer
|
||||
* @param type Content-type
|
||||
*/
|
||||
export function saveFileDialog(filename: string, contents: BlobPart | BlobPart[], type: string) {
|
||||
const data = new Blob([contents].flat(), { type });
|
||||
const url = URL.createObjectURL(data);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.)
|
||||
import { action, comparer, computed, makeObservable, observable, observe, toJS } from "mobx";
|
||||
import type { Draft } from "immer";
|
||||
import { produce, isDraft } from "immer";
|
||||
import { isEqual, isPlainObject } from "lodash";
|
||||
import assert from "assert";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
export interface StorageChange<T> {
|
||||
key: string;
|
||||
value: T | undefined;
|
||||
oldValue: T | undefined;
|
||||
}
|
||||
|
||||
export interface StorageAdapter<T> {
|
||||
[metadata: string]: unknown;
|
||||
getItem(key: string): T;
|
||||
setItem(key: string, value: T): void;
|
||||
removeItem(key: string): void;
|
||||
onChange?(change: StorageChange<T>): void;
|
||||
}
|
||||
|
||||
export interface StorageHelperOptions<T> {
|
||||
readonly storage: StorageAdapter<T>;
|
||||
readonly defaultValue: T;
|
||||
}
|
||||
|
||||
export interface StorageLayer<T> {
|
||||
isDefaultValue(val: T): boolean;
|
||||
get(): T;
|
||||
set(value: T): void;
|
||||
reset(): void;
|
||||
merge(value: Partial<T> | ((draft: Draft<T>) => Partial<T> | void)): void;
|
||||
}
|
||||
|
||||
export const storageHelperLogPrefix = "[STORAGE-HELPER]:";
|
||||
|
||||
export interface StorageHelperDependencies {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class StorageHelper<T> implements StorageLayer<T> {
|
||||
readonly storage: StorageAdapter<T>;
|
||||
|
||||
private readonly data = observable.box<T>(undefined, {
|
||||
deep: true,
|
||||
equals: comparer.structural,
|
||||
});
|
||||
|
||||
private readonly value = computed(() => this.data.get() ?? this.defaultValue);
|
||||
|
||||
get defaultValue(): T {
|
||||
// return as-is since options.defaultValue might be a getter too
|
||||
return this.options.defaultValue;
|
||||
}
|
||||
|
||||
constructor(private readonly dependencies: StorageHelperDependencies, readonly key: string, private readonly options: StorageHelperOptions<T>) {
|
||||
makeObservable(this);
|
||||
|
||||
this.storage = this.options.storage;
|
||||
|
||||
observe(this.data, (change) => {
|
||||
this.onChange(change.newValue as T | undefined, change.oldValue as T | undefined);
|
||||
});
|
||||
|
||||
try {
|
||||
const data = this.storage.getItem(this.key);
|
||||
const notEmpty = data != null;
|
||||
const notDefault = !this.isDefaultValue(data);
|
||||
|
||||
if (notEmpty && notDefault) {
|
||||
this.set(data);
|
||||
}
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`${storageHelperLogPrefix} loading error: ${error}`, this);
|
||||
}
|
||||
}
|
||||
|
||||
isDefaultValue(value: T): boolean {
|
||||
return isEqual(value, this.defaultValue);
|
||||
}
|
||||
|
||||
private onChange(value: T | undefined, oldValue: T | undefined) {
|
||||
try {
|
||||
if (value == null) {
|
||||
this.storage.removeItem(this.key);
|
||||
} else {
|
||||
this.storage.setItem(this.key, value);
|
||||
}
|
||||
|
||||
this.storage.onChange?.({ value, oldValue, key: this.key });
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`${storageHelperLogPrefix} updating storage: ${error}`, this, { value, oldValue });
|
||||
}
|
||||
}
|
||||
|
||||
get(): T {
|
||||
return this.value.get();
|
||||
}
|
||||
|
||||
@action
|
||||
set(value: T) {
|
||||
if (this.isDefaultValue(value)) {
|
||||
this.data.set(undefined);
|
||||
} else {
|
||||
this.data.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.data.set(undefined);
|
||||
}
|
||||
|
||||
@action
|
||||
merge(value: T extends object ? Partial<T> | ((draft: Draft<T>) => Partial<T> | void) : never) {
|
||||
const nextValue = produce<T>(toJS(this.get()), (draft) => {
|
||||
assert(typeof draft === "object" && draft);
|
||||
|
||||
if (typeof value == "function") {
|
||||
const newValue = value(draft);
|
||||
|
||||
// merge returned plain objects from `value-as-callback` usage
|
||||
// otherwise `draft` can be just modified inside a callback without returning any value (void)
|
||||
if (newValue && !isDraft(newValue)) {
|
||||
Object.assign(draft, newValue);
|
||||
}
|
||||
} else if (isPlainObject(value)) {
|
||||
Object.assign(draft, value);
|
||||
}
|
||||
|
||||
return draft;
|
||||
});
|
||||
|
||||
this.set(nextValue);
|
||||
}
|
||||
}
|
||||
+38
@@ -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 { beforeFrameStartsSecondInjectionToken } from "../../before-frame-starts/tokens";
|
||||
import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels";
|
||||
import createSyncBoxStateInjectable from "../../../common/utils/sync-box/sync-box-state.injectable";
|
||||
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
|
||||
import { runInAction } from "mobx";
|
||||
import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token";
|
||||
import assert from "assert";
|
||||
|
||||
const provideInitialValuesForSyncBoxesInjectable = getInjectable({
|
||||
id: "provide-initial-values-for-sync-boxes",
|
||||
|
||||
instantiate: (di) => ({
|
||||
id: "provide-initial-values-for-sync-boxes",
|
||||
run: async () => {
|
||||
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
|
||||
const syncBoxes = di.injectMany(syncBoxInjectionToken);
|
||||
const initialValues = await requestFromChannel(syncBoxInitialValueChannel);
|
||||
|
||||
runInAction(() => {
|
||||
for (const { id, value } of initialValues) {
|
||||
const syncBox = syncBoxes.find((box) => box.id === id);
|
||||
|
||||
assert(syncBox);
|
||||
di.inject(createSyncBoxStateInjectable, syncBox.id).set(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: beforeFrameStartsSecondInjectionToken,
|
||||
});
|
||||
|
||||
export default provideInitialValuesForSyncBoxesInjectable;
|
||||
Reference in new issue
Block a user