From e5ab124d76517b14fb04bc93b0e3ac0bbc3b55b6 Mon Sep 17 00:00:00 2001 From: Risha Mars Date: Tue, 14 Aug 2018 11:29:35 -0700 Subject: [PATCH] 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 --- web/app/js/components/Tap.jsx | 277 ++--------------------- web/app/js/components/TapQueryCliCmd.jsx | 67 ++++++ web/app/js/components/TapQueryForm.jsx | 270 ++++++++++++++++++++++ web/app/js/components/util/TapUtils.js | 17 ++ 4 files changed, 374 insertions(+), 257 deletions(-) create mode 100644 web/app/js/components/TapQueryCliCmd.jsx create mode 100644 web/app/js/components/TapQueryForm.jsx create mode 100644 web/app/js/components/util/TapUtils.js diff --git a/web/app/js/components/Tap.jsx b/web/app/js/components/Tap.jsx index b036d2152..f5fa40d91 100644 --- a/web/app/js/components/Tap.jsx +++ b/web/app/js/components/Tap.jsx @@ -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 ( -
- - - - - - - - - - - - - - - - { - this.state.tapRequestInProgress ? - : - - } - { - this.state.awaitingWebSocketConnection ? - : null - } - - - - - - - { !this.state.showAdvancedForm ? null : this.renderAdvancedTapForm() } -
- ); - } - - renderAdvancedTapForm = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } - - renderCurrentQuery = () => { - let emptyVals = _.countBy(_.values(this.state.query), v => _.isEmpty(v)); - - return ( -
- - { !emptyVals.false || emptyVals.false === 1 ? null : -
- Current query: - { - _.map(this.state.query, (query, queryName) => { - if (query === "") { - return null; - } - return
{queryName}: {query}
; - }) - } -
- } -
-
- ); + updateQuery = query => { + this.setState({ + query + }); } render() { @@ -593,8 +347,17 @@ class Tap extends React.Component { this.setState({ error: null })} />} - {this.renderTapForm()} - {this.renderCurrentQuery()} + + + { + return _.isEmpty(queryVal) ? null : ` ${queryLabel} ${queryVal}`; + } + + render = () => { + let { + resource, + namespace, + toResource, + toNamespace, + method, + path, + scheme, + authority, + maxRps + } = this.props.query; + + return ( +
+ { + _.isEmpty(resource) ? null : + +
Current Tap query:
+ + 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) } + +
+ } +
+ ); + } +} diff --git a/web/app/js/components/TapQueryForm.jsx b/web/app/js/components/TapQueryForm.jsx new file mode 100644 index 000000000..800f803bb --- /dev/null +++ b/web/app/js/components/TapQueryForm.jsx @@ -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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + render() { + return ( +
+ + + + + + + + + + + + + + + + { + this.props.tapRequestInProgress ? + : + + } + { + this.props.awaitingWebSocketConnection ? + : null + } + + + + + + + { !this.state.showAdvancedForm ? null : this.renderAdvancedTapForm() } +
+ ); + } +} diff --git a/web/app/js/components/util/TapUtils.js b/web/app/js/components/util/TapUtils.js new file mode 100644 index 000000000..4a073e6cd --- /dev/null +++ b/web/app/js/components/util/TapUtils.js @@ -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 +});