linkerd2/web/app/js/components/TopRoutes.jsx

250 lines
7.1 KiB
JavaScript

import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import ErrorBanner from './ErrorBanner.jsx';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Grid from '@material-ui/core/Grid';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import PropTypes from 'prop-types';
import QueryToCliCmd from './QueryToCliCmd.jsx';
import React from 'react';
import Select from '@material-ui/core/Select';
import TopRoutesModule from './TopRoutesModule.jsx';
import _ from 'lodash';
import { groupResourcesByNs } from './util/MetricUtils.jsx';
import { withContext } from './util/AppContext.jsx';
class TopRoutes extends React.Component {
static propTypes = {
api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired,
}).isRequired
}
constructor(props) {
super(props);
this.api = this.props.api;
this.state = {
query: {
resource_name: '',
namespace: '',
from_name: '',
from_type: '',
from_namespace: ''
},
error: null,
services: [],
namespaces: [],
resourcesByNs: {},
pollingInterval: 5000,
pendingRequests: false,
requestInProgress: false
};
}
componentDidMount() {
this.startServerPolling();
}
componentWillUnmount() {
this.stopServerPolling();
}
loadFromServer = () => {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
}
this.setState({ pendingRequests: true });
let allMetricsUrl = this.api.urlsForResource("all");
this.api.setCurrentRequests([
this.api.fetchServices(),
this.api.fetchMetrics(allMetricsUrl)
]);
this.serverPromise = Promise.all(this.api.getCurrentPromises())
.then(([svcList, allMetrics]) => {
let services = _.get(svcList, 'services', []);
let namespaces = _.uniq(_.map(services, 'namespace'));
let { resourcesByNs } = groupResourcesByNs(allMetrics);
this.setState({
services,
namespaces,
resourcesByNs,
pendingRequests: false,
error: null
});
})
.catch(this.handleApiError);
}
handleApiError = e => {
if (e.isCanceled) {
return;
}
this.setState({
pendingRequests: false,
error: e
});
}
startServerPolling = () => {
this.loadFromServer();
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
stopServerPolling = () => {
window.clearInterval(this.timerId);
this.api.cancelCurrentRequests();
}
handleBtnClick = inProgress => () => {
this.setState({
requestInProgress: inProgress
});
}
handleNamespaceSelect = e => {
let query = this.state.query;
query.namespace = _.get(e, 'target.value');
this.setState({ query });
};
handleResourceSelect = e => {
let query = this.state.query;
let resource = _.get(e, 'target.value');
let [resource_type, resource_name] = resource.split("/");
query.resource_name = resource_name;
query.resource_type = resource_type;
this.setState({ query });
}
renderRoutesQueryForm = () => {
return (
<CardContent>
<Grid container direction="column" spacing={16}>
<Grid item container spacing={8} alignItems="center">
<Grid item xs={6} md={3}>
{ this.renderNamespaceDropdown("Namespace", "namespace", "Namespace to query") }
</Grid>
<Grid item xs={6} md={3}>
{ this.renderServiceDropdown() }
</Grid>
</Grid>
<Grid item container spacing={8} alignItems="center">
<Grid item>
<Button
color="primary"
variant="outlined"
disabled={this.state.requestInProgress}
onClick={this.handleBtnClick(true)}>
Start
</Button>
</Grid>
<Grid item>
<Button
color="default"
variant="outlined"
disabled={!this.state.requestInProgress}
onClick={this.handleBtnClick(false)}>
Stop
</Button>
</Grid>
</Grid>
</Grid>
</CardContent>
);
}
renderNamespaceDropdown = (title, key, helperText) => {
return (
<FormControl>
<InputLabel htmlFor={`${key}-dropdown`}>{title}</InputLabel>
<Select
value={this.state.query[key]}
onChange={this.handleNamespaceSelect}
inputProps={{
name: key,
id: `${key}-dropdown`,
}}
name={key}>
{
_.map(_.sortBy(this.state.namespaces), ns =>
<MenuItem key={`namespace-${ns}`} value={ns}>{ns}</MenuItem>)
}
</Select>
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
);
}
renderServiceDropdown = () => {
let key = "resource_name";
let services = _.chain(this.state.services)
.filter(['namespace', this.state.query.namespace])
.map(svc => `service/${svc.name}`).value();
let otherResources = this.state.resourcesByNs[this.state.query.namespace] || [];
let { query } = this.state;
let dropdownOptions = _.sortBy(_.concat(services, otherResources));
let dropdownVal = _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? "" :
query.resource_type + "/" + query.resource_name;
return (
<FormControl>
<InputLabel htmlFor={`${key}-dropdown`}>Resource</InputLabel>
<Select
value={dropdownVal}
onChange={this.handleResourceSelect}
disabled={_.isEmpty(query.namespace)}
autoWidth
inputProps={{
name: key,
id: `${key}-dropdown`,
}}
name={key}>
{
_.map(dropdownOptions, resource => <MenuItem key={resource} value={resource}>{resource}</MenuItem>)
}
</Select>
<FormHelperText>Resource to query</FormHelperText>
</FormControl>
);
}
render() {
let query = this.state.query;
let from = '';
if (_.isEmpty(query.from_type)) {
from = query.from_name;
} else {
from = `${query.from_type}${_.isEmpty(query.from_name) ? "" : "/"}${query.from_name}`;
}
query.from = from;
return (
<div>
{
!this.state.error ? null :
<ErrorBanner message={this.state.error} onHideMessage={() => this.setState({ error: null })} />
}
<Card>
{ this.renderRoutesQueryForm() }
{ _.isEmpty(query.resource_name) || _.isEmpty(query.resource_type) ? null :
<QueryToCliCmd cmdName="routes" query={query} resource={query.resource_type + "/" + query.resource_name} /> }
{ !this.state.requestInProgress ? null : <TopRoutesModule query={this.state.query} /> }
</Card>
</div>
);
}
}
export default withContext(TopRoutes);