Add "Community" menu item to dashboard that displays linkerd.io content (#2476)

Closes #2327.

This PR creates a "Community" menu item on the dashboard sidebar that, when clicked, displays an iFrame of a page on linkerd.io. A yellow badge appears on the menu item if there has been an update since the user last clicked the "Community" menu item. This is calculated by comparing a date in the user's localStorage to a JSON feed at linkerd.io.
This commit is contained in:
Carol A. Scott 2019-03-14 09:55:09 -07:00 committed by GitHub
parent 468ad118f2
commit a2e63de966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 1 deletions

View File

@ -13,6 +13,7 @@ const routeToCrumbTitle = {
"tap": "Tap",
"top": "Top",
"routes": "Top Routes",
"community": "Community",
"debug": "Debug"
};

View File

@ -0,0 +1,15 @@
import Iframe from 'react-iframe';
import React from 'react';
const Community = () => {
return (
<Iframe
url="https://linkerd.io/dashboard/"
position="inherit"
display="block"
height="100vh"
border="none" />
);
};
export default Community;

View File

@ -1,5 +1,6 @@
import { githubIcon, linkerdWordLogo, slackIcon } from './util/SvgWrappers.jsx';
import AppBar from '@material-ui/core/AppBar';
import Badge from '@material-ui/core/Badge';
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
import BuildIcon from '@material-ui/icons/Build';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
@ -22,12 +23,18 @@ import NavigationResources from './NavigationResources.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
import SentimentVerySatisfiedIcon from '@material-ui/icons/SentimentVerySatisfied';
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 classNames from 'classnames';
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 styles = theme => {
const drawerWidth = theme.spacing.unit * 31;
@ -122,6 +129,9 @@ const styles = theme => {
fontSize: "18px",
paddingLeft: "3px",
paddingRight: "3px",
},
badge: {
backgroundColor: yellow[500],
}
};
};
@ -131,6 +141,7 @@ class NavigationBase extends React.Component {
super(props);
this.api = this.props.api;
this.handleApiError = this.handleApiError.bind(this);
this.handleCommunityClick = this.handleCommunityClick.bind(this);
this.state = this.getInitialState();
}
@ -139,6 +150,7 @@ class NavigationBase extends React.Component {
return {
drawerOpen: true,
helpMenuOpen: false,
hideUpdateBadge: true,
latestVersion: '',
isLatest: true,
namespaceFilter: "all"
@ -147,6 +159,7 @@ class NavigationBase extends React.Component {
componentDidMount() {
this.fetchVersion();
this.fetchLatestCommunityUpdate();
}
fetchVersion() {
@ -166,12 +179,39 @@ class NavigationBase extends React.Component {
}).catch(this.handleApiError);
}
fetchLatestCommunityUpdate() {
this.communityUpdatesPromise = fetch(jsonFeedUrl)
.then(rsp => rsp.json())
.then(rsp => rsp.data.date)
.then(rsp => {
if (rsp.length > 0) {
let lastClicked = localStorage[localStorageKey];
if (!lastClicked) {
this.setState({ hideUpdateBadge: false });
} else {
let lastClickedDateObject = new Date(lastClicked);
let latestArticle = _maxBy(rsp, update => update.date);
let latestArticleDateObject = new Date(latestArticle);
if (latestArticleDateObject > lastClickedDateObject) {
this.setState({ hideUpdateBadge: false });
}
}
}
}).catch(this.handleApiError);
}
handleApiError(e) {
this.setState({
error: e
});
}
handleCommunityClick = () => {
let lastClicked = new Date();
localStorage.setItem(localStorageKey, lastClicked);
this.setState({ hideUpdateBadge: true });
}
handleDrawerClick = () => {
this.setState(state => ({ drawerOpen: !state.drawerOpen }));
};
@ -180,7 +220,7 @@ class NavigationBase extends React.Component {
this.setState(state => ({ helpMenuOpen: !state.helpMenuOpen }));
}
menuItem(path, title, icon) {
menuItem(path, title, icon, onClick) {
const { classes, api } = this.props;
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
let isCurrentPage = path => path === normalizedPath;
@ -188,6 +228,7 @@ class NavigationBase extends React.Component {
return (
<MenuItem
component={Link}
onClick={onClick}
to={api.prefixLink(path)}
className={classes.navMenuItem}
selected={isCurrentPage(path)}>
@ -196,6 +237,7 @@ class NavigationBase extends React.Component {
</MenuItem>
);
}
render() {
const { classes, ChildComponent, ...otherProps } = this.props;
@ -245,6 +287,14 @@ class NavigationBase extends React.Component {
<ListItemIcon><LibraryBooksIcon /></ListItemIcon>
<ListItemText primary="Documentation" />
</ListItem>
{ this.menuItem("/community", "Community",
<Badge
classes={{ badge: classes.badge }}
invisible={this.state.hideUpdateBadge}
badgeContent="1">
<SentimentVerySatisfiedIcon />
</Badge>, this.handleCommunityClick
) }
<ListItem component="a" href="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank" className={classes.helpMenuItem}>
<ListItemIcon><EmailIcon /></ListItemIcon>
<ListItemText primary="Join the Mailing List" />

View File

@ -6,6 +6,7 @@ import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import ApiHelpers from './components/util/ApiHelpers.jsx';
import AppContext from './components/util/AppContext.jsx';
import Community from './components/Community.jsx';
import CssBaseline from '@material-ui/core/CssBaseline';
import Debug from './components/Debug.jsx';
import Namespace from './components/Namespace.jsx';
@ -114,6 +115,9 @@ let applicationHtml = (
<Route
path={`${pathPrefix}/debug`}
render={props => <Navigation {...props} ChildComponent={Debug} />} />
<Route
path={`${pathPrefix}/community`}
render={props => <Navigation {...props} ChildComponent={Community} />} />
<Route component={NoMatch} />
</Switch>
</RouterToUrlQuery>

View File

@ -17,6 +17,7 @@
"prop-types": "15.6.1",
"react": "16.5.0",
"react-dom": "16.5.0",
"react-iframe": "^1.5.0",
"react-router": "4.2.0",
"react-router-dom": "4.2.2",
"react-router-prop-types": "^1.0.4",

View File

@ -7198,6 +7198,15 @@ prop-types@^15.5.4, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1,
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.6.x:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
proxy-addr@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
@ -7394,11 +7403,24 @@ react-event-listener@^0.6.2:
prop-types "^15.6.0"
warning "^4.0.1"
react-iframe@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/react-iframe/-/react-iframe-1.5.0.tgz#20778988cb2782d32663a4537ef0da704dd2b9b2"
integrity sha512-hHPK0Os1iQIGD5YVM4N7DMZB9mcWHm+BmY+pSauuaX+NofilONnWUrVbCbrzy0gW6NkDW1ETAmUqlY4mrE9cxg==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.x"
react-is@^16.3.2, react-is@^16.5.2, react-is@^16.6.3, react-is@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==
react-is@^16.8.1:
version "16.8.3"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==
react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"

View File

@ -107,6 +107,7 @@ func NewServer(
server.router.GET("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
server.router.GET("/tap", handler.handleIndex)
server.router.GET("/top", handler.handleIndex)
server.router.GET("/community", handler.handleIndex)
server.router.GET("/debug", handler.handleIndex)
server.router.GET("/routes", handler.handleIndex)
server.router.GET("/profiles/new", handler.handleProfileDownload)