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:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
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"]);
});
});
});
@@ -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;
@@ -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" });
});
});
});
@@ -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;
@@ -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;
@@ -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;
+34
View File
@@ -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(" ");
}
+23
View File
@@ -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)}`;
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// 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
View File
@@ -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();
}
+50
View File
@@ -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
View File
@@ -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);
}
}
@@ -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;