mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix cluster frame display issue (#5518)
* Fix cluster frame display issue - Add some defensive code to prevent this sort of infinite loop - Add some unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * Factor out injectable for getting of cluster config data Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
8c31e56462
commit
de43df1cca
40 files changed
+2126
-815
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.centering {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 type { RenderResult } from "@testing-library/react";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { render as testingLibraryRender } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||
import { Router } from "react-router";
|
||||
import { DefaultProps } from "../../mui-base-theme";
|
||||
import { ClusterFrame } from "./cluster-frame";
|
||||
import historyInjectable from "../../navigation/history.injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
|
||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import legacyOnChannelListenInjectable from "../../ipc/legacy-channel-listen.injectable";
|
||||
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
||||
import allowedResourcesInjectable from "../../cluster-frame-context/allowed-resources.injectable";
|
||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
|
||||
|
||||
describe("<ClusterFrame />", () => {
|
||||
let render: () => RenderResult;
|
||||
let di: DiContainer;
|
||||
let cluster: Cluster;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
render = () => testingLibraryRender((
|
||||
<DiContextProvider value={{ di }}>
|
||||
<Router history={di.inject(historyInjectable)}>
|
||||
{DefaultProps(ClusterFrame)}
|
||||
</Router>
|
||||
</DiContextProvider>
|
||||
));
|
||||
|
||||
di.override(subscribeStoresInjectable, () => jest.fn().mockImplementation(() => jest.fn()));
|
||||
di.override(legacyOnChannelListenInjectable, () => jest.fn().mockImplementation(() => jest.fn()));
|
||||
di.override(directoryForUserDataInjectable, () => "/some/irrelavent/path");
|
||||
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
|
||||
const createCluster = di.inject(createClusterInjectable);
|
||||
|
||||
cluster = createCluster(
|
||||
{
|
||||
contextName: "my-cluster",
|
||||
id: "123456",
|
||||
kubeConfigPath: "/irrelavent",
|
||||
},
|
||||
{
|
||||
clusterServerUrl: "https://localhost",
|
||||
},
|
||||
);
|
||||
|
||||
di.override(hostedClusterInjectable, () => cluster);
|
||||
di.override(hostedClusterIdInjectable, () => cluster.id);
|
||||
});
|
||||
|
||||
describe("given cluster with list nodes and namespaces permissions", () => {
|
||||
beforeEach(() => {
|
||||
di.override(allowedResourcesInjectable, () => computed(() => new Set(["nodes", "namespaces"])));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const result = render();
|
||||
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows cluster overview sidebar item as active", () => {
|
||||
const result = render();
|
||||
const clusterOverviewSidebarItem = result.getByTestId("sidebar-item-cluster-overview");
|
||||
|
||||
expect(clusterOverviewSidebarItem.getAttribute("data-is-active-test")).toBe("true");
|
||||
});
|
||||
|
||||
describe("given no matching component", () => {
|
||||
beforeEach(() => {
|
||||
di.override(currentRouteComponentInjectable, () => computed(() => undefined));
|
||||
});
|
||||
|
||||
describe("given current url is starting url", () => {
|
||||
it("renders", () => {
|
||||
const result = render();
|
||||
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows warning message", () => {
|
||||
const result = render();
|
||||
|
||||
expect(
|
||||
result.getByText("An error has occured. No route can be found matching the current route, which is also the starting route."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given cluster without list nodes, but with namespaces permissions", () => {
|
||||
beforeEach(() => {
|
||||
di.override(allowedResourcesInjectable, () => computed(() => new Set(["namespaces"])));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const result = render();
|
||||
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows workloads overview sidebar item as active", () => {
|
||||
const result = render();
|
||||
const workloadsOverviewSidebarItem = result.getByTestId("sidebar-item-workloads");
|
||||
|
||||
expect(workloadsOverviewSidebarItem.getAttribute("data-is-active-test")).toBe("true");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import styles from "./cluster-frame.module.css";
|
||||
import React, { useEffect } from "react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
@@ -30,12 +31,14 @@ import { disposer } from "../../utils";
|
||||
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
||||
import startUrlInjectable from "./start-url.injectable";
|
||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||
import currentPathInjectable from "../../routes/current-path.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore;
|
||||
currentRouteComponent: IComputedValue<React.ElementType<{}> | undefined | null>;
|
||||
currentRouteComponent: IComputedValue<React.ElementType<{}> | undefined>;
|
||||
startUrl: IComputedValue<string>;
|
||||
subscribeStores: SubscribeStores;
|
||||
currentPath: IComputedValue<string>;
|
||||
}
|
||||
|
||||
export const NonInjectedClusterFrame = observer(({
|
||||
@@ -43,6 +46,7 @@ export const NonInjectedClusterFrame = observer(({
|
||||
currentRouteComponent,
|
||||
startUrl,
|
||||
subscribeStores,
|
||||
currentPath,
|
||||
}: Dependencies) => {
|
||||
useEffect(() => disposer(
|
||||
subscribeStores([
|
||||
@@ -52,6 +56,8 @@ export const NonInjectedClusterFrame = observer(({
|
||||
), []);
|
||||
|
||||
const Component = currentRouteComponent.get();
|
||||
const starting = startUrl.get();
|
||||
const current = currentPath.get();
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -62,7 +68,16 @@ export const NonInjectedClusterFrame = observer(({
|
||||
{
|
||||
Component
|
||||
? <Component />
|
||||
: <Redirect to={startUrl.get()} />
|
||||
// NOTE: this check is to prevent an infinite loop
|
||||
: starting !== current
|
||||
? <Redirect to={startUrl.get()} />
|
||||
: (
|
||||
<div className={styles.centering}>
|
||||
<div className="error">
|
||||
An error has occured. No route can be found matching the current route, which is also the starting route.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</MainLayout>
|
||||
|
||||
@@ -87,6 +102,7 @@ export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFram
|
||||
subscribeStores: di.inject(subscribeStoresInjectable),
|
||||
startUrl: di.inject(startUrlInjectable),
|
||||
currentRouteComponent: di.inject(currentRouteComponentInjectable),
|
||||
currentPath: di.inject(currentPathInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,29 +4,30 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { KubeResource } from "../../../common/rbac";
|
||||
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
|
||||
import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable";
|
||||
import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
const startUrlInjectable = getInjectable({
|
||||
id: "start-url",
|
||||
|
||||
instantiate: (di) => {
|
||||
const isAllowedResource = (resourceName: string) => di.inject(isAllowedResourceInjectable, resourceName);
|
||||
|
||||
const clusterOverviewRoute = di.inject(clusterOverviewRouteInjectable);
|
||||
const workloadOverviewRoute = di.inject(workloadsOverviewRouteInjectable);
|
||||
const clusterOverviewUrl = buildURL(clusterOverviewRoute.path);
|
||||
const workloadOverviewUrl = buildURL(workloadOverviewRoute.path);
|
||||
|
||||
return computed(() => {
|
||||
const resources: KubeResource[] = ["events", "nodes", "pods"];
|
||||
if (clusterOverviewRoute.isEnabled.get()) {
|
||||
return clusterOverviewRoute.path;
|
||||
}
|
||||
|
||||
return resources.every((resourceName) => isAllowedResource(resourceName))
|
||||
? clusterOverviewUrl
|
||||
: workloadOverviewUrl;
|
||||
if (workloadOverviewRoute.isEnabled.get()) {
|
||||
return workloadOverviewRoute.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This will never be executed as `workloadOverviewRoute.isEnabled` always is true. It
|
||||
* is here is guard against accidental changes at a distance within `workloadOverviewRoute`.
|
||||
*/
|
||||
throw new Error("Exhausted all possible starting locations and none are active. This is a bug.");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user