linkerd2/web/app/js/components/Sidebar.jsx

310 lines
11 KiB
JavaScript

import _ from 'lodash';
import ApiHelpers from './util/ApiHelpers.jsx';
import {friendlyTitle} from './util/Utils.js';
import {Link} from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
import SocialLinks from './SocialLinks.jsx';
import Version from './Version.jsx';
import {withContext} from './util/AppContext.jsx';
import {Badge, Form, Icon, Layout, Menu, Select} from 'antd';
import {
excludeResourcesFromRollup,
getSuccessRateClassification,
processMultiResourceRollup,
processSingleResourceRollup
} from './util/MetricUtils.js';
import {linkerdLogoOnly, linkerdWordLogo} from './util/SvgWrappers.jsx';
import './../../css/sidebar.css';
const classificationLabels = {
good: "success",
neutral: "warning",
bad: "error"
};
class Sidebar extends React.Component {
static defaultProps = {
productName: 'controller'
}
static propTypes = {
api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired,
}).isRequired,
location: ReactRouterPropTypes.location.isRequired,
pathPrefix: PropTypes.string.isRequired,
productName: PropTypes.string,
releaseVersion: PropTypes.string.isRequired,
uuid: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.api = this.props.api;
this.toggleCollapse = this.toggleCollapse.bind(this);
this.loadFromServer = this.loadFromServer.bind(this);
this.handleApiError = this.handleApiError.bind(this);
this.handleNamespaceSelector = this.handleNamespaceSelector.bind(this);
this.state = this.getInitialState();
}
getInitialState() {
return {
pollingInterval: 12000,
initialCollapse: false,
collapsed: true,
error: null,
latestVersion: '',
isLatest: true,
pendingRequests: false,
namespaceFilter: "all"
};
}
componentDidMount() {
this.startServerPolling();
}
componentWillUnmount() {
// the Sidebar never unmounts, but if something ever does, we should take care of it
this.stopServerPolling();
}
startServerPolling() {
this.loadFromServer();
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
stopServerPolling() {
window.clearInterval(this.timerId);
this.api.cancelCurrentRequests();
}
loadFromServer() {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
}
this.setState({ pendingRequests: true });
let versionUrl = `https://versioncheck.linkerd.io/version.json?version=${this.props.releaseVersion}?uuid=${this.props.uuid}`;
this.api.setCurrentRequests([
ApiHelpers("").fetch(versionUrl),
this.api.fetchMetrics(this.api.urlsForResource("all")),
this.api.fetchMetrics(this.api.urlsForResource("namespace"))
]);
// expose serverPromise for testing
this.serverPromise = Promise.all(this.api.getCurrentPromises())
.then(([versionRsp, allRsp, nsRsp]) => {
let allResourceGroups = processMultiResourceRollup(allRsp);
let finalResourceGroups = excludeResourcesFromRollup(allResourceGroups, ["authority", "service"]);
let nsStats = processSingleResourceRollup(nsRsp);
let namespaces = _(nsStats).map('name').sortBy().value();
this.setState({
latestVersion: versionRsp.version,
isLatest: versionRsp.version === this.props.releaseVersion,
finalResourceGroups,
namespaces,
pendingRequests: false,
});
}).catch(this.handleApiError);
}
handleApiError(e) {
this.setState({
pendingRequests: false,
error: e
});
}
toggleCollapse() {
if (this.state.initialCollapse) {
// fix weird situation where toggleCollapsed is called on pageload,
// causing the toggle states to be inconsistent. Don't toggle on the
// very first call to toggleCollapse()
this.setState({ initialCollapse: false});
} else {
this.setState({ collapsed: !this.state.collapsed });
}
}
handleNamespaceSelector(value) {
this.setState({namespaceFilter: value});
}
filterResourcesByNamespace(resources, namespace) {
let resourceFilter = namespace === "all" ? r => r.added :
r => r.namespace === namespace && r.added;
return _.mapValues(resources, o => _.filter(o, resourceFilter));
}
render() {
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
let PrefixedLink = this.api.PrefixedLink;
let namespaces = [
{value: "all", name: "All Namespaces"}
].concat(_.map(this.state.namespaces, ns => {
return {value: ns, name: ns};
}));
let sidebarComponents = this.filterResourcesByNamespace(this.state.finalResourceGroups, this.state.namespaceFilter);
return (
<Layout.Sider
width="260px"
breakpoint="lg"
collapsed={this.state.collapsed}
collapsible={true}
onCollapse={this.toggleCollapse}>
<div className="sidebar">
<div className={`sidebar-menu-header ${this.state.collapsed ? "collapsed" : ""}`}>
<PrefixedLink to="/servicemesh">
{this.state.collapsed ? linkerdLogoOnly : linkerdWordLogo}
</PrefixedLink>
</div>
<Menu
className="sidebar-menu"
theme="dark"
selectedKeys={[normalizedPath]}>
<Menu.Item className="sidebar-menu-item" key="/servicemesh">
<PrefixedLink to="/servicemesh">
<Icon type="home" />
<span>Service mesh</span>
</PrefixedLink>
</Menu.Item>
<Menu.Item className="sidebar-menu-item" key="/namespaces">
<PrefixedLink to="/namespaces">
<Icon type="dashboard" />
<span>Namespaces</span>
</PrefixedLink>
</Menu.Item>
<Menu.Item className="sidebar-menu-item" key="/tap">
<PrefixedLink to="/tap">
<Icon type="filter" />
<span>Tap</span>
</PrefixedLink>
</Menu.Item>
<Menu.Item className="sidebar-menu-item" key="/top">
<PrefixedLink to="/top">
<Icon type="caret-up" />
<span>Top</span>
</PrefixedLink>
</Menu.Item>
<Menu.SubMenu
className="sidebar-menu-item"
key="byresource"
title={<span className="sidebar-title"><Icon type="bars" />{this.state.collapsed ? "" : "Resources"}</span>}>
<Menu.Item><PrefixedLink to="/authorities">Authorities</PrefixedLink></Menu.Item>
<Menu.Item><PrefixedLink to="/deployments">Deployments</PrefixedLink></Menu.Item>
<Menu.Item><PrefixedLink to="/pods">Pods</PrefixedLink></Menu.Item>
<Menu.Item><PrefixedLink to="/replicationcontrollers">Replication Controllers</PrefixedLink></Menu.Item>
</Menu.SubMenu>
<Menu.Divider />
</Menu>
<Menu
className="sidebar-menu"
theme="dark"
mode="inline"
selectedKeys={[normalizedPath]}>
{ this.state.collapsed ? null : (
<Menu.Item className="sidebar-menu-item" key="/namespace-selector">
<Form layout="inline">
<Form.Item>
<Select
defaultValue="All Namespaces"
dropdownMatchSelectWidth={true}
onChange={this.handleNamespaceSelector}>
{
_.map(namespaces, label => {
return (
<Select.Option key={label.value} value={label.value}>{label.name}</Select.Option>
);
})
}
</Select>
</Form.Item>
</Form>
</Menu.Item>
)}
{this.state.collapsed ? null :
_.map(_.keys(sidebarComponents).sort(), resourceName => {
return (
<Menu.SubMenu
className="sidebar-menu-item"
key={resourceName}
disabled={_.isEmpty(sidebarComponents[resourceName])}
title={<span>{friendlyTitle(resourceName).plural}</span>}>
{
_.map(_.sortBy(sidebarComponents[resourceName], r => `${r.namespace}/${r.name}`), r => {
// only display resources that have been meshed
return (
<Menu.Item
className="sidebar-submenu-item"
title={`${r.namespace}/${r.name}`}
key={this.api.generateResourceURL(r)}>
<div>
<PrefixedLink
to={this.api.generateResourceURL(r)}>
{`${r.namespace}/${r.name}`}
</PrefixedLink>
<Badge status={getSuccessRateClassification(r.successRate, classificationLabels)} />
</div>
</Menu.Item>
);
})
}
</Menu.SubMenu>
);
})
}
<Menu.Item className="sidebar-menu-item" key="/docs">
<Link to="https://linkerd.io/2/overview/" target="_blank">
<Icon type="file-text" />
<span>Documentation</span>
</Link>
</Menu.Item>
{ this.state.isLatest ? null : (
<Menu.Item className="sidebar-menu-item" key="/update">
<Link to="https://versioncheck.linkerd.io/update" target="_blank">
<Icon type="exclamation-circle-o" className="update" />
<span>Update {this.props.productName}</span>
</Link>
</Menu.Item>
)}
</Menu>
{ this.state.collapsed ? null : (
<div className="sidebar-menu-footer">
<SocialLinks />
<Version
isLatest={this.state.isLatest}
latestVersion={this.state.latestVersion}
releaseVersion={this.props.releaseVersion}
error={this.state.error}
uuid={this.props.uuid} />
</div>
)}
</div>
</Layout.Sider>
);
}
}
export default withContext(Sidebar);