Re-implement sidebar resource selectors (#1810)

Signed-off-by: Andrew Seigner <siggy@buoyant.io>
This commit is contained in:
Andrew Seigner 2018-10-26 13:28:29 -07:00 committed by GitHub
parent 6cffad277b
commit c661b00f8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 300 additions and 38 deletions

View File

@ -24,6 +24,7 @@ const (
// resources to query in StatSummary when Resource.Type is "all"
var StatAllResourceTypes = []string{
// TODO: add Namespace here to decrease queries from the web process
Deployment,
ReplicationController,
Pod,

View File

@ -62,6 +62,14 @@ class Namespaces extends React.Component {
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
componentDidUpdate(prevProps) {
if (!_.isEqual(prevProps.match.params.namespace, this.props.match.params.namespace)) {
// React won't unmount this component when switching resource pages so we need to clear state
this.api.cancelCurrentRequests();
this.setState(this.getInitialState(this.props.match.params));
}
}
componentWillUnmount() {
window.clearInterval(this.timerId);
this.api.cancelCurrentRequests();

View File

@ -70,6 +70,7 @@ class NamespaceLanding extends React.Component {
}
this.setState({ pendingRequests: true });
// TODO: make this one request
let apiRequests = [
this.api.fetchMetrics(this.api.urlsForResource("namespace"))
];

View File

@ -25,13 +25,12 @@ import HomeIcon from '@material-ui/icons/Home';
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
import { Link } from 'react-router-dom';
import MenuIcon from '@material-ui/icons/Menu';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import NavigationResources from './NavigationResources.jsx';
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
import PropTypes from 'prop-types';
import React from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
import Version from './Version.jsx';
import ViewListIcon from '@material-ui/icons/ViewList';
import VisibilityIcon from '@material-ui/icons/Visibility';
import classNames from 'classnames';
import { withContext } from './util/AppContext.jsx';
@ -128,7 +127,6 @@ class NavigationBase extends React.Component {
getInitialState() {
return {
drawerOpen: true,
resourceMenuOpen: false,
helpMenuOpen: false,
latestVersion: '',
isLatest: true,
@ -171,10 +169,6 @@ class NavigationBase extends React.Component {
this.setState({ drawerOpen: false });
};
handleResourceMenuClick = () => {
this.setState(state => ({ resourceMenuOpen: !state.resourceMenuOpen }));
};
handleHelpMenuClick = () => {
this.setState(state => ({ helpMenuOpen: !state.helpMenuOpen }));
}
@ -240,24 +234,7 @@ class NavigationBase extends React.Component {
{ this.menuItem("/tap", "Tap", <VisibilityIcon />) }
{ this.menuItem("/top", "Top", <NetworkCheckIcon />) }
{ this.menuItem("/servicemesh", "Service Mesh", <CloudQueueIcon />) }
<MenuItem
className={classes.navMenuItem}
button
onClick={this.handleResourceMenuClick}>
<ListItemIcon><ViewListIcon /></ListItemIcon>
<ListItemText inset primary="Resources" />
{this.state.resourceMenuOpen ? <ExpandLess /> : <ExpandMore />}
</MenuItem>
<Collapse in={this.state.resourceMenuOpen} timeout="auto" unmountOnExit>
<MenuList dense component="div" disablePadding>
{ this.menuItem("/authorities", "Authorities", <NavigateNextIcon />) }
{ this.menuItem("/deployments", "Deployments", <NavigateNextIcon />) }
{ this.menuItem("/namespaces", "Namespaces", <NavigateNextIcon />) }
{ this.menuItem("/pods", "Pods", <NavigateNextIcon />) }
{ this.menuItem("/replicationcontrollers", "Replication Controllers", <NavigateNextIcon />) }
</MenuList>
</Collapse>
<NavigationResources />
</MenuList>
<Divider />
@ -315,9 +292,7 @@ class NavigationBase extends React.Component {
}
NavigationBase.propTypes = {
api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired,
}).isRequired,
api: PropTypes.shape({}).isRequired,
ChildComponent: PropTypes.func.isRequired,
classes: PropTypes.shape({}).isRequired,
location: ReactRouterPropTypes.location.isRequired,

View File

@ -0,0 +1,148 @@
import {
Collapse,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
} from '@material-ui/core';
import ExpandMore from '@material-ui/icons/ExpandMore';
import { Link } from 'react-router-dom';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import PropTypes from 'prop-types';
import React from 'react';
import SuccessRateDot from "./util/SuccessRateDot.jsx";
import _ from 'lodash';
import { friendlyTitle } from "./util/Utils.js";
import { processedMetricsPropType } from './util/MetricUtils.jsx';
import { withContext } from './util/AppContext.jsx';
import { withStyles } from '@material-ui/core/styles';
const styles = () => ({
navMenuItem: {
paddingLeft: "24px",
paddingRight: "12px",
},
navResourceItem: {
marginRight: "0",
paddingLeft: "38px",
},
navResourceText: {
overflow: "hidden",
padding: "0px 0px 0px 10px",
textOverflow: "ellipsis",
}
});
class NavigationResource extends React.Component {
static defaultProps = {
metrics: [],
}
static propTypes = {
api: PropTypes.shape({
prefixLink: PropTypes.func.isRequired,
}).isRequired,
classes: PropTypes.shape({}).isRequired,
metrics: PropTypes.arrayOf(processedMetricsPropType),
type: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.resources = _(props.metrics)
.filter(m => m.pods.meshedPods !== "0")
.map(m =>
_.merge(m, {
menuName: props.type === "namespaces" ? m.name : `${m.namespace}/${m.name}`
})
)
.sortBy("menuName")
.value();
this.to = props.api.prefixLink("/" + props.type);
this.state = {open: false};
}
handleOnClick = () => {
this.setState({ open: !this.state.open });
};
listItemIcon() {
let icon = <NavigateNextIcon />;
if (this.state.open) {
icon = <ExpandMore />;
}
return (<ListItemIcon>{icon}</ListItemIcon>);
}
menu() {
const { classes, type } = this.props;
return (
<MenuItem
className={classes.navMenuItem}
onClick={this.handleOnClick}>
{this.listItemIcon(this.resources)}
<ListItemText primary={friendlyTitle(type).plural} />
</MenuItem>
);
}
subMenu() {
const { api, classes } = this.props;
return (
<MenuList dense component="div" disablePadding>
<MenuItem
component={Link}
to={this.to}
className={classes.navMenuItem}
selected={this.to === window.location.pathname}>
<ListItemIcon className={classes.navResourceItem}>
<SuccessRateDot />
</ListItemIcon>
<ListItemText
disableTypography
primary="All"
className={classes.navResourceText} />
</MenuItem>
{
_.map(this.resources, r => {
let url = api.prefixLink(api.generateResourceURL(r));
return (
<MenuItem
component={Link}
to={url}
key={url}
className={classes.navMenuItem}
selected={url === window.location.pathname}>
<ListItemIcon className={classes.navResourceItem}>
<SuccessRateDot sr={r.successRate} />
</ListItemIcon>
<ListItemText
disableTypography
primary={r.menuName}
className={classes.navResourceText} />
</MenuItem>
);
})
}
</MenuList>
);
}
render() {
return (
<React.Fragment>
{this.menu()}
<Collapse in={this.state.open} timeout="auto" unmountOnExit>
{this.subMenu()}
</Collapse>
</React.Fragment>
);
}
}
export default withContext(withStyles(styles, { withTheme: true })(NavigationResource));

View File

@ -0,0 +1,105 @@
import {
Collapse,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
} from '@material-ui/core';
import { metricsPropType, processMultiResourceRollup } from './util/MetricUtils.jsx';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import NavigationResource from './NavigationResource.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import ViewListIcon from '@material-ui/icons/ViewList';
import _ from 'lodash';
import { withContext } from './util/AppContext.jsx';
import withREST from './util/withREST.jsx';
import { withStyles } from '@material-ui/core/styles';
const styles = () => ({
navMenuItem: {
paddingLeft: "24px",
paddingRight: "24px",
},
});
class NavigationResourcesBase extends React.Component {
static propTypes = {
classes: PropTypes.shape({}).isRequired,
data: PropTypes.arrayOf(metricsPropType.isRequired).isRequired,
}
constructor(props) {
super(props);
this.state = {open: false};
}
handleOnClick = () => {
this.setState({ open: !this.state.open });
};
menu() {
const { classes } = this.props;
return (
<MenuItem
className={classes.navMenuItem}
button
onClick={this.handleOnClick}>
<ListItemIcon><ViewListIcon /></ListItemIcon>
<ListItemText inset primary="Resources" />
{this.state.open ? <ExpandLess /> : <ExpandMore />}
</MenuItem>
);
}
subMenu() {
const { data } = this.props;
let allMetrics = {};
let nsMetrics = {};
if (_.has(data, '[0]')) {
allMetrics = processMultiResourceRollup(data[0]);
if (_.has(data, '[1]')) {
nsMetrics = processMultiResourceRollup(data[1]);
}
}
return (
<MenuList dense component="div" disablePadding>
<NavigationResource type="authorities" />
<NavigationResource type="deployments" metrics={allMetrics.deployment} />
<NavigationResource type="namespaces" metrics={nsMetrics.namespace} />
<NavigationResource type="pods" metrics={allMetrics.pod} />
<NavigationResource type="replicationcontrollers" metrics={allMetrics.replicationcontroller} />
</MenuList>
);
}
render() {
return (
<React.Fragment>
{this.menu()}
<Collapse in={this.state.open} timeout="auto" unmountOnExit>
{this.subMenu()}
</Collapse>
</React.Fragment>
);
}
}
export default withREST(
withContext(withStyles(styles, { withTheme: true })(NavigationResourcesBase)),
({api}) => [
// TODO: modify "all" to also retrieve namespaces, also share fetch with parent component
api.fetchMetrics(api.urlsForResource("all")),
api.fetchMetrics(api.urlsForResource("namespace")),
],
{
resetProps: ['resource'],
},
);

View File

@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { getSuccessRateClassification } from './MetricUtils.jsx';
import { statusClassNames } from './theme.js';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => statusClassNames(theme);
class SuccessRateDot extends React.Component {
render() {
const { sr, classes } = this.props;
return (
<div className={classNames("success-rate-dot", classes[getSuccessRateClassification(sr)])} />
);
}
}
SuccessRateDot.propTypes = {
classes: PropTypes.shape({}).isRequired,
sr: PropTypes.number,
};
SuccessRateDot.defaultProps = {
sr: null
};
export default withStyles(styles, { withTheme: true })(SuccessRateDot);

View File

@ -1,24 +1,20 @@
import Grid from '@material-ui/core/Grid';
import PropTypes from 'prop-types';
import React from 'react';
import SuccessRateDot from "./SuccessRateDot.jsx";
import _ from 'lodash';
import classNames from 'classnames';
import { getSuccessRateClassification } from './MetricUtils.jsx';
import { metricToFormatter } from './Utils.js';
import { statusClassNames } from './theme.js';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => statusClassNames(theme);
class SuccessRateMiniChart extends React.Component {
render() {
const { sr, classes } = this.props;
const { sr } = this.props;
return (
<Grid container alignItems="center" spacing={8}>
<Grid item>{metricToFormatter["SUCCESS_RATE"](sr)}</Grid>
<Grid item>{_.isNil(sr) ? null :
<div className={classNames("success-rate-dot", classes[getSuccessRateClassification(sr)])} />}
<SuccessRateDot sr={sr} />
}
</Grid>
</Grid>
);
@ -26,7 +22,6 @@ class SuccessRateMiniChart extends React.Component {
}
SuccessRateMiniChart.propTypes = {
classes: PropTypes.shape({}).isRequired,
sr: PropTypes.number,
};
@ -34,4 +29,4 @@ SuccessRateMiniChart.defaultProps = {
sr: null
};
export default withStyles(styles, { withTheme: true })(SuccessRateMiniChart);
export default SuccessRateMiniChart;