blob: 6180e96fe55c164329d1d72913be9915c9edc806 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
import React from "react";
import {
Avatar,
Button,
ButtonVariant,
DropdownToggle,
Page,
PageHeader,
SkipToContent,
PageHeaderTools,
PageHeaderToolsGroup,
PageHeaderToolsItem,
Nav,
NavExpandable,
NavItem,
NavList,
PageSidebar
} from "@patternfly/react-core";
import { HashRouter as Router, Switch, Route, Link, Redirect } from "react-router-dom";
import accessibleStyles from "@patternfly/patternfly/utilities/Accessibility/accessibility.css";
import { css } from "@patternfly/react-styles";
import { PowerOffIcon } from "@patternfly/react-icons";
import DropdownMenu from "../../common/DropdownMenu";
import ConnectPage from "../../connect/connectPage";
import DashboardPage from "./dashboardPage";
import OverviewPage from "../overviewPage";
import DetailsTablePage from "../../details/detailsTablePage";
import EntitiesPage from "../../details/entitiesPage";
import TopologyPage from "../../topology/topologyPage";
import MessageFlowPage from "../../chord/chordPage";
import SchemaPage from "../../details/schema/schemaPage";
import LogDetails from "../logDetails";
import ConnectForm from "../../connect/connect-form";
import NotificationDrawer from "./notificationDrawer";
import { utils } from "../../common/amqp/utilities";
import throughputData from "./throughputData";
import inflightData from "./inflightData";
import img_avatar from "../../assets/img_avatar.svg";
const SUPPRESS_NOTIFICATIONS = "noNotify";
class PageLayout extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
connected: false,
connecting: false,
isConnectFormOpen: false,
activeGroup: "overview",
activeItem: "dashboard",
isNavOpenDesktop: true,
isNavOpenMobile: false,
isMobileView: false,
user: "anonymous",
timePeriod: 60,
suppress: JSON.parse(localStorage.getItem(SUPPRESS_NOTIFICATIONS)) || false
};
this.isDropdownOpen = false;
this.service = this.props.service;
this.service.setHooks({ setLocation: this.setLocation });
this.nav = {
overview: [
{ name: "dashboard" },
{ name: "routers", pre: true },
{ name: "addresses", pre: true },
{ name: "links", pre: true },
{ name: "connections", pre: true },
{ name: "logs", pre: true }
],
visualizations: [{ name: "topology" }, { name: "flow", title: "Message flow" }],
details: [{ name: "entities" }, { name: "schema" }]
};
this.state.connecting = true;
this.tryInitialConnect();
}
componentDidMount = () => {
this.chartTimer = setInterval(this.updateCharts, 1000);
this.throughputChartData = new throughputData(this.service);
this.inflightChartData = new inflightData(this.service);
this.updateCharts();
document.title = this.props.config.title;
this.idleUnregister = idle(1000 * 60 * 60, this.handleIdleTimeout);
};
componentWillUnmount = () => {
if (this.chartTimer) {
this.throughputChartData.stop();
this.inflightChartData.stop();
clearInterval(this.chartTimer);
}
if (this.idleUnregister) {
this.idleUnregister();
}
};
handleIdleTimeout = () => {
this.props.history.replace(
`${this.props.location.pathname}${this.props.location.search}`
);
};
tryInitialConnect = () => {
const defaultPort = window.location.protocol.startsWith("https") ? "443" : "80";
const connectOptions = {
address: window.location.hostname,
port: window.location.port === "" ? defaultPort : window.location.port,
timeout: 2000,
reconnect: true
};
this.service.connect(connectOptions).then(
() => {
this.handleConnect("/dashboard");
},
() => {
//this.service.disconnect();
this.props.history.replace("/");
this.setState({ connecting: false });
}
);
};
// the connection to the routers was lost
setLocation = whatHappened => {
if (whatHappened === "disconnect") {
this.handleAddNotification(
"event",
"Connection to router dropped",
new Date(),
"warning"
);
this.lastLocation = this.props.location.pathname;
this.setState({ connected: false });
} else if (whatHappened === "reconnect") {
this.throughputChartData.reset();
this.handleAddNotification(
"event",
"Connection to router resumed",
new Date(),
"info"
);
this.redirect = true;
let to = "/dashboard";
if (this.lastLocation) {
to = this.lastLocation;
}
this.props.history.push(to);
this.setState({
isConnectFormOpen: false,
connected: true
});
}
};
updateCharts = () => {
this.throughputChartData.updateData();
this.inflightChartData.updateData();
};
onDropdownToggle = () => {
this.isDropdownOpen = !this.isDropdownOpen;
this.dropdownRef.show(this.isDropdownOpen);
};
handleDropdownLogout = () => {
// called from the user dropdown menu
// The only menu item is logout
// We must have been connected to get here
this.handleConnect();
this.dropdownRef.show(false);
this.isDropdownOpen = false;
};
handleConnect = (connectPath, result) => {
if (this.state.connected) {
this.setState({ connecting: false, connected: false }, () => {
this.handleConnectCancel();
this.service.disconnect();
this.handleAddNotification("event", "Manually disconnected", new Date(), "info");
});
} else {
this.schema = this.service.schema;
if (connectPath === "/") connectPath = "/dashboard";
const activeItem = connectPath.split("/").pop();
// find the active group for this item
let activeGroup = "overview";
for (const group in this.nav) {
if (this.nav[group].some(item => item.name === activeItem)) {
activeGroup = group;
break;
}
}
this.handleAddNotification(
"event",
`Console connected to router`,
new Date(),
"success",
true
);
//this.redirect = true;
let user = "anonymous";
let parts = this.service.management.connection.getReceiverAddress().split("/");
parts[parts.length - 1] = "$management";
let router = parts.join("/");
// get connections for router to which console is connected
this.service.management.topology.fetchEntity(
router,
"connection",
[],
(_nodeId, _entity, response) => {
response.results.some(result => {
let c = utils.flatten(response.attributeNames, result);
if (utils.isConsole(c)) {
user = c.user;
return true;
}
return false;
});
this.props.history.replace(connectPath);
this.setState({
user,
activeItem,
activeGroup,
connected: true,
isConnectFormOpen: false
});
}
);
}
};
onNavSelect = result => {
this.setState({
activeItem: result.itemId,
activeGroup: result.groupId
});
};
toggleConnectForm = () => {
this.setState({ isConnectFormOpen: !this.state.isConnectFormOpen });
};
handleConnectCancel = () => {
this.setState({ isConnectFormOpen: false });
};
onNavToggleDesktop = () => {
this.setState({
isNavOpenDesktop: !this.state.isNavOpenDesktop
});
};
onNavToggleMobile = () => {
this.setState({
isNavOpenMobile: !this.state.isNavOpenMobile
});
};
onPageResize = ({ mobileView, windowSize }) => {
this.setState({
isMobileView: mobileView
});
};
handleUserMenuHide = () => {
this.isDropdownOpen = false;
this.dropdownRef.show(false);
};
isConnected = () => {
return this.state.connected;
};
handleAddNotification = (section, message, timestamp, severity, silent) => {
if (this.notificationRef) {
this.notificationRef.addNotification({
section,
message,
timestamp,
severity,
silent
});
}
};
handleSuppress = () => {
this.setState({ suppress: !this.state.suppress ? "ok" : false }, () => {
localStorage.setItem(SUPPRESS_NOTIFICATIONS, JSON.stringify(this.state.suppress));
});
};
render() {
const { activeItem, activeGroup } = this.state;
const { isNavOpenDesktop, isNavOpenMobile, isMobileView } = this.state;
const PageNav = (
<Nav onSelect={this.onNavSelect} aria-label="Nav" className="pf-m-dark">
<NavList>
{Object.keys(this.nav).map(section => {
const Section = utils.Icap(section);
return (
<NavExpandable
title={Section}
groupId={section}
isActive={activeGroup === section}
isExpanded
key={section}
>
{this.nav[section].map(item => {
const key = item.name;
return (
<NavItem
groupId={section}
itemId={key}
isActive={activeItem === key}
key={key}
>
<Link to={`/${item.pre ? section + "/" : ""}${key}`}>
{item.title ? item.title : utils.Icap(key)}
</Link>
</NavItem>
);
})}
</NavExpandable>
);
})}
</NavList>
</Nav>
);
const PageToolbar = (
<PageHeaderTools>
<PageHeaderToolsGroup
className={css(accessibleStyles.screenReader, accessibleStyles.visibleOnLg)}
>
<PageHeaderToolsItem>
<Button
id="connectButton"
onClick={this.toggleConnectForm}
aria-label="Toggle Connect Form"
variant={ButtonVariant.plain}
>
<PowerOffIcon />
</Button>
</PageHeaderToolsItem>
<PageHeaderToolsItem className="notification-button">
<NotificationDrawer
ref={el => (this.notificationRef = el)}
suppress={this.state.suppress}
/>
</PageHeaderToolsItem>
</PageHeaderToolsGroup>
<PageHeaderToolsGroup>
<PageHeaderToolsItem
className={css(accessibleStyles.screenReader, accessibleStyles.visibleOnMd)}
>
<DropdownToggle className="user-button" onToggle={this.onDropdownToggle}>
{this.state.user}
</DropdownToggle>
<DropdownMenu
ref={el => (this.dropdownRef = el)}
handleContextHide={this.handleUserMenuHide}
handleDropdownLogout={this.handleDropdownLogout}
handleSuppress={this.handleSuppress}
suppress={this.state.suppress}
isConnected={this.isConnected}
parentClass="user-button"
/>
</PageHeaderToolsItem>
</PageHeaderToolsGroup>
<Avatar src={img_avatar} alt="Avatar image" />
</PageHeaderTools>
);
const Header = (
<PageHeader
className="topology-header"
logo={<span className="logo-text">{this.props.config.title}</span>}
headerTools={PageToolbar}
showNavToggle
onNavToggle={isMobileView ? this.onNavToggleMobile : this.onNavToggleDesktop}
isNavOpen={isMobileView ? isNavOpenMobile : isNavOpenDesktop}
/>
);
const pageId = "main-content-page-layout-manual-nav";
const PageSkipToContent = (
<SkipToContent href={`#${pageId}`}>Skip to Content</SkipToContent>
);
const sidebar = PageNav => {
if (this.state.connected) {
return (
<PageSidebar
id="page-sidebar"
nav={PageNav}
isNavOpen={isMobileView ? isNavOpenMobile : isNavOpenDesktop}
theme="dark"
/>
);
}
// this is required to prevent an axe error
return <div id="page-sidebar" />;
};
// don't allow access to this component unless we are logged in
const PrivateRoute = ({ component: Component, path: rpath, ...more }) => (
<Route
path={rpath}
{...(more.exact ? "exact" : "")}
render={props =>
this.state.connected ? (
<Component
service={this.service}
handleAddNotification={this.handleAddNotification}
{...props}
{...more}
/>
) : (
<Redirect
to={{
pathname: `/login${this.state.connecting ? "/connecting" : ""}`,
state: { from: props.location }
}}
/>
)
}
/>
);
const connectForm = () => {
return this.state.isConnectFormOpen ? (
<ConnectForm
service={this.service}
isConnectFormOpen={this.state.isConnectFormOpen}
fromPath={"/"}
handleConnect={this.handleConnect}
handleConnectCancel={this.handleConnectCancel}
isConnected={this.state.connected}
fromLayout={true}
/>
) : (
<React.Fragment />
);
};
// When we need to display a different component(page),
// we render a <Redirect> object
const redirectAfterConnect = () => {
if (this.state.connected && this.redirect) {
this.redirect = false;
return <Redirect to={this.props.location.pathname} />;
} else {
return <React.Fragment />;
}
};
return (
<Router>
{redirectAfterConnect()}
<Page
header={Header}
sidebar={sidebar(PageNav)}
onPageResize={this.onPageResize}
skipToContent={PageSkipToContent}
mainContainerId={pageId}
>
{connectForm()}
<Switch>
<PrivateRoute
path="/"
exact
throughputChartData={this.throughputChartData}
inflightChartData={this.inflightChartData}
component={DashboardPage}
/>
<PrivateRoute
path="/dashboard"
throughputChartData={this.throughputChartData}
inflightChartData={this.inflightChartData}
component={DashboardPage}
/>
<PrivateRoute path="/overview/:entity" component={OverviewPage} />
<PrivateRoute
path="/details"
schema={this.schema}
component={DetailsTablePage}
/>
<PrivateRoute path="/topology" component={TopologyPage} />
<PrivateRoute path="/flow" component={MessageFlowPage} />
<PrivateRoute path="/logs" component={LogDetails} />
<PrivateRoute path="/entities" component={EntitiesPage} />
<PrivateRoute path="/schema" schema={this.schema} component={SchemaPage} />
<Route
path="/login"
render={props => (
<ConnectPage
{...props}
connecting={this.state.connecting}
connectingTitle={
this.state.connecting ? "Attempting to auto connect" : undefined
}
connectingMessage={
this.state.connecting
? `Trying to connect to ${window.location.hostname}:${window.location.port}`
: undefined
}
fromPath={"/"}
service={this.service}
config={this.props.config}
handleConnect={this.handleConnect}
handleAddNotification={this.handleAddNotification}
/>
)}
/>
</Switch>
</Page>
</Router>
);
}
}
export default PageLayout;
const idle = (elapsed, callback) => {
let timer;
const inactive = () => {
callback();
active();
};
const active = () => {
clearTimeout(timer);
timer = setTimeout(inactive, elapsed);
};
const unload = () => {
clearTimeout(timer);
document.removeEventListener("mousemove", active);
};
document.addEventListener("mousemove", active, true);
active();
return unload;
};