mirror of https://github.com/linkerd/linkerd2.git
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:
parent
468ad118f2
commit
a2e63de966
|
@ -13,6 +13,7 @@ const routeToCrumbTitle = {
|
|||
"tap": "Tap",
|
||||
"top": "Top",
|
||||
"routes": "Top Routes",
|
||||
"community": "Community",
|
||||
"debug": "Debug"
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue