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

388 lines
11 KiB
JavaScript

import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { addUrlProps, UrlQueryParamTypes } from 'react-url-query';
import {
AutoComplete,
Button,
Col,
Form,
Icon,
Input,
Row,
Select
} from 'antd';
import {
defaultMaxRps,
emptyTapQuery,
httpMethods,
tapQueryProps,
tapQueryPropType
} from './util/TapUtils.jsx';
const colSpan = 5;
const rowGutter = 16;
// 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 };
});
class TapQueryForm extends React.Component {
static propTypes = {
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
}
constructor(props) {
super(props);
let query = _.merge({}, props.query, _.pick(this.props, _.keys(tapQueryProps)));
props.updateQuery(query);
let showAdvancedForm = _.some(
_.omit(query, ['namespace', 'resource']),
v => !_.isEmpty(v));
this.state = {
query,
showAdvancedForm,
authoritiesByNs: {},
resourcesByNs: {},
autocomplete: {
namespace: [],
resource: [],
toNamespace: [],
toResource: [],
authority: []
},
};
}
static getDerivedStateFromProps(props, state) {
if (!_.isEqual(props.resourcesByNs, state.resourcesByNs)) {
let resourcesByNs = props.resourcesByNs;
let authoritiesByNs = props.authoritiesByNs;
let namespaces = _.sortBy(_.keys(resourcesByNs));
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;
}
}
toggleAdvancedForm = show => {
this.setState({
showAdvancedForm: show
});
}
handleFormChange = (name, scopeResource, shouldScopeAuthority) => {
let state = {
query: this.state.query,
autocomplete: this.state.autocomplete
};
return formVal => {
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];
if (_.isEmpty(state.query[scopeResource]) || state.query[scopeResource].indexOf("namespace") !== -1) {
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);
};
}
autoCompleteData = name => {
return _(this.state.autocomplete[name])
.filter(d => d.indexOf(this.state.query[name]) !== -1)
.uniq()
.sortBy()
.value();
}
resetTapForm = () => {
this.setState({
query: emptyTapQuery()
});
_.each(this.state.query, (_val, name) => {
this.handleUrlUpdate(name, null);
});
this.props.updateQuery(emptyTapQuery(), true);
this.props.handleTapClear();
}
renderAdvancedTapForm = () => {
return (
<React.Fragment>
<Row gutter={rowGutter}>
<Col span={colSpan}>
<Form.Item>
<Select
showSearch
allowClear
placeholder="To Namespace"
optionFilterProp="children"
onChange={this.handleFormChange("toNamespace", "toResource")}
value={this.state.query.toNamespace}>
{
_.map(this.state.autocomplete.toNamespace, (n, i) => (
<Select.Option key={`ns-dr-${i}`} value={n}>{n}</Select.Option>
))
}
</Select>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item>
{this.renderResourceSelect("toResource", "toNamespace")}
</Form.Item>
</Col>
</Row>
<Row gutter={rowGutter}>
<Col span={2 * colSpan}>
<Form.Item
extra="Display requests with this :authority">
<AutoComplete
dataSource={this.autoCompleteData("authority")}
onSelect={this.handleFormChange("authority")}
onSearch={this.handleFormChange("authority")}
placeholder="Authority"
value={this.state.query.authority} />
</Form.Item>
</Col>
<Col span={2 * colSpan}>
<Form.Item
extra="Display requests with paths that start with this prefix">
<Input
placeholder="Path"
onChange={this.handleFormEvent("path")}
value={this.state.query.path} />
</Form.Item>
</Col>
</Row>
<Row gutter={rowGutter}>
<Col span={colSpan}>
<Form.Item
extra="Display requests with this scheme">
<Input
placeholder="Scheme"
onChange={this.handleFormEvent("scheme")}
value={this.state.query.scheme} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
extra={`Maximum requests per second to tap (default ${defaultMaxRps})`}>
<Input
placeholder="Max RPS"
onChange={this.handleFormEvent("maxRps")}
value={this.state.query.maxRps} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
extra="Display requests with this HTTP method">
<Select
allowClear
placeholder="HTTP method"
onChange={this.handleFormChange("method")}
value={this.state.query.method}>
{
_.map(httpMethods, m =>
<Select.Option key={`method-select-${m}`} value={m}>{m}</Select.Option>
)
}
</Select>
</Form.Item>
</Col>
</Row>
</React.Fragment>
);
}
renderResourceSelect = (resourceKey, namespaceKey) => {
let selectedNs = this.state.query[namespaceKey];
let nsEmpty = _.isNil(selectedNs) || _.isEmpty(selectedNs);
let resourceOptions = _.concat(
resourceTypes,
this.state.autocomplete[resourceKey] || [],
nsEmpty ? [] : [`namespace/${selectedNs}`]
);
return (
<Select
showSearch
allowClear
disabled={nsEmpty}
value={nsEmpty ? _.startCase(resourceKey) : this.state.query[resourceKey]}
placeholder={_.startCase(resourceKey)}
optionFilterProp="children"
onChange={this.handleFormChange(resourceKey)}>
{
_.map(_.sortBy(resourceOptions), resource => (
<Select.Option
key={`${resourceKey}-${resource}`}
value={resource}>{resource}
</Select.Option>
)
)
}
</Select>
);
}
renderTapButton = (tapInProgress, tapIsClosing) => {
if (tapIsClosing) {
return (<Button type="primary" className="tap-ctrl tap-stop" disabled={true}>Stop</Button>);
} else if (tapInProgress) {
return (<Button type="primary" className="tap-ctrl tap-stop" onClick={this.props.handleTapStop}>Stop</Button>);
} else {
return (
<Button
type="primary"
className="tap-ctrl tap-start"
disabled={!this.state.query.namespace || !this.state.query.resource}
onClick={this.props.handleTapStart}>
Start
</Button>);
}
}
render() {
return (
<Form className="tap-form">
<Row gutter={rowGutter}>
<Col span={colSpan}>
<Form.Item>
<Select
showSearch
allowClear
placeholder="Namespace"
optionFilterProp="children"
onChange={this.handleFormChange("namespace", "resource", true)}
value={this.state.query.namespace}>
{
_.map(this.state.autocomplete.namespace, (n, i) => (
<Select.Option key={`ns-dr-${i}`} value={n}>{n}</Select.Option>
))
}
</Select>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item>
{this.renderResourceSelect("resource", "namespace")}
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item>
{ this.renderTapButton(this.props.tapRequestInProgress, this.props.tapIsClosing) }
<Button onClick={this.resetTapForm} disabled={this.props.tapRequestInProgress}>Reset</Button>
</Form.Item>
</Col>
</Row>
{
!this.props.enableAdvancedForm ? null :
<React.Fragment>
<Button
className="tap-form-toggle"
onClick={() => this.toggleAdvancedForm(!this.state.showAdvancedForm)}>
{
this.state.showAdvancedForm ? "Hide filters" : "Show more request filters"
} <Icon type={this.state.showAdvancedForm ? 'up' : 'down'} />
</Button>
{ !this.state.showAdvancedForm ? null : this.renderAdvancedTapForm() }
</React.Fragment>
}
</Form>
);
}
}
export default addUrlProps({ urlPropsQueryConfig })(TapQueryForm);