mirror of https://github.com/linkerd/linkerd2.git
446 lines
14 KiB
JavaScript
446 lines
14 KiB
JavaScript
import { UrlQueryParamTypes, addUrlProps } from 'react-url-query';
|
|
import {
|
|
defaultMaxRps,
|
|
emptyTapQuery,
|
|
httpMethods,
|
|
tapQueryPropType,
|
|
tapQueryProps
|
|
} from './util/TapUtils.jsx';
|
|
import Button from '@material-ui/core/Button';
|
|
import Card from '@material-ui/core/Card';
|
|
import CardContent from '@material-ui/core/CardContent';
|
|
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
|
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
|
|
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
|
|
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
|
|
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 TextField from '@material-ui/core/TextField';
|
|
import Typography from '@material-ui/core/Typography';
|
|
import _each from 'lodash/each';
|
|
import _flatten from 'lodash/flatten';
|
|
import _isEmpty from 'lodash/isEmpty';
|
|
import _isEqual from 'lodash/isEqual';
|
|
import _isNil from 'lodash/isNil';
|
|
import _map from 'lodash/map';
|
|
import _mapValues from 'lodash/mapValues';
|
|
import _merge from 'lodash/merge';
|
|
import _noop from 'lodash/noop';
|
|
import _omit from 'lodash/omit';
|
|
import _pick from 'lodash/pick';
|
|
import _some from 'lodash/some';
|
|
import _startCase from 'lodash/startCase';
|
|
import _uniq from 'lodash/uniq';
|
|
import _upperFirst from 'lodash/upperFirst';
|
|
import _values from 'lodash/values';
|
|
import { withStyles } from '@material-ui/core/styles';
|
|
|
|
// you can also tap resources to tap all pods in the resource
|
|
const resourceTypes = [
|
|
"deployment",
|
|
"daemonset",
|
|
"pod",
|
|
"replicationcontroller",
|
|
"statefulset"
|
|
];
|
|
|
|
const getResourceList = (resourcesByNs, ns) => {
|
|
return resourcesByNs[ns] || _uniq(_flatten(_values(resourcesByNs)));
|
|
};
|
|
|
|
const urlPropsQueryConfig = _mapValues(tapQueryProps, () => {
|
|
return { type: UrlQueryParamTypes.string };
|
|
});
|
|
|
|
const styles = theme => ({
|
|
root: {
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
},
|
|
formControl: {
|
|
margin: theme.spacing.unit,
|
|
minWidth: 200,
|
|
},
|
|
selectEmpty: {
|
|
'margin-top': '32px'
|
|
},
|
|
card: {
|
|
maxWidth: "100%",
|
|
},
|
|
actions: {
|
|
display: 'flex',
|
|
'padding-left': '32px'
|
|
},
|
|
expand: {
|
|
transform: 'rotate(0deg)',
|
|
transition: theme.transitions.create('transform', {
|
|
duration: theme.transitions.duration.shortest,
|
|
}),
|
|
marginLeft: 'auto',
|
|
[theme.breakpoints.up('sm')]: {
|
|
marginRight: -8,
|
|
},
|
|
},
|
|
expandOpen: {
|
|
transform: 'rotate(180deg)',
|
|
}
|
|
});
|
|
|
|
|
|
class TapQueryForm extends React.Component {
|
|
static propTypes = {
|
|
classes: PropTypes.shape({}).isRequired,
|
|
cmdName: PropTypes.string.isRequired,
|
|
enableAdvancedForm: PropTypes.bool,
|
|
handleTapClear: PropTypes.func,
|
|
handleTapStart: PropTypes.func.isRequired,
|
|
handleTapStop: PropTypes.func.isRequired,
|
|
query: tapQueryPropType.isRequired,
|
|
tapIsClosing: PropTypes.bool,
|
|
tapRequestInProgress: PropTypes.bool.isRequired,
|
|
updateQuery: PropTypes.func.isRequired
|
|
}
|
|
|
|
static defaultProps = {
|
|
enableAdvancedForm: true,
|
|
handleTapClear: _noop,
|
|
tapIsClosing: false
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
if (!_isEqual(props.resourcesByNs, state.resourcesByNs)) {
|
|
let resourcesByNs = props.resourcesByNs;
|
|
let authoritiesByNs = props.authoritiesByNs;
|
|
let namespaces = Object.keys(resourcesByNs).sort();
|
|
let resourceNames = getResourceList(resourcesByNs, state.query.namespace);
|
|
let toResourceNames = getResourceList(resourcesByNs, state.query.toNamespace);
|
|
let authorities = getResourceList(authoritiesByNs, state.query.namespace);
|
|
|
|
return _merge(state, {
|
|
resourcesByNs,
|
|
authoritiesByNs,
|
|
autocomplete: {
|
|
namespace: namespaces,
|
|
resource: resourceNames,
|
|
toNamespace: namespaces,
|
|
toResource: toResourceNames,
|
|
authority: authorities
|
|
}
|
|
});
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
let query = _merge({}, props.query, _pick(this.props, Object.keys(tapQueryProps)));
|
|
props.updateQuery(query);
|
|
|
|
let advancedFormExpanded = _some(
|
|
_omit(query, ['namespace', 'resource']),
|
|
v => !_isEmpty(v));
|
|
|
|
this.state = {
|
|
query,
|
|
advancedFormExpanded,
|
|
authoritiesByNs: {},
|
|
resourcesByNs: {},
|
|
autocomplete: {
|
|
namespace: [],
|
|
resource: [],
|
|
toNamespace: [],
|
|
toResource: [],
|
|
authority: []
|
|
},
|
|
};
|
|
}
|
|
|
|
handleFormChange = (name, scopeResource) => {
|
|
let state = {
|
|
query: this.state.query,
|
|
autocomplete: this.state.autocomplete
|
|
};
|
|
|
|
let shouldScopeAuthority = name === "namespace";
|
|
|
|
return event => {
|
|
let formVal = event.target.value;
|
|
state.query[name] = formVal;
|
|
this.handleUrlUpdate(name, formVal);
|
|
|
|
if (!_isNil(scopeResource)) {
|
|
// scope the available typeahead resources to the selected namespace
|
|
state.autocomplete[scopeResource] = this.state.resourcesByNs[formVal];
|
|
state.query[scopeResource] = `namespace/${formVal}`;
|
|
this.handleUrlUpdate(scopeResource, `namespace/${formVal}`);
|
|
}
|
|
|
|
if (shouldScopeAuthority) {
|
|
state.autocomplete.authority = this.state.authoritiesByNs[formVal];
|
|
}
|
|
|
|
this.setState(state);
|
|
this.props.updateQuery(state.query);
|
|
};
|
|
}
|
|
|
|
// Each time state.query is updated, this method calls the equivalent
|
|
// onChange method to reflect the update in url query params. These onChange
|
|
// methods are automatically added to props by react-url-query.
|
|
handleUrlUpdate = (name, formVal) => {
|
|
this.props[`onChange${_upperFirst(name)}`](formVal);
|
|
}
|
|
|
|
handleFormEvent = name => {
|
|
let state = {
|
|
query: this.state.query
|
|
};
|
|
|
|
return event => {
|
|
state.query[name] = event.target.value;
|
|
this.handleUrlUpdate(name, event.target.value);
|
|
this.setState(state);
|
|
this.props.updateQuery(state.query);
|
|
};
|
|
}
|
|
|
|
handleAdvancedFormExpandClick = () => {
|
|
this.setState(state => ({advancedFormExpanded: !state.advancedFormExpanded}));
|
|
}
|
|
|
|
autoCompleteData = name => {
|
|
return _uniq(
|
|
this.state.autocomplete[name].filter(d => d.indexOf(this.state.query[name]) !== -1)
|
|
).sort();
|
|
}
|
|
|
|
resetTapForm = () => {
|
|
this.setState({
|
|
query: emptyTapQuery()
|
|
});
|
|
|
|
_each(this.state.query, (_val, name) => {
|
|
this.handleUrlUpdate(name, null);
|
|
});
|
|
|
|
this.props.updateQuery(emptyTapQuery(), true);
|
|
this.props.handleTapClear();
|
|
}
|
|
|
|
renderResourceSelect = (resourceKey, namespaceKey) => {
|
|
let selectedNs = this.state.query[namespaceKey];
|
|
let nsEmpty = _isNil(selectedNs) || _isEmpty(selectedNs);
|
|
let { classes } = this.props;
|
|
|
|
let resourceOptions = resourceTypes.concat(
|
|
this.state.autocomplete[resourceKey] || [],
|
|
nsEmpty ? [] : [`namespace/${selectedNs}`]
|
|
).sort();
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<InputLabel htmlFor={resourceKey}>{_startCase(resourceKey)}</InputLabel>
|
|
<Select
|
|
value={nsEmpty ? _startCase(resourceKey) : this.state.query[resourceKey]}
|
|
onChange={this.handleFormChange(resourceKey)}
|
|
inputProps={{ name: resourceKey, id: resourceKey }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
resourceOptions.map(resource => (
|
|
<MenuItem key={`${namespaceKey}-${resourceKey}-${resource}`} value={resource}>{resource}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
renderNamespaceSelect = (title, namespaceKey, resourceKey) => {
|
|
let { classes } = this.props;
|
|
return (
|
|
<React.Fragment>
|
|
<InputLabel htmlFor={namespaceKey}>{title}</InputLabel>
|
|
<Select
|
|
value={this.state.query[namespaceKey]}
|
|
onChange={this.handleFormChange(namespaceKey, resourceKey)}
|
|
inputProps={{ name: namespaceKey, id: namespaceKey }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
_map(this.state.autocomplete[namespaceKey], (n, i) => (
|
|
<MenuItem key={`ns-dr-${i}`} value={n}>{n}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
renderTapButton = (tapInProgress, tapIsClosing) => {
|
|
if (tapIsClosing) {
|
|
return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" disabled={true}>Stop</Button>);
|
|
} else if (tapInProgress) {
|
|
return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" onClick={this.props.handleTapStop}>Stop</Button>);
|
|
} else {
|
|
return (
|
|
<Button
|
|
color="primary"
|
|
variant="outlined"
|
|
className="tap-ctrl tap-start"
|
|
disabled={!this.state.query.namespace || !this.state.query.resource}
|
|
onClick={this.props.handleTapStart}>
|
|
Start
|
|
</Button>);
|
|
}
|
|
}
|
|
|
|
renderTextInput = (title, key, helperText) => {
|
|
let { classes } = this.props;
|
|
return (
|
|
<TextField
|
|
id={key}
|
|
label={title}
|
|
className={classes.formControl}
|
|
value={this.state.query[key]}
|
|
onChange={this.handleFormEvent(key)}
|
|
helperText={helperText}
|
|
margin="normal" />
|
|
);
|
|
}
|
|
|
|
renderAdvancedTapFormContent() {
|
|
const { classes } = this.props;
|
|
|
|
return (
|
|
<Grid container>
|
|
|
|
<Grid container spacing={24}>
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl}>
|
|
{this.renderNamespaceSelect("To Namespace", "toNamespace", "toResource")}
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl} disabled={_isEmpty(this.state.query.toNamespace)}>
|
|
{this.renderResourceSelect("toResource", "toNamespace")}
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid container spacing={24}>
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl}>
|
|
<InputLabel htmlFor="authority">Authority</InputLabel>
|
|
<Select
|
|
value={this.state.query.authority}
|
|
onChange={this.handleFormChange("authority")}
|
|
inputProps={{ name: 'authority', id: 'authority' }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
_map(this.state.autocomplete.authority, (d, i) => (
|
|
<MenuItem key={`authority-${i}`} value={d}>{d}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
<FormHelperText>Display requests with this :authority</FormHelperText>
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid item xs={6} md={3}>
|
|
{ this.renderTextInput("Path", "path", "Display requests with paths that start with this prefix") }
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid container spacing={24}>
|
|
<Grid item xs={6} md={3}>
|
|
{ this.renderTextInput("Scheme", "scheme", "Display requests with this scheme") }
|
|
</Grid>
|
|
<Grid item xs={6} md={3}>
|
|
{ this.renderTextInput("Max RPS", "maxRps", `Maximum requests per second to tap. Default ${defaultMaxRps}`) }
|
|
</Grid>
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl}>
|
|
<InputLabel htmlFor="method">HTTP method</InputLabel>
|
|
<Select
|
|
value={this.state.query.method}
|
|
onChange={this.handleFormChange("method")}
|
|
inputProps={{ name: 'method', id: 'method' }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
_map(httpMethods, d => (
|
|
<MenuItem key={`method-${d}`} value={d}>{d}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
<FormHelperText>Display requests with this HTTP method</FormHelperText>
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
renderAdvancedTapForm() {
|
|
return (
|
|
<ExpansionPanel expanded={this.state.advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick}>
|
|
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="caption" gutterBottom>
|
|
{this.state.advancedFormExpanded ? "Hide filters" : "Show more filters"}
|
|
</Typography>
|
|
</ExpansionPanelSummary>
|
|
|
|
<ExpansionPanelDetails>
|
|
{this.renderAdvancedTapFormContent()}
|
|
</ExpansionPanelDetails>
|
|
</ExpansionPanel>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { classes } = this.props;
|
|
|
|
return (
|
|
<Card className={classes.card}>
|
|
<CardContent>
|
|
<Grid container spacing={24}>
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl}>
|
|
{this.renderNamespaceSelect("Namespace", "namespace", "resource")}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={6} md={3}>
|
|
<FormControl className={classes.formControl} disabled={_isEmpty(this.state.query.namespace)}>
|
|
{this.renderResourceSelect("resource", "namespace")}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={4} md={1}>
|
|
{ this.renderTapButton(this.props.tapRequestInProgress, this.props.tapIsClosing) }
|
|
</Grid>
|
|
|
|
<Grid item xs={4} md={1}>
|
|
<Button onClick={this.resetTapForm} disabled={this.props.tapRequestInProgress}>Reset</Button>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
|
|
<QueryToCliCmd cmdName={this.props.cmdName} query={this.state.query} resource={this.state.query.resource} />
|
|
|
|
{ !this.props.enableAdvancedForm ? null : this.renderAdvancedTapForm() }
|
|
|
|
</Card>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default addUrlProps({ urlPropsQueryConfig })(withStyles(styles)(TapQueryForm));
|