diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-07-25 17:03:50 -0700 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2016-07-25 17:03:50 -0700 |
commit | 70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3 (patch) | |
tree | 8c15d471934ac4b2c898725a2929f30f8dc7f9dd /web/src | |
parent | 79ebcb046e8669f80357a6c3046ec76c6adf49be (diff) | |
download | mitmproxy-70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3.tar.gz mitmproxy-70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3.tar.bz2 mitmproxy-70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3.zip |
web: refactor ContentLoader
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/js/components/ContentView.jsx | 35 | ||||
-rw-r--r-- | web/src/js/components/ContentView/CodeEditor.jsx (renamed from web/src/js/components/common/CodeEditor.jsx) | 0 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentLoader.jsx | 97 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentViews.jsx | 44 | ||||
-rw-r--r-- | web/src/js/components/ContentView/DownloadContentButton.jsx | 18 | ||||
-rw-r--r-- | web/src/js/components/ContentView/UploadContentButton.jsx | 28 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ViewSelector.jsx | 51 | ||||
-rw-r--r-- | web/src/js/utils.js | 1 |
8 files changed, 159 insertions, 115 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 80dec0f4..9ec0266b 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -1,10 +1,11 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { MessageUtils } from '../flow/utils.js' import * as ContentViews from './ContentView/ContentViews' import * as MetaViews from './ContentView/MetaViews' -import ContentLoader from './ContentView/ContentLoader' import ViewSelector from './ContentView/ViewSelector' +import UploadContentButton from './ContentView/UploadContentButton' +import DownloadContentButton from './ContentView/DownloadContentButton' + import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow' ContentView.propTypes = { @@ -18,7 +19,7 @@ ContentView.propTypes = { ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2) function ContentView(props) { - const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, content, readonly } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, readonly } = props if (message.contentLength === 0) { return <MetaViews.ContentEmpty {...props}/> @@ -35,34 +36,14 @@ function ContentView(props) { const View = ContentViews[contentView] return ( <div> - {View.textView ? ( - <ContentLoader flow={flow} readonly={readonly} message={message}> - <View readonly={readonly} onChange={onContentChange} content="" /> - </ContentLoader> - ) : ( - <View flow={flow} readonly={readonly} onChange={onContentChange} content={content} message={message} /> - )} + <View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/> + <div className="view-options text-center"> <ViewSelector onSelectView={selectView} active={View} message={message}/> - <a className="btn btn-default btn-xs" - href={MessageUtils.getContentURL(flow, message)} - title="Download the content of the flow."> - <i className="fa fa-download"/> - </a> + <DownloadContentButton flow={flow} message={message}/> - <a className="btn btn-default btn-xs" - onClick={() => ContentView.fileInput.click()} - title="Upload a file to replace the content." - > - <i className="fa fa-upload"/> - </a> - <input - ref={ref => ContentView.fileInput = ref} - className="hidden" - type="file" - onChange={e => {if(e.target.files.length > 0) uploadContent(e.target.files[0])}} - /> + <UploadContentButton uploadContent={uploadContent}/> </div> </div> ) diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx index 95f1b98b..95f1b98b 100644 --- a/web/src/js/components/common/CodeEditor.jsx +++ b/web/src/js/components/ContentView/CodeEditor.jsx diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index eff82d05..fb022df6 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -1,58 +1,34 @@ import React, { Component, PropTypes } from 'react' import { MessageUtils } from '../../flow/utils.js' -// This is the only place where we use jQuery. -// Remove when possible. -import $ from "jquery" -export default class ContentLoader extends Component { +export default View => class extends React.Component { + + static displayName = View.displayName || View.name + static matches = View.matches static propTypes = { + ...View.propTypes, + content: PropTypes.string, // mark as non-required flow: PropTypes.object.isRequired, message: PropTypes.object.isRequired, } - constructor(props, context) { - super(props, context) - this.state = { content: null, request: null } - } - - requestContent(nextProps) { - if (this.state.request) { - this.state.request.abort() + constructor(props) { + super(props) + this.state = { + content: undefined, + request: undefined, } - - const requestUrl = MessageUtils.getContentURL(nextProps.flow, nextProps.message) - const request = $.get(requestUrl) - - this.setState({ content: null, request }) - - request - .done(content => { - this.setState({ content }) - }) - .fail((xhr, textStatus, errorThrown) => { - if (textStatus === 'abort') { - return - } - this.setState({ content: `AJAX Error: ${textStatus}\r\n${errorThrown}` }) - }) - .always(() => { - this.setState({ request: null }) - }) } componentWillMount() { - this.requestContent(this.props) + this.startRequest(this.props) } componentWillReceiveProps(nextProps) { - let reload = nextProps.message !== this.props.message - let isUserEdit = !nextProps.readonly && nextProps.message.content - - if (isUserEdit) - this.setState({content: nextProps.message.content}) - else if(reload) - this.requestContent(nextProps) + if (nextProps.message.contentHash !== this.props.message.contentHash) { + this.startRequest(nextProps) + } } componentWillUnmount() { @@ -61,15 +37,50 @@ export default class ContentLoader extends Component { } } + startRequest(props) { + if (this.state.request) { + this.state.request.abort() + } + let requestUrl = MessageUtils.getContentURL(props.flow, props.message) + + // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. + let request = new XMLHttpRequest(); + request.addEventListener("load", this.requestComplete.bind(this, request)); + request.addEventListener("error", this.requestFailed.bind(this, request)); + request.open("GET", requestUrl); + request.send(); + this.setState({ request, content: undefined }) + } + + requestComplete(request, e) { + if (request !== this.state.request) { + return // Stale request + } + this.setState({ + content: request.responseText, + request: undefined + }) + } + + requestFailed(request, e) { + if (request !== this.state.request) { + return // Stale request + } + console.error(e) + // FIXME: Better error handling + this.setState({ + content: "Error getting content.", + request: undefined + }) + } + render() { return this.state.content ? ( - React.cloneElement(this.props.children, { - content: this.state.content - }) + <View content={this.state.content} {...this.props}/> ) : ( <div className="text-center"> <i className="fa fa-spinner fa-spin"></i> </div> ) } -} +}; diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index b39e545a..a1adebea 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -1,20 +1,16 @@ import React, { PropTypes } from 'react' import ContentLoader from './ContentLoader' -import { MessageUtils } from '../../flow/utils.js' -import CodeEditor from '../common/CodeEditor' +import { MessageUtils } from '../../flow/utils' +import CodeEditor from './CodeEditor' -const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw] - -ViewImage.regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i -ViewImage.matches = msg => ViewImage.regex.test(MessageUtils.getContentType(msg)) - +const isImage = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i +ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg)) ViewImage.propTypes = { flow: PropTypes.object.isRequired, message: PropTypes.object.isRequired, } - -export function ViewImage({ flow, message }) { +function ViewImage({ flow, message }) { return ( <div className="flowview-image"> <img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/> @@ -22,26 +18,23 @@ export function ViewImage({ flow, message }) { ) } -ViewRaw.textView = true -ViewRaw.matches = () => true +ViewRaw.matches = () => true ViewRaw.propTypes = { content: React.PropTypes.string.isRequired, } - -export function ViewRaw({ content, readonly, onChange }) { +function ViewRaw({ content, readonly, onChange }) { return readonly ? <pre>{content}</pre> : <CodeEditor content={content} onChange={onChange}/> } +ViewRaw = ContentLoader(ViewRaw) -ViewJSON.textView = true -ViewJSON.regex = /^application\/json$/i -ViewJSON.matches = msg => ViewJSON.regex.test(MessageUtils.getContentType(msg)) +const isJSON = /^application\/json$/i +ViewJSON.matches = msg => isJSON.test(MessageUtils.getContentType(msg)) ViewJSON.propTypes = { content: React.PropTypes.string.isRequired, } - -export function ViewJSON({ content }) { +function ViewJSON({ content }) { let json = content try { json = JSON.stringify(JSON.parse(content), null, 2); @@ -50,23 +43,18 @@ export function ViewJSON({ content }) { } return <pre>{json}</pre> } +ViewJSON = ContentLoader(ViewJSON) ViewAuto.matches = () => false -ViewAuto.findView = msg => views.find(v => v.matches(msg)) || views[views.length - 1] - +ViewAuto.findView = msg => [ViewImage, ViewJSON, ViewRaw].find(v => v.matches(msg)) || ViewRaw ViewAuto.propTypes = { message: React.PropTypes.object.isRequired, flow: React.PropTypes.object.isRequired, } - -export function ViewAuto({ message, flow, readonly, onChange }) { +function ViewAuto({ message, flow, readonly, onChange }) { const View = ViewAuto.findView(message) - if (View.textView) { - return <ContentLoader message={message} flow={flow}><View readonly={readonly} onChange={onChange} content="" /></ContentLoader> - } else { - return <View readonly={readonly} message={message} flow={flow} /> - } + return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/> } -export default views +export { ViewImage, ViewRaw, ViewAuto, ViewJSON } diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx new file mode 100644 index 00000000..3f11f909 --- /dev/null +++ b/web/src/js/components/ContentView/DownloadContentButton.jsx @@ -0,0 +1,18 @@ +import { MessageUtils } from "../../flow/utils" +import { PropTypes } from 'react' + +DownloadContentButton.propTypes = { + flow: PropTypes.object.isRequired, + message: PropTypes.object.isRequired, +} + +export default function DownloadContentButton({ flow, message }) { + + return ( + <a className="btn btn-default btn-xs" + href={MessageUtils.getContentURL(flow, message)} + title="Download the content of the flow."> + <i className="fa fa-download"/> + </a> + ) +} diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx new file mode 100644 index 00000000..0652b584 --- /dev/null +++ b/web/src/js/components/ContentView/UploadContentButton.jsx @@ -0,0 +1,28 @@ +import { PropTypes } from 'react' + +UploadContentButton.propTypes = { + uploadContent: PropTypes.func.isRequired, +} + +export default function UploadContentButton({ uploadContent }) { + + let fileInput; + + return ( + <a className="btn btn-default btn-xs" + onClick={() => fileInput.click()} + title="Upload a file to replace the content."> + <i className="fa fa-upload"/> + <input + ref={ref => fileInput = ref} + className="hidden" + type="file" + onChange={e => { + if (e.target.files.length > 0) uploadContent(e.target.files[0]) + }} + /> + </a> + + ) +} + diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 9b151a5b..973d2333 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -1,28 +1,47 @@ import React, { PropTypes } from 'react' import classnames from 'classnames' -import views, { ViewAuto } from './ContentViews' +import { connect } from 'react-redux' +import * as ContentViews from './ContentViews' +import { setContentView } from "../../ducks/ui/flow"; + + +function ViewButton({ name, setContentView, children, activeView }) { + return ( + <button + onClick={() => setContentView(name)} + className={classnames('btn btn-default', { active: name === activeView })}> + {children} + </button> + ) +} +ViewButton = connect(state => ({ + activeView: state.ui.flow.contentView +}), { + setContentView +})(ViewButton) + ViewSelector.propTypes = { - active: PropTypes.func.isRequired, message: PropTypes.object.isRequired, - onSelectView: PropTypes.func.isRequired, } +export default function ViewSelector({ message }) { + + let autoView = ContentViews.ViewAuto.findView(message) + let autoViewName = (autoView.displayName || autoView.name) + .toLowerCase() + .replace('view', '') + .replace(/ContentLoader\((.+)\)/,"$1") -export default function ViewSelector({ active, message, onSelectView }) { return ( <div className="view-selector btn-group btn-group-xs"> - {views.map(View => ( - <button - key={View.name} - onClick={() => onSelectView(View.name)} - className={classnames('btn btn-default', { active: View === active })}> - {View === ViewAuto ? ( - `auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}` - ) : ( - View.name.toLowerCase().replace('view', '') - )} - </button> - ))} + + <ViewButton name="AutoView">auto: {autoViewName}</ViewButton> + + {Object.keys(ContentViews).map(name => + name !== "ViewAuto" && + <ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton> + )} + </div> ) } diff --git a/web/src/js/utils.js b/web/src/js/utils.js index cc17c565..e44182d0 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -124,7 +124,6 @@ export const pure = renderFn => class extends React.Component { static displayName = renderFn.name shouldComponentUpdate(nextProps) { - console.log(!shallowEqual(this.props, nextProps)) return !shallowEqual(this.props, nextProps) } |