import { UrlQueryParamTypes, addUrlProps } from 'react-url-query'; import { defaultMaxRps, emptyTapQuery, httpMethods, tapQueryPropType, tapQueryProps } 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 Typography from '@material-ui/core/Typography'; import _each from 'lodash/each'; 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 _mapValues from 'lodash/mapValues'; 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 _startCase from 'lodash/startCase'; import _uniq from 'lodash/uniq'; import _upperFirst from 'lodash/upperFirst'; import _values from 'lodash/values'; import { withStyles } from '@material-ui/core/styles'; // 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 }; }); const styles = theme => ({ root: { display: 'flex', flexWrap: 'wrap', }, formControl: { margin: theme.spacing.unit, minWidth: 200, }, 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)', } }); class TapQueryForm extends React.Component { static propTypes = { classes: PropTypes.shape({}).isRequired, cmdName: PropTypes.string.isRequired, 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 } static getDerivedStateFromProps(props, state) { if (!_isEqual(props.resourcesByNs, state.resourcesByNs)) { let resourcesByNs = props.resourcesByNs; let authoritiesByNs = props.authoritiesByNs; let namespaces = Object.keys(resourcesByNs).sort(); 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; } } constructor(props) { super(props); let query = _merge({}, props.query, _pick(this.props, Object.keys(tapQueryProps))); props.updateQuery(query); let advancedFormExpanded = _some( _omit(query, ['namespace', 'resource']), v => !_isEmpty(v)); this.state = { query, advancedFormExpanded, authoritiesByNs: {}, resourcesByNs: {}, autocomplete: { namespace: [], resource: [], toNamespace: [], toResource: [], authority: [] }, }; } handleFormChange = (name, scopeResource) => { let state = { query: this.state.query, autocomplete: this.state.autocomplete }; let shouldScopeAuthority = name === "namespace"; return event => { let formVal = event.target.value; 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]; 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); }; } handleAdvancedFormExpandClick = () => { this.setState(state => ({advancedFormExpanded: !state.advancedFormExpanded})); } autoCompleteData = name => { return _uniq( this.state.autocomplete[name].filter(d => d.indexOf(this.state.query[name]) !== -1) ).sort(); } resetTapForm = () => { this.setState({ query: emptyTapQuery() }); _each(this.state.query, (_val, name) => { this.handleUrlUpdate(name, null); }); this.props.updateQuery(emptyTapQuery(), true); this.props.handleTapClear(); } renderResourceSelect = (resourceKey, namespaceKey) => { let selectedNs = this.state.query[namespaceKey]; let nsEmpty = _isNil(selectedNs) || _isEmpty(selectedNs); let { classes } = this.props; let resourceOptions = resourceTypes.concat( this.state.autocomplete[resourceKey] || [], nsEmpty ? [] : [`namespace/${selectedNs}`] ).sort(); return ( {_startCase(resourceKey)} ); } renderNamespaceSelect = (title, namespaceKey, resourceKey) => { let { classes } = this.props; return ( {title} ); } renderTapButton = (tapInProgress, tapIsClosing) => { if (tapIsClosing) { return (); } else if (tapInProgress) { return (); } else { return ( ); } } renderTextInput = (title, key, helperText) => { let { classes } = this.props; return ( ); } renderAdvancedTapFormContent() { const { classes } = this.props; return ( {this.renderNamespaceSelect("To Namespace", "toNamespace", "toResource")} {this.renderResourceSelect("toResource", "toNamespace")} Authority Display requests with this :authority { this.renderTextInput("Path", "path", "Display requests with paths that start with this prefix") } { this.renderTextInput("Scheme", "scheme", "Display requests with this scheme") } { this.renderTextInput("Max RPS", "maxRps", `Maximum requests per second to tap. Default ${defaultMaxRps}`) } HTTP method Display requests with this HTTP method ); } renderAdvancedTapForm() { return ( }> {this.state.advancedFormExpanded ? "Hide filters" : "Show more filters"} {this.renderAdvancedTapFormContent()} ); } render() { const { classes } = this.props; return ( {this.renderNamespaceSelect("Namespace", "namespace", "resource")} {this.renderResourceSelect("resource", "namespace")} { this.renderTapButton(this.props.tapRequestInProgress, this.props.tapIsClosing) } { !this.props.enableAdvancedForm ? null : this.renderAdvancedTapForm() } ); } } export default addUrlProps({ urlPropsQueryConfig })(withStyles(styles)(TapQueryForm));