mirror of https://github.com/linkerd/linkerd2.git
483 lines
15 KiB
JavaScript
483 lines
15 KiB
JavaScript
import { StringParam, withQueryParams } from 'use-query-params';
|
|
import {
|
|
defaultMaxRps,
|
|
emptyTapQuery,
|
|
httpMethods,
|
|
tapQueryPropType,
|
|
tapQueryProps,
|
|
tapResourceTypes,
|
|
} 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 { Trans } from '@lingui/macro';
|
|
import Typography from '@material-ui/core/Typography';
|
|
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 _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 _uniq from 'lodash/uniq';
|
|
import _values from 'lodash/values';
|
|
import { withStyles } from '@material-ui/core/styles';
|
|
|
|
|
|
const getResourceList = (resourcesByNs, ns) => {
|
|
return resourcesByNs[ns] || _uniq(_flatten(_values(resourcesByNs)));
|
|
};
|
|
|
|
const urlPropsQueryConfig = {};
|
|
Object.keys(tapQueryProps).forEach(value => {
|
|
urlPropsQueryConfig[value] = StringParam;
|
|
});
|
|
|
|
const styles = theme => ({
|
|
root: {
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
},
|
|
formControlWrapper: {
|
|
minWidth: 200,
|
|
},
|
|
formControl: {
|
|
padding: theme.spacing(1),
|
|
paddingLeft: 0,
|
|
margin: 0,
|
|
minWidth: 'inherit',
|
|
maxWidth: '100%',
|
|
width: 'auto',
|
|
},
|
|
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)',
|
|
},
|
|
resetButton: {
|
|
marginLeft: theme.spacing(1),
|
|
},
|
|
});
|
|
|
|
|
|
class TapQueryForm extends React.Component {
|
|
static getDerivedStateFromProps(props, state) {
|
|
if (!_isEqual(props.resourcesByNs, state.resourcesByNs)) {
|
|
const resourcesByNs = props.resourcesByNs;
|
|
const authoritiesByNs = props.authoritiesByNs;
|
|
const namespaces = Object.keys(resourcesByNs).sort();
|
|
const resourceNames = getResourceList(resourcesByNs, state.query.namespace);
|
|
const toResourceNames = getResourceList(resourcesByNs, state.query.toNamespace);
|
|
const 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);
|
|
|
|
const query = _merge({}, props.currentQuery, _pick(props.query, Object.keys(tapQueryProps)));
|
|
props.updateQuery(query);
|
|
|
|
const advancedFormExpanded = _some(
|
|
_omit(query, ['namespace', 'resource']),
|
|
v => !_isEmpty(v),
|
|
);
|
|
|
|
this.state = {
|
|
query,
|
|
advancedFormExpanded,
|
|
authoritiesByNs: {},
|
|
resourcesByNs: {},
|
|
autocomplete: {
|
|
namespace: [],
|
|
resource: [],
|
|
toNamespace: [],
|
|
toResource: [],
|
|
authority: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
handleFormChange = (name, scopeResource) => {
|
|
const { query, autocomplete, resourcesByNs, authoritiesByNs } = this.state;
|
|
const { updateQuery } = this.props;
|
|
|
|
const state = {
|
|
query,
|
|
autocomplete,
|
|
};
|
|
|
|
const shouldScopeAuthority = name === 'namespace';
|
|
const newQueryValues = {};
|
|
|
|
return event => {
|
|
const formVal = event.target.value;
|
|
state.query[name] = formVal;
|
|
newQueryValues[name] = formVal;
|
|
|
|
if (!_isNil(scopeResource)) {
|
|
// scope the available typeahead resources to the selected namespace
|
|
state.autocomplete[scopeResource] = resourcesByNs[formVal];
|
|
state.query[scopeResource] = `namespace/${formVal}`;
|
|
newQueryValues[scopeResource] = `namespace/${formVal}`;
|
|
}
|
|
|
|
if (shouldScopeAuthority) {
|
|
state.autocomplete.authority = authoritiesByNs[formVal];
|
|
}
|
|
|
|
this.setState(state);
|
|
updateQuery(state.query);
|
|
this.handleUrlUpdate(newQueryValues);
|
|
};
|
|
}
|
|
|
|
// Each time state.query is updated, this method calls setQuery provided
|
|
// by useQueryParams HOC to partially update url query params that have
|
|
// changed
|
|
handleUrlUpdate = query => {
|
|
const { setQuery } = this.props;
|
|
setQuery({ ...query });
|
|
}
|
|
|
|
handleFormEvent = name => {
|
|
const { query } = this.state;
|
|
const { updateQuery } = this.props;
|
|
|
|
const state = {
|
|
query,
|
|
};
|
|
|
|
return event => {
|
|
state.query[name] = event.target.value;
|
|
this.handleUrlUpdate(state.query);
|
|
this.setState(state);
|
|
updateQuery(state.query);
|
|
};
|
|
}
|
|
|
|
handleAdvancedFormExpandClick = () => {
|
|
const { advancedFormExpanded } = this.state;
|
|
this.setState({ advancedFormExpanded: !advancedFormExpanded });
|
|
}
|
|
|
|
autoCompleteData = name => {
|
|
const { autocomplete, query } = this.state;
|
|
return _uniq(
|
|
autocomplete[name].filter(d => d.indexOf(query[name]) !== -1),
|
|
).sort();
|
|
}
|
|
|
|
resetTapForm = () => {
|
|
const { updateQuery, handleTapClear } = this.props;
|
|
|
|
this.setState({
|
|
query: emptyTapQuery(),
|
|
});
|
|
|
|
this.handleUrlUpdate(emptyTapQuery());
|
|
|
|
updateQuery(emptyTapQuery(), true);
|
|
handleTapClear();
|
|
}
|
|
|
|
renderResourceSelect = (resourceKey, namespaceKey) => {
|
|
const { autocomplete, query } = this.state;
|
|
const { classes } = this.props;
|
|
|
|
const selectedNs = query[namespaceKey];
|
|
const nsEmpty = _isNil(selectedNs) || _isEmpty(selectedNs);
|
|
|
|
const resourceOptions = tapResourceTypes.concat(
|
|
autocomplete[resourceKey] || [],
|
|
nsEmpty ? [] : [`namespace/${selectedNs}`],
|
|
).sort();
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<InputLabel htmlFor={resourceKey}>{resourceKey === 'resource' ? <Trans>formResource</Trans> : <Trans>formToResource</Trans>}</InputLabel>
|
|
<Select
|
|
value={!nsEmpty && resourceOptions.includes(query[resourceKey]) ? 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) => {
|
|
const { autocomplete, query } = this.state;
|
|
const { classes } = this.props;
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<InputLabel htmlFor={namespaceKey}>{title}</InputLabel>
|
|
<Select
|
|
value={autocomplete[namespaceKey].includes(query[namespaceKey]) ? query[namespaceKey] : ''}
|
|
onChange={this.handleFormChange(namespaceKey, resourceKey)}
|
|
inputProps={{ name: namespaceKey, id: namespaceKey }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
_map(autocomplete[namespaceKey], (n, i) => (
|
|
<MenuItem key={`ns-dr-${i}`} value={n}>{n}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
renderTapButton = (tapInProgress, tapIsClosing) => {
|
|
const { query } = this.state;
|
|
const { handleTapStart, handleTapStop } = this.props;
|
|
|
|
if (tapIsClosing) {
|
|
return (
|
|
<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" disabled>
|
|
<Trans>buttonStop</Trans>
|
|
</Button>
|
|
);
|
|
} else if (tapInProgress) {
|
|
return (
|
|
<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" onClick={handleTapStop}>
|
|
<Trans>buttonStop</Trans>
|
|
</Button>
|
|
);
|
|
} else {
|
|
return (
|
|
<Button
|
|
color="primary"
|
|
variant="outlined"
|
|
className="tap-ctrl tap-start"
|
|
disabled={!query.namespace || !query.resource}
|
|
onClick={handleTapStart}>
|
|
<Trans>buttonStart</Trans>
|
|
</Button>
|
|
);
|
|
}
|
|
}
|
|
|
|
renderTextInput = (title, key, helperText) => {
|
|
const { query } = this.state;
|
|
const { classes } = this.props;
|
|
|
|
return (
|
|
<TextField
|
|
id={key}
|
|
label={title}
|
|
className={classes.formControl}
|
|
value={query[key]}
|
|
onChange={this.handleFormEvent(key)}
|
|
helperText={helperText}
|
|
margin="normal" />
|
|
);
|
|
}
|
|
|
|
renderAdvancedTapFormContent() {
|
|
const { autocomplete, query } = this.state;
|
|
const { classes } = this.props;
|
|
|
|
return (
|
|
<Grid container>
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
<FormControl className={classes.formControl}>
|
|
{this.renderNamespaceSelect(<Trans>formToNamespace</Trans>, 'toNamespace', 'toResource')}
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
<FormControl className={classes.formControl} disabled={_isEmpty(query.toNamespace)}>
|
|
{this.renderResourceSelect('toResource', 'toNamespace')}
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={6} md={3} classes={{ item: classes.formControlWrapper }}>
|
|
<FormControl className={classes.formControl}>
|
|
<InputLabel htmlFor="authority"><Trans>formAuthority</Trans></InputLabel>
|
|
<Select
|
|
value={query.authority}
|
|
onChange={this.handleFormChange('authority')}
|
|
inputProps={{ name: 'authority', id: 'authority' }}
|
|
className={classes.selectEmpty}>
|
|
{
|
|
_map(autocomplete.authority, (d, i) => (
|
|
<MenuItem key={`authority-${i}`} value={d}>{d}</MenuItem>
|
|
))
|
|
}
|
|
</Select>
|
|
<FormHelperText><Trans>formAuthorityHelpText</Trans></FormHelperText>
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
{ this.renderTextInput(<Trans>formPath</Trans>, 'path', <Trans>formPathHelpText</Trans>) }
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
{ this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', <Trans>formSchemeHelpText</Trans>) }
|
|
</Grid>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
{ this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', <Trans>formMaxRPSHelpText {defaultMaxRps}</Trans>) }
|
|
</Grid>
|
|
<Grid item xs={6} md={3} className={classes.formControlWrapper}>
|
|
<FormControl className={classes.formControl}>
|
|
<InputLabel htmlFor="method"><Trans>formHTTPMethod</Trans></InputLabel>
|
|
<Select
|
|
value={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><Trans>formHTTPMethodHelpText</Trans></FormHelperText>
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
renderAdvancedTapForm() {
|
|
const { advancedFormExpanded } = this.state;
|
|
|
|
return (
|
|
<ExpansionPanel expanded={advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick} elevation={3}>
|
|
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="caption" gutterBottom>
|
|
{advancedFormExpanded ? <Trans>formHideFilters</Trans> : <Trans>formShowFilters</Trans>}
|
|
</Typography>
|
|
</ExpansionPanelSummary>
|
|
|
|
<ExpansionPanelDetails>
|
|
{this.renderAdvancedTapFormContent()}
|
|
</ExpansionPanelDetails>
|
|
</ExpansionPanel>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { query } = this.state;
|
|
const { tapRequestInProgress, tapIsClosing, cmdName, enableAdvancedForm, classes } = this.props;
|
|
|
|
return (
|
|
<Card className={classes.card} elevation={3}>
|
|
<CardContent>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={6} md="auto" className={classes.formControlWrapper}>
|
|
<FormControl className={classes.formControl} fullWidth>
|
|
{this.renderNamespaceSelect(<Trans>formNamespace</Trans>, 'namespace', 'resource')}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={6} md="auto" className={classes.formControlWrapper}>
|
|
<FormControl className={classes.formControl} disabled={_isEmpty(query.namespace)} fullWidth>
|
|
{this.renderResourceSelect('resource', 'namespace')}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item>
|
|
{ this.renderTapButton(tapRequestInProgress, tapIsClosing) }
|
|
<Button onClick={this.resetTapForm} disabled={tapRequestInProgress} className={classes.resetButton}>
|
|
<Trans>buttonReset</Trans>
|
|
</Button>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
|
|
<QueryToCliCmd cmdName={cmdName} query={query} resource={query.resource} />
|
|
|
|
{ !enableAdvancedForm ? null : this.renderAdvancedTapForm() }
|
|
|
|
</Card>
|
|
);
|
|
}
|
|
}
|
|
|
|
TapQueryForm.propTypes = {
|
|
authoritiesByNs: PropTypes.shape({}).isRequired,
|
|
cmdName: PropTypes.string.isRequired,
|
|
currentQuery: tapQueryPropType.isRequired,
|
|
enableAdvancedForm: PropTypes.bool,
|
|
handleTapClear: PropTypes.func,
|
|
handleTapStart: PropTypes.func.isRequired,
|
|
handleTapStop: PropTypes.func.isRequired,
|
|
query: tapQueryPropType.isRequired,
|
|
resourcesByNs: PropTypes.shape({}).isRequired,
|
|
setQuery: PropTypes.func.isRequired,
|
|
tapIsClosing: PropTypes.bool,
|
|
tapRequestInProgress: PropTypes.bool.isRequired,
|
|
updateQuery: PropTypes.func.isRequired,
|
|
};
|
|
|
|
TapQueryForm.defaultProps = {
|
|
enableAdvancedForm: true,
|
|
handleTapClear: _noop,
|
|
tapIsClosing: false,
|
|
};
|
|
|
|
export default withQueryParams(urlPropsQueryConfig, withStyles(styles)(TapQueryForm));
|