Replace static application window with ability to create many (#5979)

* Replace static application window with ability to create as many as you wish

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt tests for replacing static application window with ability to create as many as you wish and separate starting of main and window in behaviours

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make first render of application smaller in test that proves to be hard for CI

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen authored and GitHub committed 2022-08-16 15:55:42 +03:00
1 parent 299922c8a7
commit 63d5a34379
98 files changed
+3283 -2743

No files matched your search

@@ -13,10 +13,10 @@ import type { PreferenceNavigationItem } from "../preference-navigation-items.in
import preferenceNavigationItemsInjectable from "../preference-navigation-items.injectable";
describe.only("preferences - navigation block with links", () => {
let applicationBuilder: ApplicationBuilder;
let builder: ApplicationBuilder;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
builder = getApplicationBuilder();
});
describe("given in preferences, when rendered", () => {
@@ -24,17 +24,15 @@ describe.only("preferences - navigation block with links", () => {
describe("when general navigation items passed", () => {
beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
rendererDi.override(preferenceNavigationItemsInjectable, () =>
builder.beforeWindowStart((windowDi) => {
windowDi.override(preferenceNavigationItemsInjectable, () =>
computed(() => generalNavItems),
);
builder.preferences.navigate();
});
applicationBuilder.beforeRender(() => {
applicationBuilder.preferences.navigate();
});
renderer = await applicationBuilder.render();
renderer = await builder.render();
});
const links = ["General", "Proxy"];
@@ -52,17 +50,15 @@ describe.only("preferences - navigation block with links", () => {
describe("when general + extension navigation items passed", () => {
beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
rendererDi.override(preferenceNavigationItemsInjectable, () =>
builder.beforeWindowStart((windowDi) => {
windowDi.override(preferenceNavigationItemsInjectable, () =>
computed(() => [...generalNavItems, ...extensionNavItems]),
);
builder.preferences.navigate();
});
applicationBuilder.beforeRender(() => {
applicationBuilder.preferences.navigate();
});
renderer = await applicationBuilder.render();
renderer = await builder.render();
});
const generalLinks = ["General", "Proxy"];
@@ -6,13 +6,10 @@ import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node";
import { fireEvent } from "@testing-library/react";
import type { RenderResult } from "@testing-library/react";
import mockFs from "mock-fs";
import * as selectEvent from "react-select-event";
import type { CreateCluster } from "../../../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
import type { OpenDeleteClusterDialog } from "../open.injectable";
import openDeleteClusterDialogInjectable from "../open.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import createKubeconfigManagerInjectable from "../../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import type { ApplicationBuilder } from "../../test-utils/get-application-builder";
@@ -20,22 +17,8 @@ import { getApplicationBuilder } from "../../test-utils/get-application-builder"
import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable";
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
import type { OpenDeleteClusterDialog } from "../open.injectable";
import openDeleteClusterDialogInjectable from "../open.injectable";
const currentClusterServerUrl = "https://localhost";
const nonCurrentClusterServerUrl = "http://localhost";
@@ -87,65 +70,50 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
let config: KubeConfig;
describe("<DeleteClusterDialog />", () => {
let applicationBuilder: ApplicationBuilder;
let createCluster: CreateCluster;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let builder: ApplicationBuilder;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
beforeEach(() => {
builder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
mainDi.override(normalizedPlatformInjectable, () => "darwin");
rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true);
});
mockFs();
applicationBuilder.beforeRender(({ rendererDi }) => {
openDeleteClusterDialog = rendererDi.inject(openDeleteClusterDialogInjectable);
createCluster = rendererDi.inject(createClusterInjectionToken);
builder.beforeWindowStart((windowDi) => {
windowDi.override(storesAndApisCanBeCreatedInjectable, () => true);
});
});
afterEach(() => {
mockFs.restore();
});
it("shows context switcher when deleting current cluster", async () => {
const mockOpts = {
"temp-kube-config": multiClusterConfig,
};
const config = new KubeConfig();
mockFs(mockOpts);
config = new KubeConfig();
config.loadFromString(multiClusterConfig);
applicationBuilder.beforeRender(({ rendererDi }) => {
const createCluster = rendererDi.inject(createClusterInjectionToken);
const rendered = await builder.render();
const cluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
const windowDi = builder.applicationWindow.only.di;
openDeleteClusterDialog({ cluster, config });
const createCluster = windowDi.inject(createClusterInjectionToken);
const cluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
const rendered = await applicationBuilder.render();
const openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
openDeleteClusterDialog({ cluster, config });
const { getByText } = rendered;
@@ -158,21 +126,23 @@ describe("<DeleteClusterDialog />", () => {
expect(getByText("some-non-current-context")).toBeInTheDocument();
});
describe("Kubeconfig with different clusters", () => {
let rendered: RenderResult;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let config: KubeConfig;
beforeEach(async () => {
const mockOpts = {
"temp-kube-config": multiClusterConfig,
};
mockFs(mockOpts);
config = new KubeConfig();
config.loadFromString(multiClusterConfig);
rendered = await applicationBuilder.render();
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
createCluster = windowDi.inject(createClusterInjectionToken);
});
it("renders w/o errors", () => {
@@ -268,18 +238,21 @@ describe("<DeleteClusterDialog />", () => {
describe("Kubeconfig with single cluster", () => {
let rendered: RenderResult;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let config: KubeConfig;
beforeEach(async () => {
const mockOpts = {
"temp-kube-config": singleClusterConfig,
};
mockFs(mockOpts);
config = new KubeConfig();
config.loadFromString(singleClusterConfig);
rendered = await applicationBuilder.render();
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
createCluster = windowDi.inject(createClusterInjectionToken);
});
it("shows warning if no other contexts left", () => {
+4 -1
View File
@@ -14,6 +14,7 @@ import { cssNames, noop, stopPropagation } from "../../utils";
import type { ObservableHistory } from "mobx-observable-history";
import { withInjectables } from "@ogre-tools/injectable-react";
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
import requestAnimationFrameInjectable from "../animate/request-animation-frame.injectable";
// todo: refactor + handle animation-end in props.onClose()?
@@ -37,6 +38,7 @@ interface DialogState {
interface Dependencies {
navigation: ObservableHistory<unknown>;
requestAnimationFrame: (callback: () => void) => void;
}
@observer
@@ -95,7 +97,7 @@ class NonInjectedDialog extends React.PureComponent<DialogProps & Dependencies &
}
open() {
requestAnimationFrame(this.onOpen); // wait for render(), bind close-event to this.elem
this.props.requestAnimationFrame(this.onOpen); // wait for render(), bind close-event to this.elem
this.setState({ isOpen: true });
this.props.open?.();
}
@@ -182,5 +184,6 @@ export const Dialog = withInjectables<Dependencies, DialogProps>((props) => <Non
getProps: (di, props) => ({
...props,
navigation: di.inject(observableHistoryInjectable),
requestAnimationFrame: di.inject(requestAnimationFrameInjectable),
}),
});
@@ -7,55 +7,40 @@ import React from "react";
import "@testing-library/jest-dom/extend-expect";
import type { IObservableArray } from "mobx";
import { computed, observable } from "mobx";
import type { DiContainer } from "@ogre-tools/injectable";
import type { StatusBarItems } from "./status-bar-items.injectable";
import statusBarItemsInjectable from "./status-bar-items.injectable";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ApplicationBuilder } from "../test-utils/get-application-builder";
import { getApplicationBuilder } from "../test-utils/get-application-builder";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
class SomeTestExtension extends LensRendererExtension {
constructor(statusBarItems: IObservableArray<any>) {
super({
id: "some-id",
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: "some-id", version: "some-version", engines: { lens: "^5.5.0" }},
manifestPath: "irrelevant",
});
this.statusBarItems = statusBarItems;
}
}
describe("<StatusBar />", () => {
let di: DiContainer;
let statusBarItems: IObservableArray<any>;
let applicationBuilder: ApplicationBuilder;
let builder: ApplicationBuilder;
beforeEach(async () => {
statusBarItems = observable.array([]);
applicationBuilder = getApplicationBuilder();
builder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
rendererDi.unoverride(getRandomIdInjectable);
rendererDi.permitSideEffects(getRandomIdInjectable);
builder.beforeWindowStart((windowDi) => {
windowDi.unoverride(getRandomIdInjectable);
windowDi.permitSideEffects(getRandomIdInjectable);
windowDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
});
applicationBuilder.extensions.renderer.enable(new SomeTestExtension(statusBarItems));
di = applicationBuilder.dis.rendererDi;
builder.extensions.enable({
id: "some-id",
name: "some-name",
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
rendererOptions: {
statusBarItems,
},
});
});
it("renders w/o errors", async () => {
const { container } = await applicationBuilder.render();
const { container } = await builder.render();
expect(container).toBeInstanceOf(HTMLElement);
});
@@ -71,19 +56,21 @@ describe("<StatusBar />", () => {
])("renders w/o errors when registrations are not type compliant (%p)", async val => {
statusBarItems.replace([val]);
await expect(applicationBuilder.render()).resolves.toBeTruthy();
await expect(builder.render()).resolves.toBeTruthy();
});
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => {
const testId = "testId";
const text = "heee";
di.override(statusBarItemsInjectable, () => computed(() => ({
right: [ () => <span data-testid={testId} >{text}</span> ],
left: [],
}) as StatusBarItems));
builder.beforeWindowStart((windowDi) => {
windowDi.override(statusBarItemsInjectable, () => computed(() => ({
right: [ () => <span data-testid={testId} >{text}</span> ],
left: [],
}) as StatusBarItems));
});
const { getByTestId } = await applicationBuilder.render();
const { getByTestId } = await builder.render();
expect(getByTestId(testId)).toHaveTextContent(text);
});
@@ -96,7 +83,7 @@ describe("<StatusBar />", () => {
item: () => <span data-testid={testId} >{text}</span>,
}]);
const { getByTestId } = await applicationBuilder.render();
const { getByTestId } = await builder.render();
expect(getByTestId(testId)).toHaveTextContent(text);
});
@@ -129,7 +116,7 @@ describe("<StatusBar />", () => {
},
]);
const { getAllByTestId } = await applicationBuilder.render();
const { getAllByTestId } = await builder.render();
const elems = getAllByTestId("sortedElem");
const positions = elems.map(elem => elem.textContent);
@@ -5,27 +5,24 @@
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
import type { ObservableSet } from "mobx";
import type { IComputedValue, ObservableMap } from "mobx";
import { computed, observable, runInAction } from "mobx";
import React from "react";
import { Router } from "react-router";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import allowedResourcesInjectable from "../../cluster-frame-context/allowed-resources.injectable";
import type { RenderResult } from "@testing-library/react";
import { queryByText, fireEvent } from "@testing-library/react";
import { fireEvent, queryByText } from "@testing-library/react";
import type { KubeResource } from "../../../common/rbac";
import type { DiContainer } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import { pipeline } from "@ogre-tools/fp";
import { flatMap, compact, join, get, filter, map, matches, last } from "lodash/fp";
import { compact, filter, first, flatMap, get, join, last, map, matches } from "lodash/fp";
import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable";
import type { MenuItemConstructorOptions, MenuItem } from "electron";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { MenuItem, MenuItemConstructorOptions } from "electron";
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
@@ -39,7 +36,6 @@ import type { NamespaceStore } from "../+namespaces/store";
import historyInjectable from "../../navigation/history.injectable";
import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/electron-tray.injectable";
import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable";
import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable";
import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting";
import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting";
import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels";
@@ -52,7 +48,6 @@ import type { NavigateToRouteOptions } from "../../../common/front-end-routing/n
import { navigateToRouteInjectionToken } from "../../../common/front-end-routing/navigate-to-route-injection-token";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type { LensExtension } from "../../../extensions/lens-extension";
import extensionInjectable from "../../../extensions/extension-loader/extension/extension.injectable";
import { renderFor } from "./renderFor";
import { RootFrame } from "../../frames/root-frame/root-frame";
@@ -62,34 +57,50 @@ import activeKubernetesClusterInjectable from "../../cluster-frame-context/activ
import { catalogEntityFromCluster } from "../../../main/cluster-manager";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import { isAllowedResource } from "../../../common/cluster/is-allowed-resource";
import createApplicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-application-window.injectable";
import type { CreateElectronWindow } from "../../../main/start-main-application/lens-window/application-window/create-electron-window.injectable";
import createElectronWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-electron-window.injectable";
import { applicationWindowInjectionToken } from "../../../main/start-main-application/lens-window/application-window/application-window-injection-token";
import sendToChannelInElectronBrowserWindowInjectable from "../../../main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { FakeExtensionOptions } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake";
type Callback = (dis: DiContainers) => void | Promise<void>;
type Callback = (di: DiContainer) => void | Promise<void>;
type EnableExtensions<T> = (...extensions: T[]) => void;
type DisableExtensions<T> = (...extensions: T[]) => void;
type LensWindowWithHelpers = LensWindow & { rendered: RenderResult; di: DiContainer };
export interface ApplicationBuilder {
dis: DiContainers;
mainDi: DiContainer;
setEnvironmentToClusterFrame: () => ApplicationBuilder;
extensions: {
renderer: {
enable: EnableExtensions<LensRendererExtension>;
disable: DisableExtensions<LensRendererExtension>;
};
enable: (...extensions: FakeExtensionOptions[]) => void;
disable: (...extensions: FakeExtensionOptions[]) => void;
main: {
enable: EnableExtensions<LensMainExtension>;
disable: DisableExtensions<LensMainExtension>;
};
get: (id: string) => {
main: LensMainExtension;
enable: (...extensions: { renderer: LensRendererExtension; main: LensMainExtension }[]) => void;
disable: (...extensions: { renderer: LensRendererExtension; main: LensMainExtension }[]) => void;
applicationWindows: Record<string, LensRendererExtension> & {
only: LensRendererExtension;
};
};
};
applicationWindow: {
closeAll: () => void;
only: LensWindowWithHelpers;
get: (id: string) => LensWindowWithHelpers;
getAll: () => LensWindowWithHelpers[];
create: (id: string) => LensWindowWithHelpers;
};
allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder;
beforeApplicationStart: (callback: Callback) => ApplicationBuilder;
beforeRender: (callback: Callback) => ApplicationBuilder;
beforeWindowStart: (callback: Callback) => ApplicationBuilder;
startHidden: () => Promise<void>;
render: () => Promise<RenderResult>;
tray: {
@@ -116,17 +127,12 @@ export interface ApplicationBuilder {
};
select: {
openMenu: (id: string) => ({ selectOption: (labelText: string) => void });
openMenu: (id: string) => { selectOption: (labelText: string) => void };
selectOption: (menuId: string, labelText: string) => void;
getValue: (menuId: string) => string;
};
}
interface DiContainers {
rendererDi: DiContainer;
mainDi: DiContainer;
}
interface Environment {
RootComponent: React.ElementType;
onAllowKubeResource: () => void;
@@ -137,64 +143,22 @@ export const getApplicationBuilder = () => {
doGeneralOverrides: true,
});
mainDi.register(mainExtensionsStateInjectable);
const overrideChannelsForWindow = overrideChannels(mainDi);
const rendererDi = getRendererDi({
doGeneralOverrides: true,
});
overrideChannelsForWindow(rendererDi);
const dis = { rendererDi, mainDi };
const clusterStoreStub = {
provideInitialFromMain: () => {},
getById: (): null => null,
} as unknown as ClusterStore;
rendererDi.override(clusterStoreInjectable, () => clusterStoreStub);
rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true);
mainDi.override(clusterStoreInjectable, () => clusterStoreStub);
const beforeApplicationStartCallbacks: Callback[] = [];
const beforeRenderCallbacks: Callback[] = [];
const rendererExtensionsState = observable.set<LensRendererExtension>();
const mainExtensionsState = observable.set<LensMainExtension>();
rendererDi.override(subscribeStoresInjectable, () => () => () => {});
const environments = {
application: {
RootComponent: RootFrame,
onAllowKubeResource: () => {
throw new Error(
"Tried to allow kube resource when environment is not cluster frame.",
);
},
} as Environment,
clusterFrame: {
RootComponent: ClusterFrame,
onAllowKubeResource: () => {},
} as Environment,
};
const beforeWindowStartCallbacks: Callback[] = [];
let environment = environments.application;
rendererDi.override(
currentlyInClusterFrameInjectable,
() => environment === environments.clusterFrame,
);
mainDi.override(mainExtensionsInjectable, (di) => {
const mainExtensionsState = di.inject(mainExtensionsStateInjectable);
rendererDi.override(rendererExtensionsInjectable, () =>
computed(() => [...rendererExtensionsState]),
);
mainDi.override(mainExtensionsInjectable, () =>
computed(() => [...mainExtensionsState]),
);
return computed(() =>
[...mainExtensionsState.values()],
);
});
let trayMenuIconPath: string;
@@ -209,60 +173,135 @@ export const getApplicationBuilder = () => {
},
}));
let allowedResourcesState: KubeResource[];
let rendered: RenderResult;
const allowedResourcesState = observable.array<KubeResource>();
const enableExtensionsFor = <T extends ObservableSet>(
extensionState: T,
di: DiContainer,
) => {
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
const windowHelpers = new Map<string, { di: DiContainer; getRendered: () => RenderResult }>();
return (...extensionInstances: LensExtension[]) => {
const addAndEnableExtensions = () => {
extensionInstances.forEach(instance => {
const extension = getExtension(instance);
const createElectronWindowFake: CreateElectronWindow = (configuration) => {
const windowId = configuration.id;
extension.register();
});
const windowDi = getRendererDi({ doGeneralOverrides: true });
runInAction(() => {
extensionInstances.forEach((extension) => {
extensionState.add(extension);
});
});
};
overrideChannelsForWindow(windowDi, windowId);
if (rendered) {
addAndEnableExtensions();
} else {
builder.beforeRender(addAndEnableExtensions);
}
windowDi.register(rendererExtensionsStateInjectable);
windowDi.override(
currentlyInClusterFrameInjectable,
() => environment === environments.clusterFrame,
);
windowDi.override(rendererExtensionsInjectable, (di) => {
const rendererExtensionState = di.inject(rendererExtensionsStateInjectable);
return computed(() => [...rendererExtensionState.values()]);
});
let rendered: RenderResult;
windowHelpers.set(windowId, { di: windowDi, getRendered: () => rendered });
return {
show: () => {},
close: () => {},
loadFile: async () => {},
loadUrl: async () => {
for (const callback of beforeWindowStartCallbacks) {
await callback(windowDi);
}
const startFrame = windowDi.inject(startFrameInjectable);
await startFrame();
const history = windowDi.inject(historyInjectable);
const render = renderFor(windowDi);
rendered = render(
<Router history={history}>
<environment.RootComponent />
</Router>,
);
},
send: (arg) => {
const sendFake = mainDi.inject(sendToChannelInElectronBrowserWindowInjectable) as any;
sendFake(windowId, null, arg);
},
reload: () => {
throw new Error("Tried to reload application window which is not implemented yet.");
},
};
};
const enableRendererExtension = enableExtensionsFor(rendererExtensionsState, rendererDi);
const enableMainExtension = enableExtensionsFor(mainExtensionsState, mainDi);
const disableRendererExtension = disableExtensionsFor(rendererExtensionsState, rendererDi);
const disableMainExtension = disableExtensionsFor(mainExtensionsState, mainDi);
mainDi.override(createElectronWindowInjectable, () => createElectronWindowFake);
const selectOptionFor = (menuId: string) => (labelText: string) => {
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
`.${menuId}-options`,
);
assert(menuOptions, `Could not find select options for menu with ID "${menuId}"`);
const option = queryByText(menuOptions, labelText);
assert(option, `Could not find select option with label "${labelText}" for menu with ID "${menuId}"`);
userEvent.click(option);
};
let applicationHasStarted = false;
const builder: ApplicationBuilder = {
dis,
mainDi,
applicationWindow: {
closeAll: () => {
const closeAll = mainDi.inject(closeAllWindowsInjectable);
closeAll();
document.documentElement.innerHTML = "";
},
get only() {
const applicationWindows = builder.applicationWindow.getAll();
if (applicationWindows.length > 1) {
throw new Error(
"Tried to get only application window when there are multiple windows.",
);
}
const applicationWindow = first(applicationWindows);
if (!applicationWindow) {
throw new Error(
"Tried to get only application window when there are no windows.",
);
}
return applicationWindow;
},
getAll: () =>
mainDi
.injectMany(applicationWindowInjectionToken)
.map(toWindowWithHelpersFor(windowHelpers)),
get: (id) => {
const applicationWindows = builder.applicationWindow.getAll();
const applicationWindow = applicationWindows.find(
(window) => window.id === id,
);
if (!applicationWindow) {
throw new Error(`Tried to get application window with ID "${id}" but it was not found.`);
}
return applicationWindow;
},
create: (id) => {
const createApplicationWindow = mainDi.inject(
createApplicationWindowInjectable,
);
createApplicationWindow(id);
return builder.applicationWindow.get(id);
},
},
applicationMenu: {
click: (path: string) => {
@@ -340,29 +379,39 @@ export const getApplicationBuilder = () => {
preferences: {
close: () => {
const rendered = builder.applicationWindow.only.rendered;
const link = rendered.getByTestId("close-preferences");
fireEvent.click(link);
},
navigate: () => {
const navigateToPreferences = rendererDi.inject(navigateToPreferencesInjectable);
const windowDi = builder.applicationWindow.only.di;
const navigateToPreferences = windowDi.inject(
navigateToPreferencesInjectable,
);
navigateToPreferences();
},
navigateTo: (route: Route<any>, params: Partial<NavigateToRouteOptions<any>>) => {
const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken);
const windowDi = builder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
navigateToRoute(route, params);
},
navigation: {
click: (id: string) => {
const { di: windowDi, rendered } = builder.applicationWindow.only;
const link = rendered.queryByTestId(`tab-link-for-${id}`);
if (!link) {
const preferencesNavigationItems = rendererDi.inject(
const preferencesNavigationItems = windowDi.inject(
preferenceNavigationItemsInjectable,
);
@@ -382,7 +431,11 @@ export const getApplicationBuilder = () => {
helmCharts: {
navigate: (parameters) => {
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
const windowDi = builder.applicationWindow.only.di;
const navigateToHelmCharts = windowDi.inject(
navigateToHelmChartsInjectable,
);
navigateToHelmCharts(parameters);
},
@@ -391,81 +444,129 @@ export const getApplicationBuilder = () => {
setEnvironmentToClusterFrame: () => {
environment = environments.clusterFrame;
allowedResourcesState = observable.array();
builder.beforeWindowStart((windowDi) => {
windowDi.override(allowedResourcesInjectable, () =>
computed(() => new Set([...allowedResourcesState])),
);
rendererDi.override(allowedResourcesInjectable, () =>
computed(() => new Set([...allowedResourcesState])),
);
const clusterStub = {
accessibleNamespaces: [],
isAllowedResource: isAllowedResource(allowedResourcesState),
} as unknown as Cluster;
const clusterStub = {
accessibleNamespaces: [],
isAllowedResource: isAllowedResource(allowedResourcesState),
} as unknown as Cluster;
windowDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(clusterStub)),
);
rendererDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(clusterStub)),
);
// TODO: Figure out a way to remove this stub.
const namespaceStoreStub = {
isLoaded: true,
contextNamespaces: [],
contextItems: [],
api: {
kind: "Namespace",
},
items: [],
selectNamespaces: () => {},
getByPath: () => undefined,
pickOnlySelected: () => [],
isSelectedAll: () => false,
getTotalCount: () => 0,
} as unknown as NamespaceStore;
// TODO: Figure out a way to remove this stub.
const namespaceStoreStub = {
isLoaded: true,
contextNamespaces: [],
contextItems: [],
api: {
kind: "Namespace",
},
items: [],
selectNamespaces: () => {},
getByPath: () => undefined,
pickOnlySelected: () => [],
isSelectedAll: () => false,
getTotalCount: () => 0,
} as unknown as NamespaceStore;
const clusterFrameContextFake = new ClusterFrameContext(
clusterStub,
const clusterFrameContextFake = new ClusterFrameContext(
clusterStub,
{
namespaceStore: namespaceStoreStub,
},
);
{
namespaceStore: namespaceStoreStub,
},
);
windowDi.override(namespaceStoreInjectable, () => namespaceStoreStub);
windowDi.override(hostedClusterInjectable, () => clusterStub);
windowDi.override(hostedClusterIdInjectable, () => "irrelevant-hosted-cluster-id");
windowDi.override(clusterFrameContextInjectable, () => clusterFrameContextFake);
rendererDi.override(namespaceStoreInjectable, () => namespaceStoreStub);
rendererDi.override(hostedClusterInjectable, () => clusterStub);
rendererDi.override(hostedClusterIdInjectable, () => "irrelevant-hosted-cluster-id");
rendererDi.override(clusterFrameContextInjectable, () => clusterFrameContextFake);
// Todo: get rid of global state.
KubeObjectStore.defaultContext.set(clusterFrameContextFake);
// Todo: get rid of global state.
KubeObjectStore.defaultContext.set(clusterFrameContextFake);
});
return builder;
},
extensions: {
renderer: {
enable: enableRendererExtension,
disable: disableRendererExtension,
},
get: (id: string) => {
const windowInstances = pipeline(
builder.applicationWindow.getAll(),
main: {
enable: enableMainExtension,
disable: disableMainExtension,
map((window): [string, LensRendererExtension] => [
window.id,
findExtensionInstance(window.di, rendererExtensionsInjectable, id),
]),
items => Object.fromEntries(items),
);
return {
main: findExtensionInstance(mainDi, mainExtensionsInjectable, id),
applicationWindows: {
get only() {
return findExtensionInstance(
builder.applicationWindow.only.di,
rendererExtensionsInjectable,
id,
);
},
...windowInstances,
},
};
},
enable: (...extensions) => {
const rendererExtensions = extensions.map(extension => extension.renderer);
const mainExtensions = extensions.map(extension => extension.main);
builder.beforeWindowStart((windowDi) => {
const rendererExtensionInstances = extensions.map((options) =>
getExtensionFakeForRenderer(
windowDi,
options.id,
options.name,
options.rendererOptions || {},
),
);
enableRendererExtension(...rendererExtensions);
enableMainExtension(...mainExtensions);
rendererExtensionInstances.forEach(
enableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
});
builder.beforeApplicationStart((mainDi) => {
const mainExtensionInstances = extensions.map((extension) =>
getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}),
);
mainExtensionInstances.forEach(
enableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
});
},
disable: (...extensions) => {
const rendererExtensions = extensions.map(extension => extension.renderer);
const mainExtensions = extensions.map(extension => extension.main);
builder.beforeWindowStart(windowDi => {
extensions
.map((ext) => ext.id)
.forEach(
disableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
});
disableRendererExtension(...rendererExtensions);
disableMainExtension(...mainExtensions);
builder.beforeApplicationStart(mainDi => {
extensions
.map((ext) => ext.id)
.forEach(
disableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
});
},
},
@@ -479,56 +580,72 @@ export const getApplicationBuilder = () => {
return builder;
},
beforeApplicationStart(callback: (dis: DiContainers) => void) {
beforeApplicationStart(callback) {
if (applicationHasStarted) {
callback(mainDi);
}
beforeApplicationStartCallbacks.push(callback);
return builder;
},
beforeRender(callback: (dis: DiContainers) => void) {
beforeRenderCallbacks.push(callback);
beforeWindowStart(callback) {
const alreadyRenderedWindows = builder.applicationWindow.getAll();
alreadyRenderedWindows.forEach((window) => {
callback(window.di);
});
beforeWindowStartCallbacks.push(callback);
return builder;
},
startHidden: async () => {
mainDi.inject(lensProxyPortInjectable).set(42);
for (const callback of beforeApplicationStartCallbacks) {
await callback(mainDi);
}
const startMainApplication = mainDi.inject(
startMainApplicationInjectable,
);
await startMainApplication(false);
applicationHasStarted = true;
},
async render() {
mainDi.inject(lensProxyPortInjectable).set(42);
for (const callback of beforeApplicationStartCallbacks) {
await callback(dis);
await callback(mainDi);
}
const startMainApplication = mainDi.inject(startMainApplicationInjectable);
await startMainApplication();
const applicationWindow = mainDi.inject(applicationWindowInjectable);
await applicationWindow.start();
const startFrame = rendererDi.inject(startFrameInjectable);
await startFrame();
for (const callback of beforeRenderCallbacks) {
await callback(dis);
}
const history = rendererDi.inject(historyInjectable);
const render = renderFor(rendererDi);
rendered = render(
<Router history={history}>
<environment.RootComponent />
</Router>,
const startMainApplication = mainDi.inject(
startMainApplicationInjectable,
);
return rendered;
await startMainApplication(true);
applicationHasStarted = true;
const applicationWindow = builder.applicationWindow.get(
"first-application-window",
);
assert(applicationWindow);
return applicationWindow.rendered;
},
select: {
openMenu: (menuId) => {
const rendered = builder.applicationWindow.only.rendered;
const select = rendered.baseElement.querySelector<HTMLElement>(
`#${menuId}`,
);
@@ -538,13 +655,15 @@ export const getApplicationBuilder = () => {
openMenu(select);
return {
selectOption: selectOptionFor(menuId),
selectOption: selectOptionFor(builder, menuId),
};
},
selectOption: (menuId, labelText) => selectOptionFor(menuId)(labelText),
selectOption: (menuId, labelText) => selectOptionFor(builder, menuId)(labelText),
getValue: (menuId) => {
const rendered = builder.applicationWindow.only.rendered;
const select = rendered.baseElement.querySelector<HTMLInputElement>(
`#${menuId}`,
);
@@ -582,22 +701,134 @@ function toFlatChildren(parentId: string | null | undefined): ToFlatChildren {
];
}
const disableExtensionsFor = <T extends ObservableSet>(
extensionState: T,
export const rendererExtensionsStateInjectable = getInjectable({
id: "renderer-extensions-state",
instantiate: () => observable.map<string, LensRendererExtension>(),
});
const mainExtensionsStateInjectable = getInjectable({
id: "main-extensions-state",
instantiate: () => observable.map<string, LensMainExtension>(),
});
const findExtensionInstance = <T extends LensExtension> (di: DiContainer, injectable: Injectable<IComputedValue<T[]>, any, any>, id: string) => {
const instance = di.inject(injectable).get().find(ext => ext.id === id);
if (!instance) {
throw new Error(`Tried to get extension with ID ${id}, but it didn't exist`);
}
return instance;
};
type ApplicationWindowHelpers = Map<string, { di: DiContainer; getRendered: () => RenderResult }>;
const toWindowWithHelpersFor =
(windowHelpers: ApplicationWindowHelpers) => (applicationWindow: LensWindow) => ({
...applicationWindow,
get rendered() {
const helpers = windowHelpers.get(applicationWindow.id);
if (!helpers) {
throw new Error(
`Tried to get rendered for application window "${applicationWindow.id}" before it was started.`,
);
}
return helpers.getRendered();
},
get di() {
const helpers = windowHelpers.get(applicationWindow.id);
if (!helpers) {
throw new Error(
`Tried to get di for application window "${applicationWindow.id}" before it was started.`,
);
}
return helpers.di;
},
});
const environments = {
application: {
RootComponent: RootFrame,
onAllowKubeResource: () => {
throw new Error(
"Tried to allow kube resource when environment is not cluster frame.",
);
},
} as Environment,
clusterFrame: {
RootComponent: ClusterFrame,
onAllowKubeResource: () => {},
} as Environment,
};
const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelText: string) => {
const rendered = builder.applicationWindow.only.rendered;
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
`.${menuId}-options`,
);
assert(menuOptions, `Could not find select options for menu with ID "${menuId}"`);
const option = queryByText(menuOptions, labelText);
assert(option, `Could not find select option with label "${labelText}" for menu with ID "${menuId}"`);
userEvent.click(option);
};
const enableExtensionFor = (
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, any, any>,
) => {
const getExtension = (instance: LensExtension) =>
di.inject(extensionInjectable, instance);
const extensionState = di.inject(stateInjectable);
return (...extensionInstances: LensExtension[]) => {
extensionInstances.forEach((instance) => {
const extension = getExtension(instance);
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
runInAction(() => {
extension.deregister();
return (extensionInstance: LensExtension) => {
const extension = getExtension(extensionInstance);
extensionState.delete(instance);
});
runInAction(() => {
extension.register();
extensionState.set(extensionInstance.id, extensionInstance);
});
};
};
const disableExtensionFor =
(
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>,
) =>
(id: string) => {
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
const extensionsState = di.inject(stateInjectable);
const instance = extensionsState.get(id);
if (!instance) {
throw new Error(
`Tried to disable extension with ID "${id}", but it wasn't enabled`,
);
}
const injectable = getExtension(instance);
runInAction(() => {
injectable.deregister();
extensionsState.delete(id);
});
};
@@ -5,7 +5,6 @@
import type { Mutable } from "type-fest";
import fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import { lensExtensionDependencies } from "../../../extensions/lens-extension";
import type { ApplicationBuilder } from "./get-application-builder";
import { LensMainExtension } from "../../../extensions/lens-main-extension";
import navigateForExtensionInjectable from "../../../main/start-main-application/lens-window/navigate-for-extension.injectable";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
@@ -27,21 +26,7 @@ export interface FakeExtensionOptions {
mainOptions?: Partial<LensMainExtension>;
}
export type GetExtensionFake = (arg: FakeExtensionOptions) => {
main: TestExtensionMain;
renderer: TestExtensionRenderer;
};
export const getExtensionFakeFor =
(builder: ApplicationBuilder): GetExtensionFake =>
({ id, name, mainOptions = {}, rendererOptions = {}}) => {
const mainInstance = getExtensionFakeForMain(builder.dis.mainDi, id, name, mainOptions);
const rendererInstance = getExtensionFakeForRenderer(builder.dis.rendererDi, id, name, rendererOptions);
return { main: mainInstance, renderer: rendererInstance };
};
const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => {
export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => {
const instance = new TestExtensionMain({
id,
absolutePath: "irrelevant",
@@ -71,7 +56,7 @@ const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, opti
return instance;
};
const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => {
export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => {
const instance = new TestExtensionRenderer({
id,
absolutePath: "irrelevant",
@@ -1,28 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { SetRequired } from "type-fest";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { ApplicationBuilder } from "./get-application-builder";
import { getExtensionFakeFor } from "./get-extension-fake";
export class TestExtension extends LensRendererExtension {}
export type FakeExtensionData = SetRequired<Partial<LensRendererExtension>, "id" | "name">;
export type GetRendererExtensionFake = (fakeExtensionData: FakeExtensionData) => TestExtension;
export const getRendererExtensionFakeFor = (
builder: ApplicationBuilder,
): GetRendererExtensionFake => {
const getExtensionFake = getExtensionFakeFor(builder);
return ({ id, name, ...rest }) =>
getExtensionFake({
id,
name,
rendererOptions: rest,
}).renderer;
};
-8
View File
@@ -18,9 +18,6 @@ import extensionsStoreInjectable from "../extensions/extensions-store/extensions
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store";
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../common/cluster-store/cluster-store";
import type { Cluster } from "../common/cluster/cluster";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import type { UserStore } from "../common/user-store";
import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable";
@@ -41,7 +38,6 @@ import setupOnApiErrorListenersInjectable from "./api/setup-on-api-errors.inject
import { observable, computed } from "mobx";
import defaultShellInjectable from "./components/+preferences/default-shell.injectable";
import appVersionInjectable from "../common/vars/app-version.injectable";
import provideInitialValuesForSyncBoxesInjectable from "./utils/sync-box/provide-initial-values-for-sync-boxes.injectable";
import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable";
import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import getFilePathsInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/get-file-paths.injectable";
@@ -189,12 +185,8 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
// eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore);
di.override(setupSystemCaInjectable, () => ({ run: () => {} }));
di.override(setupOnApiErrorListenersInjectable, () => ({ run: () => {} }));
di.override(provideInitialValuesForSyncBoxesInjectable, () => ({ run: () => {} }));
di.override(defaultShellInjectable, () => "some-default-shell");
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../common/test-utils/get-global-override";
import subscribeStoresInjectable from "./subscribe-stores.injectable";
export default getGlobalOverride(
subscribeStoresInjectable,
() => () => () => {},
);
@@ -4,11 +4,17 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { storesAndApisCanBeCreatedInjectionToken } from "../common/k8s-api/stores-apis-can-be-created.token";
import { getClusterIdFromHost } from "./utils";
import hostedClusterIdInjectable from "./cluster-frame-context/hosted-cluster-id.injectable";
const storesAndApisCanBeCreatedInjectable = getInjectable({
id: "create-stores-and-apis",
instantiate: () => Boolean(getClusterIdFromHost(location.host)),
instantiate: (di) => {
const hostedClusterId = di.inject(hostedClusterIdInjectable);
return !!hostedClusterId;
},
injectionToken: storesAndApisCanBeCreatedInjectionToken,
});