Convert runMany and runManySync to use injectManyWithMeta + move to seperate package (#7244)

* Convert runMany and runManySync to use injectManyWithMeta

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup type errors due to new Runnable requirements

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add documentation for verifyRunnablesAreDAG

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify convertToWithIdWith

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move all utility functions to separate package

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move testing utilities to separate package

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move run-many and run-many-sync to separate package

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace all internal uses of utilities with new packages

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use new @k8slens/run-many package in core

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add dep to open-lens

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup uses of @k8slens/test-utils

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup getGlobalOverride

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move tests to new package too

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup uses of AsyncResult and autoBind

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup remaining import issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Finial fixups to fix build

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert moving "testUsingFakeTime" to separate package

- This fixes tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix integration tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit test failing due to spelling fix

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2023-03-10 10:07:28 +02:00
1 parent ad9bafe2a5
commit 2789bcebcb
754 files changed
+2715 -2752

No files matched your search

@@ -1,20 +0,0 @@
/**
* 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);
});
});
@@ -1,178 +0,0 @@
/**
* 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");
});
});
@@ -1,20 +0,0 @@
/**
* 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);
});
});
@@ -10,7 +10,6 @@ const startListeningOfChannelsInjectable = getInjectable({
id: "start-listening-of-channels-renderer",
instantiate: (di) => ({
id: "start-listening-of-channels-renderer",
run: () => {
const listeningOfChannels = di.inject(listeningOnMessageChannelsInjectable);
@@ -4,7 +4,7 @@
*/
import type { IpcRenderer } from "electron";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import { getGlobalOverride } from "@k8slens/test-utils";
export default getGlobalOverride(ipcRendererInjectable, () => ({
invoke: () => {},
@@ -20,7 +20,6 @@ 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);
@@ -67,7 +66,7 @@ const initializeStateInjectable = getInjectable({
}
}
},
runAfter: di.inject(setupAppPathsInjectable),
runAfter: setupAppPathsInjectable,
}),
injectionToken: beforeFrameStartsFirstInjectionToken,
});
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import { getGlobalOverride } from "@k8slens/test-utils";
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
export default getGlobalOverride(storageSaveDelayInjectable, () => 0);
@@ -1,32 +0,0 @@
/**
* 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[] | Record<string, any> | undefined | null | false | IgnoredClassNames;
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(" ");
}
@@ -1,23 +0,0 @@
/**
* 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);
},
};
}
@@ -1,11 +0,0 @@
/**
* 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
@@ -1,20 +0,0 @@
/**
* 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";
@@ -1,45 +0,0 @@
/**
* 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;
}
@@ -1,27 +0,0 @@
/**
* 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);
}
@@ -1,20 +0,0 @@
/**
* 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);
}
};
}
@@ -1,16 +0,0 @@
/**
* 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";
}
@@ -1,117 +0,0 @@
/**
* 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;
}
}
@@ -1,16 +0,0 @@
/**
* 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),
);
}
@@ -1,20 +0,0 @@
/**
* 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;
});
}
@@ -1,54 +0,0 @@
/**
* 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();
}
};
}
@@ -1,31 +0,0 @@
/**
* 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();
}
+1 -1
View File
@@ -5,7 +5,7 @@
import type { KubeResource } from "../../common/rbac";
import { apiResourceRecord } from "../../common/rbac";
import { object } from "../../common/utils";
import { object } from "@k8slens/utilities";
export const ResourceNames: Record<KubeResource, string> = {
"namespaces": "Namespaces",
@@ -15,7 +15,6 @@ 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);