mirror of https://github.com/linkerd/linkerd2.git
659 lines
22 KiB
JavaScript
659 lines
22 KiB
JavaScript
import { cronJobIcon, daemonsetIcon, deploymentIcon, githubIcon, jobIcon, linkerdWordLogo, namespaceIcon, podIcon, replicaSetIcon, slackIcon, statefulSetIcon } from './util/SvgWrappers.jsx';
|
|
import { handlePageVisibility, withPageVisibility } from './util/PageVisibility.jsx';
|
|
import AppBar from '@material-ui/core/AppBar';
|
|
import Autocomplete from '@material-ui/lab/Autocomplete';
|
|
import Badge from '@material-ui/core/Badge';
|
|
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
|
|
import Divider from '@material-ui/core/Divider';
|
|
import Drawer from '@material-ui/core/Drawer';
|
|
import EmailIcon from '@material-ui/icons/Email';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import Hidden from '@material-ui/core/Hidden';
|
|
import IconButton from '@material-ui/core/IconButton';
|
|
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
|
import { Link } from 'react-router-dom';
|
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
|
import ListItemText from '@material-ui/core/ListItemText';
|
|
import MenuItem from '@material-ui/core/MenuItem';
|
|
import MenuList from '@material-ui/core/MenuList';
|
|
import NamespaceConfirmationModal from './NamespaceConfirmationModal.jsx';
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import ReactRouterPropTypes from 'react-router-prop-types';
|
|
import TextField from '@material-ui/core/TextField';
|
|
import Toolbar from '@material-ui/core/Toolbar';
|
|
import Typography from '@material-ui/core/Typography';
|
|
import Version from './Version.jsx';
|
|
import _maxBy from 'lodash/maxBy';
|
|
import { faBars } from '@fortawesome/free-solid-svg-icons/faBars';
|
|
import { faCloud } from '@fortawesome/free-solid-svg-icons/faCloud';
|
|
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons/faExternalLinkAlt';
|
|
import { faFilter } from '@fortawesome/free-solid-svg-icons/faFilter';
|
|
import { faMicroscope } from '@fortawesome/free-solid-svg-icons/faMicroscope';
|
|
import { faRandom } from '@fortawesome/free-solid-svg-icons/faRandom';
|
|
import { faSmile } from '@fortawesome/free-regular-svg-icons/faSmile';
|
|
import { faStream } from '@fortawesome/free-solid-svg-icons/faStream';
|
|
import grey from '@material-ui/core/colors/grey';
|
|
import { processSingleResourceRollup } from './util/MetricUtils.jsx';
|
|
import { regexFilterString } from './util/Utils.js';
|
|
import { withContext } from './util/AppContext.jsx';
|
|
import { withStyles } from '@material-ui/core/styles';
|
|
import yellow from '@material-ui/core/colors/yellow';
|
|
|
|
const jsonFeedUrl = 'https://linkerd.io/dashboard/index.json';
|
|
const localStorageKey = 'linkerd-updates-last-clicked';
|
|
const minBrowserWidth = 960;
|
|
|
|
const styles = theme => {
|
|
const drawerWidth = theme.spacing(36);
|
|
const navLogoWidth = theme.spacing(22.5);
|
|
const contentPadding = theme.spacing(3);
|
|
|
|
const enteringFn = prop => theme.transitions.create(prop, {
|
|
easing: theme.transitions.easing.sharp,
|
|
duration: theme.transitions.duration.enteringScreen,
|
|
});
|
|
const leavingFn = prop => theme.transitions.create(prop, {
|
|
easing: theme.transitions.easing.sharp,
|
|
duration: theme.transitions.duration.leavingScreen,
|
|
});
|
|
|
|
const entering = enteringFn('width');
|
|
const leaving = leavingFn('width');
|
|
|
|
return {
|
|
root: {
|
|
display: 'flex',
|
|
},
|
|
appBar: {
|
|
alignItems: 'center',
|
|
position: 'permanent',
|
|
color: 'white',
|
|
transition: leaving,
|
|
},
|
|
bars: {
|
|
color: 'white',
|
|
position: 'fixed',
|
|
left: theme.spacing(2.5),
|
|
},
|
|
breadcrumbs: {
|
|
color: 'white',
|
|
marginLeft: `${drawerWidth}px`,
|
|
},
|
|
drawer: {
|
|
width: drawerWidth,
|
|
transition: entering,
|
|
},
|
|
drawerPaper: {
|
|
width: 'inherit',
|
|
},
|
|
toolbar: theme.mixins.toolbar,
|
|
navToolbar: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: `0 0 0 ${theme.spacing(2)}px`,
|
|
boxShadow: theme.shadows[4], // to match elevation == 4 on main AppBar
|
|
...theme.mixins.toolbar,
|
|
backgroundColor: theme.palette.primary.main,
|
|
},
|
|
content: {
|
|
flexGrow: 1,
|
|
width: `calc(100% - ${drawerWidth}px)`,
|
|
backgroundColor: theme.palette.background.default,
|
|
padding: contentPadding,
|
|
transition: entering,
|
|
},
|
|
linkerdNavLogo: {
|
|
margin: 'auto',
|
|
width: `${navLogoWidth}px`,
|
|
transition: enteringFn(['margin', 'opacity']),
|
|
},
|
|
linkerdMobileLogo: {
|
|
width: `${navLogoWidth}px`,
|
|
},
|
|
namespaceChangeButton: {
|
|
borderRadius: '5px',
|
|
backgroundColor: grey[400],
|
|
marginLeft: `${drawerWidth * 0.075}px`,
|
|
marginRight: `${drawerWidth * 0.075}px`,
|
|
marginTop: '11px',
|
|
width: `${drawerWidth * 0.85}px`,
|
|
},
|
|
namespaceChangeButtonInputRoot: {
|
|
backgroundColor: grey[300],
|
|
boxShadow: 'rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px',
|
|
padding: '4px 12px !important',
|
|
border: 0,
|
|
'&:hover': {
|
|
borderColor: 'transparent',
|
|
},
|
|
},
|
|
namespaceChangeButtonInput: {
|
|
textAlign: 'center',
|
|
},
|
|
namespaceChangeButtonInputFocused: {
|
|
textAlign: 'center',
|
|
},
|
|
namespaceChangeButtonPopupIndicator: {
|
|
backgroundColor: 'transparent',
|
|
'&:hover': {
|
|
backgroundColor: 'transparent',
|
|
},
|
|
},
|
|
navMenuItem: {
|
|
paddingLeft: `${contentPadding}px`,
|
|
paddingRight: `${contentPadding}px`,
|
|
},
|
|
shrinkIcon: {
|
|
fontSize: '24px',
|
|
paddingLeft: '3px',
|
|
paddingRight: '3px',
|
|
},
|
|
shrinkCloudIcon: {
|
|
fontSize: '18px',
|
|
paddingLeft: '1px',
|
|
},
|
|
// color is consistent with Octopus Graph coloring
|
|
externalLinkIcon: {
|
|
color: grey[500],
|
|
},
|
|
sidebarHeading: {
|
|
color: grey[500],
|
|
outline: 'none',
|
|
paddingTop: '9px',
|
|
paddingBottom: '9px',
|
|
marginLeft: `${drawerWidth * 0.09}px`,
|
|
},
|
|
badge: {
|
|
backgroundColor: yellow[500],
|
|
},
|
|
inputBase: {
|
|
boxSizing: 'border-box',
|
|
},
|
|
};
|
|
};
|
|
|
|
class NavigationBase extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.api = props.api;
|
|
this.handleApiError = this.handleApiError.bind(this);
|
|
this.handleConfirmNamespaceChange = this.handleConfirmNamespaceChange.bind(this);
|
|
this.handleCommunityClick = this.handleCommunityClick.bind(this);
|
|
this.handleDialogCancel = this.handleDialogCancel.bind(this);
|
|
this.handleFilterInputChange = this.handleFilterInputChange.bind(this);
|
|
this.handleNamespaceMenuClick = this.handleNamespaceMenuClick.bind(this);
|
|
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
|
|
this.handleAutocompleteClick = this.handleAutocompleteClick.bind(this);
|
|
|
|
this.state = this.getInitialState();
|
|
this.loadFromServer = this.loadFromServer.bind(this);
|
|
}
|
|
|
|
getInitialState() {
|
|
return {
|
|
mobileSidebarOpen: false,
|
|
newNamespace: '',
|
|
formattedNamespaceFilter: '',
|
|
hideUpdateBadge: true,
|
|
latestVersion: '',
|
|
isLatest: true,
|
|
namespaces: [],
|
|
pendingRequests: false,
|
|
pollingInterval: 10000,
|
|
loaded: false,
|
|
error: null,
|
|
showNamespaceChangeDialog: false,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.startServerPolling();
|
|
this.fetchVersion();
|
|
this.fetchLatestCommunityUpdate();
|
|
this.updateWindowDimensions();
|
|
window.addEventListener('resize', this.updateWindowDimensions);
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { history, checkNamespaceMatch, isPageVisible } = this.props;
|
|
if (history) {
|
|
checkNamespaceMatch(history.location.pathname);
|
|
}
|
|
|
|
handlePageVisibility({
|
|
prevVisibilityState: prevProps.isPageVisible,
|
|
currentVisibilityState: isPageVisible,
|
|
onVisible: () => this.startServerPolling(),
|
|
onHidden: () => this.stopServerPolling(),
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.updateWindowDimensions);
|
|
this.stopServerPolling();
|
|
}
|
|
|
|
startServerPolling() {
|
|
const { pollingInterval } = this.state;
|
|
this.loadFromServer();
|
|
this.timerId = window.setInterval(this.loadFromServer, pollingInterval);
|
|
}
|
|
|
|
stopServerPolling() {
|
|
window.clearInterval(this.timerId);
|
|
this.api.cancelCurrentRequests();
|
|
this.setState({ pendingRequests: false });
|
|
}
|
|
|
|
// API returns namespaces for namespace select button. No metrics returned.
|
|
loadFromServer() {
|
|
const { pendingRequests } = this.state;
|
|
|
|
if (pendingRequests) {
|
|
return;
|
|
}
|
|
this.setState({ pendingRequests: true });
|
|
|
|
const apiRequests = [
|
|
this.api.fetchMetrics(this.api.urlsForResourceNoStats('namespace')),
|
|
];
|
|
|
|
this.api.setCurrentRequests(apiRequests);
|
|
|
|
Promise.all(this.api.getCurrentPromises())
|
|
.then(([allNs]) => {
|
|
// add "All Namespaces" to the options
|
|
let namespaces = [{ name: '_all', key: 'ns-all' }];
|
|
namespaces = namespaces.concat(processSingleResourceRollup(allNs));
|
|
this.setState({
|
|
namespaces,
|
|
pendingRequests: false,
|
|
error: null,
|
|
});
|
|
})
|
|
.catch(this.handleApiError);
|
|
}
|
|
|
|
fetchVersion() {
|
|
const { releaseVersion, uuid } = this.props;
|
|
|
|
const versionUrl = `https://versioncheck.linkerd.io/version.json?version=${releaseVersion}&uuid=${uuid}&source=web`;
|
|
this.versionPromise = fetch(versionUrl, { credentials: 'include' })
|
|
.then(rsp => rsp.json())
|
|
.then(versionRsp => {
|
|
let latestVersion;
|
|
const parts = releaseVersion.split('-', 2);
|
|
if (parts.length === 2) {
|
|
latestVersion = versionRsp[parts[0]];
|
|
}
|
|
this.setState({
|
|
latestVersion,
|
|
isLatest: latestVersion === releaseVersion,
|
|
});
|
|
}).catch(this.handleApiError);
|
|
}
|
|
|
|
fetchLatestCommunityUpdate() {
|
|
this.communityUpdatesPromise = fetch(jsonFeedUrl)
|
|
.then(rsp => rsp.json())
|
|
.then(rsp => rsp.data.date)
|
|
.then(rsp => {
|
|
if (rsp.length > 0) {
|
|
const lastClicked = localStorage[localStorageKey];
|
|
if (!lastClicked) {
|
|
this.setState({ hideUpdateBadge: false });
|
|
} else {
|
|
const lastClickedDateObject = new Date(lastClicked);
|
|
const latestArticle = _maxBy(rsp, update => update.date);
|
|
const latestArticleDateObject = new Date(latestArticle);
|
|
if (latestArticleDateObject > lastClickedDateObject) {
|
|
this.setState({ hideUpdateBadge: false });
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.catch(this.handleApiError);
|
|
}
|
|
|
|
handleApiError(e) {
|
|
this.setState({
|
|
error: e,
|
|
});
|
|
}
|
|
|
|
handleCommunityClick = () => {
|
|
const lastClicked = new Date();
|
|
localStorage.setItem(localStorageKey, lastClicked);
|
|
this.setState({ hideUpdateBadge: true });
|
|
}
|
|
|
|
handleDialogCancel = () => {
|
|
this.setState({ showNamespaceChangeDialog: false });
|
|
}
|
|
|
|
handleDrawerClick = () => {
|
|
const { mobileSidebarOpen } = this.state;
|
|
if (!mobileSidebarOpen) {
|
|
this.setState({ mobileSidebarOpen: true });
|
|
} else {
|
|
this.setState({ mobileSidebarOpen: false });
|
|
window.setTimeout(() => {
|
|
const linkerdHash = document.querySelector('.linkerd-word-logo #linkerd-hash');
|
|
linkerdHash.style.display = 'none';
|
|
window.setTimeout(() => {
|
|
linkerdHash.style.display = '';
|
|
}, 15);
|
|
}, 300);
|
|
}
|
|
};
|
|
|
|
handleConfirmNamespaceChange = () => {
|
|
const { newNamespace } = this.state;
|
|
const { updateNamespaceInContext, history } = this.props;
|
|
this.setState({ showNamespaceChangeDialog: false });
|
|
updateNamespaceInContext(newNamespace);
|
|
history.push(`/namespaces/${newNamespace}`);
|
|
}
|
|
|
|
handleFilterInputChange = event => {
|
|
this.setState({
|
|
formattedNamespaceFilter: regexFilterString(event.target.value) });
|
|
}
|
|
|
|
handleAutocompleteClick = event => {
|
|
// This is necessary for the mobile sidebar, otherwise the sidebar
|
|
// would close upon click of the namespace change input.
|
|
event.stopPropagation();
|
|
}
|
|
|
|
handleNamespaceChange = (event, values) => {
|
|
const { history, updateNamespaceInContext, selectedNamespace } = this.props;
|
|
|
|
// event.stopPropagation();
|
|
const namespace = values.name;
|
|
if (namespace === selectedNamespace) {
|
|
return;
|
|
}
|
|
let path = history.location.pathname;
|
|
const pathParts = path.split('/');
|
|
if (pathParts.length === 3 || pathParts.length === 4) {
|
|
// path is /namespaces/someNamespace/resourceType
|
|
// or /namespaces/someNamespace
|
|
path = path.replace(selectedNamespace, namespace);
|
|
history.push(path);
|
|
updateNamespaceInContext(namespace);
|
|
} else if (pathParts.length === 5) {
|
|
// path is /namespace/someNamespace/resourceType/someResource
|
|
this.setState({
|
|
showNamespaceChangeDialog: true,
|
|
newNamespace: namespace,
|
|
});
|
|
} else {
|
|
// update the selectedNamespace in context with no path changes
|
|
updateNamespaceInContext(namespace);
|
|
}
|
|
}
|
|
|
|
handleNamespaceMenuClick = event => {
|
|
// ensure that mobile drawer will not close on click
|
|
event.stopPropagation();
|
|
this.setState({ formattedNamespaceFilter: '' });
|
|
}
|
|
|
|
menuItem(path, title, icon, onClick) {
|
|
const { classes, location, pathPrefix } = this.props;
|
|
const normalizedPath = location.pathname.replace(pathPrefix, '');
|
|
|
|
return (
|
|
<MenuItem
|
|
component={Link}
|
|
onClick={onClick}
|
|
to={this.api.prefixLink(path)}
|
|
className={classes.navMenuItem}
|
|
selected={path === normalizedPath}>
|
|
<ListItemIcon>{icon}</ListItemIcon>
|
|
<ListItemText primary={title} />
|
|
</MenuItem>
|
|
);
|
|
}
|
|
|
|
updateWindowDimensions() {
|
|
const browserWidth = window.innerWidth;
|
|
if (browserWidth > minBrowserWidth) {
|
|
this.setState({ mobileSidebarOpen: false });
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { api, classes, selectedNamespace, ChildComponent, uuid, releaseVersion, ...otherProps } = this.props;
|
|
const { namespaces, formattedNamespaceFilter, hideUpdateBadge, isLatest, latestVersion,
|
|
showNamespaceChangeDialog, newNamespace, mobileSidebarOpen, error } = this.state;
|
|
const filteredNamespaces = namespaces.filter(ns => {
|
|
return ns.name.match(formattedNamespaceFilter);
|
|
});
|
|
let formattedNamespaceName = selectedNamespace;
|
|
if (formattedNamespaceName === '_all') {
|
|
formattedNamespaceName = 'All Namespaces';
|
|
}
|
|
|
|
const drawer = (
|
|
<div>
|
|
{ !mobileSidebarOpen &&
|
|
<div className={classes.navToolbar}>
|
|
<div className={classes.linkerdNavLogo}>
|
|
<Link to="/namespaces">{linkerdWordLogo}</Link>
|
|
</div>
|
|
</div>
|
|
}
|
|
<Divider />
|
|
<MenuList>
|
|
<Typography variant="button" component="div" className={classes.sidebarHeading}>
|
|
Cluster
|
|
</Typography>
|
|
{ this.menuItem('/namespaces', 'Namespaces', namespaceIcon) }
|
|
|
|
|
|
{ this.menuItem('/controlplane', 'Control Plane',
|
|
<FontAwesomeIcon icon={faCloud} className={classes.shrinkCloudIcon} />) }
|
|
|
|
</MenuList>
|
|
|
|
<Divider />
|
|
|
|
<Autocomplete
|
|
id="namespace-autocomplete"
|
|
onClick={this.handleAutocompleteClick}
|
|
disableClearable
|
|
value={{ name: formattedNamespaceName.toUpperCase() }}
|
|
options={filteredNamespaces}
|
|
autoSelect
|
|
getOptionSelected={option => option.name === selectedNamespace}
|
|
getOptionLabel={option => { if (option.name !== '_all') { return option.name; } else { return 'All Namespaces'; } }}
|
|
onChange={this.handleNamespaceChange}
|
|
size="small"
|
|
classes={{
|
|
root: classes.namespaceChangeButton,
|
|
inputRoot: classes.namespaceChangeButtonInputRoot,
|
|
input: classes.namespaceChangeButtonInput,
|
|
popupIndicator: classes.namespaceChangeButtonPopupIndicator,
|
|
}}
|
|
className={classes.namespaceChangeButton}
|
|
renderInput={params => (
|
|
<TextField
|
|
{...params}
|
|
key={params.name}
|
|
variant="outlined"
|
|
fullWidth />
|
|
)} />
|
|
|
|
<NamespaceConfirmationModal
|
|
open={showNamespaceChangeDialog}
|
|
selectedNamespace={selectedNamespace}
|
|
newNamespace={newNamespace}
|
|
handleDialogCancel={this.handleDialogCancel}
|
|
handleConfirmNamespaceChange={this.handleConfirmNamespaceChange} />
|
|
|
|
<MenuList>
|
|
<Typography variant="button" component="div" className={classes.sidebarHeading}>
|
|
Workloads
|
|
</Typography>
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/cronjobs`, 'Cron Jobs', cronJobIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/daemonsets`, 'Daemon Sets', daemonsetIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/deployments`, 'Deployments', deploymentIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/jobs`, 'Jobs', jobIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/pods`, 'Pods', podIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/replicasets`, 'Replica Sets', replicaSetIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/replicationcontrollers`, 'Replication Controllers', replicaSetIcon) }
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/statefulsets`, 'Stateful Sets', statefulSetIcon) }
|
|
</MenuList>
|
|
|
|
<MenuList>
|
|
<Typography variant="button" component="div" className={classes.sidebarHeading}>
|
|
Configuration
|
|
</Typography>
|
|
|
|
{ this.menuItem(`/namespaces/${selectedNamespace}/trafficsplits`, 'Traffic Splits', <FontAwesomeIcon icon={faFilter} className={classes.shrinkIcon} />) }
|
|
|
|
</MenuList>
|
|
<Divider />
|
|
<MenuList>
|
|
<Typography variant="button" component="div" className={classes.sidebarHeading}>
|
|
Tools
|
|
</Typography>
|
|
|
|
{ this.menuItem('/tap', 'Tap', <FontAwesomeIcon icon={faMicroscope} className={classes.shrinkIcon} />) }
|
|
{ this.menuItem('/top', 'Top', <FontAwesomeIcon icon={faStream} className={classes.shrinkIcon} />) }
|
|
{ this.menuItem('/routes', 'Routes', <FontAwesomeIcon icon={faRandom} className={classes.shrinkIcon} />) }
|
|
|
|
</MenuList>
|
|
<Divider />
|
|
<MenuList>
|
|
{ this.menuItem('/community', 'Community',
|
|
<Badge
|
|
classes={{ badge: classes.badge }}
|
|
invisible={hideUpdateBadge}
|
|
badgeContent="1">
|
|
<FontAwesomeIcon icon={faSmile} className={classes.shrinkIcon} />
|
|
</Badge>, this.handleCommunityClick) }
|
|
|
|
<MenuItem component="a" href="https://linkerd.io/2/overview/" target="_blank" className={classes.navMenuItem}>
|
|
<ListItemIcon><LibraryBooksIcon className={classes.shrinkIcon} /></ListItemIcon>
|
|
<ListItemText primary="Documentation" />
|
|
<FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
|
|
</MenuItem>
|
|
|
|
<MenuItem component="a" href="https://github.com/linkerd/linkerd2/issues/new/choose" target="_blank" className={classes.navMenuItem}>
|
|
<ListItemIcon>{githubIcon}</ListItemIcon>
|
|
<ListItemText primary="GitHub" />
|
|
<FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
|
|
</MenuItem>
|
|
|
|
<MenuItem component="a" href="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank" className={classes.navMenuItem}>
|
|
<ListItemIcon><EmailIcon className={classes.shrinkIcon} /></ListItemIcon>
|
|
<ListItemText primary="Mailing List" />
|
|
<FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
|
|
</MenuItem>
|
|
|
|
<MenuItem component="a" href="https://slack.linkerd.io" target="_blank" className={classes.navMenuItem}>
|
|
<ListItemIcon>{slackIcon}</ListItemIcon>
|
|
<ListItemText primary="Slack" />
|
|
<FontAwesomeIcon icon={faExternalLinkAlt} className={classes.externalLinkIcon} size="xs" />
|
|
</MenuItem>
|
|
|
|
<Version
|
|
isLatest={isLatest}
|
|
latestVersion={latestVersion}
|
|
releaseVersion={releaseVersion}
|
|
error={error}
|
|
uuid={uuid} />
|
|
|
|
</MenuList>
|
|
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={classes.root}>
|
|
|
|
<Hidden smDown>
|
|
<Drawer
|
|
className={classes.drawer}
|
|
classes={{ paper: classes.drawerPaper }}
|
|
variant="permanent">
|
|
{drawer}
|
|
</Drawer>
|
|
<AppBar>
|
|
<Toolbar>
|
|
<Typography variant="h6" color="inherit" className={classes.breadcrumbs} noWrap>
|
|
<BreadcrumbHeader {...this.props} />
|
|
</Typography>
|
|
</Toolbar>
|
|
</AppBar>
|
|
</Hidden>
|
|
|
|
<Hidden mdUp>
|
|
<AppBar className={classes.appBar}>
|
|
<Toolbar>
|
|
<div className={classes.linkerdMobileLogo}>
|
|
{linkerdWordLogo}
|
|
</div>
|
|
{ !mobileSidebarOpen && // mobile view but no sidebar
|
|
<React.Fragment>
|
|
<IconButton onClick={this.handleDrawerClick} className={classes.bars}>
|
|
<FontAwesomeIcon icon={faBars} />
|
|
</IconButton>
|
|
</React.Fragment>
|
|
}
|
|
</Toolbar>
|
|
</AppBar>
|
|
<Drawer
|
|
className={classes.drawer}
|
|
classes={{ paper: classes.drawerPaper }}
|
|
variant="temporary"
|
|
onClick={this.handleDrawerClick}
|
|
onClose={this.handleDrawerClick}
|
|
open={mobileSidebarOpen}>
|
|
{drawer}
|
|
</Drawer>
|
|
</Hidden>
|
|
|
|
<main className={classes.content}>
|
|
<div className={classes.toolbar} />
|
|
<div>
|
|
<ChildComponent {...otherProps} />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
NavigationBase.propTypes = {
|
|
api: PropTypes.shape({}).isRequired,
|
|
checkNamespaceMatch: PropTypes.func.isRequired,
|
|
ChildComponent: PropTypes.oneOfType([
|
|
PropTypes.func,
|
|
PropTypes.object,
|
|
]).isRequired,
|
|
isPageVisible: PropTypes.bool.isRequired,
|
|
history: ReactRouterPropTypes.history.isRequired,
|
|
location: ReactRouterPropTypes.location.isRequired,
|
|
pathPrefix: PropTypes.string.isRequired,
|
|
releaseVersion: PropTypes.string.isRequired,
|
|
selectedNamespace: PropTypes.string.isRequired,
|
|
theme: PropTypes.shape({}).isRequired,
|
|
updateNamespaceInContext: PropTypes.func.isRequired,
|
|
uuid: PropTypes.string.isRequired,
|
|
};
|
|
|
|
export default withPageVisibility(withContext(withStyles(styles, { withTheme: true })(NavigationBase)));
|