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 (
{this.state.collapsed ? linkerdLogoOnly : linkerdWordLogo}
Service mesh Namespaces Tap Top {this.state.collapsed ? "" : "Resources"}}> Authorities Deployments Pods Replication Controllers { this.state.collapsed ? null : (
)} {this.state.collapsed ? null : _.map(_.keys(sidebarComponents).sort(), resourceName => { return ( {friendlyTitle(resourceName).plural}}> { _.map(_.sortBy(sidebarComponents[resourceName], r => `${r.namespace}/${r.name}`), r => { // only display resources that have been meshed return (
{`${r.namespace}/${r.name}`}
); }) }
); }) } Documentation { this.state.isLatest ? null : ( Update {this.props.productName} )}
{ this.state.collapsed ? null : (
)}
); } } export default withContext(Sidebar);