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 +});