Merge remote-tracking branch 'origin/master' into issue-3498

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-02-28 15:15:09 -05:00
commit 9f4f4ec108
70 files changed
+1713 -639

No files matched your search

+6 -6
View File
@@ -57,7 +57,7 @@
"test:unit": "jest --testPathIgnorePatterns integration",
"test:watch": "func() { jest ${1} --watch --testPathIgnorePatterns integration; }; func",
"lint": "PROD=true eslint --ext js,ts,tsx --max-warnings=0 .",
"lint:fix": "npm run lint --fix"
"lint:fix": "npm run lint -- --fix"
},
"config": {
"k8sProxyVersion": "0.3.0",
@@ -130,11 +130,11 @@
"@k8slens/node-fetch": "^6.4.0-beta.13",
"@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "^12.0.1",
"@ogre-tools/injectable": "^12.0.1",
"@ogre-tools/injectable-extension-for-auto-registration": "^12.0.1",
"@ogre-tools/injectable-extension-for-mobx": "^12.0.1",
"@ogre-tools/injectable-react": "^12.0.1",
"@ogre-tools/fp": "^15.1.1",
"@ogre-tools/injectable": "^15.1.1",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.1",
"@ogre-tools/injectable-extension-for-mobx": "^15.1.1",
"@ogre-tools/injectable-react": "^15.1.1",
"@sentry/electron": "^3.0.8",
"@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.1.0",
@@ -74,8 +74,7 @@ describe("cluster-store", () => {
di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync = di.inject(writeJsonSyncInjectable);
writeFileSync = di.inject(writeFileSyncInjectable);
writeBufferSync = di.inject(writeBufferSyncInjectable);
@@ -85,6 +84,9 @@ describe("cluster-store", () => {
describe("empty config", () => {
beforeEach(async () => {
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {});
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
@@ -198,6 +200,10 @@ describe("cluster-store", () => {
},
],
});
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
@@ -249,6 +255,10 @@ describe("cluster-store", () => {
},
],
});
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
@@ -262,6 +272,11 @@ describe("cluster-store", () => {
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
di.override(clusterStoreMigrationVersionInjectable, () => "3.6.0");
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
__internal__: {
migrations: {
@@ -281,8 +296,6 @@ describe("cluster-store", () => {
});
writeBufferSync("/some-directory-for-user-data/icon_path", testDataIcon);
di.override(clusterStoreMigrationVersionInjectable, () => "3.6.0");
clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
});
@@ -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 { DiContainer } from "@ogre-tools/injectable";
import kubectlApplyAllInjectable from "../../main/kubectl/kubectl-apply-all.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { KubernetesCluster } from "../catalog-entities";
import readDirectoryInjectable from "../fs/read-directory.injectable";
import readFileInjectable from "../fs/read-file.injectable";
import createResourceStackInjectable from "../k8s/create-resource-stack.injectable";
import appPathsStateInjectable from "../app-paths/app-paths-state.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
describe("create resource stack tests", () => {
let di: DiContainer;
let cluster: KubernetesCluster;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
cluster = {
getId: () => "test-cluster",
} as any;
di.override(readDirectoryInjectable, () => () => Promise.resolve(["file1"]) as any);
di.override(readFileInjectable, () => () => Promise.resolve("filecontents"));
di.override(appPathsStateInjectable, () => ({
get: () => ({}),
}));
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
});
describe("kubectlApplyFolder", () => {
it("returns response", async () => {
di.override(kubectlApplyAllInjectable, () => () => Promise.resolve({
callWasSuccessful: true as const,
response: "success",
}));
const createResourceStack = di.inject(createResourceStackInjectable);
const resourceStack = createResourceStack(cluster, "test");
const response = await resourceStack.kubectlApplyFolder("/foo/bar");
expect(response).toEqual("success");
});
it("throws on error", async () => {
di.override(kubectlApplyAllInjectable, () => () => Promise.resolve({
callWasSuccessful: false as const,
error: "No permissions",
}));
const createResourceStack = di.inject(createResourceStackInjectable);
const resourceStack = createResourceStack(cluster, "test");
await expect(() => resourceStack.kubectlApplyFolder("/foo/bar")).rejects.toThrow("No permissions");
});
});
});
@@ -30,9 +30,9 @@ describe("user store tests", () => {
get: () => "latest" as const,
init: async () => {},
}));
await di.inject(defaultUpdateChannelInjectable).init();
userStore = di.inject(userStoreInjectable);
});
describe("for an empty config", () => {
@@ -42,6 +42,8 @@ describe("user store tests", () => {
writeJsonSync("/some-directory-for-user-data/lens-user-store.json", {});
writeJsonSync("/some-directory-for-user-data/kube_config", {});
userStore = di.inject(userStoreInjectable);
userStore.load();
});
@@ -90,6 +92,8 @@ describe("user store tests", () => {
di.override(userStoreMigrationVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable);
userStore.load();
});
@@ -4,7 +4,6 @@
*/
import type { DiContainer } from "@ogre-tools/injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
@@ -18,6 +17,7 @@ import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
import { KubeObjectStore } from "../kube-object.store";
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token";
// eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
@@ -43,7 +43,7 @@ describe("ApiManager", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
const createCluster = di.inject(createClusterInjectionToken);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
@@ -15,7 +15,6 @@ import setupAutoRegistrationInjectable from "../../../renderer/before-frame-star
import { createMockResponseFromString } from "../../../test-utils/mock-responses";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import apiManagerInjectable from "../api-manager/manager.injectable";
@@ -23,6 +22,7 @@ import type { DiContainer } from "@ogre-tools/injectable";
import ingressApiInjectable from "../endpoints/ingress.api.injectable";
import loggerInjectable from "../../logger.injectable";
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token";
describe("KubeApi", () => {
let fetchMock: AsyncFnMock<Fetch>;
@@ -39,7 +39,7 @@ describe("KubeApi", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
const createCluster = di.inject(createClusterInjectionToken);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
@@ -24,7 +24,6 @@ import setupAutoRegistrationInjectable from "../../../renderer/before-frame-star
import { createMockResponseFromStream, createMockResponseFromString } from "../../../test-utils/mock-responses";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
@@ -36,6 +35,7 @@ import namespaceApiInjectable from "../endpoints/namespace.api.injectable";
// NOTE: this is fine because we are testing something that only exported
// eslint-disable-next-line no-restricted-imports
import { PodsApi } from "../../../extensions/common-api/k8s-api";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token";
describe("createKubeApiForRemoteCluster", () => {
let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster;
@@ -48,7 +48,7 @@ describe("createKubeApiForRemoteCluster", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
const createCluster = di.inject(createClusterInjectionToken);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
@@ -154,7 +154,7 @@ describe("KubeApi", () => {
fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock);
const createCluster = di.inject(createClusterInjectable);
const createCluster = di.inject(createClusterInjectionToken);
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
di.override(hostedClusterInjectable, () => createCluster({
@@ -51,7 +51,7 @@ export class ResourceStack {
this.dependencies.logger.warn(`[RESOURCE-STACK]: failed to apply resources: ${result.error}`);
return "";
throw new Error(result.error);
}
/**
@@ -3,13 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { createLogger, format } from "winston";
import type { Logger } from "./logger";
import winstonLoggerInjectable from "./winston-logger.injectable";
import { loggerTransportInjectionToken } from "./logger/transports";
const loggerInjectable = getInjectable({
id: "logger",
instantiate: (di): Logger => {
const baseLogger = di.inject(winstonLoggerInjectable);
const baseLogger = createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
});
return {
debug: (message, ...data) => baseLogger.debug(message, ...data),
@@ -19,7 +19,10 @@ 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";
import {
getRequestChannelListenerInjectable,
requestChannelListenerInjectionToken,
} from "../../../main/utils/channel/channel-listeners/listener-tokens";
type TestMessageChannel = MessageChannel<string>;
type TestRequestChannel = RequestChannel<string, string>;
@@ -199,21 +202,32 @@ describe("channel", () => {
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",
const someChannelListenerInjectable = getInjectable({
id: "some-channel-listener",
instantiate: () => ({
channel: testRequestChannel,
handler: () => () => "irrelevant",
}),
injectionToken: requestChannelListenerInjectionToken,
});
testChannelListenerInMain2Injectable.id += "2";
const someOtherChannelListenerInjectable = getInjectable({
id: "some-other-channel-listener",
instantiate: () => ({
channel: testRequestChannel,
handler: () => () => "irrelevant",
}),
injectionToken: requestChannelListenerInjectionToken,
});
applicationBuilder.beforeApplicationStart((mainDi) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testChannelListenerInMain2Injectable);
mainDi.register(someChannelListenerInjectable);
mainDi.register(someOtherChannelListenerInjectable);
});
});
@@ -19,7 +19,7 @@ const withOrphanPromiseInjectable = getInjectable({
toBeDecorated,
withErrorLoggingFor(() => "Orphan promise rejection encountered"),
withErrorSuppression,
);
) as ((...args: any[]) => any);
decorated(...args);
};
+1 -1
View File
@@ -18,6 +18,6 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
// Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
export const slackUrl = "https://k8slens.dev/slack.html" as string;
export const supportUrl = "https://docs.k8slens.dev/support/" as string;
export const docsUrl = "https://docs.k8slens.dev" as string;
export const forumsUrl = "https://forums.k8slens.dev" as string;
@@ -1,20 +0,0 @@
/**
* 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 { createLogger, format } from "winston";
import { loggerTransportInjectionToken } from "./logger/transports";
const winstonLoggerInjectable = getInjectable({
id: "winston-logger",
instantiate: (di) => createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
}),
});
export default winstonLoggerInjectable;
@@ -11,7 +11,7 @@ import isWindowsInjectable from "../../common/vars/is-windows.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
import { slackUrl, issuesTrackerUrl } from "../../common/vars";
import { issuesTrackerUrl } from "../../common/vars";
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
@@ -53,6 +53,9 @@ export const App = {
return di.inject(isLinuxInjectable);
},
slackUrl,
/**
* @deprecated This value is now `""` and is left here for backwards compatability.
*/
slackUrl: "",
issuesTrackerUrl,
} as const;
@@ -107,11 +107,11 @@ exports[`extension special characters in page registrations renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -107,11 +107,11 @@ exports[`navigate to extension page renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -107,11 +107,11 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`installing update when started renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -413,11 +413,11 @@ exports[`installing update when started when user checks for updates renders 1`]
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -718,11 +718,11 @@ exports[`installing update when started when user checks for updates when new up
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1048,11 +1048,11 @@ exports[`installing update when started when user checks for updates when new up
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1378,11 +1378,11 @@ exports[`installing update when started when user checks for updates when new up
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1683,11 +1683,11 @@ exports[`installing update when started when user checks for updates when no new
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -133,11 +133,11 @@ exports[`encourage user to update when sufficient time passed since update was d
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -438,11 +438,11 @@ exports[`encourage user to update when sufficient time passed since update was d
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`installing update using tray when started renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -413,11 +413,11 @@ exports[`installing update using tray when started when user checks for updates
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -718,11 +718,11 @@ exports[`installing update using tray when started when user checks for updates
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1048,11 +1048,11 @@ exports[`installing update using tray when started when user checks for updates
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1378,11 +1378,11 @@ exports[`installing update using tray when started when user checks for updates
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1683,11 +1683,11 @@ exports[`installing update using tray when started when user checks for updates
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -133,11 +133,11 @@ exports[`force user to update when too long since update was downloaded when app
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -463,11 +463,11 @@ exports[`force user to update when too long since update was downloaded when app
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -840,11 +840,11 @@ exports[`force user to update when too long since update was downloaded when app
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`periodical checking of updates given updater is enabled and configurati
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`selection of update stability when started renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`opening catalog entity details panel renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -199,11 +199,11 @@ exports[`Command Pallet: keyboard shortcut tests when on linux renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -595,11 +595,11 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing ESC
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -991,11 +991,11 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHI
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1531,11 +1531,11 @@ exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHI
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1836,11 +1836,11 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -2141,11 +2141,11 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing ESC
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -2446,11 +2446,11 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHI
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -2847,11 +2847,11 @@ exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHI
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`Showing correct entity settings renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -894,7 +894,40 @@ exports[`Showing correct entity settings when navigating to non-local cluster en
>
Proxy
</h2>
<section />
<section>
<section>
<div
class="SubTitle"
id="http-proxy"
>
HTTP Proxy
</div>
<div
class="Input theme round black"
>
<label
class="input-area flex gaps align-center"
id=""
>
<input
class="input box grow"
placeholder="http://<address>:<port>"
spellcheck="false"
value=""
/>
</label>
<div
class="input-info flex gaps"
/>
</div>
<small
class="hint"
>
HTTP Proxy server. Used for communicating with Kubernetes API.
</small>
</section>
</section>
</section>
</div>
<div
@@ -107,11 +107,11 @@ exports[`extensions - navigation using application menu renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -107,11 +107,11 @@ exports[`preferences - navigation using application menu renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`show-about-using-tray renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`status-bar-items-originating-from-extensions when application starts wh
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -108,11 +108,11 @@ exports[`extendability-using-extension-api given an extension with a weakly type
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -422,11 +422,11 @@ exports[`extendability-using-extension-api given an extension with top-bar items
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -727,11 +727,11 @@ exports[`extendability-using-extension-api given an extension with top-bar items
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1032,11 +1032,11 @@ exports[`extendability-using-extension-api renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -107,11 +107,11 @@ exports[`welcome - navigation using application menu renders 1`] = `
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -1182,11 +1182,11 @@ exports[`welcome - navigation using application menu when navigated somewhere el
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -16,7 +16,8 @@ export function registerInjectables(di: DiContainer) {
autoRegister({
di,
requireContexts: [
targetModule: module,
getRequireContexts: () => [
require.context("./", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../extensions", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../common", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
@@ -10,8 +10,8 @@ export const lensWebsiteLinkName = "Lens Website";
export const lensDocumentationWeblinkId = "lens-documentation-link";
export const lensDocumentationWeblinkName = "Lens Documentation";
export const lensSlackWeblinkId = "lens-slack-link";
export const lensSlackWeblinkName = "Lens Community Slack";
export const lensForumsWeblinkId = "lens-forums-link";
export const lensForumsWeblinkName = "Lens Forums";
export const lensTwitterWeblinkId = "lens-twitter-link";
export const lensTwitterWeblinkName = "Lens on Twitter";
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, slackUrl } from "../../../common/vars";
import { docsUrl, forumsUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks-store/weblink-store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinkStoreMigrationInjectionToken } from "../../../common/weblinks-store/migration-token";
@@ -20,7 +20,7 @@ const v514WeblinkStoreMigrationInjectable = getInjectable({
weblinks.push(
{ id: "https://k8slens.dev", name: links.lensWebsiteLinkName, url: "https://k8slens.dev" },
{ id: docsUrl, name: links.lensDocumentationWeblinkName, url: docsUrl },
{ id: slackUrl, name: links.lensSlackWeblinkName, url: slackUrl },
{ id: forumsUrl, name: links.lensForumsWeblinkName, url: forumsUrl },
{ id: "https://twitter.com/k8slens", name: links.lensTwitterWeblinkName, url: "https://twitter.com/k8slens" },
{ id: "https://medium.com/k8slens", name: links.lensBlogWeblinkName, url: "https://medium.com/k8slens" },
{ id: "https://kubernetes.io/docs/home/", name: links.kubernetesDocumentationWeblinkName, url: "https://kubernetes.io/docs/home/" },
@@ -16,40 +16,40 @@ const v545Beta1WeblinkStoreMigrationInjectable = getInjectable({
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const lensWebsiteLink = weblinks.find(weblink => weblink.name === links.lensWebsiteLinkName);
const lensWebsite = weblinks.find(weblink => weblink.name === links.lensWebsiteLinkName);
if (lensWebsiteLink) {
lensWebsiteLink.id = links.lensWebsiteWeblinkId;
if (lensWebsite) {
lensWebsite.id = links.lensWebsiteWeblinkId;
}
const lensDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === links.lensDocumentationWeblinkName);
const lensDocumentationWeblink = weblinks.find(weblink => weblink.name === links.lensDocumentationWeblinkName);
if (lensDocumentationWeblinkLink) {
lensDocumentationWeblinkLink.id = links.lensDocumentationWeblinkId;
if (lensDocumentationWeblink) {
lensDocumentationWeblink.id = links.lensDocumentationWeblinkId;
}
const lensSlackWeblinkLink = weblinks.find(weblink => weblink.name === links.lensSlackWeblinkName);
const lensForumsWeblink = weblinks.find(weblink => weblink.name === links.lensForumsWeblinkName);
if (lensSlackWeblinkLink) {
lensSlackWeblinkLink.id = links.lensSlackWeblinkId;
if (lensForumsWeblink) {
lensForumsWeblink.id = links.lensForumsWeblinkId;
}
const lensTwitterWeblinkLink = weblinks.find(weblink => weblink.name === links.lensTwitterWeblinkName);
const lensTwitterWeblink = weblinks.find(weblink => weblink.name === links.lensTwitterWeblinkName);
if (lensTwitterWeblinkLink) {
lensTwitterWeblinkLink.id = links.lensTwitterWeblinkId;
if (lensTwitterWeblink) {
lensTwitterWeblink.id = links.lensTwitterWeblinkId;
}
const lensBlogWeblinkLink = weblinks.find(weblink => weblink.name === links.lensBlogWeblinkName);
const lensBlogWeblink = weblinks.find(weblink => weblink.name === links.lensBlogWeblinkName);
if (lensBlogWeblinkLink) {
lensBlogWeblinkLink.id = links.lensBlogWeblinkId;
if (lensBlogWeblink) {
lensBlogWeblink.id = links.lensBlogWeblinkId;
}
const kubernetesDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === links.kubernetesDocumentationWeblinkName);
const kubernetesDocumentationWeblink = weblinks.find(weblink => weblink.name === links.kubernetesDocumentationWeblinkName);
if (kubernetesDocumentationWeblinkLink) {
kubernetesDocumentationWeblinkLink.id = links.kubernetesDocumentationWeblinkId;
if (kubernetesDocumentationWeblink) {
kubernetesDocumentationWeblink.id = links.kubernetesDocumentationWeblinkId;
}
store.set("weblinks", weblinks);
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, slackUrl } from "../../../common/vars";
import { docsUrl, forumsUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks-store/weblink-store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinkStoreMigrationInjectionToken } from "../../../common/weblinks-store/migration-token";
import { lensDocumentationWeblinkId, lensSlackWeblinkId } from "../links";
import { lensDocumentationWeblinkId, lensForumsWeblinkId } from "../links";
import { applicationInformationToken } from "@k8slens/application";
const currentVersionWeblinkStoreMigrationInjectable = getInjectable({
@@ -20,10 +20,10 @@ const currentVersionWeblinkStoreMigrationInjectable = getInjectable({
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const slackWeblink = weblinks.find(weblink => weblink.id === lensSlackWeblinkId);
const forumsWeblink = weblinks.find(weblink => weblink.id === lensForumsWeblinkId);
if (slackWeblink) {
slackWeblink.url = slackUrl;
if (forumsWeblink) {
forumsWeblink.url = forumsUrl;
}
const docsWeblink = weblinks.find(weblink => weblink.id === lensDocumentationWeblinkId);
@@ -32,7 +32,9 @@ const currentVersionWeblinkStoreMigrationInjectable = getInjectable({
docsWeblink.url = docsUrl;
}
store.set("weblinks", weblinks);
const removedSlackLink = weblinks.filter(weblink => weblink.id !== "lens-slack-link");
store.set("weblinks", removedSlackLink);
},
};
},
+1 -3
View File
@@ -46,9 +46,7 @@ export async function bootstrap(di: DiContainer) {
}
try {
await initializeApp(() => {
unmountComponentAtNode(rootElem);
});
await initializeApp(() => unmountComponentAtNode(rootElem));
} catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href,
@@ -10,7 +10,7 @@ import type { IComputedValue } from "mobx";
import type { CarouselProps } from "react-material-ui-carousel";
import LegacyCarousel from "react-material-ui-carousel";
import { Icon } from "../icon";
import { slackUrl } from "../../../common/vars";
import { forumsUrl } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable";
import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration";
@@ -93,12 +93,12 @@ const NonInjectedWelcome = observer(({
<br />
{"If you have any questions or feedback, please join our "}
<a
href={slackUrl}
href={forumsUrl}
target="_blank"
rel="noreferrer"
className="link"
>
Lens Community slack channel
Lens Forums
</a>
.
</p>
@@ -9,7 +9,7 @@ import type { ErrorInfo } from "react";
import React from "react";
import { observer } from "mobx-react";
import { Button } from "../button";
import { issuesTrackerUrl, slackUrl } from "../../../common/vars";
import { issuesTrackerUrl, forumsUrl } from "../../../common/vars";
import type { SingleOrMany } from "../../utils";
import type { ObservableHistory } from "mobx-observable-history";
import { withInjectables } from "@ogre-tools/injectable-react";
@@ -53,15 +53,15 @@ class NonInjectedErrorBoundary extends React.Component<ErrorBoundaryProps & Depe
</h5>
<p>
{"To help us improve the product please report bugs to "}
{"To help us improve the product please report bugs on"}
<a
href={slackUrl}
href={forumsUrl}
rel="noreferrer"
target="_blank"
>
Slack
Lens Forums
</a>
{" community or "}
{" or on our"}
<a
href={issuesTrackerUrl}
rel="noreferrer"
@@ -19,8 +19,8 @@ import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-create
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import type { PodStore } from "../+workloads-pods/store";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
describe("kube-object-list-layout", () => {
let di: DiContainer;
@@ -34,7 +34,7 @@ describe("kube-object-list-layout", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
const createCluster = di.inject(createClusterInjectionToken);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
@@ -12,7 +12,6 @@ import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.
import loadExtensionsInjectable from "../../load-extensions.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable";
import closeRendererLogFileInjectable from "../../../logger/close-renderer-log-file.injectable";
const initClusterFrameInjectable = getInjectable({
id: "init-cluster-frame",
@@ -30,7 +29,6 @@ const initClusterFrameInjectable = getInjectable({
emitAppEvent: di.inject(emitAppEventInjectable),
logger: di.inject(loggerInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
closeFileLogging: di.inject(closeRendererLogFileInjectable),
});
},
});
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { once } from "lodash";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
import type { ShowNotification } from "../../../components/notifications";
@@ -19,7 +18,6 @@ interface Dependencies {
emitAppEvent: EmitAppEvent;
logger: Logger;
showErrorNotification: ShowNotification;
closeFileLogging: () => void;
}
const logPrefix = "[CLUSTER-FRAME]:";
@@ -32,7 +30,6 @@ export const initClusterFrame = ({
emitAppEvent,
logger,
showErrorNotification,
closeFileLogging,
}: Dependencies) =>
async (unmountRoot: () => void) => {
await requestSetClusterFrameId(hostedCluster.id);
@@ -72,14 +69,11 @@ export const initClusterFrame = ({
});
});
const onCloseFrame = once(() => {
window.onbeforeunload = () => {
logger.info(
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
);
closeFileLogging();
unmountRoot();
});
window.addEventListener("beforeunload", onCloseFrame);
window.addEventListener("pagehide", onCloseFrame);
unmountRoot();
};
};
@@ -12,7 +12,6 @@ import loggerInjectable from "../../../common/logger.injectable";
import { delay } from "../../../common/utils";
import { broadcastMessage } from "../../../common/ipc";
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable";
const initRootFrameInjectable = getInjectable({
id: "init-root-frame",
@@ -23,7 +22,6 @@ const initRootFrameInjectable = getInjectable({
const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable);
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
const logger = di.inject(loggerInjectable);
const closeRendererLogFile = di.inject(closeRendererLogFileInjectable);
return async (unmountRoot: () => void) => {
try {
@@ -57,7 +55,7 @@ const initRootFrameInjectable = getInjectable({
window.addEventListener("beforeunload", () => {
logger.info("[ROOT-FRAME]: Unload app");
closeRendererLogFile();
unmountRoot();
});
};
@@ -20,6 +20,7 @@ import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injecta
import type { GlobalOverride } from "../common/test-utils/get-global-override";
import nodeEnvInjectionToken from "../common/vars/node-env-injection-token";
import { applicationInformationFakeInjectable } from "../common/vars/application-information-fake-injectable";
import { registerInjectableReact } from "@ogre-tools/injectable-react";
export const getDiForUnitTesting = (
opts: { doGeneralOverrides?: boolean } = {},
@@ -46,6 +47,7 @@ export const getDiForUnitTesting = (
) as Injectable<any, any, any>[];
registerMobX(di);
registerInjectableReact(di);
runInAction(() => {
di.register(applicationInformationFakeInjectable);
@@ -1,22 +0,0 @@
/**
* 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 winstonLoggerInjectable from "../../common/winston-logger.injectable";
import rendererFileLoggerTransportInjectable from "./file-transport.injectable";
const closeRendererLogFileInjectable = getInjectable({
id: "close-renderer-log-file",
instantiate: (di) => {
const winstonLogger = di.inject(winstonLoggerInjectable);
const fileLoggingTransport = di.inject(rendererFileLoggerTransportInjectable);
return () => {
fileLoggingTransport.close?.();
winstonLogger.remove(fileLoggingTransport);
};
},
});
export default closeRendererLogFileInjectable;
@@ -1,44 +0,0 @@
/**
* 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 { transports } from "winston";
import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable";
import { loggerTransportInjectionToken } from "../../common/logger/transports";
import windowLocationInjectable from "../../common/k8s-api/window-location.injectable";
import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable";
import { getClusterIdFromHost } from "../utils";
const rendererFileLoggerTransportInjectable = getInjectable({
id: "renderer-file-logger-transport",
instantiate: (di) => {
let frameId: string;
const currentlyInClusterFrame = di.inject(
currentlyInClusterFrameInjectable,
);
if (currentlyInClusterFrame) {
const { host } = di.inject(windowLocationInjectable);
const clusterId = getClusterIdFromHost(host);
frameId = clusterId ? `cluster-${clusterId}` : "cluster";
} else {
frameId = "main";
}
return new transports.File({
handleExceptions: false,
level: "info",
filename: `lens-renderer-${frameId}.log`,
dirname: di.inject(directoryForLogsInjectable),
maxsize: 1024 * 1024,
maxFiles: 2,
tailable: true,
});
},
injectionToken: loggerTransportInjectionToken,
});
export default rendererFileLoggerTransportInjectable;
@@ -6,18 +6,22 @@
import type { DiContainer } from "@ogre-tools/injectable";
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import { registerInjectableReact } from "@ogre-tools/injectable-react";
import { runInAction } from "mobx";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export function registerInjectables(di: DiContainer) {
setLegacyGlobalDiForExtensionApi(di, Environments.renderer);
registerMobX(di);
registerInjectableReact(di);
runInAction(() => {
registerMobX(di);
autoRegister({
di,
requireContexts: [
targetModule: module,
getRequireContexts: () => [
require.context("./", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../common", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../extensions", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
+5 -5
View File
@@ -199,11 +199,11 @@
"@k8slens/core": "^6.4.0-beta.13",
"@k8slens/ensure-binaries": "^6.4.0-beta.13",
"@k8slens/generate-tray-icons": "^6.4.0-beta.13",
"@ogre-tools/fp": "^12.0.1",
"@ogre-tools/injectable": "^12.0.1",
"@ogre-tools/injectable-extension-for-auto-registration": "^12.0.1",
"@ogre-tools/injectable-extension-for-mobx": "^12.0.1",
"@ogre-tools/injectable-react": "^12.0.1",
"@ogre-tools/fp": "^15.1.1",
"@ogre-tools/injectable": "^15.1.1",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.1",
"@ogre-tools/injectable-extension-for-mobx": "^15.1.1",
"@ogre-tools/injectable-react": "^15.1.1",
"mobx": "^6.8.0",
"rimraf": "^4.1.2"
},
+2 -1
View File
@@ -13,7 +13,8 @@ runInAction(() => {
try {
autoRegister({
di,
requireContexts: [
targetModule: module,
getRequireContexts: () => [
require.context("./", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../common", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
],
+2 -1
View File
@@ -13,7 +13,8 @@ const app = createApp({
runInAction(() => {
autoRegister({
di,
requireContexts: [
targetModule: module,
getRequireContexts: () => [
require.context("./", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
require.context("../common", true, CONTEXT_MATCHER_FOR_NON_FEATURES),
],
+5 -6
View File
@@ -16,15 +16,14 @@
"devDependencies": {
"@swc/cli": "^0.1.61",
"@swc/core": "^1.3.35",
"@types/command-line-args": "^5.2.0",
"@types/fs-extra": "^11.0.1",
"@types/inquirer": "^9.0.3",
"@types/node": "^16.18.11",
"@types/semver": "^7.3.13",
"command-line-args": "^5.2.1",
"fs-extra": "^11.1.0",
"semver": "^7.3.8"
"rimraf": "^4.1.2"
},
"dependencies": {
"rimraf": "^4.1.2"
"chalk": "^5.2.0",
"inquirer": "^9.1.4",
"semver": "^7.3.8"
}
}
+267 -238
View File
@@ -3,122 +3,34 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import child_process from "child_process";
import commandLineArgs from "command-line-args";
import fse from "fs-extra";
import { basename } from "path";
import { createInterface } from "readline";
import assert from "assert";
import chalk from "chalk";
import child_process, { spawn } from "child_process";
import { readFile } from "fs/promises";
import inquirer from "inquirer";
import { createInterface, ReadLine } from "readline";
import semver from "semver";
import { promisify } from "util";
const {
SemVer,
valid: semverValid,
rcompare: semverRcompare,
lte: semverLte,
} = semver;
type SemVer = semver.SemVer;
const { SemVer } = semver;
const exec = promisify(child_process.exec);
const execFile = promisify(child_process.execFile);
const options = commandLineArgs([
{
name: "type",
defaultOption: true,
},
{
name: "preid",
},
{
name: "check-commits",
type: Boolean,
},
]);
async function pipeExecFile(file: string, args: string[], opts?: { stdin: string }) {
const p = execFile(file, args);
const validReleaseValues = [
"major",
"minor",
"patch",
];
const validPrereleaseValues = [
"premajor",
"preminor",
"prepatch",
"prerelease",
];
const validPreidValues = [
"alpha",
"beta",
];
p.child.stdout?.pipe(process.stdout);
p.child.stderr?.pipe(process.stderr);
const errorMessages = {
noReleaseType: `No release type provided. Valid options are: ${[...validReleaseValues, ...validPrereleaseValues].join(", ")}`,
invalidRelease: (invalid: string) => `Invalid release type was provided (value was "${invalid}"). Valid options are: ${[...validReleaseValues, ...validPrereleaseValues].join(", ")}`,
noPreid: `No preid was provided. Use '--preid' to specify. Valid options are: ${validPreidValues.join(", ")}`,
invalidPreid: (invalid: string) => `Invalid preid was provided (value was "${invalid}"). Valid options are: ${validPreidValues.join(", ")}`,
wrongCwd: "It looks like you are running this script from the 'scripts' directory. This script assumes it is run from the root of the git repo",
};
if (!options.type) {
console.error(errorMessages.noReleaseType);
process.exit(1);
}
if (validReleaseValues.includes(options.type)) {
// do nothing, is valid
} else if (validPrereleaseValues.includes(options.type)) {
if (!options.preid) {
console.error(errorMessages.noPreid);
process.exit(1);
if (opts) {
p.child.stdin?.end(opts.stdin);
}
if (!validPreidValues.includes(options.preid)) {
console.error(errorMessages.invalidPreid(options.preid));
process.exit(1);
}
} else {
console.error(errorMessages.invalidRelease(options.type));
process.exit(1);
await p;
}
if (basename(process.cwd()) === "scripts") {
console.error(errorMessages.wrongCwd);
}
const currentVersion = new SemVer((await fse.readJson("./lerna.json")).version);
console.log(`current version: ${currentVersion.format()}`);
const newVersion = currentVersion.inc(options.type, options.preid);
const newVersionMilestone = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
const prBranch = `release/v${newVersion.format()}`;
await exec(`npm run bump-version --yes ${newVersion.format()}`);
await exec(`git checkout -b ${prBranch}`);
await exec("git add lerna.json packages/*/package.json");
await exec(`git commit -sm "Release ${newVersion.format()}"`);
console.log(`new version: ${newVersion.format()}`);
console.log("fetching tags...");
await exec("git fetch --tags --force");
const actualTags = (await exec("git tag --list", { encoding: "utf-8" })).stdout.split(/\r?\n/).map(line => line.trim());
const [previousReleasedVersion] = actualTags
.map((value) => semverValid(value))
.filter((v): v is string => typeof v === "string")
.sort((l, r) => semverRcompare(l, r))
.filter(version => semverLte(version, currentVersion));
const getMergedPrsArgs = [
"gh",
"pr",
"list",
"--limit=500", // Should be big enough, if not we need to release more often ;)
"--state=merged",
"--base=master",
"--json mergeCommit,title,author,labels,number,milestone,mergedAt",
];
interface GithubPrData {
author: {
login: string;
@@ -147,168 +59,285 @@ interface ExtendedGithubPrData extends Omit<GithubPrData, "mergedAt"> {
mergedAt: Date;
}
console.log("retreiving last 500 PRs to create release PR body...");
const mergedPrs = JSON.parse((await exec(getMergedPrsArgs.join(" "), { encoding: "utf-8" })).stdout) as GithubPrData[];
const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === newVersionMilestone);
const relaventPrsQuery = await Promise.all(
milestoneRelevantPrs.map(async pr => ({
pr,
stdout: (await exec(`git tag v${previousReleasedVersion} --no-contains ${pr.mergeCommit.oid}`)).stdout,
})),
);
const relaventPrs = relaventPrsQuery
.filter(query => query.stdout)
.map(query => query.pr)
.filter(pr => pr.labels.every(label => label.name !== "skip-changelog"))
.map(pr => ({ ...pr, mergedAt: new Date(pr.mergedAt) } as ExtendedGithubPrData))
.sort((left, right) => {
const leftAge = left.mergedAt.valueOf();
const rightAge = right.mergedAt.valueOf();
async function getCurrentBranch(): Promise<string> {
return (await exec("git branch --show-current")).stdout.trim();
}
if (leftAge === rightAge) {
return 0;
}
async function getAbsolutePathToRepoRoot(): Promise<string> {
return (await exec("git rev-parse --show-toplevel")).stdout.trim();
}
if (leftAge > rightAge) {
return 1;
}
async function fetchAllGitTags(): Promise<string[]> {
await execFile("git", ["fetch", "--tags", "--force"]);
return -1;
const { stdout } = await exec("git tag --list", { encoding: "utf-8" });
return stdout
.split(/\r?\n/)
.map(line => line.trim());
}
function bumpPackageVersions() {
const bumpPackages = spawn("npm", ["run", "bump-version"], {
stdio: "inherit"
});
const cleaners: (() => void)[] = [
() => bumpPackages.stdout?.unpipe(),
() => bumpPackages.stderr?.unpipe(),
];
const cleanup = () => cleaners.forEach(clean => clean());
const enhancementPrLabelName = "enhancement";
const bugfixPrLabelName = "bug";
return new Promise<void>((resolve, reject) => {
const onExit = (code: number | null) => {
cleanup();
if (code) {
reject(new Error(`"npm run bump-version" failed with code ${code}`));
} else {
resolve();
}
};
const onError = (error: Error) => {
cleanup();
reject(error);
};
const isEnhancementPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === enhancementPrLabelName);
const isBugfixPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === bugfixPrLabelName);
bumpPackages.once("error", onError);
cleaners.push(() => bumpPackages.off("error", onError));
const prLines = {
enhancement: [] as string[],
bugfix: [] as string[],
maintenence: [] as string[],
};
bumpPackages.once("exit", onExit);
cleaners.push(() => bumpPackages.off("exit", onExit));
});
}
function getPrEntry(pr: ExtendedGithubPrData) {
function isDefined<T>(value: T | null | undefined): value is T {
return value != null;
}
function findClosestVersionTagLessThanVersion(tags: string[], version: SemVer): string {
const lessThanTags = tags
.map((value) => semver.parse(value))
.filter(isDefined)
.filter(version => !version.prerelease.includes("cron"))
.sort(semver.rcompare)
.filter(version => semver.lte(version, version));
assert(lessThanTags.length > 0, `Cannot find version tag less than ${version.format()}`);
return lessThanTags[0].format();
}
async function getCurrentVersionOfSubPackage(packageName: string): Promise<SemVer> {
const packageJson = JSON.parse(await readFile(`./packages/${packageName}/package.json`, "utf-8"));
return new SemVer(packageJson.version);
}
async function checkCurrentWorkingDirectory(): Promise<void> {
const repoRoot = await getAbsolutePathToRepoRoot();
if (process.cwd() !== repoRoot) {
console.error("It looks like you are running this script from the 'scripts' directory. This script assumes it is run from the root of the git repo");
process.exit(1);
}
}
function formatSemverForMilestone(version: SemVer): string {
return `${version.major}.${version.minor}.${version.patch}`;
}
async function createReleaseBranchAndCommit(prBase: string, version: SemVer, prBody: string): Promise<void> {
const prBranch = `release/v${version.format()}`;
await pipeExecFile("git", ["checkout", "-b", prBranch]);
await pipeExecFile("git", ["add", "lerna.json", "packages/*/package.json"]);
await pipeExecFile("git", ["commit", "-sm", `Release ${version.format()}`]);
await pipeExecFile("git", ["push", "--set-upstream", "origin", prBranch]);
await pipeExecFile("gh", [
"pr",
"create",
"--base", prBase,
"--title", `Release ${version.format()}`,
"--label", "skip-changelog",
"--label", "release",
"--milestone", formatSemverForMilestone(version),
"--body-file", "-",
], {
stdin: prBody,
});
}
function sortExtendedGithubPrData(left: ExtendedGithubPrData, right: ExtendedGithubPrData): number {
const leftAge = left.mergedAt.valueOf();
const rightAge = right.mergedAt.valueOf();
if (leftAge === rightAge) {
return 0;
}
if (leftAge > rightAge) {
return 1;
}
return -1;
}
async function getRelevantPRs(milestone: string, previousReleasedVersion: string): Promise<ExtendedGithubPrData[]> {
console.log("retrieving previous 200 PRs...");
const getMergedPrsArgs = [
"gh",
"pr",
"list",
"--limit=500", // Should be big enough, if not we need to release more often ;)
"--state=merged",
"--base=master",
"--json mergeCommit,title,author,labels,number,milestone,mergedAt",
];
const mergedPrs = JSON.parse((await exec(getMergedPrsArgs.join(" "), { encoding: "utf-8" })).stdout) as GithubPrData[];
const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === milestone);
const relevantPrsQuery = await Promise.all(
milestoneRelevantPrs.map(async pr => ({
pr,
stdout: (await exec(`git tag v${previousReleasedVersion} --no-contains ${pr.mergeCommit.oid}`)).stdout,
})),
);
return relevantPrsQuery
.filter(query => query.stdout)
.map(query => query.pr)
.filter(pr => pr.labels.every(label => label.name !== "skip-changelog"))
.map(pr => ({ ...pr, mergedAt: new Date(pr.mergedAt) } as ExtendedGithubPrData))
.sort(sortExtendedGithubPrData);
}
function formatPrEntry(pr: ExtendedGithubPrData) {
return `- ${pr.title} (**[#${pr.number}](https://github.com/lensapp/lens/pull/${pr.number})**) https://github.com/${pr.author.login}`;
}
const rl = createInterface(process.stdin);
const prBase = newVersion.patch === 0
? "master"
: `release/v${newVersion.major}.${newVersion.minor}`;
const isEnhancementPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === "enhancement");
const isBugfixPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === "bug");
function askQuestion(question: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
function _askQuestion() {
console.log(question);
const cherryPickCommitWith = (rl: ReadLine) => async (commit: string) => {
try {
await pipeExecFile("git", ["cherry-pick", commit]);
} catch {
console.error(chalk.bold("Please resolve conflicts in a separate terminal and then press enter here..."));
await new Promise<void>(resolve => rl.once("line", () => resolve()));
}
};
rl.once("line", (answer) => {
const cleaned = answer.trim().toLowerCase();
if (cleaned === "y") {
resolve(true);
} else if (cleaned === "n") {
resolve(false);
} else {
_askQuestion();
}
});
}
_askQuestion();
async function pickWhichPRsToUse(prs: ExtendedGithubPrData[]): Promise<ExtendedGithubPrData[]> {
const answers = await inquirer.prompt<{ commits: number[] }>({
type: "checkbox",
name: `commits`,
message: "Pick which commits to use...",
default: [],
choices: prs.map(pr => ({
checked: true,
key: pr.number,
name: `#${pr.number}: ${pr.title} (https://github.com/lensapp/lens/pull/${pr.number})`,
value: pr.number,
short: `#${pr.number}`,
})),
loop: false,
});
return prs.filter(pr => answers.commits.includes(pr.number));
}
async function handleRelaventPr(pr: ExtendedGithubPrData) {
if (options["check-commits"] && !(await askQuestion(`Would you like to use #${pr.number}: ${pr.title}? - Y/N`))) {
return;
}
function formatChangelog(previousReleasedVersion: string, prs: ExtendedGithubPrData[]): string {
const enhancementPrLines: string[] = [];
const bugPrLines: string[] = [];
const maintenancePrLines: string[] = [];
if (prBase !== "master") {
try {
const promise = exec(`git cherry-pick ${pr.mergeCommit.oid}`);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
promise.child.stdout!.pipe(process.stdout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
promise.child.stderr!.pipe(process.stderr);
await promise;
} catch {
console.error(`Failed to cherry-pick ${pr.mergeCommit.oid}, please resolve conflicts and then press enter here:`);
await new Promise<void>(resolve => rl.once("line", () => resolve()));
for (const pr of prs) {
if (isEnhancementPr(pr)) {
enhancementPrLines.push(formatPrEntry(pr));
} else if (isBugfixPr(pr)) {
bugPrLines.push(formatPrEntry(pr));
} else {
maintenancePrLines.push(formatPrEntry(pr));
}
}
if (isEnhancementPr(pr)) {
prLines.enhancement.push(getPrEntry(pr));
} else if (isBugfixPr(pr)) {
prLines.bugfix.push(getPrEntry(pr));
} else {
prLines.maintenence.push(getPrEntry(pr));
if (enhancementPrLines.length > 0) {
enhancementPrLines.unshift("## 🚀 Features", "");
enhancementPrLines.push("");
}
if (bugPrLines.length > 0) {
bugPrLines.unshift("## 🐛 Bug Fixes", "");
bugPrLines.push("");
}
if (maintenancePrLines.length > 0) {
maintenancePrLines.unshift("## 🧰 Maintenance", "");
maintenancePrLines.push("");
}
return [
`## Changes since ${previousReleasedVersion}`,
"",
...enhancementPrLines,
...bugPrLines,
...maintenancePrLines,
].join("\n");
}
for (const pr of relaventPrs) {
await handleRelaventPr(pr);
async function cherryPickCommits(prs: ExtendedGithubPrData[]): Promise<void> {
const rl = createInterface(process.stdin);
const cherryPickCommit = cherryPickCommitWith(rl);
for (const pr of prs) {
await cherryPickCommit(pr.mergeCommit.oid);
}
rl.close();
}
rl.close();
async function pickRelevantPrs(prs: ExtendedGithubPrData[], isMasterBranch: boolean): Promise<ExtendedGithubPrData[]> {
if (isMasterBranch) {
return prs;
}
const prBodyLines = [
`## Changes since ${previousReleasedVersion}`,
"",
...(
prLines.enhancement.length > 0
? [
"## 🚀 Features",
"",
...prLines.enhancement,
"",
]
: []
),
...(
prLines.bugfix.length > 0
? [
"## 🐛 Bug Fixes",
"",
...prLines.bugfix,
"",
]
: []
),
...(
prLines.maintenence.length > 0
? [
"## 🧰 Maintenance",
"",
...prLines.maintenence,
"",
]
: []
),
];
const prBody = prBodyLines.join("\n");
const createPrArgs = [
"pr",
"create",
"--base", prBase,
"--title", `Release ${newVersion.format()}`,
"--label", "skip-changelog",
"--label", "release",
"--milestone", `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`,
"--body-file", "-",
];
let selectedPrs: ExtendedGithubPrData[];
await exec(`git push --set-upstream origin ${prBranch}`);
do {
selectedPrs = await pickWhichPRsToUse(prs);
} while (selectedPrs.length === 0 && (console.warn("[WARNING]: must pick at least once commit"), true));
const createPrProcess = execFile("gh", createPrArgs);
await cherryPickCommits(selectedPrs);
createPrProcess.child.stdout?.pipe(process.stdout);
createPrProcess.child.stderr?.pipe(process.stderr);
return selectedPrs;
}
createPrProcess.child.stdin?.write(prBody);
createPrProcess.child.stdin?.end();
async function createRelease(): Promise<void> {
await checkCurrentWorkingDirectory();
await createPrProcess;
const currentK8slensCoreVersion = await getCurrentVersionOfSubPackage("core");
const prBase = await getCurrentBranch();
const isMasterBranch = prBase === "master";
const tags = await fetchAllGitTags();
const previousReleasedVersion = findClosestVersionTagLessThanVersion(tags, currentK8slensCoreVersion);
if (isMasterBranch) {
await bumpPackageVersions();
}
const prMilestone = formatSemverForMilestone(await getCurrentVersionOfSubPackage("core"));
const relevantPrs = await getRelevantPRs(prMilestone, previousReleasedVersion);
const selectedPrs = await pickRelevantPrs(relevantPrs, isMasterBranch);
const prBody = formatChangelog(previousReleasedVersion, selectedPrs);
if (!isMasterBranch) {
await bumpPackageVersions();
}
const newK8slensCoreVersion = await getCurrentVersionOfSubPackage("core");
await createReleaseBranchAndCommit(prBase, newK8slensCoreVersion, prBody);
}
await createRelease();
@@ -29,8 +29,8 @@
"test": "jest --coverage --runInBand"
},
"peerDependencies": {
"@ogre-tools/fp": "^12.0.1",
"@ogre-tools/injectable": "^12.0.1",
"@ogre-tools/fp": "^15.1.1",
"@ogre-tools/injectable": "^15.1.1",
"lodash": "^4.17.15"
}
}
@@ -0,0 +1,41 @@
# @k8slens/feature-core
Feature is set of injectables that are registered and deregistered simultaneously.
## Install
```bash
$ npm install @k8slens/feature-core
```
## Usage
```typescript
import { createContainer } from "@ogre-tools/injectable"
import { getFeature, registerFeature, deregisterFeature } from "@k8slens/feature-core"
// Notice that this Feature is usually exported from another NPM package.
const someFeature = getFeature({
id: "some-feature",
register: (di) => {
di.register(someInjectable, someOtherInjectable);
},
// Feature dependencies are automatically registered and
// deregistered when necessary.
dependencies: [someOtherFeature]
});
const di = createContainer("some-container");
registerFeature(di, someFeature);
// Or perhaps you want to deregister?
deregisterFeature(di, someFeature);
```
## Need to know
#### NPM packages exporting a Feature
- Prefer `peerDependencies` since they are installed from the application and are not allowed to be in the built bundle.
- Prefer exporting `injectionToken` instead of `injectable` for not allowing other features to access technical details like the `injectable`
@@ -0,0 +1,3 @@
export { getFeature } from "./src/feature";
export { registerFeature } from "./src/register-feature";
export type { Feature, GetFeatureArgs } from "./src/feature";
@@ -0,0 +1,2 @@
module.exports =
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
@@ -0,0 +1,30 @@
{
"name": "@k8slens/feature-core",
"private": false,
"version": "0.0.1",
"description": "Code that is common to all Features and those registering them.",
"type": "commonjs",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": {
"name": "OpenLens Authors",
"email": "info@k8slens.dev"
},
"license": "MIT",
"homepage": "https://github.com/lensapp/lens",
"scripts": {
"build": "webpack",
"dev": "webpack --mode=development --watch",
"test": "jest --coverage --runInBand"
},
"peerDependencies": {
"@ogre-tools/injectable": "^15.1.1"
}
}
@@ -0,0 +1,82 @@
import type { DiContainer } from "@ogre-tools/injectable";
import type { Feature } from "./feature";
import { featureContextMapInjectable } from "./feature-context-map-injectable";
export const deregisterFeature = (di: DiContainer, ...features: Feature[]) => {
features.forEach((feature) => {
deregisterFeatureRecursed(di, feature);
});
};
const deregisterFeatureRecursed = (
di: DiContainer,
feature: Feature,
dependedBy?: Feature
) => {
const featureContextMap = di.inject(featureContextMapInjectable);
const featureContext = featureContextMap.get(feature);
if (!featureContext) {
throw new Error(
`Tried to deregister feature "${feature.id}", but it was not registered.`
);
}
featureContext.numberOfRegistrations--;
const getDependingFeatures = getDependingFeaturesFor(featureContextMap);
const dependingFeatures = getDependingFeatures(feature);
if (!dependedBy && dependingFeatures.length) {
throw new Error(
`Tried to deregister Feature "${
feature.id
}", but it is the dependency of Features "${dependingFeatures.join(
", "
)}"`
);
}
if (dependedBy) {
const oldNumberOfDependents = featureContext.dependedBy.get(dependedBy)!;
const newNumberOfDependants = oldNumberOfDependents - 1;
featureContext.dependedBy.set(dependedBy, newNumberOfDependants);
if (newNumberOfDependants === 0) {
featureContext.dependedBy.delete(dependedBy);
}
}
if (featureContext.numberOfRegistrations === 0) {
featureContextMap.delete(feature);
featureContext.deregister();
}
feature.dependencies?.forEach((dependency) => {
deregisterFeatureRecursed(di, dependency, feature);
});
};
const getDependingFeaturesFor = (
featureContextMap: Map<Feature, { dependedBy: Map<Feature, number> }>
) => {
const getDependingFeaturesForRecursion = (
feature: Feature,
atRoot = true
): string[] => {
const context = featureContextMap.get(feature);
if (context?.dependedBy.size) {
return [...context!.dependedBy.entries()].flatMap(([dependant]) =>
getDependingFeaturesForRecursion(dependant, false)
);
}
return atRoot ? [] : [feature.id];
};
return getDependingFeaturesForRecursion;
};
@@ -0,0 +1,27 @@
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import type { Feature } from "./feature";
export type FeatureContextMap = Map<
Feature,
{
register: () => void;
deregister: () => void;
dependedBy: Map<Feature, number>;
numberOfRegistrations: number;
}
>;
export const featureContextMapInjectionToken =
getInjectionToken<FeatureContextMap>({
id: "feature-context-map-injection-token",
});
const featureContextMapInjectable = getInjectable({
id: "feature-store",
instantiate: (): FeatureContextMap => new Map(),
injectionToken: featureContextMapInjectionToken,
});
export { featureContextMapInjectable };
@@ -0,0 +1,281 @@
import {
createContainer,
DiContainer,
getInjectable,
Injectable,
} from "@ogre-tools/injectable";
import type { Feature } from "./feature";
import { registerFeature } from "./register-feature";
import { deregisterFeature } from "./deregister-feature";
import { getFeature } from "./feature" ;
describe("feature-dependencies", () => {
describe("given a parent Feature with another Features as dependency", () => {
let di: DiContainer;
let someInjectable: Injectable<string>;
let someInjectableInDependencyFeature: Injectable<string>;
let someParentFeature: Feature;
let someDependencyFeature: Feature;
beforeEach(() => {
di = createContainer("irrelevant");
someInjectable = getInjectable({
id: "some-injectable-2",
instantiate: () => "some-instance",
});
someInjectableInDependencyFeature = getInjectable({
id: "some-injectable",
instantiate: () => "some-instance-2",
});
someDependencyFeature = getFeature({
id: "some-dependency-feature",
register: (di) => di.register(someInjectableInDependencyFeature),
});
someParentFeature = getFeature({
id: "some-feature",
register: (di) => di.register(someInjectable),
dependencies: [someDependencyFeature],
});
registerFeature(di, someParentFeature);
});
it("when an injectable from the dependency Feature is injected, does so", () => {
const actual = di.inject(someInjectableInDependencyFeature);
expect(actual).toBe("some-instance-2");
});
it("when the dependency Feature is deregistered, throws", () => {
expect(() => {
deregisterFeature(di, someDependencyFeature);
}).toThrow(
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature"'
);
});
it("given the parent Feature is already deregistered, when also the dependency Feature is deregistered, throws", () => {
deregisterFeature(di, someParentFeature);
expect(() => {
deregisterFeature(di, someDependencyFeature);
}).toThrow(
'Tried to deregister feature "some-dependency-feature", but it was not registered.'
);
});
it("given the parent Feature is deregistered, when injecting an injectable from the dependency Feature, throws", () => {
deregisterFeature(di, someParentFeature);
expect(() => {
di.inject(someInjectableInDependencyFeature);
}).toThrow(
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable".'
);
});
});
describe("given a first Feature is registered, when second Feature using the first Feature as dependency gets registered", () => {
let di: DiContainer;
let someInjectable: Injectable<string>;
let someFeature2: Feature;
let someFeature1: Feature;
beforeEach(() => {
di = createContainer("irrelevant");
someInjectable = getInjectable({
id: "some-injectable",
instantiate: () => "some-instance",
});
someFeature1 = getFeature({
id: "some-feature-1",
register: (di) => di.register(someInjectable),
});
someFeature2 = getFeature({
id: "some-feature-2",
register: () => {},
dependencies: [someFeature1],
});
registerFeature(di, someFeature1, someFeature2);
});
it("when the first Feature is deregistered, throws", () => {
expect(() => {
deregisterFeature(di, someFeature1);
}).toThrow(
'Tried to deregister Feature "some-feature-1", but it is the dependency of Features "some-feature-2"'
);
});
it("given the second Feature is deregistered, when injecting an injectable from the first Feature, still does so", () => {
deregisterFeature(di, someFeature2);
const actual = di.inject(someInjectable);
expect(actual).toBe("some-instance");
});
});
describe("given parent Features with a shared Feature as dependency", () => {
let di: DiContainer;
let someInjectableInDependencyFeature: Injectable<string>;
let someFeature1: Feature;
let someFeature2: Feature;
let someSharedDependencyFeature: Feature;
beforeEach(() => {
di = createContainer("irrelevant");
someInjectableInDependencyFeature = getInjectable({
id: "some-injectable-in-dependency-feature",
instantiate: () => "some-instance",
});
someSharedDependencyFeature = getFeature({
id: "some-dependency-feature",
register: (di) => di.register(someInjectableInDependencyFeature),
});
const someFeatureForAdditionalHierarchy = getFeature({
id: "some-feature-for-additional-hierarchy",
register: () => {},
dependencies: [someSharedDependencyFeature],
});
someFeature1 = getFeature({
id: "some-feature-1",
register: () => {},
dependencies: [someFeatureForAdditionalHierarchy],
});
someFeature2 = getFeature({
id: "some-feature-2",
register: () => {},
dependencies: [someFeatureForAdditionalHierarchy],
});
registerFeature(di, someFeature1, someFeature2);
});
it("when the shared Feature is deregistered, throws", () => {
expect(() => {
deregisterFeature(di, someSharedDependencyFeature);
}).toThrow(
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature-1, some-feature-2"'
);
});
it("given only part of the parent Features get deregistered, when injecting an injectable from the shared Feature, does so", () => {
deregisterFeature(di, someFeature1);
const actual = di.inject(someInjectableInDependencyFeature);
expect(actual).toBe("some-instance");
});
it("given all of the parent Features get deregistered, when injecting an injectable from the shared Feature, throws", () => {
deregisterFeature(di, someFeature1, someFeature2);
expect(() => {
di.inject(someInjectableInDependencyFeature);
}).toThrow(
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".'
);
});
});
describe("given parent Features with a shared Feature as dependency and registered, when the shared Feature gets registered again", () => {
let di: DiContainer;
let someInjectableInDependencyFeature: Injectable<string>;
let someFeature1: Feature;
let someFeature2: Feature;
let someSharedDependencyFeature: Feature;
beforeEach(() => {
di = createContainer("irrelevant");
someInjectableInDependencyFeature = getInjectable({
id: "some-injectable-in-dependency-feature",
instantiate: () => "some-instance",
});
someSharedDependencyFeature = getFeature({
id: "some-dependency-feature",
register: (di) => di.register(someInjectableInDependencyFeature),
});
const someFeatureForAdditionalHierarchy = getFeature({
id: "some-feature-for-additional-hierarchy",
register: () => {},
dependencies: [someSharedDependencyFeature],
});
someFeature1 = getFeature({
id: "some-feature-1",
register: () => {},
dependencies: [someFeatureForAdditionalHierarchy],
});
someFeature2 = getFeature({
id: "some-feature-2",
register: () => {},
dependencies: [someFeatureForAdditionalHierarchy],
});
registerFeature(
di,
someFeature1,
someFeature2,
someSharedDependencyFeature
);
});
it("when the shared Feature is deregistered, throws", () => {
expect(() => {
deregisterFeature(di, someSharedDependencyFeature);
}).toThrow(
'Tried to deregister Feature "some-dependency-feature", but it is the dependency of Features "some-feature-1, some-feature-2"'
);
});
it("given only part of the parent Features get deregistered, when injecting an injectable from the shared Feature, does so", () => {
deregisterFeature(di, someFeature1);
const actual = di.inject(someInjectableInDependencyFeature);
expect(actual).toBe("some-instance");
});
it("given all of the parent Features get deregistered, when injecting an injectable from the shared Feature, still does so", () => {
deregisterFeature(di, someFeature1, someFeature2);
const actual = di.inject(someInjectableInDependencyFeature);
expect(actual).toBe("some-instance");
});
it("given all of the Features get deregistered, when injecting an injectable from the shared Feature, throws", () => {
deregisterFeature(
di,
someFeature1,
someFeature2,
someSharedDependencyFeature
);
expect(() => {
di.inject(someInjectableInDependencyFeature);
}).toThrow(
'Tried to inject non-registered injectable "irrelevant" -> "some-injectable-in-dependency-feature".'
);
});
});
});
@@ -0,0 +1,12 @@
import type { DiContainerForInjection } from "@ogre-tools/injectable";
export interface Feature {
id: string;
register: (di: DiContainerForInjection) => void;
dependencies?: Feature[];
}
export interface GetFeatureArgs extends Feature {}
export const getFeature = (getFeatureArgs: GetFeatureArgs): Feature =>
getFeatureArgs;
@@ -0,0 +1,90 @@
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { Feature } from "./feature";
import {
featureContextMapInjectable,
featureContextMapInjectionToken,
} from "./feature-context-map-injectable";
export const registerFeature = (di: DiContainer, ...features: Feature[]) => {
features.forEach((feature) => {
registerFeatureRecursed(di, feature);
});
};
const registerFeatureRecursed = (
di: DiContainer,
feature: Feature,
dependedBy?: Feature
) => {
const featureContextMaps = di.injectMany(featureContextMapInjectionToken);
if (featureContextMaps.length === 0) {
di.register(featureContextMapInjectable);
}
const featureContextMap = di.inject(featureContextMapInjectable);
const existingFeatureContext = featureContextMap.get(feature);
if (
!dependedBy &&
existingFeatureContext &&
existingFeatureContext.dependedBy.size === 0
) {
throw new Error(
`Tried to register feature "${feature.id}", but it was already registered.`
);
}
const featureContext =
existingFeatureContext || createFeatureContext(feature, di);
featureContext.numberOfRegistrations++;
if (dependedBy) {
const oldNumberOfDependents =
featureContext.dependedBy.get(dependedBy) || 0;
const newNumberOfDependants = oldNumberOfDependents + 1;
featureContext.dependedBy.set(dependedBy, newNumberOfDependants);
}
if (!existingFeatureContext) {
featureContext.register();
}
feature.dependencies?.forEach((dependency) => {
registerFeatureRecursed(di, dependency, feature);
});
};
const createFeatureContext = (feature: Feature, di: DiContainer) => {
const featureContextInjectable = getInjectable({
id: feature.id,
instantiate: (diForContextOfFeature) => ({
register: () => {
feature.register(diForContextOfFeature);
},
deregister: () => {
diForContextOfFeature.deregister(featureContextInjectable);
},
dependedBy: new Map<Feature, number>(),
numberOfRegistrations: 0,
}),
scope: true,
});
di.register(featureContextInjectable);
const featureContextMap = di.inject(featureContextMapInjectable);
const featureContext = di.inject(featureContextInjectable);
featureContextMap.set(feature, featureContext);
return featureContext;
};
@@ -0,0 +1,147 @@
import { registerFeature } from "./register-feature";
import {
createContainer,
DiContainer,
getInjectable,
Injectable,
} from "@ogre-tools/injectable";
import type { Feature } from "./feature";
import { getFeature } from "./feature";
import { deregisterFeature } from "./deregister-feature";
describe("register-feature", () => {
describe("given di-container and a Features with injectables, and given Features are registered", () => {
let di: DiContainer;
let someInjectable: Injectable<string>;
let someInjectable2: Injectable<string>;
let someFeature: Feature;
let someFeature2: Feature;
let instance: string;
beforeEach(() => {
di = createContainer("irrelevant");
someInjectable = getInjectable({
id: "some-injectable",
instantiate: () => "some-instance",
});
someInjectable2 = getInjectable({
id: "some-injectable-2",
instantiate: () => "some-instance-2",
});
someFeature = getFeature({
id: "some-feature-1",
register: (di) => di.register(someInjectable),
});
someFeature2 = getFeature({
id: "some-feature-2",
register: (di) => di.register(someInjectable2),
});
registerFeature(di, someFeature);
registerFeature(di, someFeature2);
});
it("when an injectable is injected, does so", () => {
instance = di.inject(someInjectable);
expect(instance).toBe("some-instance");
});
describe("given a Feature is deregistered", () => {
beforeEach(() => {
deregisterFeature(di, someFeature);
});
it("when injecting a related injectable, throws", () => {
expect(() => {
di.inject(someInjectable);
}).toThrow();
});
it("when injecting an unrelated injectable, does so", () => {
const instance = di.inject(someInjectable2);
expect(instance).toBe("some-instance-2");
});
describe("given the Feature is registered again", () => {
beforeEach(() => {
registerFeature(di, someFeature);
});
it("when injecting a related injectable, does so", () => {
const instance = di.inject(someInjectable);
expect(instance).toBe("some-instance");
});
it("when injecting an unrelated injectable, does so", () => {
const instance = di.inject(someInjectable2);
expect(instance).toBe("some-instance-2");
});
});
});
it("when a Feature is registered again, throws", () => {
expect(() => {
registerFeature(di, someFeature);
}).toThrow(
'Tried to register feature "some-feature-1", but it was already registered.'
);
});
it("given a Feature deregistered, when deregistered again, throws", () => {
deregisterFeature(di, someFeature);
expect(() => {
deregisterFeature(di, someFeature);
}).toThrow(
'Tried to deregister feature "some-feature-1", but it was not registered.'
);
});
});
it("given di-container and registered Features with injectables forming a cycle, when an injectable is injected, throws with namespaced error about cycle", () => {
const someInjectable: Injectable<any> = getInjectable({
id: "some-injectable-1",
instantiate: (di) => di.inject(someInjectable2),
});
const someInjectable2: Injectable<any> = getInjectable({
id: "some-injectable-2",
instantiate: (di) => di.inject(someInjectable),
});
const di = createContainer("some-container");
const someFeature = getFeature({
id: "some-feature-1",
register: (di) => {
di.register(someInjectable);
},
});
const someFeature2 = getFeature({
id: "some-feature-2",
register: (di) => {
di.register(someInjectable2);
},
});
registerFeature(di, someFeature, someFeature2);
expect(() => {
di.inject(someInjectable);
}).toThrow(
// 'Cycle of injectables encountered: "some-container" -> "some-feature-1:some-injectable-1" -> "some-feature-2:some-injectable-2" -> "some-feature-1:some-injectable-1"'
'Maximum call stack size exceeded'
);
});
});
@@ -0,0 +1,3 @@
{
"extends": "@k8slens/typescript/config/base.json"
}
@@ -0,0 +1 @@
module.exports = require("@k8slens/webpack").configForNode;