mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
426 lines
13 KiB
TypeScript
426 lines
13 KiB
TypeScript
/**
|
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
*/
|
|
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 { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
|
|
import type { IObservableArray } from "mobx";
|
|
import { computed, observable, runInAction } from "mobx";
|
|
import { renderFor } from "./renderFor";
|
|
import React from "react";
|
|
import { Router } from "react-router";
|
|
import { Observer } from "mobx-react";
|
|
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
|
import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable";
|
|
import type { RenderResult } from "@testing-library/react";
|
|
import { fireEvent } from "@testing-library/react";
|
|
import type { KubeResource } from "../../../common/rbac";
|
|
import { Sidebar } from "../layout/sidebar";
|
|
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
|
|
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 mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
|
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
|
import { pipeline } from "@ogre-tools/fp";
|
|
import { flatMap, compact, join, get, filter, find, 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 { MenuItem, MenuItemConstructorOptions } from "electron";
|
|
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
|
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
|
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable";
|
|
import { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context";
|
|
import type { Cluster } from "../../../common/cluster/cluster";
|
|
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
|
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
|
import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable";
|
|
import startFrameInjectable from "../../start-frame/start-frame.injectable";
|
|
import { flushPromises } from "../../../common/test-utils/flush-promises";
|
|
import type { NamespaceStore } from "../+namespaces/store";
|
|
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
|
import historyInjectable from "../../navigation/history.injectable";
|
|
import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable";
|
|
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
|
|
import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable";
|
|
import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable";
|
|
|
|
type Callback = (dis: DiContainers) => void | Promise<void>;
|
|
|
|
export interface ApplicationBuilder {
|
|
dis: DiContainers;
|
|
setEnvironmentToClusterFrame: () => ApplicationBuilder;
|
|
addExtensions: (...extensions: LensRendererExtension[]) => Promise<ApplicationBuilder>;
|
|
allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder;
|
|
beforeApplicationStart: (callback: Callback) => ApplicationBuilder;
|
|
beforeRender: (callback: Callback) => ApplicationBuilder;
|
|
render: () => Promise<RenderResult>;
|
|
|
|
tray: {
|
|
click: (id: string) => Promise<void>;
|
|
get: (id: string) => TrayMenuItem | undefined;
|
|
};
|
|
|
|
applicationMenu: {
|
|
click: (path: string) => Promise<void>;
|
|
};
|
|
|
|
preferences: {
|
|
close: () => void;
|
|
navigate: () => void;
|
|
navigation: {
|
|
click: (id: string) => void;
|
|
};
|
|
};
|
|
|
|
helmCharts: {
|
|
navigate: () => void;
|
|
};
|
|
}
|
|
|
|
interface DiContainers {
|
|
rendererDi: DiContainer;
|
|
mainDi: DiContainer;
|
|
}
|
|
|
|
interface Environment {
|
|
renderSidebar: () => React.ReactNode;
|
|
onAllowKubeResource: () => void;
|
|
}
|
|
|
|
export const getApplicationBuilder = () => {
|
|
const { rendererDi, mainDi } = getDisForUnitTesting({
|
|
doGeneralOverrides: true,
|
|
});
|
|
|
|
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 extensionsState = observable.array<LensRendererExtension>();
|
|
|
|
rendererDi.override(subscribeStoresInjectable, () => () => () => {});
|
|
|
|
const environments = {
|
|
application: {
|
|
renderSidebar: () => null,
|
|
|
|
onAllowKubeResource: () => {
|
|
throw new Error(
|
|
"Tried to allow kube resource when environment is not cluster frame.",
|
|
);
|
|
},
|
|
} as Environment,
|
|
|
|
clusterFrame: {
|
|
renderSidebar: () => <Sidebar />,
|
|
onAllowKubeResource: () => {},
|
|
} as Environment,
|
|
};
|
|
|
|
let environment = environments.application;
|
|
|
|
rendererDi.override(
|
|
currentlyInClusterFrameInjectable,
|
|
() => environment === environments.clusterFrame,
|
|
);
|
|
|
|
rendererDi.override(rendererExtensionsInjectable, () =>
|
|
computed(() => extensionsState),
|
|
);
|
|
|
|
mainDi.override(mainExtensionsInjectable, () =>
|
|
computed(() => []),
|
|
);
|
|
|
|
let trayMenuItemsStateFake: TrayMenuItem[];
|
|
|
|
mainDi.override(electronTrayInjectable, () => ({
|
|
start: () => {},
|
|
stop: () => {},
|
|
|
|
setMenuItems: (items) => {
|
|
trayMenuItemsStateFake = items;
|
|
},
|
|
}));
|
|
|
|
let allowedResourcesState: IObservableArray<KubeResource>;
|
|
let rendered: RenderResult;
|
|
|
|
const builder: ApplicationBuilder = {
|
|
dis,
|
|
|
|
applicationMenu: {
|
|
click: async (path: string) => {
|
|
const applicationMenuItems = mainDi.inject(
|
|
applicationMenuItemsInjectable,
|
|
);
|
|
|
|
const menuItems = pipeline(
|
|
applicationMenuItems.get(),
|
|
flatMap(toFlatChildren(null)),
|
|
filter((menuItem) => !!menuItem.click),
|
|
);
|
|
|
|
const menuItem = menuItems.find((menuItem) => menuItem.path === path);
|
|
|
|
if (!menuItem) {
|
|
const availableIds = menuItems.map(get("path")).join('", "');
|
|
|
|
throw new Error(
|
|
`Tried to click application menu item with ID "${path}" which does not exist. Available IDs are: "${availableIds}"`,
|
|
);
|
|
}
|
|
|
|
menuItem.click?.(
|
|
{
|
|
menu: null as never,
|
|
commandId: 0,
|
|
...menuItem,
|
|
} as MenuItem,
|
|
undefined,
|
|
{},
|
|
);
|
|
|
|
await flushPromises();
|
|
},
|
|
},
|
|
|
|
tray: {
|
|
get: (id: string) => {
|
|
return trayMenuItemsStateFake.find(matches({ id }));
|
|
},
|
|
|
|
click: async (id: string) => {
|
|
const menuItem = pipeline(
|
|
trayMenuItemsStateFake,
|
|
find((menuItem) => menuItem.id === id),
|
|
);
|
|
|
|
if (!menuItem) {
|
|
const availableIds = pipeline(
|
|
trayMenuItemsStateFake,
|
|
filter(item => !!item.click),
|
|
map(item => item.id),
|
|
join(", "),
|
|
);
|
|
|
|
throw new Error(`Tried to click tray menu item with ID ${id} which does not exist. Available IDs are: "${availableIds}"`);
|
|
}
|
|
|
|
await menuItem.click?.();
|
|
},
|
|
},
|
|
|
|
preferences: {
|
|
close: () => {
|
|
const link = rendered.getByTestId("close-preferences");
|
|
|
|
fireEvent.click(link);
|
|
},
|
|
|
|
navigate: () => {
|
|
const navigateToPreferences = rendererDi.inject(navigateToPreferencesInjectable);
|
|
|
|
navigateToPreferences();
|
|
},
|
|
|
|
navigation: {
|
|
click: (id: string) => {
|
|
const link = rendered.queryByTestId(`tab-link-for-${id}`);
|
|
|
|
if (!link) {
|
|
const preferencesNavigationItems = rendererDi.inject(
|
|
preferenceNavigationItemsInjectable,
|
|
);
|
|
|
|
const availableIds = preferencesNavigationItems
|
|
.get()
|
|
.map(get("id"));
|
|
|
|
throw new Error(
|
|
`Tried to click navigation item "${id}" which does not exist in preferences. Available IDs are "${availableIds.join('", "')}"`,
|
|
);
|
|
}
|
|
|
|
fireEvent.click(link);
|
|
},
|
|
},
|
|
},
|
|
|
|
helmCharts: {
|
|
navigate: () => {
|
|
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
|
|
|
|
navigateToHelmCharts();
|
|
},
|
|
},
|
|
|
|
setEnvironmentToClusterFrame: () => {
|
|
environment = environments.clusterFrame;
|
|
|
|
allowedResourcesState = observable.array();
|
|
|
|
rendererDi.override(allowedResourcesInjectable, () =>
|
|
computed(() => new Set([...allowedResourcesState])),
|
|
);
|
|
|
|
const clusterStub = {
|
|
accessibleNamespaces: [],
|
|
} as unknown as Cluster;
|
|
|
|
const namespaceStoreStub = {
|
|
contextNamespaces: [],
|
|
} as unknown as NamespaceStore;
|
|
|
|
const clusterFrameContextFake = new ClusterFrameContext(
|
|
clusterStub,
|
|
|
|
{
|
|
namespaceStore: namespaceStoreStub,
|
|
},
|
|
);
|
|
|
|
rendererDi.override(namespaceStoreInjectable, () => namespaceStoreStub);
|
|
rendererDi.override(hostedClusterInjectable, () => clusterStub);
|
|
rendererDi.override(clusterFrameContextInjectable, () => clusterFrameContextFake);
|
|
|
|
// Todo: get rid of global state.
|
|
KubeObjectStore.defaultContext.set(clusterFrameContextFake);
|
|
|
|
return builder;
|
|
},
|
|
|
|
addExtensions: async (...extensions) => {
|
|
const extensionRegistrators = rendererDi.injectMany(
|
|
extensionRegistratorInjectionToken,
|
|
);
|
|
|
|
const addAndEnableExtensions = async () => {
|
|
const registratorPromises = extensions.flatMap((extension) =>
|
|
extensionRegistrators.map((registrator) => registrator(extension, 1)),
|
|
);
|
|
|
|
await Promise.all(registratorPromises);
|
|
|
|
runInAction(() => {
|
|
extensions.forEach((extension) => {
|
|
extensionsState.push(extension);
|
|
});
|
|
});
|
|
};
|
|
|
|
if (rendered) {
|
|
await addAndEnableExtensions();
|
|
} else {
|
|
builder.beforeRender(addAndEnableExtensions);
|
|
}
|
|
|
|
return builder;
|
|
},
|
|
|
|
allowKubeResource: (resourceName) => {
|
|
environment.onAllowKubeResource();
|
|
|
|
runInAction(() => {
|
|
allowedResourcesState.push(resourceName);
|
|
});
|
|
|
|
return builder;
|
|
},
|
|
|
|
beforeApplicationStart(callback: (dis: DiContainers) => void) {
|
|
beforeApplicationStartCallbacks.push(callback);
|
|
|
|
return builder;
|
|
},
|
|
|
|
beforeRender(callback: (dis: DiContainers) => void) {
|
|
beforeRenderCallbacks.push(callback);
|
|
|
|
return builder;
|
|
},
|
|
|
|
async render() {
|
|
for (const callback of beforeApplicationStartCallbacks) {
|
|
await callback(dis);
|
|
}
|
|
|
|
const startMainApplication = mainDi.inject(startMainApplicationInjectable);
|
|
|
|
await startMainApplication();
|
|
|
|
const applicationWindow = mainDi.inject(applicationWindowInjectable);
|
|
|
|
await applicationWindow.show();
|
|
|
|
const startFrame = rendererDi.inject(startFrameInjectable);
|
|
|
|
await startFrame();
|
|
|
|
const render = renderFor(rendererDi);
|
|
const history = rendererDi.inject(historyInjectable);
|
|
const currentRouteComponent = rendererDi.inject(currentRouteComponentInjectable);
|
|
|
|
for (const callback of beforeRenderCallbacks) {
|
|
await callback(dis);
|
|
}
|
|
|
|
rendered = render(
|
|
<Router history={history}>
|
|
{environment.renderSidebar()}
|
|
|
|
<Observer>
|
|
{() => {
|
|
const Component = currentRouteComponent.get();
|
|
|
|
if (!Component) {
|
|
return null;
|
|
}
|
|
|
|
return <Component />;
|
|
}}
|
|
</Observer>
|
|
</Router>,
|
|
);
|
|
|
|
return rendered;
|
|
},
|
|
};
|
|
|
|
return builder;
|
|
};
|
|
|
|
export type ToFlatChildren = (opts: MenuItemConstructorOptions) => (MenuItemOpts & { path: string })[];
|
|
|
|
function toFlatChildren(parentId: string | null | undefined): ToFlatChildren {
|
|
return ({ submenu = [], ...menuItem }) => [
|
|
{
|
|
...menuItem,
|
|
path: pipeline([parentId, menuItem.id], compact, join(".")),
|
|
},
|
|
...(
|
|
Array.isArray(submenu)
|
|
? submenu.flatMap(toFlatChildren(menuItem.id))
|
|
: [{
|
|
...submenu,
|
|
path: pipeline([parentId, menuItem.id], compact, join(".")),
|
|
}]
|
|
),
|
|
];
|
|
}
|