mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Technical requirements for behavioural unit tests (#5084)
* Implement a lot of technical requirements for behavioural unit tests Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced. Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables. Note: another chunk of global shared state is no more. Note: a lot of explicit side effects have been cornered to injectables. Note: a lot of stuff has become reactive as part if this. Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make a directory commonly available Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Require id for <Select /> to prevent non-deterministic renders This was caused by global state in a 3rd party lib: "react-select". Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Specify id for all <Select /> to satisfy previous commit Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent explicit side effect in component by using existing dependency instead Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Extract instantiation of "conf" as injectables for causing side effects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make legacy unit tests for hotbar green and more simple by using the new legacy helper Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Temporarily kludge all unit tests green with a disclaimer about allowing side-effects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent old unit test with side effects from accessing file system Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Migrate to actual typing for di.permitSideEffects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Adapt integration tests to recent changes Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix code style Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix artifact from bad rebase Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Add a deprecation from a review comment Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove change that is not required Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant comment Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix code style Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant file Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix bad merge Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Improve variable name Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Tweak logger interface to be more descriptive Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make injecting legacy singleton always provide new instance Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove conditional typing when not needed Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Improve naming of variable Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove unnecessary code style changes Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove flag for causing side effects from too broad scope Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Override side-effects in unit test using injectable instead of monkey patching Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Flag some side-effects and add general overrides Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix unit tests in CI by removing explicit side-effect Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove explicit side-effect from getting default shell Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce abstraction for getting absolute paths Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using abstraction for getting absolute path to control explicit side effect Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce abstraction for joining paths Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using abstraction for joining paths to control explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix fake implementation for join paths Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix test after removing explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove explicit side effects from kubeconfig-syncs Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix arguments after removing explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make registrators not async for not being needed anymore Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make generalCatalogEntities non-observable, as there is no requirement Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove redundant code Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify logic for registering general catalog entity sources Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add TODO Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Replace function for getting application menu items with reactive solution Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typo in interface name Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove global shared state usages of hot bar store Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant enum Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
1 parent
ab2cd27a27
commit
a277cfcf02
583 files changed
+13073
-6556
No files matched your search
@@ -5,17 +5,14 @@
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import { SidebarCluster } from "../sidebar-cluster";
|
||||
import { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||
|
||||
jest.mock("../../../../common/hotbar-store", () => ({
|
||||
HotbarStore: {
|
||||
getInstance: () => ({
|
||||
isAddedToActive: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../../common/hotbar-store";
|
||||
|
||||
const clusterEntity = new KubernetesCluster({
|
||||
metadata: {
|
||||
@@ -34,6 +31,18 @@ const clusterEntity = new KubernetesCluster({
|
||||
});
|
||||
|
||||
describe("<SidebarCluster/>", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({
|
||||
isAddedToActive: () => {},
|
||||
}) as unknown as HotbarStore);
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<SidebarCluster clusterEntity={clusterEntity}/>);
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { DiContainer, getInjectable } from "@ogre-tools/injectable";
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
import React from "react";
|
||||
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
|
||||
import { SidebarItemRegistration, sidebarItemsInjectionToken } from "./sidebar-items.injectable";
|
||||
import { computed } from "mobx";
|
||||
import routesInjectable from "../../routes/routes.injectable";
|
||||
import { matches, noop } from "lodash/fp";
|
||||
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
|
||||
import { getExtensionRoutePath } from "../../routes/get-extension-route-path";
|
||||
import { navigateToRouteInjectionToken } from "../../../common/front-end-routing/navigate-to-route-injection-token";
|
||||
|
||||
const extensionSidebarItemRegistratorInjectable = getInjectable({
|
||||
id: "extension-sidebar-item-registrator",
|
||||
|
||||
instantiate:
|
||||
(di: DiContainer) =>
|
||||
(
|
||||
extension: LensRendererExtension,
|
||||
extensionInstallationCount: number,
|
||||
) => {
|
||||
const sidebarItemsForExtensionInjectable = getInjectable({
|
||||
id: `sidebar-items-for-extension-${extension.sanitizedExtensionId}-instance-${extensionInstallationCount}`,
|
||||
injectionToken: sidebarItemsInjectionToken,
|
||||
|
||||
instantiate: (di) => {
|
||||
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
|
||||
const routes = di.inject(routesInjectable);
|
||||
|
||||
return computed((): SidebarItemRegistration[] => {
|
||||
const extensionRoutes = routes.get().filter(matches({ extension }));
|
||||
|
||||
return extension.clusterPageMenus.map((registration) => {
|
||||
const targetRoutePath = getExtensionRoutePath(
|
||||
extension,
|
||||
registration.target?.pageId,
|
||||
);
|
||||
|
||||
const targetRoute = extensionRoutes.find(
|
||||
matches({ path: targetRoutePath }),
|
||||
);
|
||||
|
||||
return {
|
||||
id: `${extension.sanitizedExtensionId}-${registration.id}`,
|
||||
extension,
|
||||
orderNumber: 9999,
|
||||
|
||||
parentId: registration.parentId
|
||||
? `${extension.sanitizedExtensionId}-${registration.parentId}`
|
||||
: null,
|
||||
|
||||
title: registration.title,
|
||||
getIcon: registration.components.Icon
|
||||
? () => <registration.components.Icon />
|
||||
: null,
|
||||
|
||||
...(targetRoute
|
||||
? {
|
||||
onClick: () => navigateToRoute(targetRoute),
|
||||
|
||||
isActive: di.inject(
|
||||
routeIsActiveInjectable,
|
||||
targetRoute,
|
||||
),
|
||||
}
|
||||
: { onClick: noop }),
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
di.register(sidebarItemsForExtensionInjectable);
|
||||
},
|
||||
|
||||
injectionToken: extensionRegistratorInjectionToken,
|
||||
});
|
||||
|
||||
export default extensionSidebarItemRegistratorInjectable;
|
||||
@@ -8,9 +8,10 @@ import "./setting-layout.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { navigation } from "../../navigation";
|
||||
import { catalogURL } from "../../../common/routes";
|
||||
import { CloseButton } from "./close-button";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
|
||||
|
||||
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
className?: IClassName;
|
||||
@@ -19,16 +20,22 @@ export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
contentGaps?: boolean;
|
||||
navigation?: React.ReactNode;
|
||||
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
|
||||
closeButtonProps?: { "data-testid"?: string };
|
||||
}
|
||||
|
||||
const defaultProps: Partial<SettingLayoutProps> = {
|
||||
closeButtonProps: {},
|
||||
provideBackButtonNavigation: true,
|
||||
contentGaps: true,
|
||||
back: () => {
|
||||
if (navigation.length <= 1) {
|
||||
navigation.push(catalogURL());
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
||||
const observableHistory = di.inject(observableHistoryInjectable);
|
||||
|
||||
if (observableHistory.length <= 1) {
|
||||
navigateToCatalog();
|
||||
} else {
|
||||
navigation.goBack();
|
||||
observableHistory.goBack();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -68,7 +75,7 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
render() {
|
||||
const {
|
||||
contentClass, provideBackButtonNavigation,
|
||||
contentGaps, navigation, children, back, ...elemProps
|
||||
contentGaps, navigation, children, back, closeButtonProps, ...elemProps
|
||||
} = this.props;
|
||||
const className = cssNames("SettingLayout", { showNavigation: navigation }, this.props.className);
|
||||
|
||||
@@ -89,7 +96,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
{
|
||||
this.props.provideBackButtonNavigation && (
|
||||
<div className="fixed top-[60px]">
|
||||
<CloseButton onClick={back}/>
|
||||
|
||||
<CloseButton onClick={back} {...closeButtonProps}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import siblingTabsInjectable from "../../routes/sibling-tabs.injectable";
|
||||
import { TabLayout } from "./tab-layout-2";
|
||||
import type { HierarchicalSidebarItem } from "./sidebar-items.injectable";
|
||||
|
||||
interface SiblingTabLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
tabs: IComputedValue<HierarchicalSidebarItem[]>;
|
||||
}
|
||||
|
||||
const NonInjectedSiblingsInTabLayout = observer(
|
||||
({ tabs, children }: Dependencies & SiblingTabLayoutProps) => {
|
||||
const dereferencedTabs = tabs.get();
|
||||
|
||||
if (dereferencedTabs.length) {
|
||||
return (
|
||||
<TabLayout tabs={dereferencedTabs}
|
||||
>
|
||||
{children}
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
},
|
||||
);
|
||||
|
||||
export const SiblingsInTabLayout = withInjectables<Dependencies, SiblingTabLayoutProps>(
|
||||
NonInjectedSiblingsInTabLayout,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
tabs: di.inject(siblingTabsInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -6,7 +6,6 @@
|
||||
import styles from "./sidebar-cluster.module.scss";
|
||||
import { observable } from "mobx";
|
||||
import React, { useState } from "react";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||
@@ -16,6 +15,9 @@ import { navigate } from "../../navigation";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
const contextMenu: CatalogEntityContextMenuContext = observable({
|
||||
@@ -60,7 +62,15 @@ function renderLoadingSidebarCluster() {
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: CatalogEntity }) => {
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
interface SidebarClusterProps {
|
||||
clusterEntity: CatalogEntity;
|
||||
}
|
||||
|
||||
const NonInjectedSidebarCluster = observer(({ clusterEntity, hotbarStore }: Dependencies & SidebarClusterProps) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
if (!clusterEntity) {
|
||||
@@ -68,8 +78,7 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
|
||||
}
|
||||
|
||||
const onMenuOpen = () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
const isAddedToActive = HotbarStore.getInstance().isAddedToActive(clusterEntity);
|
||||
const isAddedToActive = hotbarStore.isAddedToActive(clusterEntity);
|
||||
const title = isAddedToActive
|
||||
? "Remove from Hotbar"
|
||||
: "Add to Hotbar";
|
||||
@@ -140,3 +149,14 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const SidebarCluster = withInjectables<Dependencies, SidebarClusterProps>(
|
||||
NonInjectedSidebarCluster,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -25,11 +25,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
background: var(--blue);
|
||||
color: var(--sidebarActiveColor);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
--size: 20px;
|
||||
}
|
||||
@@ -39,10 +34,6 @@
|
||||
$borderSize: 4px;
|
||||
border-left: $borderSize solid transparent;
|
||||
|
||||
&.active {
|
||||
border-left-color: var(--blue);
|
||||
}
|
||||
|
||||
> .SidebarItem {
|
||||
color: var(--textColorPrimary);
|
||||
padding-left: 30px + $borderSize;
|
||||
@@ -56,10 +47,49 @@
|
||||
&.expandable {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
color: var(--sidebarSubmenuActiveColor);
|
||||
background: none;
|
||||
// TODO: Make less flaky.
|
||||
.sidebar-active-status {
|
||||
> .SidebarItem {
|
||||
.nav-item {
|
||||
&.active, &:hover {
|
||||
background: var(--blue);
|
||||
color: var(--sidebarActiveColor);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
&.active {
|
||||
border-left-color: var(--blue);
|
||||
}
|
||||
|
||||
> .SidebarItem {
|
||||
.nav-item {
|
||||
&.active, &:hover {
|
||||
color: var(--sidebarSubmenuActiveColor);
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.expandable {
|
||||
color: var(--textColorPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
&.active {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
&.active, &:hover {
|
||||
color: var(--sidebarSubmenuActiveColor);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,39 +7,28 @@ import "./sidebar-item.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { cssNames, prevDefault, StorageHelper } from "../../utils";
|
||||
import { cssNames, StorageHelper } from "../../utils";
|
||||
import { observer } from "mobx-react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Icon } from "../icon";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import sidebarStorageInjectable, { SidebarStorageState } from "./sidebar-storage/sidebar-storage.injectable";
|
||||
|
||||
export interface SidebarItemProps {
|
||||
/**
|
||||
* Unique id, used in storage and integration tests
|
||||
*/
|
||||
id: string;
|
||||
url: string;
|
||||
className?: string;
|
||||
text: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
isHidden?: boolean;
|
||||
/**
|
||||
* Forces this item to be also show as active or not.
|
||||
*
|
||||
* Default: dynamically checks the location against the `url` props to determine if
|
||||
* this item should be shown as active
|
||||
*/
|
||||
isActive?: boolean;
|
||||
}
|
||||
import sidebarStorageInjectable, {
|
||||
SidebarStorageState,
|
||||
} from "./sidebar-storage/sidebar-storage.injectable";
|
||||
import type { HierarchicalSidebarItem } from "./sidebar-items.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
sidebarStorage: StorageHelper<SidebarStorageState>;
|
||||
}
|
||||
|
||||
export interface SidebarItemProps {
|
||||
item: HierarchicalSidebarItem;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedSidebarItem extends React.Component<SidebarItemProps & Dependencies> {
|
||||
class NonInjectedSidebarItem extends React.Component<
|
||||
SidebarItemProps & Dependencies
|
||||
> {
|
||||
static displayName = "SidebarItem";
|
||||
|
||||
constructor(props: SidebarItemProps & Dependencies) {
|
||||
@@ -48,65 +37,85 @@ class NonInjectedSidebarItem extends React.Component<SidebarItemProps & Dependen
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.props.id;
|
||||
return this.registration.id;
|
||||
}
|
||||
|
||||
@computed get expanded(): boolean {
|
||||
return Boolean(this.props.sidebarStorage.get().expanded[this.id]);
|
||||
}
|
||||
|
||||
@computed get isActive(): boolean {
|
||||
return this.props.isActive ?? isActiveRoute({
|
||||
path: this.props.url,
|
||||
exact: true,
|
||||
});
|
||||
@computed get isExpandable(): boolean {
|
||||
return this.props.item.children.length > 0;
|
||||
}
|
||||
|
||||
@computed get isExpandable(): boolean {
|
||||
return React.Children.count(this.props.children) > 0;
|
||||
@computed get isActive(): boolean {
|
||||
return this.props.item.isActive.get();
|
||||
}
|
||||
|
||||
get registration() {
|
||||
return this.props.item.registration;
|
||||
}
|
||||
|
||||
toggleExpand = () => {
|
||||
this.props.sidebarStorage.merge(draft => {
|
||||
this.props.sidebarStorage.merge((draft) => {
|
||||
draft.expanded[this.id] = !draft.expanded[this.id];
|
||||
});
|
||||
};
|
||||
|
||||
renderSubMenu() {
|
||||
const { isExpandable, expanded, isActive } = this;
|
||||
const { isExpandable, expanded } = this;
|
||||
|
||||
if (!isExpandable || !expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={cssNames("sub-menu", { active: isActive })}>
|
||||
{this.props.children}
|
||||
<ul className={cssNames("sub-menu", { active: this.isActive })}>
|
||||
{this.props.item.children.map(item => <SidebarItem key={item.registration.id} item={item} />)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isHidden, icon, text, url, className } = this.props;
|
||||
|
||||
if (isHidden) return null;
|
||||
|
||||
const { isActive, id, expanded, isExpandable, toggleExpand } = this;
|
||||
const classNames = cssNames("SidebarItem", className);
|
||||
|
||||
return (
|
||||
<div className={classNames} data-test-id={id}>
|
||||
<div
|
||||
className={cssNames("SidebarItem")}
|
||||
data-testid="sidebar-item"
|
||||
data-test-id={this.id}
|
||||
data-id-test={this.id}
|
||||
data-is-active-test={this.isActive}
|
||||
data-parent-id-test={this.registration.parentId}
|
||||
>
|
||||
<NavLink
|
||||
to={url}
|
||||
isActive={() => isActive}
|
||||
className={cssNames("nav-item flex gaps align-center", { expandable: isExpandable })}
|
||||
onClick={isExpandable ? prevDefault(toggleExpand) : undefined}>
|
||||
{icon}
|
||||
<span className="link-text box grow">{text}</span>
|
||||
{isExpandable && <Icon
|
||||
className="expand-icon box right"
|
||||
material={expanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}
|
||||
/>}
|
||||
to={""}
|
||||
isActive={() => this.isActive}
|
||||
|
||||
className={cssNames("nav-item flex gaps align-center", {
|
||||
expandable: this.isExpandable,
|
||||
})}
|
||||
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.isExpandable) {
|
||||
this.toggleExpand();
|
||||
} else {
|
||||
this.registration.onClick();
|
||||
}
|
||||
}}
|
||||
data-testid={`sidebar-item-link-for-${this.id}`}
|
||||
>
|
||||
{this.registration.getIcon?.()}
|
||||
<span className="link-text box grow">{this.registration.title}</span>
|
||||
{this.isExpandable && (
|
||||
<Icon
|
||||
className="expand-icon box right"
|
||||
material={
|
||||
this.expanded ? "keyboard_arrow_up" : "keyboard_arrow_down"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</NavLink>
|
||||
{this.renderSubMenu()}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
|
||||
import {
|
||||
filter,
|
||||
flatMap,
|
||||
identity,
|
||||
invokeMap,
|
||||
isEmpty,
|
||||
map,
|
||||
orderBy,
|
||||
overSome,
|
||||
some,
|
||||
} from "lodash/fp";
|
||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
|
||||
export interface SidebarItemRegistration {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
title: React.ReactNode;
|
||||
onClick: () => void;
|
||||
getIcon?: () => React.ReactNode;
|
||||
isActive?: IComputedValue<boolean>;
|
||||
isVisible?: IComputedValue<boolean>;
|
||||
orderNumber: number;
|
||||
extension?: LensRendererExtension;
|
||||
}
|
||||
|
||||
export const sidebarItemsInjectionToken = getInjectionToken<
|
||||
IComputedValue<SidebarItemRegistration[]>
|
||||
>({ id: "sidebar-items-injection-token" });
|
||||
|
||||
export interface HierarchicalSidebarItem {
|
||||
registration: SidebarItemRegistration;
|
||||
children: HierarchicalSidebarItem[];
|
||||
isActive: IComputedValue<boolean>;
|
||||
}
|
||||
|
||||
const sidebarItemsInjectable = getInjectable({
|
||||
id: "sidebar-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const extensions = di.inject(rendererExtensionsInjectable);
|
||||
|
||||
return computed((): HierarchicalSidebarItem[] => {
|
||||
const enabledExtensions = extensions.get();
|
||||
|
||||
const sidebarItemRegistrations = di.injectMany(
|
||||
sidebarItemsInjectionToken,
|
||||
);
|
||||
|
||||
const registrations = pipeline(
|
||||
sidebarItemRegistrations,
|
||||
flatMap(dereference),
|
||||
|
||||
filter(
|
||||
overSome([
|
||||
isNonExtensionSidebarItem,
|
||||
isEnabledExtensionSidebarItemFor(enabledExtensions),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const getSidebarItemsHierarchy = (
|
||||
registrations: SidebarItemRegistration[],
|
||||
) => {
|
||||
const _getSidebarItemsHierarchy = (
|
||||
parentId: string,
|
||||
): HierarchicalSidebarItem[] =>
|
||||
pipeline(
|
||||
registrations,
|
||||
|
||||
filter((item) => item.parentId === parentId),
|
||||
|
||||
map((registration) => {
|
||||
const children = _getSidebarItemsHierarchy(registration.id);
|
||||
|
||||
return {
|
||||
registration,
|
||||
children,
|
||||
|
||||
isActive: computed(() => {
|
||||
if (isEmpty(children)) {
|
||||
return registration.isActive ? registration.isActive.get() : false;
|
||||
}
|
||||
|
||||
return pipeline(
|
||||
children,
|
||||
invokeMap("isActive.get"),
|
||||
some(identity),
|
||||
);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
filter(item => item.registration.isVisible?.get() ?? true),
|
||||
|
||||
(items) =>
|
||||
orderBy(
|
||||
["registration.orderNumber"],
|
||||
["asc"],
|
||||
items,
|
||||
),
|
||||
);
|
||||
|
||||
return _getSidebarItemsHierarchy(null);
|
||||
};
|
||||
|
||||
return getSidebarItemsHierarchy(registrations);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const isNonExtensionSidebarItem = (sidebarItem: SidebarItemRegistration) =>
|
||||
!sidebarItem.extension;
|
||||
|
||||
const isEnabledExtensionSidebarItemFor =
|
||||
(enabledExtensions: LensRendererExtension[]) =>
|
||||
(sidebarItem: SidebarItemRegistration) =>
|
||||
!!enabledExtensions.find((x) => x === sidebarItem.extension);
|
||||
|
||||
const dereference = (items: IComputedValue<SidebarItemRegistration[]>) =>
|
||||
items.get();
|
||||
|
||||
export default sidebarItemsInjectable;
|
||||
@@ -4,137 +4,61 @@
|
||||
*/
|
||||
|
||||
import styles from "./sidebar.module.scss";
|
||||
import type { TabLayoutRoute } from "./tab-layout";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry, ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries";
|
||||
import { SidebarItem } from "./sidebar-item";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { SidebarCluster } from "./sidebar-cluster";
|
||||
import { renderTabRoutesSidebarItems } from "./tab-routes-sidebar-items";
|
||||
import { ConfigSidebarItem } from "../+config/sidebar-item";
|
||||
import { ClusterSidebarItem } from "../+cluster/sidebar-item";
|
||||
import { NodesSidebarItem } from "../+nodes/sidebar-item";
|
||||
import { WorkloadsSidebarItem } from "../+workloads/sidebar-item";
|
||||
import { NetworkSidebarItem } from "../+network/sidebar-item";
|
||||
import { StorageSidebarItem } from "../+storage/sidebar-item";
|
||||
import { NamespacesSidebarItem } from "../+namespaces/sidebar-item";
|
||||
import { EventsSidebarItem } from "../+events/sidebar-item";
|
||||
import { HelmSidebarItem } from "../+helm/sidebar-item";
|
||||
import { UserManagementSidebarItem } from "../+user-management/sidebar-item";
|
||||
import { CustomResourcesSidebarItem } from "../+custom-resources/sidebar-item";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import sidebarItemsInjectable, {
|
||||
HierarchicalSidebarItem,
|
||||
} from "./sidebar-items.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export interface SidebarProps {
|
||||
className?: string;
|
||||
interface Dependencies {
|
||||
sidebarItems: IComputedValue<HierarchicalSidebarItem[]>;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Sidebar extends React.Component<SidebarProps> {
|
||||
class NonInjectedSidebar extends React.Component<Dependencies> {
|
||||
static displayName = "Sidebar";
|
||||
|
||||
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
|
||||
if (!menu.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
const subMenus = ClusterPageMenuRegistry.getInstance().getSubItems(menu);
|
||||
const clusterPageRegistry = ClusterPageRegistry.getInstance();
|
||||
|
||||
for (const subMenu of subMenus) {
|
||||
const page = clusterPageRegistry.getByPageTarget(subMenu.target);
|
||||
|
||||
if (!page) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { extensionId, id: pageId, url, components } = page;
|
||||
|
||||
if (subMenu.components.Icon) {
|
||||
console.warn(
|
||||
"ClusterPageMenuRegistration has components.Icon defined and a valid parentId. Icon will not be displayed",
|
||||
{
|
||||
id: subMenu.id,
|
||||
parentId: subMenu.parentId,
|
||||
target: subMenu.target,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
routes.push({
|
||||
routePath: url,
|
||||
url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }),
|
||||
title: subMenu.title,
|
||||
component: components.Page,
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
renderRegisteredMenus() {
|
||||
return ClusterPageMenuRegistry.getInstance().getRootItems().map((menuItem, index) => {
|
||||
const registeredPage = ClusterPageRegistry.getInstance().getByPageTarget(menuItem.target);
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
const id = `registered-item-${index}`;
|
||||
let pageUrl: string;
|
||||
let isActive = false;
|
||||
|
||||
if (registeredPage) {
|
||||
const { extensionId, id: pageId } = registeredPage;
|
||||
|
||||
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
||||
isActive = isActiveRoute(registeredPage.url);
|
||||
} else if (tabRoutes.length > 0) {
|
||||
pageUrl = tabRoutes[0].url;
|
||||
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarItem
|
||||
key={id}
|
||||
id={id}
|
||||
url={pageUrl}
|
||||
isActive={isActive}
|
||||
text={menuItem.title}
|
||||
icon={<menuItem.components.Icon/>}
|
||||
>
|
||||
{renderTabRoutesSidebarItems(tabRoutes)}
|
||||
</SidebarItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get clusterEntity() {
|
||||
return catalogEntityRegistry.activeEntity;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cssNames("flex flex-col", className)} data-testid="cluster-sidebar">
|
||||
<SidebarCluster clusterEntity={this.clusterEntity}/>
|
||||
<div className={styles.sidebarNav}>
|
||||
<ClusterSidebarItem />
|
||||
<NodesSidebarItem />
|
||||
<WorkloadsSidebarItem />
|
||||
<ConfigSidebarItem />
|
||||
<NetworkSidebarItem />
|
||||
<StorageSidebarItem />
|
||||
<NamespacesSidebarItem />
|
||||
<EventsSidebarItem />
|
||||
<HelmSidebarItem />
|
||||
<UserManagementSidebarItem />
|
||||
<CustomResourcesSidebarItem />
|
||||
{this.renderRegisteredMenus()}
|
||||
<div className={cssNames("flex flex-col")} data-testid="cluster-sidebar">
|
||||
<SidebarCluster clusterEntity={this.clusterEntity} />
|
||||
|
||||
<div className={`${styles.sidebarNav} sidebar-active-status`}>
|
||||
{this.props.sidebarItems.get().map((
|
||||
hierarchicalSidebarItem: HierarchicalSidebarItem,
|
||||
) => (
|
||||
<SidebarItem
|
||||
item={hierarchicalSidebarItem}
|
||||
key={hierarchicalSidebarItem.registration.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Sidebar = withInjectables<Dependencies>(
|
||||
NonInjectedSidebar,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
sidebarItems: di.inject(sidebarItemsInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./tab-layout.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { ErrorBoundary } from "../error-boundary";
|
||||
import type { HierarchicalSidebarItem } from "./sidebar-items.injectable";
|
||||
|
||||
export interface TabLayoutProps {
|
||||
tabs?: HierarchicalSidebarItem[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabLayout = observer(
|
||||
({
|
||||
tabs = [],
|
||||
children,
|
||||
}: TabLayoutProps) => {
|
||||
const hasTabs = tabs.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cssNames("TabLayout")}
|
||||
data-testid="tab-layout"
|
||||
>
|
||||
|
||||
{hasTabs && (
|
||||
<Tabs center>
|
||||
{tabs.map(({ registration, isActive }) => {
|
||||
const active = isActive.get();
|
||||
|
||||
return (
|
||||
<Tab
|
||||
onClick={registration.onClick}
|
||||
key={registration.id}
|
||||
label={registration.title}
|
||||
active={active}
|
||||
data-is-active-test={active}
|
||||
data-testid={`tab-link-for-${registration.id}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import { SidebarItem } from "./sidebar-item";
|
||||
import type { TabLayoutRoute } from "./tab-layout";
|
||||
|
||||
function withId(src: TabLayoutRoute) {
|
||||
return {
|
||||
...src,
|
||||
id: `tab-route-item-${src.url ?? src.routePath}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a sidebar item for each route
|
||||
*
|
||||
* NOTE: this cannot be a component because then the `<SidebarItem>.isExandable`
|
||||
* check will always return true because a component that renders to `null` is
|
||||
* still a present child to the parent `<SidebarItem>`
|
||||
*/
|
||||
export const renderTabRoutesSidebarItems = (routes: TabLayoutRoute[]) => (
|
||||
routes
|
||||
.map(withId)
|
||||
.map(({ title, routePath, url = routePath, exact = true, id }) => (
|
||||
<SidebarItem
|
||||
key={id}
|
||||
id={id}
|
||||
url={url}
|
||||
text={title}
|
||||
isActive={isActiveRoute({ path: routePath, exact })}
|
||||
/>
|
||||
))
|
||||
);
|
||||
@@ -7,41 +7,31 @@ import React from "react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { TopBar } from "./top-bar";
|
||||
import * as vars from "../../../../common/vars";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
|
||||
|
||||
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
|
||||
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
|
||||
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
|
||||
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
|
||||
jest.mock("../../../../common/ipc");
|
||||
jest.mock("../../../ipc");
|
||||
|
||||
jest.mock("../../../../common/vars", () => {
|
||||
const { SemVer } = require("semver");
|
||||
|
||||
return {
|
||||
...jest.requireActual<{}>("../../../../common/vars"),
|
||||
__esModule: true,
|
||||
isWindows: null,
|
||||
isLinux: null,
|
||||
appSemVer: new SemVer("1.0.0"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<TopBar/> in Windows and Linux", () => {
|
||||
let render: DiRender;
|
||||
let di: DiContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
di.override(isMacInjectable, () => false);
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
await di.runSetups();
|
||||
mockFs();
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
@@ -51,8 +41,7 @@ describe("<TopBar/> in Windows and Linux", () => {
|
||||
});
|
||||
|
||||
it("shows window controls on Windows", () => {
|
||||
mockConfig.isWindows = true;
|
||||
mockConfig.isLinux = false;
|
||||
di.override(isWindowsInjectable, () => true);
|
||||
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
@@ -63,8 +52,7 @@ describe("<TopBar/> in Windows and Linux", () => {
|
||||
});
|
||||
|
||||
it("shows window controls on Linux", () => {
|
||||
mockConfig.isWindows = false;
|
||||
mockConfig.isLinux = true;
|
||||
di.override(isLinuxInjectable, () => true);
|
||||
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
@@ -75,7 +63,7 @@ describe("<TopBar/> in Windows and Linux", () => {
|
||||
});
|
||||
|
||||
it("triggers ipc events on click", () => {
|
||||
mockConfig.isWindows = true;
|
||||
di.override(isWindowsInjectable, () => true);
|
||||
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import { Icon } from "../../icon";
|
||||
import { observable } from "mobx";
|
||||
import { ipcRendererOn } from "../../../../common/ipc";
|
||||
import { watchHistoryState } from "../../../remote-helpers/history-updater";
|
||||
import { isActiveRoute, navigate } from "../../../navigation";
|
||||
import { catalogRoute, catalogURL } from "../../../../common/routes";
|
||||
import { cssNames } from "../../../utils";
|
||||
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
@@ -21,10 +19,13 @@ import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc"
|
||||
import { WindowAction } from "../../../../common/ipc/window";
|
||||
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
|
||||
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
|
||||
|
||||
export interface TopBarProps {}
|
||||
import navigateToCatalogInjectable, { NavigateToCatalog } from "../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
import catalogRouteInjectable from "../../../../common/front-end-routing/routes/catalog/catalog-route.injectable";
|
||||
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
catalogRouteIsActive: IComputedValue<boolean>;
|
||||
items: IComputedValue<TopBarRegistration[]>;
|
||||
isWindows: boolean;
|
||||
isLinux: boolean;
|
||||
@@ -41,7 +42,7 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
|
||||
nextEnabled.set(state);
|
||||
});
|
||||
|
||||
const NonInjectedTopBar = observer(({ items, isWindows, isLinux }: TopBarProps & Dependencies) => {
|
||||
const NonInjectedTopBar = observer(({ items, navigateToCatalog, catalogRouteIsActive, isWindows, isLinux }: Dependencies) => {
|
||||
const elem = useRef<HTMLDivElement>();
|
||||
|
||||
const openAppContextMenu = () => {
|
||||
@@ -49,7 +50,7 @@ const NonInjectedTopBar = observer(({ items, isWindows, isLinux }: TopBarProps &
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
navigate(catalogURL());
|
||||
navigateToCatalog();
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
@@ -98,7 +99,7 @@ const NonInjectedTopBar = observer(({ items, isWindows, isLinux }: TopBarProps &
|
||||
data-testid="home-button"
|
||||
material="home"
|
||||
onClick={goHome}
|
||||
disabled={isActiveRoute(catalogRoute)}
|
||||
disabled={catalogRouteIsActive.get()}
|
||||
/>
|
||||
<Icon
|
||||
data-testid="history-back"
|
||||
@@ -149,11 +150,23 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => (
|
||||
})
|
||||
);
|
||||
|
||||
export const TopBar = withInjectables<Dependencies, TopBarProps>(NonInjectedTopBar, {
|
||||
getProps: (di, props) => ({
|
||||
items: di.inject(topBarItemsInjectable),
|
||||
isLinux: di.inject(isLinuxInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
export const TopBar = withInjectables<Dependencies>(
|
||||
NonInjectedTopBar,
|
||||
{
|
||||
getProps: (di) => {
|
||||
const catalogRoute = di.inject(catalogRouteInjectable);
|
||||
|
||||
return {
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
items: di.inject(topBarItemsInjectable),
|
||||
isLinux: di.inject(isLinuxInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
|
||||
catalogRouteIsActive: di.inject(
|
||||
routeIsActiveInjectable,
|
||||
catalogRoute,
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user