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",
|
"tap": "Tap",
|
||||||
"top": "Top",
|
"top": "Top",
|
||||||
"routes": "Top Routes",
|
"routes": "Top Routes",
|
||||||
|
"community": "Community",
|
||||||
"debug": "Debug"
|
"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 { githubIcon, linkerdWordLogo, slackIcon } from './util/SvgWrappers.jsx';
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
|
import Badge from '@material-ui/core/Badge';
|
||||||
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
|
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
|
||||||
import BuildIcon from '@material-ui/icons/Build';
|
import BuildIcon from '@material-ui/icons/Build';
|
||||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||||
|
@ -22,12 +23,18 @@ import NavigationResources from './NavigationResources.jsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactRouterPropTypes from 'react-router-prop-types';
|
import ReactRouterPropTypes from 'react-router-prop-types';
|
||||||
|
import SentimentVerySatisfiedIcon from '@material-ui/icons/SentimentVerySatisfied';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import Version from './Version.jsx';
|
import Version from './Version.jsx';
|
||||||
|
import _maxBy from 'lodash/maxBy';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withContext } from './util/AppContext.jsx';
|
import { withContext } from './util/AppContext.jsx';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
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 styles = theme => {
|
||||||
const drawerWidth = theme.spacing.unit * 31;
|
const drawerWidth = theme.spacing.unit * 31;
|
||||||
|
@ -122,6 +129,9 @@ const styles = theme => {
|
||||||
fontSize: "18px",
|
fontSize: "18px",
|
||||||
paddingLeft: "3px",
|
paddingLeft: "3px",
|
||||||
paddingRight: "3px",
|
paddingRight: "3px",
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: yellow[500],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -131,6 +141,7 @@ class NavigationBase extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.api = this.props.api;
|
this.api = this.props.api;
|
||||||
this.handleApiError = this.handleApiError.bind(this);
|
this.handleApiError = this.handleApiError.bind(this);
|
||||||
|
this.handleCommunityClick = this.handleCommunityClick.bind(this);
|
||||||
|
|
||||||
this.state = this.getInitialState();
|
this.state = this.getInitialState();
|
||||||
}
|
}
|
||||||
|
@ -139,6 +150,7 @@ class NavigationBase extends React.Component {
|
||||||
return {
|
return {
|
||||||
drawerOpen: true,
|
drawerOpen: true,
|
||||||
helpMenuOpen: false,
|
helpMenuOpen: false,
|
||||||
|
hideUpdateBadge: true,
|
||||||
latestVersion: '',
|
latestVersion: '',
|
||||||
isLatest: true,
|
isLatest: true,
|
||||||
namespaceFilter: "all"
|
namespaceFilter: "all"
|
||||||
|
@ -147,6 +159,7 @@ class NavigationBase extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchVersion();
|
this.fetchVersion();
|
||||||
|
this.fetchLatestCommunityUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchVersion() {
|
fetchVersion() {
|
||||||
|
@ -166,12 +179,39 @@ class NavigationBase extends React.Component {
|
||||||
}).catch(this.handleApiError);
|
}).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) {
|
handleApiError(e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
error: e
|
error: e
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCommunityClick = () => {
|
||||||
|
let lastClicked = new Date();
|
||||||
|
localStorage.setItem(localStorageKey, lastClicked);
|
||||||
|
this.setState({ hideUpdateBadge: true });
|
||||||
|
}
|
||||||
|
|
||||||
handleDrawerClick = () => {
|
handleDrawerClick = () => {
|
||||||
this.setState(state => ({ drawerOpen: !state.drawerOpen }));
|
this.setState(state => ({ drawerOpen: !state.drawerOpen }));
|
||||||
};
|
};
|
||||||
|
@ -180,7 +220,7 @@ class NavigationBase extends React.Component {
|
||||||
this.setState(state => ({ helpMenuOpen: !state.helpMenuOpen }));
|
this.setState(state => ({ helpMenuOpen: !state.helpMenuOpen }));
|
||||||
}
|
}
|
||||||
|
|
||||||
menuItem(path, title, icon) {
|
menuItem(path, title, icon, onClick) {
|
||||||
const { classes, api } = this.props;
|
const { classes, api } = this.props;
|
||||||
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
|
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
|
||||||
let isCurrentPage = path => path === normalizedPath;
|
let isCurrentPage = path => path === normalizedPath;
|
||||||
|
@ -188,6 +228,7 @@ class NavigationBase extends React.Component {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
|
onClick={onClick}
|
||||||
to={api.prefixLink(path)}
|
to={api.prefixLink(path)}
|
||||||
className={classes.navMenuItem}
|
className={classes.navMenuItem}
|
||||||
selected={isCurrentPage(path)}>
|
selected={isCurrentPage(path)}>
|
||||||
|
@ -196,6 +237,7 @@ class NavigationBase extends React.Component {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, ChildComponent, ...otherProps } = this.props;
|
const { classes, ChildComponent, ...otherProps } = this.props;
|
||||||
|
|
||||||
|
@ -245,6 +287,14 @@ class NavigationBase extends React.Component {
|
||||||
<ListItemIcon><LibraryBooksIcon /></ListItemIcon>
|
<ListItemIcon><LibraryBooksIcon /></ListItemIcon>
|
||||||
<ListItemText primary="Documentation" />
|
<ListItemText primary="Documentation" />
|
||||||
</ListItem>
|
</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}>
|
<ListItem component="a" href="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank" className={classes.helpMenuItem}>
|
||||||
<ListItemIcon><EmailIcon /></ListItemIcon>
|
<ListItemIcon><EmailIcon /></ListItemIcon>
|
||||||
<ListItemText primary="Join the Mailing List" />
|
<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 ApiHelpers from './components/util/ApiHelpers.jsx';
|
||||||
import AppContext from './components/util/AppContext.jsx';
|
import AppContext from './components/util/AppContext.jsx';
|
||||||
|
import Community from './components/Community.jsx';
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import Debug from './components/Debug.jsx';
|
import Debug from './components/Debug.jsx';
|
||||||
import Namespace from './components/Namespace.jsx';
|
import Namespace from './components/Namespace.jsx';
|
||||||
|
@ -114,6 +115,9 @@ let applicationHtml = (
|
||||||
<Route
|
<Route
|
||||||
path={`${pathPrefix}/debug`}
|
path={`${pathPrefix}/debug`}
|
||||||
render={props => <Navigation {...props} ChildComponent={Debug} />} />
|
render={props => <Navigation {...props} ChildComponent={Debug} />} />
|
||||||
|
<Route
|
||||||
|
path={`${pathPrefix}/community`}
|
||||||
|
render={props => <Navigation {...props} ChildComponent={Community} />} />
|
||||||
<Route component={NoMatch} />
|
<Route component={NoMatch} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</RouterToUrlQuery>
|
</RouterToUrlQuery>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"prop-types": "15.6.1",
|
"prop-types": "15.6.1",
|
||||||
"react": "16.5.0",
|
"react": "16.5.0",
|
||||||
"react-dom": "16.5.0",
|
"react-dom": "16.5.0",
|
||||||
|
"react-iframe": "^1.5.0",
|
||||||
"react-router": "4.2.0",
|
"react-router": "4.2.0",
|
||||||
"react-router-dom": "4.2.2",
|
"react-router-dom": "4.2.2",
|
||||||
"react-router-prop-types": "^1.0.4",
|
"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"
|
loose-envify "^1.3.1"
|
||||||
object-assign "^4.1.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:
|
proxy-addr@~2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
|
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"
|
prop-types "^15.6.0"
|
||||||
warning "^4.0.1"
|
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:
|
react-is@^16.3.2, react-is@^16.5.2, react-is@^16.6.3, react-is@^16.7.0:
|
||||||
version "16.7.0"
|
version "16.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
|
||||||
integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==
|
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:
|
react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
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("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
|
||||||
server.router.GET("/tap", handler.handleIndex)
|
server.router.GET("/tap", handler.handleIndex)
|
||||||
server.router.GET("/top", handler.handleIndex)
|
server.router.GET("/top", handler.handleIndex)
|
||||||
|
server.router.GET("/community", handler.handleIndex)
|
||||||
server.router.GET("/debug", handler.handleIndex)
|
server.router.GET("/debug", handler.handleIndex)
|
||||||
server.router.GET("/routes", handler.handleIndex)
|
server.router.GET("/routes", handler.handleIndex)
|
||||||
server.router.GET("/profiles/new", handler.handleProfileDownload)
|
server.router.GET("/profiles/new", handler.handleProfileDownload)
|
||||||
|
|
Loading…
Reference in New Issue