Refactor Tap query form into its own component (#1446)

Tap.jsx is really large and contains a lot of logic that pertains only to the Tap Query Form.
This PR tries to separate the concerns of the form and the query display from the main 
Tap querying and rendering logic. 
This will also allow us to easily reuse this form/CLI formatting for the Top page.

Changes in this PR:

* moves all the code for the form into its own component (TapQueryForm)
* moves the code that displays the current query into its own component (TapQueryCliCmd)
* formats the current tap query as the equivalent command line format that you 
can paste into a terminal
This commit is contained in:
Risha Mars 2018-08-14 11:29:35 -07:00 committed by GitHub
parent 0750e47203
commit e5ab124d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 374 additions and 257 deletions

View File

@ -5,23 +5,12 @@ import PropTypes from 'prop-types';
import { publicAddressToString } from './util/Utils.js';
import React from 'react';
import TapEventTable from './TapEventTable.jsx';
import TapQueryCliCmd from './TapQueryCliCmd.jsx';
import TapQueryForm from './TapQueryForm.jsx';
import { withContext } from './util/AppContext.jsx';
import {
AutoComplete,
Button,
Col,
Form,
Icon,
Input,
Row,
Select
} from 'antd';
import { defaultMaxRps, httpMethods } from './util/TapUtils.js';
import './../../css/tap.css';
const colSpan = 5;
const rowGutter = 16;
const httpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
const defaultMaxRps = 1.0;
const maxNumFilterOptions = 12;
class Tap extends React.Component {
static propTypes = {
@ -53,17 +42,9 @@ class Tap extends React.Component {
authority: "",
maxRps: defaultMaxRps
},
autocomplete: {
namespace: [],
resource: [],
toNamespace: [],
toResource: [],
authority: []
},
maxLinesToDisplay: 40,
awaitingWebSocketConnection: false,
tapRequestInProgress: false,
showAdvancedForm: false,
pollingInterval: 10000,
pendingRequests: false
};
@ -82,8 +63,8 @@ class Tap extends React.Component {
}
onWebsocketOpen = () => {
let query = this.state.query;
query.maxRps = parseFloat(query.maxRps) || defaultMaxRps;
let query = _.cloneDeep(this.state.query);
query.maxRps = parseFloat(query.maxRps);
this.ws.send(JSON.stringify({
id: "tap-web",
@ -160,10 +141,6 @@ class Tap extends React.Component {
};
}
getResourceList(resourcesByNs, ns) {
return resourcesByNs[ns] || _.uniq(_.flatten(_.values(resourcesByNs)));
}
parseTapResult = data => {
let d = JSON.parse(data);
let filters = this.state.tapResultFilterOptions;
@ -311,12 +288,6 @@ class Tap extends React.Component {
});
}
toggleAdvancedForm = show => {
this.setState({
showAdvancedForm: show
});
}
handleTapStart = e => {
e.preventDefault();
this.startTapSteaming();
@ -326,44 +297,6 @@ class Tap extends React.Component {
this.ws.close(1000);
}
handleFormChange = (name, scopeResource, shouldScopeAuthority) => {
let state = {
query: this.state.query,
autocomplete: this.state.autocomplete
};
return formVal => {
state.query[name] = formVal;
if (!_.isNil(scopeResource)) {
// scope the available typeahead resources to the selected namespace
state.autocomplete[scopeResource] = this.state.resourcesByNs[formVal];
}
if (shouldScopeAuthority) {
state.autocomplete.authority = this.state.authoritiesByNs[formVal];
}
this.setState(state);
};
}
handleFormEvent = name => {
let state = {
query: this.state.query
};
return event => {
state.query[name] = event.target.value;
this.setState(state);
};
}
autoCompleteData = name => {
return _(this.state.autocomplete[name])
.filter(d => d.indexOf(this.state.query[name]) !== -1)
.sortBy()
.value();
}
loadFromServer() {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
@ -377,21 +310,10 @@ class Tap extends React.Component {
this.serverPromise = Promise.all(this.api.getCurrentPromises())
.then(rsp => {
let { resourcesByNs, authoritiesByNs } = this.getResourcesByNs(rsp);
let namespaces = _.sortBy(_.keys(resourcesByNs));
let resourceNames = this.getResourceList(resourcesByNs, this.state.query.namespace);
let toResourceNames = this.getResourceList(resourcesByNs, this.state.query.toNamespace);
let authorities = this.getResourceList(authoritiesByNs, this.state.query.namespace);
this.setState({
resourcesByNs,
authoritiesByNs,
autocomplete: {
namespace: namespaces,
resource: resourceNames,
toNamespace: namespaces,
toResource: toResourceNames,
authority: authorities
},
pendingRequests: false
});
})
@ -409,178 +331,10 @@ class Tap extends React.Component {
});
}
renderTapForm = () => {
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)}>
{
_.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>
<AutoComplete
dataSource={this.autoCompleteData("resource")}
onSelect={this.handleFormChange("resource")}
onSearch={this.handleFormChange("resource")}
placeholder="Resource" />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item>
{
this.state.tapRequestInProgress ?
<Button type="primary" className="tap-stop" onClick={this.handleTapStop}>Stop</Button> :
<Button type="primary" className="tap-start" onClick={this.handleTapStart}>Start</Button>
}
{
this.state.awaitingWebSocketConnection ?
<Icon type="loading" style={{ paddingLeft: rowGutter, fontSize: 20, color: '#08c' }} /> : null
}
</Form.Item>
</Col>
</Row>
<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() }
</Form>
);
}
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")}>
{
_.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>
<AutoComplete
dataSource={this.autoCompleteData("toResource")}
onSelect={this.handleFormChange("toResource")}
onSearch={this.handleFormChange("toResource")}
placeholder="To Resource" />
</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" />
</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")} />
</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")} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
extra="Maximum requests per second to tap">
<Input
defaultValue={defaultMaxRps}
placeholder="Max RPS"
onChange={this.handleFormEvent("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")}>
{
_.map(httpMethods, m =>
<Select.Option key={`method-select-${m}`} value={m}>{m}</Select.Option>
)
}
</Select>
</Form.Item>
</Col>
</Row>
</React.Fragment>
);
}
renderCurrentQuery = () => {
let emptyVals = _.countBy(_.values(this.state.query), v => _.isEmpty(v));
return (
<div className="tap-query">
<code>
{ !emptyVals.false || emptyVals.false === 1 ? null :
<div>
Current query:
{
_.map(this.state.query, (query, queryName) => {
if (query === "") {
return null;
}
return <div key={queryName}>{queryName}: {query}</div>;
})
}
</div>
}
</code>
</div>
);
updateQuery = query => {
this.setState({
query
});
}
render() {
@ -593,8 +347,17 @@ class Tap extends React.Component {
<ErrorBanner message={this.state.error} onHideMessage={() => this.setState({ error: null })} />}
<PageHeader header="Tap" />
{this.renderTapForm()}
{this.renderCurrentQuery()}
<TapQueryForm
tapRequestInProgress={this.state.tapRequestInProgress}
awaitingWebSocketConnection={this.state.awaitingWebSocketConnection}
handleTapStart={this.handleTapStart}
handleTapStop={this.handleTapStop}
resourcesByNs={this.state.resourcesByNs}
authoritiesByNs={this.state.authoritiesByNs}
updateQuery={this.updateQuery}
query={this.state.query} />
<TapQueryCliCmd query={this.state.query} />
<TapEventTable
tableRows={tableRows}

View File

@ -0,0 +1,67 @@
import _ from 'lodash';
import React from 'react';
import { tapQueryPropType } from './util/TapUtils.js';
/*
prints a given tap query in an equivalent CLI format, such that it
could be pasted into a terminal
*/
export default class TapQueryCliCmd extends React.Component {
static propTypes = {
query: tapQueryPropType
}
static defaultProps = {
query: {
resource: "",
namespace: "",
toResource: "",
toNamespace: "",
method: "",
path: "",
scheme: "",
authority: "",
maxRps: ""
}
}
renderCliItem = (queryLabel, queryVal) => {
return _.isEmpty(queryVal) ? null : ` ${queryLabel} ${queryVal}`;
}
render = () => {
let {
resource,
namespace,
toResource,
toNamespace,
method,
path,
scheme,
authority,
maxRps
} = this.props.query;
return (
<div className="tap-query">
{
_.isEmpty(resource) ? null :
<React.Fragment>
<div>Current Tap query:</div>
<code>
linkerd tap {resource}
{ this.renderCliItem("--namespace", namespace) }
{ this.renderCliItem("--to", toResource) }
{ this.renderCliItem("--to-namespace", toNamespace) }
{ this.renderCliItem("--method", method) }
{ this.renderCliItem("--scheme", scheme) }
{ this.renderCliItem("--authority", authority) }
{ this.renderCliItem("--path", path) }
{ this.renderCliItem("--max-rps", maxRps) }
</code>
</React.Fragment>
}
</div>
);
}
}

View File

@ -0,0 +1,270 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import {
AutoComplete,
Button,
Col,
Form,
Icon,
Input,
Row,
Select
} from 'antd';
import { defaultMaxRps, httpMethods, tapQueryPropType } from './util/TapUtils.js';
const colSpan = 5;
const rowGutter = 16;
const getResourceList = (resourcesByNs, ns) => {
return resourcesByNs[ns] || _.uniq(_.flatten(_.values(resourcesByNs)));
};
export default class TapQueryForm extends React.Component {
static propTypes = {
awaitingWebSocketConnection: PropTypes.bool.isRequired,
handleTapStart: PropTypes.func.isRequired,
handleTapStop: PropTypes.func.isRequired,
query: tapQueryPropType.isRequired,
tapRequestInProgress: PropTypes.bool.isRequired,
updateQuery: PropTypes.func.isRequired
}
constructor(props) {
super(props);
this.state = {
authoritiesByNs: {},
resourcesByNs: {},
showAdvancedForm: false,
query: props.query,
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;
if (!_.isNil(scopeResource)) {
// scope the available typeahead resources to the selected namespace
state.autocomplete[scopeResource] = this.state.resourcesByNs[formVal];
}
if (shouldScopeAuthority) {
state.autocomplete.authority = this.state.authoritiesByNs[formVal];
}
this.setState(state);
this.props.updateQuery(state.query);
};
}
handleFormEvent = name => {
let state = {
query: this.state.query
};
return event => {
state.query[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)
.sortBy()
.value();
}
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")}>
{
_.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>
<AutoComplete
dataSource={this.autoCompleteData("toResource")}
onSelect={this.handleFormChange("toResource")}
onSearch={this.handleFormChange("toResource")}
placeholder="To Resource" />
</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" />
</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")} />
</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")} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
extra="Maximum requests per second to tap">
<Input
defaultValue={defaultMaxRps}
placeholder="Max RPS"
onChange={this.handleFormEvent("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")}>
{
_.map(httpMethods, m =>
<Select.Option key={`method-select-${m}`} value={m}>{m}</Select.Option>
)
}
</Select>
</Form.Item>
</Col>
</Row>
</React.Fragment>
);
}
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)}>
{
_.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>
<AutoComplete
dataSource={this.autoCompleteData("resource")}
onSelect={this.handleFormChange("resource")}
onSearch={this.handleFormChange("resource")}
placeholder="Resource" />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item>
{
this.props.tapRequestInProgress ?
<Button type="primary" className="tap-stop" onClick={this.props.handleTapStop}>Stop</Button> :
<Button type="primary" className="tap-start" onClick={this.props.handleTapStart}>Start</Button>
}
{
this.props.awaitingWebSocketConnection ?
<Icon type="loading" style={{ paddingLeft: rowGutter, fontSize: 20, color: '#08c' }} /> : null
}
</Form.Item>
</Col>
</Row>
<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() }
</Form>
);
}
}

View File

@ -0,0 +1,17 @@
import PropTypes from 'prop-types';
export const httpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
export const defaultMaxRps = "1.0";
export const tapQueryPropType = PropTypes.shape({
resource: PropTypes.string,
namespace: PropTypes.string,
toResource: PropTypes.string,
toNamespace: PropTypes.string,
method: PropTypes.string,
path: PropTypes.string,
scheme: PropTypes.string,
authority: PropTypes.string,
maxRps: PropTypes.string
});