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,30 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getClusterIdFromHost } from "../cluster-id-url-parsing";
|
||||
|
||||
describe("getClusterIdFromHost", () => {
|
||||
const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a";
|
||||
|
||||
it("should return undefined for non cluster frame hosts", () => {
|
||||
expect(getClusterIdFromHost("lens.app:45345")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return ClusterId for cluster frame hosts", () => {
|
||||
expect(getClusterIdFromHost(`${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
});
|
||||
|
||||
it("should return ClusterId for cluster frame hosts with additional subdomains", () => {
|
||||
expect(getClusterIdFromHost(`abc.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { bytesToUnits, unitsToBytes } from "../convertMemory";
|
||||
|
||||
describe("unitsToBytes", () => {
|
||||
it("without any units, just parse as float", () => {
|
||||
expect(unitsToBytes("1234")).toBe(1234);
|
||||
});
|
||||
|
||||
it("should parse with B suffix", () => {
|
||||
expect(unitsToBytes("1234B")).toBe(1234);
|
||||
});
|
||||
|
||||
it("should parse with Ki suffix", () => {
|
||||
expect(unitsToBytes("1234Ki")).toBe(1234 * (1024));
|
||||
});
|
||||
|
||||
it("should parse with Ki suffix as the same as KiB", () => {
|
||||
expect(unitsToBytes("1234Ki")).toBe(unitsToBytes("1234KiB"));
|
||||
});
|
||||
|
||||
it("should parse with Mi suffix", () => {
|
||||
expect(unitsToBytes("1234Mi")).toBe(1234 * (1024 ** 2));
|
||||
});
|
||||
|
||||
it("should parse with Mi suffix as the same as MiB", () => {
|
||||
expect(unitsToBytes("1234Mi")).toBe(unitsToBytes("1234MiB"));
|
||||
});
|
||||
|
||||
it("should parse with Gi suffix", () => {
|
||||
expect(unitsToBytes("1234Gi")).toBe(1234 * (1024 ** 3));
|
||||
});
|
||||
|
||||
it("should parse with Gi suffix as the same as GiB", () => {
|
||||
expect(unitsToBytes("1234Gi")).toBe(unitsToBytes("1234GiB"));
|
||||
});
|
||||
|
||||
it("should parse with Ti suffix", () => {
|
||||
expect(unitsToBytes("1234Ti")).toBe(1234 * (1024 ** 4));
|
||||
});
|
||||
|
||||
it("should parse with Ti suffix as the same as TiB", () => {
|
||||
expect(unitsToBytes("1234Ti")).toBe(unitsToBytes("1234TiB"));
|
||||
});
|
||||
|
||||
it("should parse with Pi suffix", () => {
|
||||
expect(unitsToBytes("1234Pi")).toBe(1234 * (1024 ** 5));
|
||||
});
|
||||
|
||||
it("should parse with Pi suffix as the same as PiB", () => {
|
||||
expect(unitsToBytes("1234Pi")).toBe(unitsToBytes("1234PiB"));
|
||||
});
|
||||
|
||||
it("given unrelated data, return NaN", () => {
|
||||
expect(unitsToBytes("I am not a number")).toBeNaN();
|
||||
});
|
||||
|
||||
it("given unrelated data, but has number, return that", () => {
|
||||
expect(unitsToBytes("I am not a number, but this is 0.1")).toBe(0.1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bytesToUnits", () => {
|
||||
it("should return N/A for invalid bytes", () => {
|
||||
expect(bytesToUnits(-1)).toBe("N/A");
|
||||
expect(bytesToUnits(Infinity)).toBe("N/A");
|
||||
expect(bytesToUnits(NaN)).toBe("N/A");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 0..124, format with B", () => {
|
||||
expect(bytesToUnits(100)).toBe("100.0B");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 1024..1024^2, format with KiB", () => {
|
||||
expect(bytesToUnits(1024)).toBe("1.0KiB");
|
||||
expect(bytesToUnits(2048)).toBe("2.0KiB");
|
||||
expect(bytesToUnits(1900)).toBe("1.9KiB");
|
||||
expect(bytesToUnits(50*1024 + 1)).toBe("50.0KiB");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 1024^2..1024^3, format with MiB", () => {
|
||||
expect(bytesToUnits(1024**2)).toBe("1.0MiB");
|
||||
expect(bytesToUnits(2048**2)).toBe("4.0MiB");
|
||||
expect(bytesToUnits(1900 * 1024)).toBe("1.9MiB");
|
||||
expect(bytesToUnits(50*(1024 ** 2) + 1)).toBe("50.0MiB");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 1024^3..1024^4, format with GiB", () => {
|
||||
expect(bytesToUnits(1024**3)).toBe("1.0GiB");
|
||||
expect(bytesToUnits(2048**3)).toBe("8.0GiB");
|
||||
expect(bytesToUnits(1900 * 1024 ** 2)).toBe("1.9GiB");
|
||||
expect(bytesToUnits(50*(1024 ** 3) + 1)).toBe("50.0GiB");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 1024^4..1024^5, format with TiB", () => {
|
||||
expect(bytesToUnits(1024**4)).toBe("1.0TiB");
|
||||
expect(bytesToUnits(2048**4)).toBe("16.0TiB");
|
||||
expect(bytesToUnits(1900 * 1024 ** 3)).toBe("1.9TiB");
|
||||
expect(bytesToUnits(50*(1024 ** 4) + 1)).toBe("50.0TiB");
|
||||
});
|
||||
|
||||
it("given a number within the magnitude of 1024^5..1024^6, format with PiB", () => {
|
||||
expect(bytesToUnits(1024**5)).toBe("1.0PiB");
|
||||
expect(bytesToUnits(2048**5)).toBe("32.0PiB");
|
||||
expect(bytesToUnits(1900 * 1024 ** 4)).toBe("1.9PiB");
|
||||
expect(bytesToUnits(50*(1024 ** 5) + 1)).toBe("50.0PiB");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bytesToUnits -> unitsToBytes", () => {
|
||||
it("given an input, round trip to the same value, given enough precision", () => {
|
||||
expect(unitsToBytes(bytesToUnits(123))).toBe(123);
|
||||
expect(unitsToBytes(bytesToUnits(1024**0 + 1, { precision: 2 }))).toBe(1024**0 + 1);
|
||||
expect(unitsToBytes(bytesToUnits(1024**1 + 2, { precision: 3 }))).toBe(1024**1 + 2);
|
||||
expect(unitsToBytes(bytesToUnits(1024**2 + 3, { precision: 6 }))).toBe(1024**2 + 3);
|
||||
expect(unitsToBytes(bytesToUnits(1024**3 + 4, { precision: 9 }))).toBe(1024**3 + 4);
|
||||
expect(unitsToBytes(bytesToUnits(1024**4 + 5, { precision: 12 }))).toBe(1024**4 + 5);
|
||||
expect(unitsToBytes(bytesToUnits(1024**5 + 6, { precision: 16 }))).toBe(1024**5 + 6);
|
||||
expect(unitsToBytes(bytesToUnits(1024**6 + 7, { precision: 20 }))).toBe(1024**6 + 7);
|
||||
});
|
||||
|
||||
it("given an invalid input, round trips to NaN", () => {
|
||||
expect(unitsToBytes(bytesToUnits(-1))).toBeNaN();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import { formatDuration } from "../formatDuration";
|
||||
|
||||
const second = 1000;
|
||||
const minute = 60 * second;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
const year = 365 * day;
|
||||
|
||||
describe("human format durations", () => {
|
||||
test("small duration should output something", () => {
|
||||
expect(formatDuration(1)).toBe("0s");
|
||||
expect(formatDuration(3)).toBe("0s");
|
||||
});
|
||||
|
||||
test("returns seconds for duration under 1 min", () => {
|
||||
const res = formatDuration(8 * second);
|
||||
|
||||
expect(res).toBe("8s");
|
||||
});
|
||||
|
||||
test("zero duration should output something", () => {
|
||||
expect(formatDuration(0)).toBe("0s");
|
||||
});
|
||||
|
||||
describe("when compact is true", () => {
|
||||
|
||||
test("duration under 3 hours return minutes", () => {
|
||||
const res = formatDuration(1 * hour + 35 * minute);
|
||||
|
||||
expect(res).toBe("95m");
|
||||
});
|
||||
|
||||
test("duration under 8 hours return hours and minutes", () => {
|
||||
const res = formatDuration(6 * hour + 15 * minute + 20 * second);
|
||||
|
||||
expect(res).toBe("6h15m");
|
||||
});
|
||||
|
||||
test("duration under 48 hours return hours", () => {
|
||||
const res = formatDuration(1 * day + 4 * hour + 15 * minute);
|
||||
|
||||
expect(res).toBe("28h");
|
||||
});
|
||||
|
||||
test("duration under 2 years return days", () => {
|
||||
const res = formatDuration(400 * day + 4 * hour + 15 * minute);
|
||||
|
||||
expect(res).toBe("400d");
|
||||
});
|
||||
|
||||
test("durations less than 8 years returns years and days", () => {
|
||||
const timeValue = new Date(2020, 0, 10, 12, 0, 0, 0).getTime() - new Date(2018, 0, 4, 12, 0, 0, 0).getTime();
|
||||
|
||||
const res = formatDuration(timeValue);
|
||||
|
||||
expect(res).toBe("2y5d");
|
||||
});
|
||||
|
||||
test("durations more than 8 years returns years", () => {
|
||||
const timeValue = Date.now() - new Date(moment().subtract(9, "years").subtract(5, "days").toDate()).getTime();
|
||||
|
||||
const res = formatDuration(timeValue);
|
||||
|
||||
expect(res).toBe("9y");
|
||||
});
|
||||
|
||||
test("durations more than 8 years returns years", () => {
|
||||
const res = formatDuration(10 * year + 25 * day);
|
||||
|
||||
expect(res).toBe("10y");
|
||||
});
|
||||
test("durations shouldn't include zero magnitude parts", () => {
|
||||
const zeroSeconds = formatDuration(8 * minute);
|
||||
|
||||
expect(zeroSeconds).toBe("8m");
|
||||
|
||||
const zeroMinutes = formatDuration(8 * hour + 15 * minute);
|
||||
|
||||
expect(zeroMinutes).toBe("8h");
|
||||
|
||||
const zeroHours = formatDuration(6 * day + 2 * minute);
|
||||
|
||||
expect(zeroHours).toBe("6d");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { HashSet, ObservableHashSet } from "../hash-set";
|
||||
|
||||
describe("ObservableHashSet<T>", () => {
|
||||
it("should not throw on creation", () => {
|
||||
expect(() => new ObservableHashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
|
||||
});
|
||||
|
||||
it("should be initializable", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
});
|
||||
|
||||
it("has should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("forEach should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
let a = 1;
|
||||
|
||||
res.forEach((item) => {
|
||||
expect(item.a).toEqual(a++);
|
||||
});
|
||||
});
|
||||
|
||||
it("delete should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.delete({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
expect(res.delete({ a: 5 })).toBe(false);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("toggle should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.toggle({ a: 1 });
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.toggle({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.add({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should treat the hash to be the same as equality", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1, foobar: "hello" },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.add({ a: 1, foobar: "goodbye" });
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("clear should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.clear();
|
||||
expect(res.size).toBe(0);
|
||||
});
|
||||
|
||||
it("replace should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.replace([{ a: 13 }]);
|
||||
expect(res.size).toBe(1);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
expect(res.has({ a: 13 })).toBe(true);
|
||||
});
|
||||
|
||||
it("toJSON should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.toJSON()).toStrictEqual([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("values should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.values();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keys should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.keys();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("entries should work as expected", () => {
|
||||
const res = new ObservableHashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.entries();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 1 }, { a: 1 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 2 }, { a: 2 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 3 }, { a: 3 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 4 }, { a: 4 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HashSet<T>", () => {
|
||||
it("should not throw on creation", () => {
|
||||
expect(() => new HashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
|
||||
});
|
||||
|
||||
it("should be initializable", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
});
|
||||
|
||||
it("has should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("forEach should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
let a = 1;
|
||||
|
||||
res.forEach((item) => {
|
||||
expect(item.a).toEqual(a++);
|
||||
});
|
||||
});
|
||||
|
||||
it("delete should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
expect(res.delete({ a: 1 })).toBe(true);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
expect(res.delete({ a: 5 })).toBe(false);
|
||||
expect(res.has({ a: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("toggle should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.toggle({ a: 1 });
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.toggle({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 6 })).toBe(false);
|
||||
res.add({ a: 6 });
|
||||
expect(res.has({ a: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("add should treat the hash to be the same as equality", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1, foobar: "hello" },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
res.add({ a: 1, foobar: "goodbye" });
|
||||
expect(res.has({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("clear should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.clear();
|
||||
expect(res.size).toBe(0);
|
||||
});
|
||||
|
||||
it("replace should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.size).toBe(4);
|
||||
res.replace([{ a: 13 }]);
|
||||
expect(res.size).toBe(1);
|
||||
expect(res.has({ a: 1 })).toBe(false);
|
||||
expect(res.has({ a: 13 })).toBe(true);
|
||||
});
|
||||
|
||||
it("toJSON should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
|
||||
expect(res.toJSON()).toStrictEqual([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("values should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.values();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keys should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.keys();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 1 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 2 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 3 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: { a: 4 },
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("entries should work as expected", () => {
|
||||
const res = new HashSet([
|
||||
{ a: 1 },
|
||||
{ a: 2 },
|
||||
{ a: 3 },
|
||||
{ a: 4 },
|
||||
], item => item.a.toString());
|
||||
const iter = res.entries();
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 1 }, { a: 1 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 2 }, { a: 2 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 3 }, { a: 3 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: [{ a: 4 }, { a: 4 }],
|
||||
done: false,
|
||||
});
|
||||
|
||||
expect(iter.next()).toStrictEqual({
|
||||
value: undefined,
|
||||
done: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { join, nth, reduce, concat } from "../iter";
|
||||
|
||||
describe("iter", () => {
|
||||
describe("reduce", () => {
|
||||
it("can reduce a value", () => {
|
||||
expect(reduce([1, 2, 3], (acc: number[], current: number) => [current, ...acc], [0])).toEqual([3, 2, 1, 0]);
|
||||
});
|
||||
|
||||
it("can reduce an empty iterable", () => {
|
||||
expect(reduce([], (acc: number[], current: number) => [acc[0] + current], [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("join", () => {
|
||||
it("should not prefix the output by the seperator", () => {
|
||||
expect(join(["a", "b", "c"].values(), " ")).toBe("a b c");
|
||||
});
|
||||
|
||||
it("should return empty string if iterator is empty", () => {
|
||||
expect(join([].values(), " ")).toBe("");
|
||||
});
|
||||
|
||||
it("should return just first entry if iterator is of size 1", () => {
|
||||
expect(join(["d"].values(), " ")).toBe("d");
|
||||
});
|
||||
});
|
||||
|
||||
describe("nth", () => {
|
||||
it("should return undefined past the end of the iterator", () => {
|
||||
expect(nth(["a"], 123)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should by 0-indexing the index", () => {
|
||||
expect(nth(["a", "b"], 0)).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("concat", () => {
|
||||
it("should yield undefined for empty args", () => {
|
||||
const iter = concat();
|
||||
|
||||
expect(iter.next()).toEqual({ done: true });
|
||||
});
|
||||
|
||||
it("should yield undefined for only empty args", () => {
|
||||
const iter = concat([].values(), [].values(), [].values(), [].values());
|
||||
|
||||
expect(iter.next()).toEqual({ done: true });
|
||||
});
|
||||
|
||||
it("should yield all of the first and then all of the second", () => {
|
||||
const iter = concat([1, 2, 3].values(), [4, 5, 6].values());
|
||||
|
||||
expect(iter.next()).toEqual({ done: false, value: 1 });
|
||||
expect(iter.next()).toEqual({ done: false, value: 2 });
|
||||
expect(iter.next()).toEqual({ done: false, value: 3 });
|
||||
expect(iter.next()).toEqual({ done: false, value: 4 });
|
||||
expect(iter.next()).toEqual({ done: false, value: 5 });
|
||||
expect(iter.next()).toEqual({ done: false, value: 6 });
|
||||
expect(iter.next()).toEqual({ done: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { nFircate } from "../n-fircate";
|
||||
|
||||
describe("nFircate", () => {
|
||||
it("should produce an empty array if no parts are provided", () => {
|
||||
expect(nFircate([{ a: 1 }, { a: 2 }], "a", []).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should ignore non-matching parts", () => {
|
||||
const res = nFircate([{ a: 1 }, { a: 2 }], "a", [1]);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it("should include all matching parts in each type", () => {
|
||||
const res = nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2]);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
expect(res[0].length).toBe(2);
|
||||
expect(res[0][0].b).toBe("a");
|
||||
expect(res[0][1].b).toBe("c");
|
||||
expect(res[1].length).toBe(1);
|
||||
expect(res[1][0].b).toBe("b");
|
||||
});
|
||||
|
||||
it("should throw a type error if the same part is provided more than once", () => {
|
||||
try {
|
||||
nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2, 1]);
|
||||
fail("Expected error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
|
||||
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
|
||||
import getDirnameOfPathInjectable from "../../path/get-dirname.injectable";
|
||||
import type { IsLogicalChildPath } from "../../path/is-logical-child-path.injectable";
|
||||
import isLogicalChildPathInjectable from "../../path/is-logical-child-path.injectable";
|
||||
|
||||
describe("isLogicalChildPath", () => {
|
||||
let di: DiContainer;
|
||||
let isLogicalChildPath: IsLogicalChildPath;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
});
|
||||
|
||||
describe("when using win32 paths", () => {
|
||||
beforeEach(() => {
|
||||
di.override(getAbsolutePathInjectable, () => path.win32.resolve);
|
||||
di.override(getDirnameOfPathInjectable, () => path.win32.dirname);
|
||||
isLogicalChildPath = di.inject(isLogicalChildPathInjectable);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:\\Foo\\Bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:\\Bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:/Bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:/Foo/Bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "D:\\Foo\\Bar",
|
||||
expected: false,
|
||||
},
|
||||
])("test %#", (testData) => {
|
||||
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when using posix paths", () => {
|
||||
beforeEach(() => {
|
||||
di.override(getAbsolutePathInjectable, () => path.posix.resolve);
|
||||
di.override(getDirnameOfPathInjectable, () => path.posix.dirname);
|
||||
isLogicalChildPath = di.inject(isLogicalChildPathInjectable);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foobar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/../bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/./bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/.bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/..bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/...bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/..\\.bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/bar/../foo",
|
||||
testPath: "/foo/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/\\bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "./bar",
|
||||
expected: false,
|
||||
},
|
||||
])("test %#", (testData) => {
|
||||
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { bifurcateArray, splitArray } from "../splitArray";
|
||||
|
||||
describe("split array on element tests", () => {
|
||||
it("empty array", () => {
|
||||
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
|
||||
});
|
||||
|
||||
it("one element, not in array", () => {
|
||||
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
|
||||
});
|
||||
|
||||
it("ten elements, not in array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
||||
});
|
||||
|
||||
it("one elements, in array", () => {
|
||||
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
|
||||
});
|
||||
|
||||
it("ten elements, in front array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
it("ten elements, in middle array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
it("ten elements, in end array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bifurcateArray", () => {
|
||||
it("should return tuple of empty arrays from empty array", () => {
|
||||
const [left, right] = bifurcateArray([], () => true);
|
||||
|
||||
expect(left).toStrictEqual([]);
|
||||
expect(right).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should return all true condition returning items in the right array", () => {
|
||||
const [left, right] = bifurcateArray([1, 2, 3], () => true);
|
||||
|
||||
expect(left).toStrictEqual([]);
|
||||
expect(right).toStrictEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("should return all false condition returning items in the right array", () => {
|
||||
const [left, right] = bifurcateArray([1, 2, 3], () => false);
|
||||
|
||||
expect(left).toStrictEqual([1, 2, 3]);
|
||||
expect(right).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should split array as specified", () => {
|
||||
const [left, right] = bifurcateArray([1, 2, 3], (i) => Boolean(i % 2));
|
||||
|
||||
expect(left).toStrictEqual([2]);
|
||||
expect(right).toStrictEqual([1, 3]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { isObservable, observable } from "mobx";
|
||||
import { toJS } from "../toJS";
|
||||
|
||||
describe("utils/toJS(data: any)", () => {
|
||||
const y = { y: 2 };
|
||||
|
||||
const data = observable({ x: 1, y }, {}, {
|
||||
deep: false, // this will keep ref to "y"
|
||||
});
|
||||
const data2 = {
|
||||
x: 1, // partially observable
|
||||
y: observable(y),
|
||||
};
|
||||
|
||||
test("converts mobx-observable to corresponding js struct with links preserving", () => {
|
||||
expect(toJS(data).y).toBe(y);
|
||||
expect(isObservable(toJS(data).y)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("converts partially observable js struct", () => {
|
||||
expect(toJS(data2).y).not.toBe(y);
|
||||
expect(toJS(data2).y).toEqual(y);
|
||||
expect(isObservable(toJS(data2).y)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { tuple } from "../../utils";
|
||||
|
||||
describe("tuple tests", () => {
|
||||
describe("zip()", () => {
|
||||
it("should yield 0 times and return 1 tuple of empty arrays when given empty array", () => {
|
||||
expect(tuple.zip([]).next()).toEqual({
|
||||
done: true,
|
||||
value: [[]],
|
||||
});
|
||||
});
|
||||
|
||||
it("should yield 1 times and return 2 tuple of empty arrays when given one element array tuples", () => {
|
||||
const i = tuple.zip([1], [2]);
|
||||
|
||||
expect(i.next()).toEqual({
|
||||
done: false,
|
||||
value: [1, 2],
|
||||
});
|
||||
expect(i.next()).toEqual({
|
||||
done: true,
|
||||
value: [[], []],
|
||||
});
|
||||
});
|
||||
|
||||
it("should yield 1 times and return 2 tuple of partial arrays when given one element array tuples", () => {
|
||||
const i = tuple.zip([1], [2, 3]);
|
||||
|
||||
expect(i.next()).toEqual({
|
||||
done: false,
|
||||
value: [1, 2],
|
||||
});
|
||||
expect(i.next()).toEqual({
|
||||
done: true,
|
||||
value: [[], [3]],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import { unionPATHs } from "../union-env-path";
|
||||
|
||||
describe("unionPATHs", () => {
|
||||
it("return the same path if given only one with no double delimiters", () => {
|
||||
expect(unionPATHs(`/bin/bar${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
|
||||
});
|
||||
|
||||
it("return equivalent path if given only one with no double delimiters", () => {
|
||||
expect(unionPATHs(`/bin/bar${path.delimiter}${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
|
||||
});
|
||||
|
||||
it("should remove duplicate entries, appending non duplicates in order received", () => {
|
||||
expect(unionPATHs(
|
||||
`/bin/bar${path.delimiter}/usr/bin`,
|
||||
`/bin/bar${path.delimiter}/usr/lens/bat`,
|
||||
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat`);
|
||||
});
|
||||
|
||||
it("should remove duplicate entries, appending non duplicates in order received, 3", () => {
|
||||
expect(unionPATHs(
|
||||
`/bin/bar${path.delimiter}/usr/bin`,
|
||||
`/bin/bar${path.delimiter}/usr/lens/bat`,
|
||||
`/usr/local/lens${path.delimiter}/usr/bin`,
|
||||
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat${path.delimiter}/usr/local/lens`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import AbortController from "abort-controller";
|
||||
|
||||
/**
|
||||
* This is like an `AbortController` but will also abort if the parent aborts,
|
||||
* but won't make the parent abort if this aborts (single direction)
|
||||
*/
|
||||
export class WrappedAbortController extends AbortController {
|
||||
constructor(parent?: AbortController | undefined) {
|
||||
super();
|
||||
|
||||
parent?.signal.addEventListener("abort", () => {
|
||||
this.abort();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setTimeoutFor(controller: AbortController, timeout: number): void {
|
||||
const handle = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
controller.signal.addEventListener("abort", () => clearTimeout(handle));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { addSeparator } from "./add-separator";
|
||||
|
||||
describe("add-separator", () => {
|
||||
it("given multiple items, adds separators", () => {
|
||||
const items = ["first", "second", "third"];
|
||||
|
||||
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
|
||||
|
||||
expect(actual).toEqual([
|
||||
"first",
|
||||
"separator-between-first-and-second",
|
||||
"second",
|
||||
"separator-between-second-and-third",
|
||||
"third",
|
||||
]);
|
||||
});
|
||||
|
||||
it("given multiple items including falsy ones, adds separators", () => {
|
||||
const items = [false, undefined, null, NaN];
|
||||
|
||||
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
|
||||
|
||||
expect(actual).toEqual([
|
||||
false,
|
||||
"separator-between-false-and-undefined",
|
||||
undefined,
|
||||
"separator-between-undefined-and-null",
|
||||
null,
|
||||
"separator-between-null-and-NaN",
|
||||
NaN,
|
||||
]);
|
||||
});
|
||||
|
||||
it("given no items, does not add separator", () => {
|
||||
const items: any[] = [];
|
||||
|
||||
const actual = addSeparator(() => "separator", items);
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it("given one item, does not add separator", () => {
|
||||
const items = ["first"];
|
||||
|
||||
const actual = addSeparator(() => "separator", items);
|
||||
|
||||
expect(actual).toEqual(["first"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type GetSeparator<Item, Separator> = (left: Item, right: Item) => Separator;
|
||||
|
||||
export const addSeparator = <Item, Separator>(
|
||||
getSeparator: GetSeparator<Item, Separator>,
|
||||
items: Item[],
|
||||
) => items.flatMap(toSeparatedTupleUsing(getSeparator));
|
||||
|
||||
const toSeparatedTupleUsing =
|
||||
<Item, Separator>(getSeparator: GetSeparator<Item, Separator>) =>
|
||||
(leftItem: Item, index: number, arr: Item[]) => {
|
||||
const itemIsLast = arr.length === index + 1;
|
||||
|
||||
if (itemIsLast) {
|
||||
return [leftItem];
|
||||
}
|
||||
|
||||
const rightItem = arr[index + 1];
|
||||
const separator = getSeparator(leftItem, rightItem);
|
||||
|
||||
return [leftItem, separator];
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A inference typed version of `Array(length).fill(value)`
|
||||
* @param length The number of entries
|
||||
* @param value The value of each of the indices
|
||||
*/
|
||||
export function filled<T>(length: number, value: T): T[] {
|
||||
return Array(length).fill(value);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export type AsyncResult<Response, Error = string> =
|
||||
| (
|
||||
Response extends void
|
||||
? { callWasSuccessful: true; response?: undefined }
|
||||
: { callWasSuccessful: true; response: Response }
|
||||
)
|
||||
| { callWasSuccessful: false; error: Error };
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Options } from "auto-bind";
|
||||
import autoBindClass from "auto-bind";
|
||||
import autoBindReactClass from "auto-bind/react";
|
||||
import React from "react";
|
||||
|
||||
// Automatically bind methods to their class instance
|
||||
export function autoBind<T extends object>(obj: T, opts?: Options): T {
|
||||
if (obj instanceof React.Component) {
|
||||
return autoBindReactClass(obj, opts);
|
||||
}
|
||||
|
||||
return autoBindClass(obj, opts);
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Encode/decode utf-8 base64 string
|
||||
import * as Base64 from "crypto-js/enc-base64";
|
||||
import * as Utf8 from "crypto-js/enc-utf8";
|
||||
|
||||
/**
|
||||
* Computes utf-8 from base64
|
||||
* @param data A Base64 encoded string
|
||||
* @returns The original utf-8 string
|
||||
*/
|
||||
export function decode(data: string): string {
|
||||
return Base64.parse(data).toString(Utf8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes base64 from utf-8
|
||||
* @param data A normal string
|
||||
* @returns A base64 encoded version
|
||||
*/
|
||||
export function encode(data: string): string {
|
||||
return Utf8.parse(data).toString(Base64);
|
||||
}
|
||||
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
|
||||
|
||||
const binaryNameInjectable = getInjectable({
|
||||
id: "binary-name",
|
||||
instantiate: (di, binaryName) => {
|
||||
const normalizedPlatform = di.inject(normalizedPlatformInjectable);
|
||||
|
||||
if (normalizedPlatform === "windows") {
|
||||
return `${binaryName}.exe`;
|
||||
}
|
||||
|
||||
return binaryName;
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, binaryName: string) => binaryName,
|
||||
}),
|
||||
});
|
||||
|
||||
export default binaryNameInjectable;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { compile } from "path-to-regexp";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { isDefined } from "./type-narrowing";
|
||||
|
||||
export interface UrlRouteProps extends RouteProps {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface URLParams<P extends object = {}, Q extends object = {}> {
|
||||
params?: P;
|
||||
query?: Q;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
export function buildURL<P extends object = {}, Q extends object = {}>(path: string, { params, query, fragment }: URLParams<P, Q> = {}) {
|
||||
const pathBuilder = compile(String(path));
|
||||
|
||||
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
|
||||
const parts = [
|
||||
pathBuilder(params),
|
||||
queryParams && `?${queryParams}`,
|
||||
fragment && `#${fragment}`,
|
||||
];
|
||||
|
||||
return parts.filter(isDefined).join("");
|
||||
}
|
||||
|
||||
export function buildURLPositional<P extends object = {}, Q extends object = {}>(path: string) {
|
||||
return function (params?: P, query?: Q, fragment?: string): string {
|
||||
return buildURL(path, { params, query, fragment });
|
||||
};
|
||||
}
|
||||
|
||||
export type UrlParamsFor<Pathname extends string> =
|
||||
Pathname extends `${string}/:${infer A}?/${infer Tail}`
|
||||
? Partial<Record<A, string>> & UrlParamsFor<`/${Tail}`>
|
||||
: Pathname extends `${string}/:${infer A}/${infer Tail}`
|
||||
? Record<A, string> & UrlParamsFor<`/${Tail}`>
|
||||
: Pathname extends `${string}/:${infer A}?`
|
||||
? Partial<Record<A, string>>
|
||||
: Pathname extends `${string}/:${infer A}`
|
||||
? Record<A, string>
|
||||
: {};
|
||||
|
||||
export interface UrlBuilder<Pathname extends string> {
|
||||
compile(params: UrlParamsFor<Pathname>, query?: object, fragment?: string): string;
|
||||
}
|
||||
|
||||
export function urlBuilderFor<Pathname extends string>(pathname: Pathname): UrlBuilder<Pathname> {
|
||||
return {
|
||||
compile: buildURLPositional(pathname),
|
||||
};
|
||||
}
|
||||
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import joinPathsInjectable from "../path/join-paths.injectable";
|
||||
import baseBundledBinariesDirectoryInjectable from "../vars/base-bundled-binaries-dir.injectable";
|
||||
import binaryNameInjectable from "./binary-name.injectable";
|
||||
|
||||
const bundledBinaryPathInjectable = getInjectable({
|
||||
id: "bundled-binary-path",
|
||||
instantiate: (di, name) => {
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const binaryName = di.inject(binaryNameInjectable, name);
|
||||
const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable);
|
||||
|
||||
return joinPaths(baseBundledBinariesDirectory, binaryName);
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, binaryName: string) => binaryName,
|
||||
}),
|
||||
});
|
||||
|
||||
export default bundledBinaryPathInjectable;
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Convert object's keys to camelCase format
|
||||
import { camelCase } from "lodash";
|
||||
import type { SingleOrMany } from "./types";
|
||||
import { isObject, isString } from "./type-narrowing";
|
||||
import * as object from "./objects";
|
||||
|
||||
export function toCamelCase<T extends Record<string, unknown>[]>(obj: T): T;
|
||||
export function toCamelCase<T extends Record<string, unknown>>(obj: T): T;
|
||||
|
||||
export function toCamelCase(obj: SingleOrMany<Record<string, unknown> | unknown>): SingleOrMany<Record<string, unknown> | unknown> {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toCamelCase);
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
return object.fromEntries(
|
||||
object.entries(obj)
|
||||
.filter((pair): pair is [string, unknown] => isString(pair[0]))
|
||||
.map(([key, value]) => [camelCase(key), isObject(value) ? toCamelCase(value) : value]),
|
||||
);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
|
||||
export interface Channel<MessageTemplate = void, ReturnTemplate = void> {
|
||||
id: string;
|
||||
_messageTemplate?: MessageTemplate;
|
||||
_returnTemplate?: ReturnTemplate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { SendMessageToChannel } from "./message-to-channel-injection-token";
|
||||
import { sendMessageToChannelInjectionToken } from "./message-to-channel-injection-token";
|
||||
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
|
||||
import type { MessageChannel } from "./message-channel-listener-injection-token";
|
||||
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
|
||||
import type { RequestFromChannel } from "./request-from-channel-injection-token";
|
||||
import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
|
||||
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { getPromiseStatus } from "../../test-utils/get-promise-status";
|
||||
import { runInAction } from "mobx";
|
||||
import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens";
|
||||
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens";
|
||||
|
||||
type TestMessageChannel = MessageChannel<string>;
|
||||
type TestRequestChannel = RequestChannel<string, string>;
|
||||
|
||||
describe("channel", () => {
|
||||
describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
|
||||
let messageListenerInWindowMock: jest.Mock;
|
||||
let mainDi: DiContainer;
|
||||
let messageToChannel: SendMessageToChannel;
|
||||
let builder: ApplicationBuilder;
|
||||
|
||||
beforeEach(async () => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
messageListenerInWindowMock = jest.fn();
|
||||
|
||||
const testChannelListenerInTestWindowInjectable = getInjectable({
|
||||
id: "test-channel-listener-in-test-window",
|
||||
|
||||
instantiate: () => ({
|
||||
channel: testMessageChannel,
|
||||
handler: messageListenerInWindowMock,
|
||||
}),
|
||||
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
runInAction(() => {
|
||||
windowDi.register(testChannelListenerInTestWindowInjectable);
|
||||
});
|
||||
});
|
||||
|
||||
mainDi = builder.mainDi;
|
||||
|
||||
await builder.startHidden();
|
||||
|
||||
messageToChannel = mainDi.inject(sendMessageToChannelInjectionToken);
|
||||
});
|
||||
|
||||
describe("given window is started", () => {
|
||||
let someWindowFake: LensWindow;
|
||||
|
||||
beforeEach(async () => {
|
||||
someWindowFake = builder.applicationWindow.create("some-window");
|
||||
|
||||
await someWindowFake.start();
|
||||
});
|
||||
|
||||
it("when sending message, triggers listener in window", () => {
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message");
|
||||
});
|
||||
|
||||
it("given window is hidden, when sending message, does not trigger listener in window", () => {
|
||||
someWindowFake.close();
|
||||
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("given multiple started windows, when sending message, triggers listeners in all windows", async () => {
|
||||
const someWindowFake = builder.applicationWindow.create("some-window");
|
||||
const someOtherWindowFake = builder.applicationWindow.create("some-other-window");
|
||||
|
||||
await someWindowFake.start();
|
||||
await someOtherWindowFake.start();
|
||||
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInWindowMock.mock.calls).toEqual([
|
||||
["some-message"],
|
||||
["some-message"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
|
||||
let messageListenerInMainMock: jest.Mock;
|
||||
let messageToChannel: SendMessageToChannel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
messageListenerInMainMock = jest.fn();
|
||||
|
||||
const testChannelListenerInMainInjectable = getInjectable({
|
||||
id: "test-channel-listener-in-main",
|
||||
|
||||
instantiate: () => ({
|
||||
channel: testMessageChannel,
|
||||
handler: messageListenerInMainMock,
|
||||
}),
|
||||
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
});
|
||||
|
||||
applicationBuilder.beforeApplicationStart((mainDi) => {
|
||||
runInAction(() => {
|
||||
mainDi.register(testChannelListenerInMainInjectable);
|
||||
});
|
||||
});
|
||||
|
||||
await applicationBuilder.render();
|
||||
|
||||
const windowDi = applicationBuilder.applicationWindow.only.di;
|
||||
|
||||
messageToChannel = windowDi.inject(sendMessageToChannelInjectionToken);
|
||||
});
|
||||
|
||||
it("when sending message, triggers listener in main", () => {
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("requesting from main in renderer, given listener for channel in a main and application has started", () => {
|
||||
let requestListenerInMainMock: AsyncFnMock<RequestChannelHandler<TestRequestChannel>>;
|
||||
let requestFromChannel: RequestFromChannel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
requestListenerInMainMock = asyncFn();
|
||||
|
||||
const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({
|
||||
channel: testRequestChannel,
|
||||
handler: () => requestListenerInMainMock,
|
||||
});
|
||||
|
||||
applicationBuilder.beforeApplicationStart((mainDi) => {
|
||||
runInAction(() => {
|
||||
mainDi.register(testChannelListenerInMainInjectable);
|
||||
});
|
||||
});
|
||||
|
||||
await applicationBuilder.render();
|
||||
|
||||
const windowDi = applicationBuilder.applicationWindow.only.di;
|
||||
|
||||
requestFromChannel = windowDi.inject(
|
||||
requestFromChannelInjectionToken,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when requesting from channel", () => {
|
||||
let actualPromise: Promise<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = requestFromChannel(testRequestChannel, "some-request");
|
||||
});
|
||||
|
||||
it("triggers listener in main", () => {
|
||||
expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request");
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("when main resolves with response, resolves with response", async () => {
|
||||
await requestListenerInMainMock.resolve("some-response");
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toBe("some-response");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("when registering multiple handlers for the same channel, throws", async () => {
|
||||
const applicationBuilder = getApplicationBuilder();
|
||||
|
||||
const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({
|
||||
channel: testRequestChannel,
|
||||
handler: () => () => "some-value",
|
||||
});
|
||||
const testChannelListenerInMain2Injectable = getRequestChannelListenerInjectable({
|
||||
channel: testRequestChannel,
|
||||
handler: () => () => "some-other-value",
|
||||
});
|
||||
|
||||
testChannelListenerInMain2Injectable.id += "2";
|
||||
|
||||
applicationBuilder.beforeApplicationStart((mainDi) => {
|
||||
runInAction(() => {
|
||||
mainDi.register(testChannelListenerInMainInjectable);
|
||||
mainDi.register(testChannelListenerInMain2Injectable);
|
||||
});
|
||||
});
|
||||
|
||||
await expect(applicationBuilder.render()).rejects.toThrow('Tried to register a multiple channel handlers for "some-request-channel-id", only one handler is supported for a request channel.');
|
||||
});
|
||||
});
|
||||
|
||||
const testMessageChannel: TestMessageChannel = {
|
||||
id: "some-message-channel-id",
|
||||
};
|
||||
|
||||
const testRequestChannel: TestRequestChannel = {
|
||||
id: "some-request-channel-id",
|
||||
};
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { Disposer } from "../disposer";
|
||||
import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token";
|
||||
|
||||
export type EnlistMessageChannelListener = (listener: MessageChannelListener<MessageChannel<unknown>>) => Disposer;
|
||||
|
||||
export const enlistMessageChannelListenerInjectionToken = getInjectionToken<EnlistMessageChannelListener>({
|
||||
id: "enlist-message-channel-listener",
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||
|
||||
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
|
||||
id,
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { getStartableStoppable } from "../get-startable-stoppable";
|
||||
import { disposer } from "../index";
|
||||
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
|
||||
import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token";
|
||||
|
||||
const listeningOnMessageChannelsInjectable = getInjectable({
|
||||
id: "listening-on-message-channels",
|
||||
|
||||
instantiate: (di) => {
|
||||
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
|
||||
const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken);
|
||||
|
||||
return getStartableStoppable("listening-on-channels", () => (
|
||||
disposer(messageChannelListeners.map(enlistMessageChannelListener))
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default listeningOnMessageChannelsInjectable;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainerForInjection } from "@ogre-tools/injectable";
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
export interface MessageChannel<Message> {
|
||||
id: string;
|
||||
_messageSignature?: Message; // only used to mark `Message` as used
|
||||
}
|
||||
|
||||
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
|
||||
? (message: Message) => void
|
||||
: never;
|
||||
|
||||
export interface MessageChannelListener<Channel> {
|
||||
channel: Channel;
|
||||
handler: MessageChannelHandler<Channel>;
|
||||
}
|
||||
|
||||
export const messageChannelListenerInjectionToken = getInjectionToken<MessageChannelListener<MessageChannel<unknown>>>(
|
||||
{
|
||||
id: "message-channel-listener",
|
||||
},
|
||||
);
|
||||
|
||||
export interface GetMessageChannelListenerInfo<
|
||||
Channel extends MessageChannel<Message>,
|
||||
Message,
|
||||
> {
|
||||
id: string;
|
||||
channel: Channel;
|
||||
handler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>;
|
||||
causesSideEffects?: boolean;
|
||||
}
|
||||
|
||||
export function getMessageChannelListenerInjectable<
|
||||
Channel extends MessageChannel<Message>,
|
||||
Message,
|
||||
>(info: GetMessageChannelListenerInfo<Channel, Message>) {
|
||||
return getInjectable({
|
||||
id: `${info.channel.id}-listener-${info.id}`,
|
||||
instantiate: (di) => ({
|
||||
channel: info.channel,
|
||||
handler: info.handler(di),
|
||||
}),
|
||||
injectionToken: messageChannelListenerInjectionToken,
|
||||
causesSideEffects: info.causesSideEffects,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { MessageChannel } from "./message-channel-listener-injection-token";
|
||||
|
||||
export interface SendMessageToChannel {
|
||||
(channel: MessageChannel<void>): void;
|
||||
<Message>(channel: MessageChannel<Message>, message: Message): void;
|
||||
}
|
||||
|
||||
export type MessageChannelSender<Channel> = Channel extends MessageChannel<void | undefined>
|
||||
? () => void
|
||||
: Channel extends MessageChannel<infer Message>
|
||||
? (message: Message) => void
|
||||
: never;
|
||||
|
||||
export const sendMessageToChannelInjectionToken = getInjectionToken<SendMessageToChannel>({
|
||||
id: "send-message-to-message-channel",
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export interface RequestChannel<Request, Response> {
|
||||
id: string;
|
||||
_requestSignature?: Request; // used only to mark `Request` as "used"
|
||||
_responseSignature?: Response; // used only to mark `Response` as "used"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { RequestChannel } from "./request-channel-listener-injection-token";
|
||||
|
||||
export interface RequestFromChannel {
|
||||
<Request, Response>(channel: RequestChannel<Request, Response>, request: Request): Promise<Response>;
|
||||
<Response>(channel: RequestChannel<void, Response>): Promise<Response>;
|
||||
}
|
||||
|
||||
export const requestFromChannelInjectionToken = getInjectionToken<RequestFromChannel>({
|
||||
id: "request-from-request-channel",
|
||||
});
|
||||
@@ -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 { ClusterId } from "../cluster-types";
|
||||
|
||||
/**
|
||||
* Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing
|
||||
* @param host The host section of a URL
|
||||
* @returns The `ClusterId` part of the host, or `undefined`
|
||||
*/
|
||||
export function getClusterIdFromHost(host: string): ClusterId | undefined {
|
||||
// e.g host == "%clusterId.localhost:45345"
|
||||
const subDomains = host.split(":")[0].split(".");
|
||||
|
||||
return subDomains.slice(-3, -2)[0]; // ClusterId or undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OpenLens backend routing host for a given `ClusterId`
|
||||
* @param clusterId The ID to put in front of the current host
|
||||
* @returns a new URL host section
|
||||
*/
|
||||
export function getClusterFrameUrl(clusterId: ClusterId) {
|
||||
return `//${clusterId}.${location.host}`;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { runInAction } from "mobx";
|
||||
import { inspect } from "util";
|
||||
import { isDefined } from "./type-narrowing";
|
||||
|
||||
/**
|
||||
* Get the value behind `key`. If it was not present, first insert `value`
|
||||
* @param map The map to interact with
|
||||
* @param key The key to insert into the map with
|
||||
* @param value The value to optional add to the map
|
||||
* @returns The value in the map
|
||||
*/
|
||||
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, value);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return map.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates map and returns the value that was just inserted
|
||||
*/
|
||||
export function put<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
map.set(key, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but specifically for when `V` is `Map<MK, MV>` so that
|
||||
* the typings are inferred correctly.
|
||||
*/
|
||||
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
||||
return getOrInsert(map, key, new Map<MK, MV>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but specifically for when `V` is `Set<any>` so that
|
||||
* the typings are inferred.
|
||||
*/
|
||||
export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
|
||||
return getOrInsert(map, key, new Set<SK>());
|
||||
}
|
||||
|
||||
/**
|
||||
* A currying version of {@link getOrInsertSet}
|
||||
*/
|
||||
export function getOrInsertSetFor<K, SK>(map: Map<K, Set<SK>>): (key: K) => Set<SK> {
|
||||
return (key) => getOrInsertSet(map, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but with delayed creation of the item. Which is useful
|
||||
* if it is very expensive to create the initial value.
|
||||
*/
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V;
|
||||
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V;
|
||||
|
||||
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, builder());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return map.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link getOrInsertWith} but the builder is async and will be awaited before inserting into the map
|
||||
*/
|
||||
export async function getOrInsertWithAsync<K, V>(map: Map<K, V>, key: K, asyncBuilder: () => Promise<V>): Promise<V> {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, await asyncBuilder());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return map.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value associated with `key` iff there was not a previous value
|
||||
* @param map The map to interact with
|
||||
* @throws if `key` already in map
|
||||
* @returns `this` so that `strictSet` can be chained
|
||||
*/
|
||||
export function strictSet<K, V>(map: Map<K, V>, key: K, val: V): typeof map {
|
||||
if (map.has(key)) {
|
||||
throw new TypeError(`Map already contains key: ${inspect(key)}`);
|
||||
}
|
||||
|
||||
return map.set(key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value associated with `key`
|
||||
* @param map The map to interact with
|
||||
* @throws if `key` did not a value associated with it
|
||||
*/
|
||||
export function strictGet<K, V>(map: Map<K, V>, key: K): V {
|
||||
if (!map.has(key)) {
|
||||
throw new TypeError(`Map does not contains key: ${inspect(key)}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return map.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `key` is in `set`, remove it otherwise add it.
|
||||
* @param set The set to manipulate
|
||||
* @param key The key to toggle the "is in"-ness of
|
||||
*/
|
||||
export function toggle<K>(set: Set<K>, key: K): void {
|
||||
runInAction(() => {
|
||||
// Returns true if value was already in Set; otherwise false.
|
||||
if (!set.delete(key)) {
|
||||
set.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to also check for defined-ness
|
||||
*/
|
||||
export function includes<T>(src: T[], value: T | null | undefined): boolean {
|
||||
return isDefined(value) && src.includes(value);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
|
||||
export interface Discriminable<T extends string> { readonly kind: T }
|
||||
|
||||
// Note: this will fail at transpilation time, if all kinds are not instructed in switch/case.
|
||||
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
|
||||
export const checkThatAllDiscriminablesAreExhausted = <T extends never>(value: T) => {
|
||||
const _exhaustiveCheck: never = value;
|
||||
|
||||
return new Error(
|
||||
`Tried to exhaust discriminables, but no instructions were found for ${(_exhaustiveCheck as any).kind}`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export interface Labelable {
|
||||
readonly label: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { sortBy } from "lodash/fp";
|
||||
|
||||
export interface Orderable {
|
||||
readonly orderNumber: number;
|
||||
}
|
||||
|
||||
export type MaybeOrderable = Orderable | object;
|
||||
|
||||
export const orderByOrderNumber = <T extends MaybeOrderable>(maybeOrderables: T[]) =>
|
||||
sortBy(
|
||||
(orderable) =>
|
||||
"orderNumber" in orderable
|
||||
? orderable.orderNumber
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
maybeOrderables,
|
||||
);
|
||||
|
||||
export const byOrderNumber = <T extends Orderable>(left: T, right: T) => (
|
||||
left.orderNumber - right.orderNumber
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { isBoolean } from "../../type-narrowing";
|
||||
|
||||
export interface Showable {
|
||||
readonly isShown: IComputedValue<boolean> | boolean;
|
||||
}
|
||||
|
||||
export type MaybeShowable = Showable | object;
|
||||
|
||||
export const isShown = (showable: MaybeShowable) => {
|
||||
if (!("isShown" in showable)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (showable.isShown === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isBoolean(showable.isShown)) {
|
||||
return showable.isShown;
|
||||
}
|
||||
|
||||
return showable.isShown.get();
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
import { compositeHasDescendant } from "./composite-has-descendant";
|
||||
import { getCompositeFor } from "../get-composite/get-composite";
|
||||
|
||||
describe("composite-has-descendant, given composite with children and grand children", () => {
|
||||
let composite: Composite<{ id: string; parentId?: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
const items = [
|
||||
{ id: "some-root-id", parentId: undefined },
|
||||
{ id: "some-child-item", parentId: "some-root-id" },
|
||||
|
||||
{
|
||||
id: "some-grand-child-item",
|
||||
parentId: "some-child-item",
|
||||
},
|
||||
];
|
||||
|
||||
const getComposite = getCompositeFor<{
|
||||
id: string;
|
||||
parentId?: string;
|
||||
}>({
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
composite = getComposite(items);
|
||||
});
|
||||
|
||||
it("has a child as descendant", () => {
|
||||
const actual = compositeHasDescendant<typeof composite["value"]>(
|
||||
(referenceComposite) => referenceComposite.value.id === "some-child-item",
|
||||
)(composite);
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("has a grand child as descendant", () => {
|
||||
const actual = compositeHasDescendant<typeof composite["value"]>(
|
||||
(referenceComposite) =>
|
||||
referenceComposite.value.id === "some-grand-child-item",
|
||||
)(composite);
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("does not have an unrelated descendant", () => {
|
||||
const actual = compositeHasDescendant<typeof composite["value"]>(
|
||||
(referenceComposite) =>
|
||||
referenceComposite.value.id === "some-unknown-item",
|
||||
)(composite);
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
|
||||
const compositeHasDescendant = <T>(
|
||||
predicate: (referenceComposite: Composite<T>) => boolean,
|
||||
) => {
|
||||
const _compositeHasDescendant = (composite: Composite<T>): boolean =>
|
||||
predicate(composite) ||
|
||||
!!composite.children.find((childComposite) =>
|
||||
_compositeHasDescendant(childComposite),
|
||||
);
|
||||
|
||||
return _compositeHasDescendant;
|
||||
};
|
||||
|
||||
export { compositeHasDescendant };
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
import { findComposite } from "./find-composite";
|
||||
import { getCompositeFor } from "../get-composite/get-composite";
|
||||
|
||||
describe("find-composite", () => {
|
||||
let composite: Composite<{ id: string; parentId?: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
const getComposite = getCompositeFor<{
|
||||
id: string;
|
||||
parentId?: string;
|
||||
}>({
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
composite = getComposite([
|
||||
{ id: "some-root-id" },
|
||||
{ id: "some-child-id", parentId: "some-root-id" },
|
||||
{ id: "some-grandchild-id", parentId: "some-child-id" },
|
||||
{ id: "some-other-grandchild-id", parentId: "some-child-id" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("when finding root using path, does so", () => {
|
||||
const actual = findComposite("some-root-id")(composite);
|
||||
|
||||
expect(actual.id).toBe("some-root-id");
|
||||
});
|
||||
|
||||
it("when finding child using path, does so", () => {
|
||||
const actual = findComposite("some-root-id", "some-child-id")(composite);
|
||||
|
||||
expect(actual.id).toBe("some-child-id");
|
||||
});
|
||||
|
||||
it("when finding grandchild using path, does so", () => {
|
||||
const actual = findComposite(
|
||||
"some-root-id",
|
||||
"some-child-id",
|
||||
"some-grandchild-id",
|
||||
)(composite);
|
||||
|
||||
expect(actual.id).toBe("some-grandchild-id");
|
||||
});
|
||||
|
||||
it("when finding with non existing leaf-level path, throws", () => {
|
||||
expect(() => {
|
||||
findComposite(
|
||||
"some-root-id",
|
||||
"some-child-id",
|
||||
"some-non-existing-grandchild-id",
|
||||
)(composite);
|
||||
}).toThrow(`Tried to find 'some-root-id -> some-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
|
||||
|
||||
Node 'some-root-id -> some-child-id' had only following children:
|
||||
some-grandchild-id
|
||||
some-other-grandchild-id`);
|
||||
});
|
||||
|
||||
it("when finding with non-existing mid-level path, throws", () => {
|
||||
expect(() => {
|
||||
findComposite(
|
||||
"some-root-id",
|
||||
"some-non-existing-child-id",
|
||||
"some-non-existing-grandchild-id",
|
||||
)(composite);
|
||||
}).toThrow(`Tried to find 'some-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
|
||||
|
||||
Node 'some-root-id' had only following children:
|
||||
some-child-id`);
|
||||
});
|
||||
|
||||
it("when finding with non-existing root-level path, throws", () => {
|
||||
expect(() => {
|
||||
findComposite(
|
||||
"some-non-existing-root-id",
|
||||
"some-non-existing-child-id",
|
||||
"some-non-existing-grandchild-id",
|
||||
)(composite);
|
||||
}).toThrow(`Tried to find 'some-non-existing-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
|
||||
|
||||
Node 'some-root-id' had only following children:
|
||||
some-child-id`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
|
||||
const _findComposite = <T>(currentLeftIds: string[], currentId: string, currentRightIds: string[], composite: Composite<T>): Composite<T> => {
|
||||
const [nextId, ...nextRightIds] = currentRightIds;
|
||||
const nextLeftIds = [...currentLeftIds, currentId];
|
||||
|
||||
if (currentRightIds.length === 0 && composite.id === currentId) {
|
||||
return composite;
|
||||
}
|
||||
|
||||
const foundChildComposite = composite.children.find((child) => child.id === nextId);
|
||||
|
||||
if (foundChildComposite) {
|
||||
return _findComposite(nextLeftIds, nextId, nextRightIds, foundChildComposite);
|
||||
}
|
||||
|
||||
const fullPathString = [...currentLeftIds, currentId, ...currentRightIds].join(" -> ");
|
||||
|
||||
throw new Error(`Tried to find '${fullPathString}' from a composite, but found nothing.
|
||||
|
||||
Node '${[...currentLeftIds, composite.id].join(" -> ")}' had only following children:
|
||||
${composite.children.map((child) => child.id).join("\n")}`);
|
||||
};
|
||||
|
||||
export const findComposite =
|
||||
(...path: string[]) =>
|
||||
<T>(composite: Composite<T>): Composite<T> => {
|
||||
const [currentId, ...rightIds] = path;
|
||||
const leftIds: string[] = [];
|
||||
|
||||
return _findComposite(leftIds, currentId, rightIds, composite);
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getCompositeNormalization } from "./get-composite-normalization";
|
||||
import { getCompositeFor } from "../get-composite/get-composite";
|
||||
|
||||
|
||||
describe("get-composite-normalization", () => {
|
||||
it("given a composite, flattens it to paths and composites", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
id: "some-child-id",
|
||||
parentId: "some-id",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const getComposite = getCompositeFor<{
|
||||
id: string;
|
||||
parentId?: string;
|
||||
orderNumber?: number;
|
||||
}>({
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
const composite = getComposite(items);
|
||||
|
||||
const actual = getCompositeNormalization(composite);
|
||||
|
||||
expect(actual).toEqual([
|
||||
[["some-root-id"], expect.objectContaining({ value: someRootItem })],
|
||||
|
||||
[["some-root-id", "some-id"], expect.objectContaining({ value: someItem })],
|
||||
|
||||
[
|
||||
["some-root-id", "some-id", "some-child-id"],
|
||||
expect.objectContaining({ value: someNestedItem }),
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
|
||||
export const getCompositeNormalization = <T>(composite: Composite<T>) => {
|
||||
const _normalizeComposite = <T>(
|
||||
composite: Composite<T>,
|
||||
previousPath: string[] = [],
|
||||
): (readonly [path: string[], composite: Composite<T>])[] => {
|
||||
const currentPath = [...previousPath, composite.id];
|
||||
|
||||
const pathAndCompositeTuple = [currentPath, composite] as const;
|
||||
|
||||
return [
|
||||
pathAndCompositeTuple,
|
||||
|
||||
...composite.children.flatMap((child) =>
|
||||
_normalizeComposite(child, currentPath),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
return _normalizeComposite(composite);
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getCompositePaths } from "./get-composite-paths";
|
||||
import { sortBy } from "lodash/fp";
|
||||
import { getCompositeFor } from "../get-composite/get-composite";
|
||||
|
||||
describe("get-composite-paths", () => {
|
||||
it("given composite with transformed children, returns paths of transformed children in hierarchical order", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
};
|
||||
|
||||
const someChildItem1 = {
|
||||
id: "some-child-id-1",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someChildItem2 = {
|
||||
id: "some-child-id-2",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const someGrandchildItem1 = {
|
||||
id: "some-grandchild-id-1",
|
||||
parentId: "some-child-id-1",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someGrandchildItem2 = {
|
||||
id: "some-grandchild-id-2",
|
||||
parentId: "some-child-id-1",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const items = [
|
||||
someRootItem,
|
||||
// Note: not in order yet.
|
||||
someChildItem2,
|
||||
someChildItem1,
|
||||
someGrandchildItem2,
|
||||
someGrandchildItem1,
|
||||
];
|
||||
|
||||
const getComposite = getCompositeFor<{
|
||||
id: string;
|
||||
parentId?: string;
|
||||
orderNumber?: number;
|
||||
}>({
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
transformChildren: children => sortBy(child => child.orderNumber, children),
|
||||
});
|
||||
|
||||
const composite = getComposite(items);
|
||||
|
||||
const actual = getCompositePaths(composite);
|
||||
|
||||
expect(actual).toEqual([
|
||||
["some-root-id"],
|
||||
["some-root-id", "some-child-id-1"],
|
||||
["some-root-id", "some-child-id-1", "some-grandchild-id-1"],
|
||||
["some-root-id", "some-child-id-1", "some-grandchild-id-2"],
|
||||
["some-root-id", "some-child-id-2"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { map } from "lodash/fp";
|
||||
import type { Composite } from "../get-composite/get-composite";
|
||||
import { getCompositeNormalization } from "../get-composite-normalization/get-composite-normalization";
|
||||
|
||||
export const getCompositePaths = (
|
||||
composite: Composite<unknown>,
|
||||
): string[][] => pipeline(composite, getCompositeNormalization, map(([path]) => path));
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Composite } from "./get-composite";
|
||||
import { getCompositePaths } from "../get-composite-paths/get-composite-paths";
|
||||
import { sortBy } from "lodash/fp";
|
||||
import { getCompositeFor } from "./get-composite";
|
||||
|
||||
interface SomeItem {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
orderNumber?: number;
|
||||
}
|
||||
|
||||
describe("get-composite", () => {
|
||||
it("given items and an explicit root id, creates a composite", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
someProperty: "some-root-content",
|
||||
};
|
||||
|
||||
const someIrrelevantRootItem = {
|
||||
id: "some-irrelevant-root-id",
|
||||
someProperty: "some-other-root-content",
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
someProperty: "some-content",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
someProperty: "some-nested-content",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someIrrelevantRootItem, someItem, someNestedItem];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
const composite = getComposite(items);
|
||||
|
||||
expect(composite).toEqual({
|
||||
id: "some-root-id",
|
||||
value: someRootItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
value: someItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
value: someNestedItem,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("given items and implicit root, creates a composite", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
someProperty: "some-root-content",
|
||||
// Notice: no "parentId" makes this the implicit root.
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
someProperty: "some-content",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
someProperty: "some-nested-content",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
// Notice: no root id
|
||||
// rootId: "some-root-id",
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
const composite = getComposite(items);
|
||||
|
||||
expect(composite).toEqual({
|
||||
id: "some-root-id",
|
||||
value: someRootItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
value: someItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
value: someNestedItem,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("given items and an unspecified root id and multiple items without parent as root, throws", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
// Notice: no "parentId" makes this a root.
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const someOtherRootItem = {
|
||||
id: "some-other-root-id",
|
||||
// Notice: no "parentId" makes also this a root.
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const items = [someRootItem, someOtherRootItem];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
getComposite(items);
|
||||
}).toThrow(
|
||||
'Tried to get a composite, but multiple roots where encountered: "some-root-id", "some-other-root-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given non-unique ids, throws", () => {
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "irrelevant",
|
||||
};
|
||||
|
||||
const someOtherItem = {
|
||||
id: "some-id",
|
||||
parentId: "irrelevant",
|
||||
};
|
||||
|
||||
const items = [someItem, someOtherItem];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
getComposite(items);
|
||||
}).toThrow(
|
||||
'Tried to get a composite but encountered non-unique ids: "some-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given items with missing parent ids, when creating composite without handling for unknown parents, throws", () => {
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const someItemWithMissingParentId = {
|
||||
id: "some-other-id",
|
||||
parentId: "some-missing-id",
|
||||
};
|
||||
|
||||
const items = [someItem, someItemWithMissingParentId];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
getComposite(items);
|
||||
}).toThrow(
|
||||
`Tried to get a composite but encountered missing parent ids: "some-missing-id".
|
||||
|
||||
Available parent ids are:
|
||||
"some-id",
|
||||
"some-other-id"`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("given items with missing parents, when creating composite with handling for missing parents", () => {
|
||||
let composite: Composite<any>;
|
||||
let handleMissingParentIdMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const someItem = {
|
||||
id: "some-root-id",
|
||||
};
|
||||
|
||||
const someItemWithMissingParentId = {
|
||||
id: "some-orphan-id",
|
||||
// Note: the item corresponding to this id does not exist,
|
||||
// making this item have a "missing parent".
|
||||
parentId: "some-missing-id",
|
||||
};
|
||||
|
||||
const items = [someItem, someItemWithMissingParentId];
|
||||
|
||||
handleMissingParentIdMock = jest.fn();
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
handleMissingParentIds: handleMissingParentIdMock,
|
||||
});
|
||||
|
||||
composite = getComposite(items);
|
||||
});
|
||||
|
||||
it("creates composite without the orphan item, and without throwing", () => {
|
||||
const paths = getCompositePaths(composite);
|
||||
|
||||
expect(paths).toEqual([["some-root-id"]]);
|
||||
});
|
||||
|
||||
it("handles the missing parent ids", () => {
|
||||
expect(handleMissingParentIdMock).toHaveBeenCalledWith({
|
||||
missingParentIds: ["some-missing-id"],
|
||||
availableParentIds: ["some-root-id", "some-orphan-id"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("given items with same id and parent id, throws", () => {
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-id",
|
||||
};
|
||||
|
||||
const someRoot = {
|
||||
id: "root",
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const items = [someItem, someRoot];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
getComposite(items);
|
||||
}).toThrow(
|
||||
'Tried to get a composite, but found items with self as parent: "some-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given undefined ids, throws", () => {
|
||||
const root = {
|
||||
parentId: undefined,
|
||||
id: "some-root",
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
parentId: "some-root",
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
const someOtherItem = {
|
||||
parentId: "some-root",
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
const items = [root, someItem, someOtherItem];
|
||||
|
||||
const getComposite = getCompositeFor<any>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
getComposite(items);
|
||||
}).toThrow("Tried to get a composite but encountered 2 undefined ids");
|
||||
});
|
||||
|
||||
it("given transformed children, creates a composite with transformed children", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem1 = {
|
||||
id: "some-id-1",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem2 = {
|
||||
id: "some-id-2",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const someChildItem1 = {
|
||||
id: "some-child-id-1",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someChildItem2 = {
|
||||
id: "some-child-id-2",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const items = [
|
||||
someRootItem,
|
||||
// Note: not in order yet.
|
||||
someItem2,
|
||||
someItem1,
|
||||
someChildItem2,
|
||||
someChildItem1,
|
||||
];
|
||||
|
||||
const getComposite = getCompositeFor<SomeItem>({
|
||||
getId: (x) => x.id,
|
||||
getParentId: (x) => x.parentId,
|
||||
transformChildren: (things) =>
|
||||
sortBy((thing) => thing.orderNumber, things),
|
||||
});
|
||||
|
||||
const composite = getComposite(items);
|
||||
|
||||
const orderedPaths = getCompositePaths(composite);
|
||||
|
||||
expect(orderedPaths).toEqual([
|
||||
["some-root-id"],
|
||||
["some-root-id", "some-id-1"],
|
||||
["some-root-id", "some-id-1", "some-child-id-1"],
|
||||
["some-root-id", "some-id-1", "some-child-id-2"],
|
||||
["some-root-id", "some-id-2"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import {
|
||||
countBy,
|
||||
filter,
|
||||
toPairs,
|
||||
nth,
|
||||
map,
|
||||
uniq,
|
||||
without,
|
||||
compact,
|
||||
identity,
|
||||
} from "lodash/fp";
|
||||
|
||||
export interface Composite<T> {
|
||||
id: string;
|
||||
parentId: string | undefined;
|
||||
value: T;
|
||||
children: Composite<T>[];
|
||||
}
|
||||
|
||||
interface Configuration<T> {
|
||||
rootId?: string;
|
||||
getId: (thing: T) => string;
|
||||
getParentId: (thing: T) => string | undefined;
|
||||
transformChildren?: (things: T[]) => T[];
|
||||
handleMissingParentIds?: (parentIdsForHandling: ParentIdsForHandling) => void;
|
||||
}
|
||||
|
||||
export const getCompositeFor = <T>({
|
||||
rootId = undefined,
|
||||
getId,
|
||||
getParentId,
|
||||
transformChildren = identity,
|
||||
handleMissingParentIds = throwMissingParentIds,
|
||||
}: Configuration<T>) => (source: T[]) => {
|
||||
const undefinedIds = pipeline(
|
||||
source,
|
||||
filter((x) => getId(x) === undefined),
|
||||
);
|
||||
|
||||
if (undefinedIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered ${undefinedIds.length} undefined ids`,
|
||||
);
|
||||
}
|
||||
|
||||
const selfReferencingIds = pipeline(
|
||||
source,
|
||||
filter((x) => getId(x) === getParentId(x)),
|
||||
map(getId),
|
||||
);
|
||||
|
||||
if (selfReferencingIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite, but found items with self as parent: "${selfReferencingIds.join(
|
||||
'", ',
|
||||
)}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const duplicateIds = pipeline(
|
||||
source,
|
||||
countBy(getId),
|
||||
toPairs,
|
||||
filter(([, count]) => count > 1),
|
||||
map(nth(0)),
|
||||
);
|
||||
|
||||
if (duplicateIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered non-unique ids: "${duplicateIds
|
||||
.map((x) => String(x))
|
||||
.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const allIds = pipeline(source, map(getId));
|
||||
|
||||
const allParentIds = pipeline(source, map(getParentId), uniq, compact);
|
||||
|
||||
const missingParentIds = without(allIds, allParentIds);
|
||||
|
||||
if (missingParentIds.length) {
|
||||
handleMissingParentIds({ missingParentIds, availableParentIds: allIds });
|
||||
}
|
||||
|
||||
const toComposite = (thing: T): Composite<T> => {
|
||||
const thingId = getId(thing);
|
||||
|
||||
return {
|
||||
id: thingId,
|
||||
parentId: getParentId(thing),
|
||||
value: thing,
|
||||
|
||||
children: pipeline(
|
||||
source,
|
||||
|
||||
filter((childThing) => {
|
||||
const parentId = getParentId(childThing);
|
||||
|
||||
return parentId === thingId;
|
||||
}),
|
||||
|
||||
transformChildren,
|
||||
|
||||
map(toComposite),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const isRootId = rootId
|
||||
? (thing: T) => getId(thing) === rootId
|
||||
: (thing: T) => getParentId(thing) === undefined;
|
||||
|
||||
const roots = source.filter(isRootId);
|
||||
|
||||
if (roots.length > 1) {
|
||||
throw new Error(
|
||||
`Tried to get a composite, but multiple roots where encountered: "${roots
|
||||
.map(getId)
|
||||
.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return toComposite(roots[0]);
|
||||
};
|
||||
|
||||
interface ParentIdsForHandling {
|
||||
missingParentIds: string[];
|
||||
availableParentIds: string[];
|
||||
}
|
||||
|
||||
const throwMissingParentIds = ({
|
||||
missingParentIds,
|
||||
availableParentIds,
|
||||
}: ParentIdsForHandling) => {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered missing parent ids: "${missingParentIds.join(
|
||||
'", "',
|
||||
)}".\n\nAvailable parent ids are:\n"${availableParentIds.join('",\n"')}"`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export interface ParentOfChildComposite<Id extends string = string> {
|
||||
id: Id;
|
||||
}
|
||||
|
||||
export interface ChildOfParentComposite<ParentId extends string = string> {
|
||||
parentId: ParentId;
|
||||
}
|
||||
|
||||
export type RootComposite<Id extends string = string> =
|
||||
& { parentId: undefined }
|
||||
& ParentOfChildComposite<Id>;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed } from "mobx";
|
||||
|
||||
export const computedOr = (...values: IComputedValue<boolean>[]) => computed((
|
||||
() => values.some(value => value.get())
|
||||
));
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
|
||||
// Helper to convert CPU K8S units to numbers
|
||||
|
||||
const unitConverters = new Map([
|
||||
["n", 1000 ** -3],
|
||||
["u", 1000 ** -2],
|
||||
["m", 1000 ** -1], // milli
|
||||
["", 1000 ** 0], // no units
|
||||
["k", 1000 ** 1],
|
||||
["M", 1000 ** 2],
|
||||
["G", 1000 ** 3],
|
||||
["P", 1000 ** 4],
|
||||
["T", 1000 ** 5],
|
||||
["E", 1000 ** 6],
|
||||
]);
|
||||
|
||||
const cpuUnitsRegex = TypedRegEx("^(?<digits>[+-]?[0-9.]+(e[-+]?[0-9]+)?)(?<unit>[EinumkKMGTP]*)$");
|
||||
|
||||
export function cpuUnitsToNumber(value: string) {
|
||||
const match = cpuUnitsRegex.captures(value);
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { digits = "", unit } = match;
|
||||
const conversion = unitConverters.get(unit);
|
||||
|
||||
if (conversion === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseFloat(digits) * conversion;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import * as iter from "./iter";
|
||||
|
||||
// Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa
|
||||
|
||||
const baseMagnitude = 1024;
|
||||
const maxMagnitude = ["PiB", baseMagnitude ** 5] as const;
|
||||
const magnitudes = new Map([
|
||||
["B", 1] as const,
|
||||
["KiB", baseMagnitude ** 1] as const,
|
||||
["MiB", baseMagnitude ** 2] as const,
|
||||
["GiB", baseMagnitude ** 3] as const,
|
||||
["TiB", baseMagnitude ** 4] as const,
|
||||
maxMagnitude,
|
||||
]);
|
||||
const unitRegex = /(?<value>[0-9]+(\.[0-9]*)?)(?<suffix>(B|[KMGTP]iB?))?/;
|
||||
|
||||
type BinaryUnit = typeof magnitudes extends Map<infer Key, any> ? Key : never;
|
||||
|
||||
export function unitsToBytes(value: string): number {
|
||||
const unitsMatch = value.match(unitRegex);
|
||||
|
||||
if (!unitsMatch?.groups) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const parsedValue = parseFloat(unitsMatch.groups.value);
|
||||
|
||||
if (!unitsMatch.groups?.suffix) {
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
const magnitude = magnitudes.get(unitsMatch.groups.suffix as BinaryUnit)
|
||||
?? magnitudes.get(`${unitsMatch.groups.suffix}B` as BinaryUnit);
|
||||
|
||||
assert(magnitude, "UnitRegex is wrong some how");
|
||||
|
||||
return parseInt((parsedValue * magnitude).toFixed(1));
|
||||
}
|
||||
|
||||
export interface BytesToUnitesOptions {
|
||||
/**
|
||||
* The number of decimal places. MUST be an integer. MUST be in the range [0, 20].
|
||||
* @default 1
|
||||
*/
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
export function bytesToUnits(bytes: number, { precision = 1 }: BytesToUnitesOptions = {}): string {
|
||||
if (bytes <= 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
const index = Math.floor(Math.log(bytes) / Math.log(baseMagnitude));
|
||||
const [suffix, magnitude] = iter.nth(magnitudes.entries(), index) ?? maxMagnitude;
|
||||
|
||||
return `${(bytes / magnitude).toFixed(precision)}${suffix}`;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import moment from "moment";
|
||||
|
||||
export const getCurrentDateTime = () => moment().utc().format();
|
||||
|
||||
export const getMillisecondsFromUnixEpoch = () => Date.now();
|
||||
|
||||
export const getSecondsFromUnixEpoch = () => Math.floor(getMillisecondsFromUnixEpoch() / 1000);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Debouncing promise evaluation
|
||||
|
||||
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
return (...params: F) => new Promise(resolve => {
|
||||
clearTimeout(timer);
|
||||
timer = global.setTimeout(() => resolve(func(...params)), timeout);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type AbortController from "abort-controller";
|
||||
|
||||
/**
|
||||
* Return a promise that will be resolved after at least `timeout` ms have
|
||||
* passed. If `failFast` is provided then the promise is also resolved if it has
|
||||
* been aborted.
|
||||
* @param timeout The number of milliseconds before resolving
|
||||
* @param failFast An abort controller instance to cause the delay to short-circuit
|
||||
*/
|
||||
export function delay(timeout = 1000, failFast?: AbortController): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
failFast?.signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { SingleOrMany } from "./types";
|
||||
|
||||
|
||||
|
||||
export interface Disposer {
|
||||
(): void;
|
||||
}
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface ExtendableDisposer extends Disposer {
|
||||
push(...vals: (Disposer | ExtendableDisposer | Disposable)[]): void;
|
||||
}
|
||||
|
||||
export function disposer(...items: SingleOrMany<Disposer | Disposable | undefined | null>[]): ExtendableDisposer {
|
||||
return Object.assign(() => {
|
||||
for (const item of items.flat()) {
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof item === "function") {
|
||||
item();
|
||||
} else {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
items.length = 0;
|
||||
}, {
|
||||
push: (...newItems) => items.push(...newItems),
|
||||
} as Pick<ExtendableDisposer, "push">);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Helper to sanitize / escape special chars for passing to RegExp-constructor
|
||||
|
||||
export function escapeRegExp(str: string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { findExactlyOne } from "./find-exactly-one";
|
||||
|
||||
describe("find-exactly-one", () => {
|
||||
it("when predicate matches to single item, returns the item", () => {
|
||||
const actual = findExactlyOne((item) => item === "some-item")([
|
||||
"some-item",
|
||||
"some-other-item",
|
||||
]);
|
||||
|
||||
expect(actual).toBe("some-item");
|
||||
});
|
||||
|
||||
it("when predicate matches to many items, throws", () => {
|
||||
expect(() => {
|
||||
findExactlyOne((item) => item === "some-item")([
|
||||
"some-item",
|
||||
"some-item",
|
||||
]);
|
||||
}).toThrow("Tried to find exactly one, but found many");
|
||||
});
|
||||
|
||||
it("when predicate does not match, throws", () => {
|
||||
expect(() => {
|
||||
findExactlyOne((item) => item === "some-item")([
|
||||
"some-other-item",
|
||||
]);
|
||||
}).toThrow("Tried to find exactly one, but didn't find any");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collection: T[]): T => {
|
||||
const itemsFound = collection.filter(predicate);
|
||||
|
||||
if (!itemsFound.length) {
|
||||
throw new Error(
|
||||
"Tried to find exactly one, but didn't find any",
|
||||
);
|
||||
}
|
||||
|
||||
if (itemsFound.length > 1) {
|
||||
throw new Error(
|
||||
"Tried to find exactly one, but found many",
|
||||
);
|
||||
}
|
||||
|
||||
return itemsFound[0];
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
/**
|
||||
* This function formats durations in a more human readable form.
|
||||
* @param timeValue the duration in milliseconds to format
|
||||
*/
|
||||
export function formatDuration(timeValue: number, compact = true) {
|
||||
const duration = moment.duration(timeValue, "milliseconds");
|
||||
const seconds = Math.floor(duration.asSeconds());
|
||||
const separator = compact ? "": " ";
|
||||
|
||||
if (seconds < 0) {
|
||||
return "0s";
|
||||
} else if (seconds < 60*2 ) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration.asMinutes());
|
||||
|
||||
if (minutes < 10) {
|
||||
const seconds = duration.seconds();
|
||||
|
||||
return getMeaningfulValues([minutes, seconds], ["m", "s"], separator);
|
||||
} else if (minutes < 60 * 3) {
|
||||
if (!compact) {
|
||||
return getMeaningfulValues([minutes, duration.seconds()], ["m", "s"]);
|
||||
}
|
||||
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(duration.asHours());
|
||||
|
||||
if(hours < 8) {
|
||||
const minutes = duration.minutes();
|
||||
|
||||
return getMeaningfulValues([hours, minutes], ["h", "m"], separator);
|
||||
} else if (hours < 48) {
|
||||
if (compact) {
|
||||
return `${hours}h`;
|
||||
} else {
|
||||
return getMeaningfulValues([hours, duration.minutes()], ["h", "m"]);
|
||||
}
|
||||
}
|
||||
|
||||
const days = Math.floor(duration.asDays());
|
||||
|
||||
if (days < 8) {
|
||||
const hours = duration.hours();
|
||||
|
||||
if (compact) {
|
||||
return getMeaningfulValues([days, hours], ["d", "h"], separator);
|
||||
} else {
|
||||
return getMeaningfulValues([days, hours, duration.minutes()], ["d", "h", "m"]);
|
||||
}
|
||||
}
|
||||
const years = Math.floor(duration.asYears());
|
||||
|
||||
if (years < 2) {
|
||||
if (compact) {
|
||||
return `${days}d`;
|
||||
} else {
|
||||
return getMeaningfulValues([days, duration.hours(), duration.minutes()], ["d", "h", "m"]);
|
||||
}
|
||||
} else if (years < 8) {
|
||||
const days = duration.days();
|
||||
|
||||
if (compact) {
|
||||
return getMeaningfulValues([years, days], ["y", "d"], separator);
|
||||
}
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return `${years}y`;
|
||||
}
|
||||
|
||||
return getMeaningfulValues([years, duration.days(), duration.hours(), duration.minutes()], ["y", "d", "h", "m"]);
|
||||
}
|
||||
|
||||
function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") {
|
||||
return values
|
||||
.map((a, i): [number, string] => [a, suffixes[i]])
|
||||
.filter(([dur]) => dur > 0)
|
||||
.map(([dur, suf]) => dur + suf)
|
||||
.join(separator);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export function generateNewIdFor(cluster: { kubeConfigPath: string; contextName: string }): string {
|
||||
return createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return JSON.stringify(error);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import getRandomIdInjectable from "./get-random-id.injectable";
|
||||
|
||||
export default getGlobalOverride(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
|
||||
@@ -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 { v4 as getRandomId } from "uuid";
|
||||
|
||||
const getRandomIdInjectable = getInjectable({
|
||||
id: "get-random-id",
|
||||
instantiate: () => () => getRandomId(),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default getRandomIdInjectable;
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { StartableStoppable } from "./get-startable-stoppable";
|
||||
import { getStartableStoppable } from "./get-startable-stoppable";
|
||||
|
||||
describe("getStartableStoppable", () => {
|
||||
let stopMock: jest.MockedFunction<() => void>;
|
||||
let startMock: jest.MockedFunction<() => () => void>;
|
||||
let actual: StartableStoppable;
|
||||
|
||||
beforeEach(() => {
|
||||
stopMock = jest.fn();
|
||||
startMock = jest.fn().mockImplementation(() => stopMock);
|
||||
actual = getStartableStoppable("some-id", startMock);
|
||||
});
|
||||
|
||||
it("does not start yet", () => {
|
||||
expect(startMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not stop yet", () => {
|
||||
expect(stopMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when stopping before ever starting, throws", () => {
|
||||
expect(() => actual.stop()).toThrow("Tried to stop \"some-id\", but it is already stopped.");
|
||||
});
|
||||
|
||||
it("is not started", () => {
|
||||
expect(actual.started).toBe(false);
|
||||
});
|
||||
|
||||
describe("when started", () => {
|
||||
beforeEach(() => {
|
||||
actual.start();
|
||||
});
|
||||
|
||||
it("calls start function", () => {
|
||||
expect(startMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is started", () => {
|
||||
expect(actual.started).toBe(true);
|
||||
});
|
||||
|
||||
describe("when stopped", () => {
|
||||
beforeEach(() => {
|
||||
actual.stop();
|
||||
});
|
||||
|
||||
it("calls stop function", () => {
|
||||
expect(stopMock).toBeCalled();
|
||||
});
|
||||
|
||||
it("is stopped", () => {
|
||||
expect(actual.started).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type Stopper = () => void;
|
||||
export type Starter = () => Stopper;
|
||||
|
||||
export interface StartableStoppable {
|
||||
readonly started: boolean;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
type StartableStoppableState = "stopped" | "started" | "starting";
|
||||
|
||||
export function getStartableStoppable(id: string, startAndGetStopper: Starter): StartableStoppable {
|
||||
let stop: Stopper;
|
||||
let state: StartableStoppableState = "stopped";
|
||||
|
||||
return {
|
||||
get started() {
|
||||
return state === "started";
|
||||
},
|
||||
|
||||
start: () => {
|
||||
if (state !== "stopped") {
|
||||
throw new Error(`Tried to start "${id}", but it is already ${state}.`);
|
||||
}
|
||||
|
||||
state = "starting";
|
||||
stop = startAndGetStopper();
|
||||
state = "started";
|
||||
},
|
||||
|
||||
stop: () => {
|
||||
if (state !== "started") {
|
||||
throw new Error(`Tried to stop "${id}", but it is already ${state}.`);
|
||||
}
|
||||
|
||||
stop();
|
||||
state = "stopped";
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Create random system name
|
||||
|
||||
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
|
||||
const randId = () => Math.random().toString(16).slice(2);
|
||||
|
||||
return [prefix, randId(), suffix].filter(s => s).join(sep);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IInterceptable, IInterceptor, IListenable, ISetWillChange, ObservableMap } from "mobx";
|
||||
import { action, observable, ObservableSet } from "mobx";
|
||||
|
||||
export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
|
||||
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
|
||||
|
||||
return iterator as IterableIterator<T>;
|
||||
}
|
||||
|
||||
export class HashSet<T> implements Set<T> {
|
||||
#hashmap: Map<string, T>;
|
||||
|
||||
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
|
||||
this.#hashmap = new Map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]));
|
||||
}
|
||||
|
||||
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||
if (other === null || other === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
|
||||
for (const value of other) {
|
||||
this.add(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#hashmap.clear();
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
this.#hashmap.set(this.hasher(value), value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toggle(value: T): void {
|
||||
const hash = this.hasher(value);
|
||||
|
||||
if (this.#hashmap.has(hash)) {
|
||||
this.#hashmap.delete(hash);
|
||||
} else {
|
||||
this.#hashmap.set(hash, value);
|
||||
}
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this.#hashmap.delete(this.hasher(value));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.#hashmap.has(this.hasher(value));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#hashmap.size;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[T, T]> {
|
||||
let nextIndex = 0;
|
||||
const keys = Array.from(this.keys());
|
||||
const values = Array.from(this.values());
|
||||
|
||||
return makeIterableIterator<[T, T]>({
|
||||
next() {
|
||||
const index = nextIndex++;
|
||||
|
||||
return index < values.length
|
||||
? { value: [keys[index], values[index]], done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<T> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<T>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++], done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.#hashmap.values();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Set";
|
||||
}
|
||||
|
||||
toJSON(): T[] {
|
||||
return Array.from(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "[object Set]";
|
||||
}
|
||||
}
|
||||
|
||||
export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
|
||||
#hashmap: ObservableMap<string, T>;
|
||||
|
||||
get interceptors_(): IInterceptor<ISetWillChange<T>>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
get changeListeners_(): Function[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
|
||||
this.#hashmap = observable.map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]), undefined);
|
||||
}
|
||||
|
||||
@action
|
||||
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
|
||||
if (other === null || other === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
|
||||
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
|
||||
for (const value of other) {
|
||||
this.add(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#hashmap.clear();
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
this.#hashmap.set(this.hasher(value), value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@action
|
||||
toggle(value: T): void {
|
||||
const hash = this.hasher(value);
|
||||
|
||||
if (this.#hashmap.has(hash)) {
|
||||
this.#hashmap.delete(hash);
|
||||
} else {
|
||||
this.#hashmap.set(hash, value);
|
||||
}
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this.#hashmap.delete(this.hasher(value));
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.#hashmap.has(this.hasher(value));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#hashmap.size;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[T, T]> {
|
||||
let nextIndex = 0;
|
||||
const keys = Array.from(this.keys());
|
||||
const values = Array.from(this.values());
|
||||
|
||||
return makeIterableIterator<[T, T]>({
|
||||
next() {
|
||||
const index = nextIndex++;
|
||||
|
||||
return index < values.length
|
||||
? { value: [keys[index], values[index]], done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<T> {
|
||||
let nextIndex = 0;
|
||||
const observableValues = Array.from(this.#hashmap.values());
|
||||
|
||||
return makeIterableIterator<T>({
|
||||
next: () => {
|
||||
return nextIndex < observableValues.length
|
||||
? { value: observableValues[nextIndex++], done: false }
|
||||
: { done: true, value: undefined };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.#hashmap.values();
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Set";
|
||||
}
|
||||
|
||||
toJSON(): T[] {
|
||||
return Array.from(this);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "[object ObservableSet]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./abort-controller";
|
||||
export * from "./autobind";
|
||||
export * from "./camelCase";
|
||||
export * from "./cluster-id-url-parsing";
|
||||
export * from "./collection-functions";
|
||||
export * from "./convertCpu";
|
||||
export * from "./convertMemory";
|
||||
export * from "./debouncePromise";
|
||||
export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./formatDuration";
|
||||
export * from "./getRandId";
|
||||
export * from "./hash-set";
|
||||
export * from "./n-fircate";
|
||||
export * from "./noop";
|
||||
export * from "./observable-crate/impl";
|
||||
export * from "./promise-exec";
|
||||
export * from "./readonly";
|
||||
export * from "./reject-promise";
|
||||
export * from "./singleton";
|
||||
export * from "./sort-compare";
|
||||
export * from "./splitArray";
|
||||
export * from "./tar";
|
||||
export * from "./toJS";
|
||||
export * from "./type-narrowing";
|
||||
export * from "./types";
|
||||
export * from "./wait-for-path";
|
||||
export * from "./wait";
|
||||
|
||||
export type { Tuple } from "./tuple";
|
||||
|
||||
import * as iter from "./iter";
|
||||
import * as array from "./array";
|
||||
import * as tuple from "./tuple";
|
||||
import * as base64 from "./base64";
|
||||
import * as object from "./objects";
|
||||
import * as json from "./json";
|
||||
|
||||
export {
|
||||
iter,
|
||||
array,
|
||||
tuple,
|
||||
base64,
|
||||
object,
|
||||
json,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { isPromise } from "./is-promise";
|
||||
|
||||
describe("isPromise", () => {
|
||||
it("given promise, returns true", () => {
|
||||
const actual = isPromise(new Promise(() => {}));
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("given non-promise, returns false", () => {
|
||||
const actual = isPromise({});
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("given thenable, returns false", () => {
|
||||
const actual = isPromise({ then: () => {} });
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("given nothing, returns false", () => {
|
||||
const actual = isPromise(undefined);
|
||||
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export function isPromise(reference: any): reference is Promise<any> {
|
||||
return reference?.constructor === Promise;
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type Falsey = false | 0 | "" | null | undefined;
|
||||
|
||||
interface Iterator<T> extends Iterable<T> {
|
||||
filter(fn: (val: T) => unknown): Iterator<T>;
|
||||
filterMap<U>(fn: (val: T) => Falsey | U): Iterator<U>;
|
||||
find(fn: (val: T) => unknown): T | undefined;
|
||||
collect<U>(fn: (values: Iterable<T>) => U): U;
|
||||
map<U>(fn: (val: T) => U): Iterator<U>;
|
||||
flatMap<U>(fn: (val: T) => U[]): Iterator<U>;
|
||||
concat(src2: IterableIterator<T>): Iterator<T>;
|
||||
join(sep?: string): string;
|
||||
}
|
||||
|
||||
export function chain<T>(src: IterableIterator<T>): Iterator<T> {
|
||||
return {
|
||||
filter: (fn) => chain(filter(src, fn)),
|
||||
filterMap: (fn) => chain(filterMap(src, fn)),
|
||||
map: (fn) => chain(map(src, fn)),
|
||||
flatMap: (fn) => chain(flatMap(src, fn)),
|
||||
find: (fn) => find(src, fn),
|
||||
join: (sep) => join(src, sep),
|
||||
collect: (fn) => fn(src),
|
||||
concat: (src2) => chain(concat(src, src2)),
|
||||
[Symbol.iterator]: () => src,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new type safe empty Iterable
|
||||
* @returns An `Iterable` that yields 0 items
|
||||
*/
|
||||
export function* newEmpty<T>(): IterableIterator<T> {}
|
||||
|
||||
/**
|
||||
* Creates a new `Iterable` that yields at most n items from src.
|
||||
* Does not modify `src` which can be used later.
|
||||
* @param src An initial iterator
|
||||
* @param n The maximum number of elements to take from src. Yields up to the floor of `n` and 0 items if n < 0
|
||||
*/
|
||||
export function* take<T>(src: Iterable<T>, n: number): IterableIterator<T> {
|
||||
outer: for (let i = 0; i < n; i += 1) {
|
||||
for (const item of src) {
|
||||
yield item;
|
||||
continue outer;
|
||||
}
|
||||
|
||||
// if we are here that means that `src` has been exhausted. Don't bother trying again.
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` for each item.
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* map<T, U>(src: Iterable<T>, fn: (from: T) => U): IterableIterator<U> {
|
||||
for (const from of src) {
|
||||
yield fn(from);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The single layer flattening of an iterator, discarding `Falsey` values.
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that returns either an iterable over items that should be filtered out or a `Falsey` value indicating that it should be ignored
|
||||
*/
|
||||
export function* filterFlatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U | Falsey> | Falsey): IterableIterator<U> {
|
||||
for (const from of src) {
|
||||
if (!from) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapping = fn(from);
|
||||
|
||||
if (!mapping) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const mapped of mapping) {
|
||||
if (mapped) {
|
||||
yield mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new iterator that yields the items that each call to `fn` would produce
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn A function that returns an iterator
|
||||
*/
|
||||
export function* flatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U>): IterableIterator<U> {
|
||||
for (const from of src) {
|
||||
yield* fn(from);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* items that return a `truthy` value from `fn`.
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filter<T>(src: Iterable<T>, fn: (from: T) => any): IterableIterator<T> {
|
||||
for (const from of src) {
|
||||
if (fn(from)) {
|
||||
yield from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` when it is `truthy`
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U | Falsey): IterableIterator<U> {
|
||||
for (const from of src) {
|
||||
const res = fn(from);
|
||||
|
||||
if (res) {
|
||||
yield res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` when it is not null or undefined
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U | null | undefined): IterableIterator<U> {
|
||||
for (const from of src) {
|
||||
const res = fn(from);
|
||||
|
||||
if (res != null) {
|
||||
yield res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through `src` until `match` returns a truthy value
|
||||
* @param src A type that can be iterated over
|
||||
* @param match A function that should return a truthy value for the item that you want to find
|
||||
* @returns The first entry that `match` returns a truthy value for, or `undefined`
|
||||
*/
|
||||
export function find<T>(src: Iterable<T>, match: (i: T) => any): T | undefined {
|
||||
for (const from of src) {
|
||||
if (match(from)) {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over `src` calling `reducer` with the previous produced value and the current
|
||||
* yielded value until `src` is exausted. Then return the final value.
|
||||
* @param src The value to iterate over
|
||||
* @param reducer A function for producing the next item from an accumilation and the current item
|
||||
* @param initial The initial value for the iteration
|
||||
*/
|
||||
export function reduce<T, R extends Iterable<any>>(src: Iterable<T>, reducer: (acc: R, cur: T) => R, initial: R): R;
|
||||
|
||||
export function reduce<T, R = T>(src: Iterable<T>, reducer: (acc: R, cur: T) => R, initial: R): R {
|
||||
let acc = initial;
|
||||
|
||||
for (const item of src) {
|
||||
acc = reducer(acc, item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function for reducing over an iterator of strings and concatenating them together
|
||||
* @param src The value to iterate over
|
||||
* @param connector The string value to intersperse between the yielded values
|
||||
* @returns The concatenated entries of `src` interspersed with copies of `connector`
|
||||
*/
|
||||
export function join(src: IterableIterator<unknown>, connector = ","): string {
|
||||
const iterSrc = src[Symbol.iterator]();
|
||||
const first = iterSrc.next();
|
||||
|
||||
if (first.done === true) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return reduce(iterSrc, (acc, cur) => `${acc}${connector}${cur}`, `${first.value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate `n` times and then return the next value.
|
||||
* @param src The value to iterate over
|
||||
* @param n The zero-index value for the item to return to.
|
||||
*/
|
||||
export function nth<T>(src: Iterable<T>, n: number): T | undefined {
|
||||
const iterator = src[Symbol.iterator]();
|
||||
|
||||
while (n --> 0) {
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
return iterator.next().value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function to get the first item of an iterator
|
||||
* @param src The value to iterate over
|
||||
*/
|
||||
export function first<T>(src: Iterable<T>): T | undefined {
|
||||
return nth(src, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through `src` and return `true` if `fn` returns a thruthy value for every yielded value.
|
||||
* Otherwise, return `false`. This function short circuits.
|
||||
* @param src The type to be iterated over
|
||||
* @param fn A function to check each iteration
|
||||
*/
|
||||
export function every<T>(src: Iterable<T>, fn: (val: T) => any): boolean {
|
||||
for (const val of src) {
|
||||
if (!fn(val)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function* concat<T>(...sources: IterableIterator<T>[]): IterableIterator<T> {
|
||||
for (const source of sources) {
|
||||
for (const val of source) {
|
||||
yield val;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { JsonValue } from "type-fest";
|
||||
|
||||
export function parse(input: string): JsonValue {
|
||||
return JSON.parse(input);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Split an iterable into several arrays with matching fields
|
||||
* @param from The iterable of items to split up
|
||||
* @param field The field of each item to split over
|
||||
* @param parts What each array will be filtered to
|
||||
* @returns A `parts.length` tuple of `T[]` where each array has matching `field` values
|
||||
*/
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: []): [];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field]]): [T[]];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field]]): [T[], T[]];
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field], T[typeof field]]): [T[], T[], T[]];
|
||||
|
||||
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: T[typeof field][]): T[][] {
|
||||
if (new Set(parts).size !== parts.length) {
|
||||
throw new TypeError("Duplicate parts entries");
|
||||
}
|
||||
|
||||
const res = Array.from(parts, () => [] as T[]);
|
||||
|
||||
for (const item of from) {
|
||||
const index = parts.indexOf(item[field]);
|
||||
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res[index].push(item);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
/**
|
||||
* A function that does nothing
|
||||
*/
|
||||
export function noop<T extends any[]>(...args: T): void {
|
||||
return void args;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A better typed version of `Object.fromEntries` where the keys are known to
|
||||
* be a specific subset
|
||||
*/
|
||||
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T> {
|
||||
return Object.fromEntries(entries) as Record<Key, T>;
|
||||
}
|
||||
|
||||
export function keys<K extends keyof any>(obj: Partial<Record<K, any>>): K[];
|
||||
|
||||
export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
|
||||
return Object.keys(obj) as K[];
|
||||
}
|
||||
|
||||
export function entries<K extends string, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
|
||||
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
|
||||
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];
|
||||
|
||||
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][] {
|
||||
if (obj && typeof obj == "object") {
|
||||
return Object.entries(obj) as never;
|
||||
}
|
||||
|
||||
return [] as never;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { observable, runInAction } from "mobx";
|
||||
import { getOrInsertMap } from "../collection-functions";
|
||||
import { noop } from "../noop";
|
||||
|
||||
export interface ObservableCrate<T> {
|
||||
get(): T;
|
||||
set(value: T): void;
|
||||
}
|
||||
|
||||
export interface ObservableCrateFactory {
|
||||
<T>(initialValue: T, transitionHandlers?: ObservableCrateTransitionHandlers<T>): ObservableCrate<T>;
|
||||
}
|
||||
|
||||
export interface ObservableCrateTransitionHandler<T> {
|
||||
from: T;
|
||||
to: T;
|
||||
onTransition: () => void;
|
||||
}
|
||||
export type ObservableCrateTransitionHandlers<T> = ObservableCrateTransitionHandler<T>[];
|
||||
|
||||
function convertToHandlersMap<T>(handlers: ObservableCrateTransitionHandlers<T>): Map<T, Map<T, () => void>> {
|
||||
const res: ReturnType<typeof convertToHandlersMap<T>> = new Map();
|
||||
|
||||
for (const { from, to, onTransition } of handlers) {
|
||||
getOrInsertMap(res, from).set(to, onTransition);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export const observableCrate = ((initialValue, transitionHandlers = []) => {
|
||||
const crate = observable.box(initialValue);
|
||||
const handlers = convertToHandlersMap(transitionHandlers);
|
||||
|
||||
return {
|
||||
get() {
|
||||
return crate.get();
|
||||
},
|
||||
set(value) {
|
||||
const onTransition = handlers.get(crate.get())?.get(value) ?? noop;
|
||||
|
||||
runInAction(() => {
|
||||
crate.set(value);
|
||||
onTransition();
|
||||
});
|
||||
},
|
||||
};
|
||||
}) as ObservableCrateFactory;
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ObservableCrate } from "./impl";
|
||||
import { observableCrate } from "./impl";
|
||||
|
||||
describe("observable-crate", () => {
|
||||
it("can be constructed with initial value", () => {
|
||||
expect(() => observableCrate(0)).not.toThrow();
|
||||
});
|
||||
|
||||
it("has a definite type if the initial value is provided", () => {
|
||||
expect (() => {
|
||||
const res: ObservableCrate<number> = observableCrate(0);
|
||||
|
||||
void res;
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts an array of transitionHandlers", () => {
|
||||
expect(() => observableCrate(0, [])).not.toThrow();
|
||||
});
|
||||
|
||||
describe("with a crate over an enum, and some transition handlers", () => {
|
||||
enum Test {
|
||||
Start,
|
||||
T1,
|
||||
End,
|
||||
}
|
||||
|
||||
let crate: ObservableCrate<Test>;
|
||||
let correctHandler: jest.MockedFunction<() => void>;
|
||||
let incorrectHandler: jest.MockedFunction<() => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
correctHandler = jest.fn();
|
||||
incorrectHandler = jest.fn();
|
||||
crate = observableCrate(Test.Start, [
|
||||
{
|
||||
from: Test.Start,
|
||||
to: Test.Start,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.Start,
|
||||
to: Test.T1,
|
||||
onTransition: correctHandler,
|
||||
},
|
||||
{
|
||||
from: Test.Start,
|
||||
to: Test.End,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.T1,
|
||||
to: Test.Start,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.T1,
|
||||
to: Test.T1,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.T1,
|
||||
to: Test.End,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.End,
|
||||
to: Test.Start,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.End,
|
||||
to: Test.T1,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
{
|
||||
from: Test.End,
|
||||
to: Test.End,
|
||||
onTransition: incorrectHandler,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("initial value is available", () => {
|
||||
expect(crate.get()).toBe(Test.Start);
|
||||
});
|
||||
|
||||
it("does not call any transition handler", () => {
|
||||
expect(correctHandler).not.toBeCalled();
|
||||
expect(incorrectHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe("when setting a new value", () => {
|
||||
beforeEach(() => {
|
||||
crate.set(Test.T1);
|
||||
});
|
||||
|
||||
it("calls the associated transition handler", () => {
|
||||
expect(correctHandler).toBeCalled();
|
||||
});
|
||||
|
||||
it("does not call any other transition handler", () => {
|
||||
expect(incorrectHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("new value is available", () => {
|
||||
expect(crate.get()).toBe(Test.T1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import openLinkInBrowserInjectable from "./open-link-in-browser.injectable";
|
||||
|
||||
export default getGlobalOverride(openLinkInBrowserInjectable, () => async () => {});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { shell } from "electron";
|
||||
|
||||
const allowedProtocols = new Set(["http:", "https:"]);
|
||||
|
||||
export type OpenLinkInBrowser = (url: string) => Promise<void>;
|
||||
|
||||
const openLinkInBrowserInjectable = getInjectable({
|
||||
id: "open-link-in-browser",
|
||||
instantiate: (): OpenLinkInBrowser => (
|
||||
async (url) => {
|
||||
const { protocol } = new URL(url);
|
||||
|
||||
if (!allowedProtocols.has(protocol)) {
|
||||
throw new TypeError("not an http(s) URL");
|
||||
}
|
||||
|
||||
await shell.openExternal(url);
|
||||
}
|
||||
),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default openLinkInBrowserInjectable;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import * as util from "util";
|
||||
import { execFile } from "child_process";
|
||||
|
||||
export const promiseExecFile = util.promisify(execFile);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../test-utils/get-global-override";
|
||||
import randomBytesInjectable from "./random-bytes.injectable";
|
||||
|
||||
export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
|
||||
const res = Buffer.alloc(size);
|
||||
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
res[i] = i;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 { randomBytes } from "crypto";
|
||||
import { promisify } from "util";
|
||||
|
||||
export type RandomBytes = (size: number) => Promise<Buffer>;
|
||||
|
||||
const randomBytesInjectable = getInjectable({
|
||||
id: "random-bytes",
|
||||
instantiate: (): RandomBytes => promisify(randomBytes),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default randomBytesInjectable;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import { render } from "@testing-library/react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, observe } from "mobx";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { advanceFakeTime, testUsingFakeTime } from "../../test-utils/use-fake-time";
|
||||
import { reactiveNow } from "./reactive-now";
|
||||
|
||||
describe("reactiveNow", () => {
|
||||
let someComputed: IComputedValue<boolean>;
|
||||
|
||||
beforeEach(() => {
|
||||
testUsingFakeTime("2015-10-21T07:28:00Z");
|
||||
|
||||
someComputed = computed(() => {
|
||||
const currentTimestamp = reactiveNow();
|
||||
|
||||
return currentTimestamp > new Date("2015-10-21T07:28:00Z").getTime();
|
||||
});
|
||||
});
|
||||
|
||||
describe("react-context", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
const TestComponent = observer(
|
||||
({ someComputed }: { someComputed: IComputedValue<boolean> }) => (
|
||||
<div>{someComputed.get() ? "true" : "false"}</div>
|
||||
),
|
||||
);
|
||||
|
||||
rendered = render(<TestComponent someComputed={someComputed} />);
|
||||
});
|
||||
|
||||
it("given time passes, works", () => {
|
||||
advanceFakeTime(1000);
|
||||
|
||||
expect(rendered.container.textContent).toBe("true");
|
||||
});
|
||||
|
||||
it("does not share the state from previous test", () => {
|
||||
expect(rendered.container.textContent).toBe("false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-react-context", () => {
|
||||
let actual: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
observe(someComputed, (changed) => {
|
||||
actual = changed.newValue as boolean;
|
||||
}, true);
|
||||
});
|
||||
|
||||
it("given time passes, works", () => {
|
||||
advanceFakeTime(1000);
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("does not share the state from previous test", () => {
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { _isComputingDerivation } from "mobx";
|
||||
import type { IResource } from "mobx-utils";
|
||||
import { fromResource } from "mobx-utils";
|
||||
|
||||
// Note: This file is copy-pasted from mobx-utils to fix very specific issue.
|
||||
// TODO: Remove this file once https://github.com/mobxjs/mobx-utils/issues/306 is fixed.
|
||||
const tickers: Record<number|string, IResource<number>> = {};
|
||||
|
||||
export function reactiveNow(interval?: number | "frame") {
|
||||
if (interval === void 0) { interval = 1000; }
|
||||
|
||||
if (!_isComputingDerivation()) {
|
||||
// See #40
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
// Note: This is the kludge until https://github.com/mobxjs/mobx-utils/issues/306 is fixed
|
||||
const synchronizationIsEnabled = !process.env.JEST_WORKER_ID;
|
||||
|
||||
if (!tickers[interval] || !synchronizationIsEnabled) {
|
||||
if (typeof interval === "number")
|
||||
tickers[interval] = createIntervalTicker(interval);
|
||||
else
|
||||
tickers[interval] = createAnimationFrameTicker();
|
||||
}
|
||||
|
||||
return tickers[interval].current();
|
||||
}
|
||||
|
||||
function createIntervalTicker(interval: number) {
|
||||
let subscriptionHandle: NodeJS.Timer;
|
||||
|
||||
return fromResource(function (sink) {
|
||||
sink(Date.now());
|
||||
subscriptionHandle = setInterval(function () { return sink(Date.now()); }, interval);
|
||||
}, function () {
|
||||
clearInterval(subscriptionHandle);
|
||||
}, Date.now());
|
||||
}
|
||||
|
||||
function createAnimationFrameTicker() {
|
||||
const frameBasedTicker = fromResource(function (sink) {
|
||||
sink(Date.now());
|
||||
|
||||
function scheduleTick() {
|
||||
window.requestAnimationFrame(function () {
|
||||
sink(Date.now());
|
||||
if (frameBasedTicker.isAlive())
|
||||
scheduleTick();
|
||||
});
|
||||
}
|
||||
scheduleTick();
|
||||
}, function () { }, Date.now());
|
||||
|
||||
return frameBasedTicker;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { Readable } from "readable-stream";
|
||||
import type { TypedArray } from "type-fest";
|
||||
|
||||
/**
|
||||
* ReadableWebToNodeStream
|
||||
*
|
||||
* Copied from https://github.com/Borewit/readable-web-to-node-stream
|
||||
*
|
||||
* Adds read error handler
|
||||
*
|
||||
* */
|
||||
export class ReadableWebToNodeStream<T extends TypedArray> extends Readable {
|
||||
|
||||
public bytesRead = 0;
|
||||
public released = false;
|
||||
|
||||
/**
|
||||
* Default web API stream reader
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader
|
||||
*/
|
||||
private reader: ReadableStreamDefaultReader<T>;
|
||||
private pendingRead?: Promise<ReadableStreamReadResult<T>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param stream ReadableStream: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
|
||||
*/
|
||||
constructor(stream: ReadableStream<T>) {
|
||||
super();
|
||||
this.reader = stream.getReader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of readable._read(size).
|
||||
* When readable._read() is called, if data is available from the resource,
|
||||
* the implementation should begin pushing that data into the read queue
|
||||
* https://nodejs.org/api/stream.html#stream_readable_read_size_1
|
||||
*/
|
||||
public async _read() {
|
||||
// Should start pushing data into the queue
|
||||
// Read data from the underlying Web-API-readable-stream
|
||||
if (this.released) {
|
||||
this.push(null); // Signal EOF
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.pendingRead = this.reader.read();
|
||||
const data = await this.pendingRead;
|
||||
|
||||
// clear the promise before pushing pushing new data to the queue and allow sequential calls to _read()
|
||||
delete this.pendingRead;
|
||||
|
||||
if (data.done || this.released) {
|
||||
this.push(null); // Signal EOF
|
||||
} else {
|
||||
this.bytesRead += data.value.length;
|
||||
this.push(data.value); // Push new data to the queue
|
||||
}
|
||||
} catch (error) {
|
||||
this.push(null); // Signal EOF
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is no unresolved read call to Web-API ReadableStream immediately returns;
|
||||
* otherwise will wait until the read is resolved.
|
||||
*/
|
||||
public async waitForReadToComplete() {
|
||||
if (this.pendingRead) {
|
||||
await this.pendingRead;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close wrapper
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
await this.syncAndRelease();
|
||||
}
|
||||
|
||||
private async syncAndRelease() {
|
||||
this.released = true;
|
||||
await this.waitForReadToComplete();
|
||||
await this.reader.releaseLock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ReadonlyDeep } from "type-fest";
|
||||
|
||||
export function readonly<T>(src: T): ReadonlyDeep<T> {
|
||||
return src as ReadonlyDeep<T>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { AbortSignal } from "abort-controller";
|
||||
|
||||
/**
|
||||
* Creates a new promise that will be rejected when the signal rejects.
|
||||
*
|
||||
* Useful for `Promise.race()` applications.
|
||||
* @param signal The AbortController's signal to reject with
|
||||
*/
|
||||
export function rejectPromiseBy(signal: AbortSignal): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
signal.addEventListener("abort", reject);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
|
||||
|
||||
export type ResolveSystemProxyChannel = RequestChannel<string, string>;
|
||||
|
||||
export const resolveSystemProxyChannel: ResolveSystemProxyChannel = {
|
||||
id: "resolve-system-proxy-channel",
|
||||
};
|
||||
+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 { getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
export type ResolveSystemProxy = (url: string) => Promise<string>;
|
||||
|
||||
export const resolveSystemProxyInjectionToken = getInjectionToken<ResolveSystemProxy>({
|
||||
id: "resolve-system-proxy",
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export interface StaticThis<T, R extends any[]> { new(...args: R): T }
|
||||
|
||||
/**
|
||||
* @deprecated This is a form of global shared state
|
||||
*/
|
||||
export class Singleton {
|
||||
private static readonly instances = new WeakMap<object, Singleton>();
|
||||
private static creating = "";
|
||||
|
||||
constructor() {
|
||||
if (Singleton.creating.length === 0) {
|
||||
throw new TypeError("A singleton class must be created by createInstance()");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the single instance of the child class if one was not already created.
|
||||
*
|
||||
* Multiple calls will return the same instance.
|
||||
* Essentially throwing away the arguments to the subsequent calls.
|
||||
*
|
||||
* Note: this is a racy function, if two (or more) calls are racing to call this function
|
||||
* only the first's arguments will be used.
|
||||
* @param this Implicit argument that is the child class type
|
||||
* @param args The constructor arguments for the child class
|
||||
* @returns An instance of the child class
|
||||
*/
|
||||
static createInstance<T extends Singleton, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||
if (!Singleton.instances.has(this)) {
|
||||
if (Singleton.creating.length > 0) {
|
||||
throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`);
|
||||
}
|
||||
|
||||
try {
|
||||
Singleton.creating = this.name;
|
||||
Singleton.instances.set(this, new this(...args));
|
||||
} finally {
|
||||
Singleton.creating = "";
|
||||
}
|
||||
}
|
||||
|
||||
return Singleton.instances.get(this) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of the child class that was previously created.
|
||||
* @param this Implicit argument that is the child class type
|
||||
* @param strict If false will return `undefined` instead of throwing when an instance doesn't exist.
|
||||
* Default: `true`
|
||||
* @returns An instance of the child class
|
||||
*/
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict?: true): T;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict: false): T | undefined;
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
|
||||
if (!Singleton.instances.has(this) && strict) {
|
||||
throw new TypeError(`instance of ${this.name} is not created`);
|
||||
}
|
||||
|
||||
return Singleton.instances.get(this) as (T | undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the instance of the child class.
|
||||
*
|
||||
* Note: this doesn't prevent callers of `getInstance` from storing the result in a global.
|
||||
*
|
||||
* There is *no* way in JS or TS to prevent globals like that.
|
||||
*/
|
||||
static resetInstance() {
|
||||
Singleton.instances.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
export default Singleton;
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import semver, { coerce } from "semver";
|
||||
|
||||
export enum Ordering {
|
||||
LESS = -1,
|
||||
EQUAL = 0,
|
||||
GREATER = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* This function switches the direction of `ordering` if `direction` is `"desc"`
|
||||
* @param ordering The original ordering (assumed to be an "asc" ordering)
|
||||
* @param direction The new desired direction
|
||||
*/
|
||||
export function rectifyOrdering(ordering: Ordering, direction: "asc" | "desc"): Ordering {
|
||||
if (direction === "desc") {
|
||||
return -ordering;
|
||||
}
|
||||
|
||||
return ordering;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ascending sorting function
|
||||
* @param left An item from an array
|
||||
* @param right An item from an array
|
||||
* @returns The relative ordering in an ascending manner.
|
||||
* - Less if left < right
|
||||
* - Equal if left == right
|
||||
* - Greater if left > right
|
||||
*/
|
||||
export function sortCompare<T>(left: T, right: T): Ordering {
|
||||
if (left < right) {
|
||||
return Ordering.LESS;
|
||||
}
|
||||
|
||||
if (left === right) {
|
||||
return Ordering.EQUAL;
|
||||
}
|
||||
|
||||
return Ordering.GREATER;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function sorts of list of items that have what should be a semver version formated string
|
||||
* as the field `version` but if it is not loosely coercable to semver falls back to sorting them
|
||||
* alphanumerically
|
||||
*/
|
||||
export function sortBySemverVersion<T extends { version: string }>(versioneds: T[]): T[] {
|
||||
return versioneds
|
||||
.map(versioned => ({
|
||||
__version: coerce(versioned.version, { loose: true }),
|
||||
raw: versioned,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (left.__version && right.__version) {
|
||||
return semver.compare(right.__version, left.__version);
|
||||
}
|
||||
|
||||
if (!left.__version && right.__version) {
|
||||
return Ordering.GREATER;
|
||||
}
|
||||
|
||||
if (left.__version && !right.__version) {
|
||||
return Ordering.LESS;
|
||||
}
|
||||
|
||||
return sortCompare(left.raw.version, right.raw.version);
|
||||
})
|
||||
.map(({ raw }) => raw);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get an ordering function based on the function getter
|
||||
*/
|
||||
export function byValue<T>(getOrderValue: (src: T) => number): (left: T, right: T) => number {
|
||||
return (left, right) => {
|
||||
const leftValue = getOrderValue(left);
|
||||
const rightValue = getOrderValue(right);
|
||||
|
||||
return leftValue - rightValue;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function splits an array into two sub arrays on the first instance of
|
||||
* element (from the left). If the array does not contain the element. The
|
||||
* return value is defined to be `[array, [], false]`. If the element is in
|
||||
* the array then the return value is `[left, right, true]` where `left` is
|
||||
* the elements of `array` from `[0, index)` and `right` is `(index, length)`
|
||||
* @param array the full array to split into two sub-arrays
|
||||
* @param element the element in the middle of the array
|
||||
* @returns the left and right sub-arrays which when conjoined with `element`
|
||||
* is the same as `array`, and `true`
|
||||
*/
|
||||
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
||||
const index = array.indexOf(element);
|
||||
|
||||
if (index < 0) {
|
||||
return [array, [], false];
|
||||
}
|
||||
|
||||
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array into two parts based on the outcome of `condition`. If `true`
|
||||
* the value will be returned as part of the right array. If `false` then part of
|
||||
* the left array.
|
||||
* @param src the full array to bifurcate
|
||||
* @param condition the function to determine which set each is in
|
||||
*/
|
||||
export function bifurcateArray<T>(src: T[], condition: (item: T) => boolean): [falses: T[], trues: T[]] {
|
||||
const res: [T[], T[]] = [[], []];
|
||||
|
||||
for (const item of src) {
|
||||
res[+condition(item)].push(item);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { syncBoxChannel } from "./channels";
|
||||
import { getMessageChannelListenerInjectable } from "../channel/message-channel-listener-injection-token";
|
||||
import syncBoxStateInjectable from "./sync-box-state.injectable";
|
||||
|
||||
const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({
|
||||
id: "init",
|
||||
channel: syncBoxChannel,
|
||||
handler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value),
|
||||
});
|
||||
|
||||
export default syncBoxChannelListenerInjectable;
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { MessageChannel } from "../channel/message-channel-listener-injection-token";
|
||||
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
|
||||
|
||||
export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>;
|
||||
|
||||
export const syncBoxChannel: SyncBoxChannel = {
|
||||
id: "sync-box-channel",
|
||||
};
|
||||
|
||||
export type SyncBoxInitialValueChannel = RequestChannel<
|
||||
void,
|
||||
{ id: string; value: any }[]
|
||||
>;
|
||||
|
||||
export const syncBoxInitialValueChannel: SyncBoxInitialValueChannel = {
|
||||
id: "sync-box-initial-value-channel",
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { IObservableValue } from "mobx";
|
||||
import { computed } from "mobx";
|
||||
import { syncBoxChannel } from "./channels";
|
||||
import { sendMessageToChannelInjectionToken } from "../channel/message-to-channel-injection-token";
|
||||
import syncBoxStateInjectable from "./sync-box-state.injectable";
|
||||
import type { SyncBox } from "./sync-box-injection-token";
|
||||
import { toJS } from "../toJS";
|
||||
|
||||
const createSyncBoxInjectable = getInjectable({
|
||||
id: "create-sync-box",
|
||||
|
||||
instantiate: (di) => {
|
||||
const messageToChannel = di.inject(sendMessageToChannelInjectionToken);
|
||||
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
|
||||
|
||||
return <Value>(id: string, initialValue: Value): SyncBox<Value> => {
|
||||
const state = getSyncBoxState(id) as IObservableValue<Value>;
|
||||
|
||||
state.set(initialValue);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
value: computed(() => toJS(state.get())),
|
||||
|
||||
set: (value) => {
|
||||
state.set(value);
|
||||
|
||||
messageToChannel(syncBoxChannel, { id, value });
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createSyncBoxInjectable;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 { MessageChannelHandler } from "../channel/message-channel-listener-injection-token";
|
||||
import type { SyncBoxChannel } from "./channels";
|
||||
import syncBoxStateInjectable from "./sync-box-state.injectable";
|
||||
|
||||
const syncBoxChannelHandlerInjectable = getInjectable({
|
||||
id: "sync-box-channel-handler",
|
||||
instantiate: (di): MessageChannelHandler<SyncBoxChannel> => {
|
||||
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
|
||||
|
||||
return ({ id, value }) => getSyncBoxState(id)?.set(value);
|
||||
},
|
||||
});
|
||||
|
||||
export default syncBoxChannelHandlerInjectable;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
export interface SyncBox<Value> {
|
||||
id: string;
|
||||
value: IComputedValue<Value>;
|
||||
set: (value: Value) => void;
|
||||
}
|
||||
|
||||
export const syncBoxInjectionToken = getInjectionToken<SyncBox<any>>({
|
||||
id: "sync-box",
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { observable } from "mobx";
|
||||
|
||||
const syncBoxStateInjectable = getInjectable({
|
||||
id: "sync-box-state",
|
||||
|
||||
instantiate: () => observable.box(),
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, id: string) => id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default syncBoxStateInjectable;
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { observe, runInAction } from "mobx";
|
||||
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import createSyncBoxInjectable from "./create-sync-box.injectable";
|
||||
import type { SyncBox } from "./sync-box-injection-token";
|
||||
import { syncBoxInjectionToken } from "./sync-box-injection-token";
|
||||
|
||||
describe("sync-box", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationBuilder = getApplicationBuilder();
|
||||
|
||||
applicationBuilder.beforeApplicationStart(mainDi => {
|
||||
runInAction(() => {
|
||||
mainDi.register(someInjectable);
|
||||
});
|
||||
});
|
||||
|
||||
applicationBuilder.beforeWindowStart((windowDi) => {
|
||||
runInAction(() => {
|
||||
windowDi.register(someInjectable);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given application is started, when value is set in main", () => {
|
||||
let valueInMain: string;
|
||||
let syncBoxInMain: SyncBox<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.startHidden();
|
||||
|
||||
syncBoxInMain = applicationBuilder.mainDi.inject(someInjectable);
|
||||
|
||||
observe(syncBoxInMain.value, ({ newValue }) => {
|
||||
valueInMain = newValue as string;
|
||||
}, true);
|
||||
|
||||
runInAction(() => {
|
||||
syncBoxInMain.set("some-value-from-main");
|
||||
});
|
||||
});
|
||||
|
||||
it("knows value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when window starts", () => {
|
||||
let valueInRenderer: string;
|
||||
let syncBoxInRenderer: SyncBox<string>;
|
||||
let rendererDi: DiContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
const applicationWindow =
|
||||
applicationBuilder.applicationWindow.create("some-window-id");
|
||||
|
||||
await applicationWindow.start();
|
||||
|
||||
rendererDi = applicationWindow.di;
|
||||
|
||||
syncBoxInRenderer = rendererDi.inject(someInjectable);
|
||||
|
||||
observe(syncBoxInRenderer.value, ({ newValue }) => {
|
||||
valueInRenderer = newValue as string;
|
||||
}, true);
|
||||
});
|
||||
|
||||
it("has the value from main", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when value is set from renderer", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInRenderer.set("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-renderer");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when application starts with a window", () => {
|
||||
let valueInRenderer: string;
|
||||
let valueInMain: string;
|
||||
let syncBoxInMain: SyncBox<string>;
|
||||
let syncBoxInRenderer: SyncBox<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.render();
|
||||
|
||||
const applicationWindow = applicationBuilder.applicationWindow.only;
|
||||
|
||||
syncBoxInMain = applicationBuilder.mainDi.inject(someInjectable);
|
||||
syncBoxInRenderer = applicationWindow.di.inject(someInjectable);
|
||||
|
||||
observe(syncBoxInRenderer.value, ({ newValue }) => {
|
||||
valueInRenderer = newValue as string;
|
||||
}, true);
|
||||
|
||||
observe(syncBoxInMain.value, ({ newValue }) => {
|
||||
valueInMain = newValue as string;
|
||||
}, true);
|
||||
});
|
||||
|
||||
it("knows initial value in main", () => {
|
||||
expect(valueInMain).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
it("knows initial value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-initial-value");
|
||||
});
|
||||
|
||||
describe("when value is set from main", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInMain.set("some-value-from-main");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-main");
|
||||
});
|
||||
|
||||
describe("when value is set from renderer", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(() => {
|
||||
syncBoxInRenderer.set("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
|
||||
it("has value in main", () => {
|
||||
expect(valueInMain).toBe("some-value-from-renderer");
|
||||
});
|
||||
|
||||
it("has value in renderer", () => {
|
||||
expect(valueInRenderer).toBe("some-value-from-renderer");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const someInjectable = getInjectable({
|
||||
id: "some-injectable",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createSyncBox = di.inject(createSyncBoxInjectable);
|
||||
|
||||
return createSyncBox("some-sync-box", "some-initial-value");
|
||||
},
|
||||
|
||||
injectionToken: syncBoxInjectionToken,
|
||||
});
|
||||
Loaded 100 of 3321 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user