1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Topbar history buttons (#3696)

* HistoryStore initial draft

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding history icons

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Jump history using electron methods

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using 1 topbar component for all views

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add Catalog title to sidebar

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Show cluster name in sidebar

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Updating topbar tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing Welcome tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Removing padding for external topbar items

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix cluster name prop

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using material icons for history buttons

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-08-31 14:44:47 +03:00 committed by GitHub
parent 9d6bed8786
commit 7981e79cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 163 additions and 132 deletions

View File

@ -244,6 +244,7 @@ export class WindowManager extends Singleton {
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
} else {
webContents.getFocusedWebContents()?.reload();
webContents.getFocusedWebContents()?.clearHistory();
}
}

View File

@ -28,3 +28,8 @@
color: var(--textColorAccent);
font-size: small;
}
.catalog {
@apply p-5 font-bold text-2xl;
color: var(--textColorAccent);
}

View File

@ -58,6 +58,7 @@ export function CatalogMenu(props: Props) {
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
<StylesProvider injectFirst>
<div className="flex flex-col w-full">
<div className={styles.catalog}>Catalog</div>
<TreeView
defaultExpanded={["catalog"]}
defaultCollapseIcon={<Icon material="expand_more"/>}

View File

@ -43,7 +43,7 @@ import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
import { CatalogMenu } from "./catalog-menu";
import { HotbarIcon } from "../hotbar/hotbar-icon";
import { RenderDelay } from "../render-delay/render-delay";
import { CatalogTopbar } from "../cluster-manager/catalog-topbar";
import { TopBar } from "../layout/topbar";
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
@ -247,7 +247,7 @@ export class Catalog extends React.Component<Props> {
return (
<>
<CatalogTopbar/>
<TopBar/>
<MainLayout sidebar={this.renderNavigation()}>
<div className="p-6 h-full">
{ this.renderList() }

View File

@ -26,6 +26,15 @@ import { Welcome } from "../welcome";
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
import { defaultWidth } from "../welcome";
jest.mock(
"electron",
() => ({
ipcRenderer: {
on: jest.fn(),
},
})
);
describe("<Welcome/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();

View File

@ -26,8 +26,8 @@ import Carousel from "react-material-ui-carousel";
import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars";
import { WelcomeMenuRegistry } from "../../../extensions/registries";
import { WelcomeTopbar } from "../cluster-manager/welcome-topbar";
import { WelcomeBannerRegistry } from "../../../extensions/registries";
import { TopBar } from "../layout/topbar";
export const defaultWidth = 320;
@ -49,7 +49,7 @@ export class Welcome extends React.Component {
return (
<>
<WelcomeTopbar/>
<TopBar/>
<div className="flex justify-center Welcome align-center">
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
{welcomeBanner.length > 0 ? (

View File

@ -72,6 +72,7 @@ import { catalogEntityRegistry } from "../api/catalog-entity-registry";
import { getHostedClusterId } from "../utils";
import { ClusterStore } from "../../common/cluster-store";
import type { ClusterId } from "../../common/cluster-types";
import { watchHistoryState } from "../remote-helpers/history-updater";
@observer
export class App extends React.Component {
@ -128,7 +129,9 @@ export class App extends React.Component {
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], {
preload: true,
})
}),
watchHistoryState()
]);
}

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { welcomeURL } from "../../../common/routes";
import { navigate } from "../../navigation";
import { Icon } from "../icon";
import { TopBar } from "../layout/topbar";
export function CatalogTopbar() {
return (
<TopBar label="Catalog">
<div>
<Icon
style={{ cursor: "default" }}
material="close"
onClick={() => navigate(welcomeURL())}
tooltip="Close Catalog"
/>
</div>
</TopBar>
);
}

View File

@ -1,61 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { observer } from "mobx-react";
import React from "react";
import { previousActiveTab } from "../+catalog";
import { ClusterStore } from "../../../common/cluster-store";
import { catalogURL } from "../../../common/routes";
import { navigate } from "../../navigation";
import { Icon } from "../icon";
import { TopBar } from "../layout/topbar";
import type { RouteComponentProps } from "react-router";
import type { ClusterViewRouteParams } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
import { TooltipPosition } from "../tooltip";
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
}
export const ClusterTopbar = observer((props: Props) => {
const getCluster = (): Cluster | undefined => {
return ClusterStore.getInstance().getById(props.match.params.clusterId);
};
return (
<TopBar label={getCluster()?.name}>
<div>
<Icon
style={{ cursor: "default" }}
material="close"
onClick={() => {
navigate(`${catalogURL()}/${previousActiveTab.get()}`);
}}
tooltip={{
preferredPositions: TooltipPosition.BOTTOM_RIGHT,
children: "Back to Catalog"
}}
/>
</div>
</TopBar>
);
});

View File

@ -34,7 +34,7 @@ import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation";
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
import { previousActiveTab } from "../+catalog";
import { ClusterTopbar } from "./cluster-topbar";
import { TopBar } from "../layout/topbar";
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
}
@ -105,7 +105,7 @@ export class ClusterView extends React.Component<Props> {
render() {
return (
<div className="ClusterView flex column align-center">
<ClusterTopbar {...this.props}/>
<TopBar/>
{this.renderStatus()}
</div>
);

View File

@ -20,11 +20,46 @@
*/
import React from "react";
import { render } from "@testing-library/react";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "../topbar";
import { TopBarRegistry } from "../../../../extensions/registries";
jest.mock(
"electron",
() => ({
ipcRenderer: {
on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => {
if (channel === "history:can-go-back") {
listener({}, true);
}
if (channel === "history:can-go-forward") {
listener({}, true);
}
}
),
},
})
);
const goBack = jest.fn();
const goForward = jest.fn();
jest.mock("@electron/remote", () => {
return {
webContents: {
getFocusedWebContents: () => {
return {
goBack,
goForward
};
}
}
};
});
describe("<TopBar/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
@ -35,15 +70,38 @@ describe("<TopBar/>", () => {
});
it("renders w/o errors", () => {
const { container } = render(<TopBar label="test bar" />);
const { container } = render(<TopBar/>);
expect(container).toBeInstanceOf(HTMLElement);
});
it("renders title", async () => {
const { getByTestId } = render(<TopBar label="topbar" />);
it("renders history arrows", async () => {
const { getByTestId } = render(<TopBar/>);
expect(await getByTestId("topbarLabel")).toHaveTextContent("topbar");
expect(await getByTestId("history-back")).toBeInTheDocument();
expect(await getByTestId("history-forward")).toBeInTheDocument();
});
it("enables arrow by ipc event", async () => {
const { getByTestId } = render(<TopBar/>);
expect(await getByTestId("history-back")).not.toHaveClass("disabled");
expect(await getByTestId("history-forward")).not.toHaveClass("disabled");
});
it("triggers browser history back and forward", async () => {
const { getByTestId } = render(<TopBar/>);
const prevButton = await getByTestId("history-back");
const nextButton = await getByTestId("history-forward");
fireEvent.click(prevButton);
expect(goBack).toBeCalled();
fireEvent.click(nextButton);
expect(goForward).toBeCalled();
});
it("renders items", async () => {
@ -58,7 +116,7 @@ describe("<TopBar/>", () => {
}
]);
const { getByTestId } = render(<TopBar label="topbar" />);
const { getByTestId } = render(<TopBar/>);
expect(await getByTestId(testId)).toHaveTextContent(text);
});

View File

@ -45,4 +45,12 @@
padding: $padding;
text-align: center;
}
.cluster-name {
padding: 1.25rem;
font-weight: bold;
font-size: 1.5rem;
word-break: break-all;
color: var(--textColorAccent);
}
}

View File

@ -40,6 +40,8 @@ import { SidebarItem } from "./sidebar-item";
import { Apps } from "../+apps";
import * as routes from "../../../common/routes";
import { Config } from "../+config";
import { ClusterStore } from "../../../common/cluster-store";
import { App } from "../app";
interface Props {
className?: string;
@ -181,6 +183,9 @@ export class Sidebar extends React.Component<Props> {
return (
<div className={cssNames(Sidebar.displayName, "flex column", className)}>
<div className="cluster-name">
{ClusterStore.getInstance().getById(App.clusterId)?.name}
</div>
<div className={cssNames("sidebar-nav flex column box grow-fixed")}>
<SidebarItem
id="cluster"

View File

@ -30,11 +30,8 @@
grid-area: topbar;
}
.title {
@apply font-bold px-6;
color: var(--textColorAccent);
align-items: center;
display: flex;
.history {
@apply flex items-center;
}
.controls {

View File

@ -20,15 +20,30 @@
*/
import styles from "./topbar.module.css";
import React from "react";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { TopBarRegistry } from "../../../extensions/registries";
import { Icon } from "../icon";
import { webContents } from "@electron/remote";
import { observable } from "mobx";
import { ipcRendererOn } from "../../../common/ipc";
import { watchHistoryState } from "../../remote-helpers/history-updater";
interface Props extends React.HTMLAttributes<any> {
label: React.ReactNode;
}
export const TopBar = observer(({ label, children, ...rest }: Props) => {
const prevEnabled = observable.box(false);
const nextEnabled = observable.box(false);
ipcRendererOn("history:can-go-back", (event, state: boolean) => {
prevEnabled.set(state);
});
ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
nextEnabled.set(state);
});
export const TopBar = observer(({ children, ...rest }: Props) => {
const renderRegisteredItems = () => {
const items = TopBarRegistry.getInstance().getItems();
@ -37,7 +52,7 @@ export const TopBar = observer(({ label, children, ...rest }: Props) => {
}
return (
<div className="px-6">
<div>
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
@ -53,9 +68,38 @@ export const TopBar = observer(({ label, children, ...rest }: Props) => {
);
};
const goBack = () => {
webContents.getFocusedWebContents()?.goBack();
};
const goForward = () => {
webContents.getFocusedWebContents()?.goForward();
};
useEffect(() => {
const disposer = watchHistoryState();
return () => disposer();
}, []);
return (
<div className={styles.topBar} {...rest}>
<div className={styles.title} data-testid="topbarLabel">{label}</div>
<div className={styles.history}>
<Icon
data-testid="history-back"
material="arrow_back"
className="ml-5"
onClick={goBack}
disabled={!prevEnabled.get()}
/>
<Icon
data-testid="history-forward"
material="arrow_forward"
className="ml-5"
onClick={goForward}
disabled={!nextEnabled.get()}
/>
</div>
<div className={styles.controls}>
{renderRegisteredItems()}
{children}

View File

@ -19,12 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { TopBar } from "../layout/topbar";
import { webContents } from "@electron/remote";
import { reaction } from "mobx";
import { broadcastMessage } from "../../common/ipc";
import { navigation } from "../navigation";
export function WelcomeTopbar() {
return (
<TopBar label="Welcome">
</TopBar>
);
export function watchHistoryState() {
return reaction(() => navigation.location, () => {
broadcastMessage("history:can-go-back", webContents.getFocusedWebContents()?.canGoBack());
broadcastMessage("history:can-go-forward", webContents.getFocusedWebContents()?.canGoForward());
});
}