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 ( ); } 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 = (