From 61f192434fade43af81dc74602aef218d5c5be19 Mon Sep 17 00:00:00 2001 From: Clemens Date: Thu, 21 Jul 2016 11:50:02 +0200 Subject: [PATCH 01/15] moved editor to raw_view mode --- web/src/js/components/ContentView.jsx | 65 ++++++++----------- .../components/ContentView/ContentViews.jsx | 6 +- web/src/js/components/FlowView/Messages.jsx | 6 +- web/src/js/ducks/ui/flow.js | 3 +- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index f7eafc89b..93f16ec07 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -6,7 +6,6 @@ import * as MetaViews from './ContentView/MetaViews' import ContentLoader from './ContentView/ContentLoader' import ViewSelector from './ContentView/ViewSelector' import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow' -import CodeEditor from './common/CodeEditor' ContentView.propTypes = { // It may seem a bit weird at the first glance: @@ -19,7 +18,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, onContentChange, isFlowEditorOpen, setModifiedFlowContent } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, onContentChange, readonly, isFlowEditorOpen } = props if (message.contentLength === 0) { return @@ -37,43 +36,35 @@ function ContentView(props) { return (
- {isFlowEditorOpen ? ( + {View.textView ? ( - {setModifiedFlowContent(content)}}/> - - ): ( -
- {View.textView ? ( - - - - ) : ( - - )} -
- -   - - - -   - ContentView.fileInput.click()} - title="Upload a file to replace the content." - > - - - ContentView.fileInput = ref} - className="hidden" - type="file" - onChange={e => {if(e.target.files.length > 0) onContentChange(e.target.files[0])}} - /> -
-
+ + + ) : ( + )} +
+ +   + + + +   + ContentView.fileInput.click()} + title="Upload a file to replace the content." + > + + + ContentView.fileInput = ref} + className="hidden" + type="file" + onChange={e => {if(e.target.files.length > 0) onContentChange(e.target.files[0])}} + /> +
) } diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 82ee0adcc..0eaf3ea81 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react' import ContentLoader from './ContentLoader' import { MessageUtils } from '../../flow/utils.js' +import CodeEditor from '../common/CodeEditor' const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw] @@ -28,8 +29,9 @@ ViewRaw.propTypes = { content: React.PropTypes.string.isRequired, } -export function ViewRaw({ content }) { - return
{content}
+export function ViewRaw({ content, isFlowEditorOpen, readonly }) { + let showEditor = isFlowEditorOpen && !readonly + return showEditor ? alert(content)}/> :
{content}
} ViewJSON.textView = true diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 133b28831..505687997 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -144,7 +144,11 @@ export class Response extends Component { onChange={headers => updateFlow({ response: { headers } })} />
- + ) } diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index b1fe535f9..c70ebfa93 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -22,7 +22,8 @@ export default function reducer(state = defaultState, action) { case START_EDIT: return { ...state, - modifiedFlow: action.flow + modifiedFlow: action.flow, + contentView: 'ViewRaw' } case UPDATE_EDIT: From 70ca10b423de4a57a395798aa94189ba4da7840f Mon Sep 17 00:00:00 2001 From: Clemens Date: Thu, 21 Jul 2016 19:13:16 +0200 Subject: [PATCH 02/15] moved editor to raw_view mode, add content_file_upload --- web/src/js/components/ContentView.jsx | 9 +++---- .../components/ContentView/ContentViews.jsx | 11 ++++---- web/src/js/components/FlowView/Messages.jsx | 18 +++++++++---- web/src/js/components/FlowView/ToggleEdit.jsx | 7 ++--- web/src/js/components/common/CodeEditor.jsx | 4 --- web/src/js/ducks/ui/flow.js | 26 +++++++++++++++++-- 6 files changed, 50 insertions(+), 25 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 93f16ec07..06272627a 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -18,7 +18,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, onContentChange, readonly, isFlowEditorOpen } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, readonly } = props if (message.contentLength === 0) { return @@ -38,10 +38,10 @@ function ContentView(props) {
{View.textView ? ( - + ) : ( - + )}
@@ -62,7 +62,7 @@ function ContentView(props) { ref={ref => ContentView.fileInput = ref} className="hidden" type="file" - onChange={e => {if(e.target.files.length > 0) onContentChange(e.target.files[0])}} + onChange={e => {if(e.target.files.length > 0) uploadContent(e.target.files[0])}} />
@@ -73,7 +73,6 @@ export default connect( state => ({ contentView: state.ui.flow.contentView, displayLarge: state.ui.flow.displayLarge, - isFlowEditorOpen : !!state.ui.flow.modifiedFlow // FIXME }), { selectView: setContentView, diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 0eaf3ea81..6c7f49009 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -29,9 +29,8 @@ ViewRaw.propTypes = { content: React.PropTypes.string.isRequired, } -export function ViewRaw({ content, isFlowEditorOpen, readonly }) { - let showEditor = isFlowEditorOpen && !readonly - return showEditor ? alert(content)}/> :
{content}
+export function ViewRaw({ content, readonly, onChange }) { + return readonly ?
{content}
: } ViewJSON.textView = true @@ -61,12 +60,12 @@ ViewAuto.propTypes = { flow: React.PropTypes.object.isRequired, } -export function ViewAuto({ message, flow }) { +export function ViewAuto({ message, flow, readonly }) { const View = ViewAuto.findView(message) if (View.textView) { - return + return } else { - return + return } } diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 505687997..0f6df8dad 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -9,7 +9,7 @@ import ValidateEditor from '../ValueEditor/ValidateEditor' import ValueEditor from '../ValueEditor/ValueEditor' import Headers from './Headers' -import { startEdit, updateEdit } from '../../ducks/ui/flow' +import { startEdit, updateEdit, uploadContent } from '../../ducks/ui/flow' import ToggleEdit from './ToggleEdit' function RequestLine({ flow, readonly, updateFlow }) { @@ -68,17 +68,18 @@ function ResponseLine({ flow, readonly, updateFlow }) { const Message = connect( state => ({ - flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], + flow: state.flows.byId[state.flows.selected[0]], isEdit: !!state.ui.flow.modifiedFlow, }), { updateFlow: updateEdit, + uploadContent: uploadContent } ) export class Request extends Component { render() { - const { flow, isEdit, updateFlow } = this.props + const { flow, isEdit, updateFlow, uploadContent } = this.props return (
@@ -94,7 +95,12 @@ export class Request extends Component { />
- + updateFlow({ request: {content}})} + uploadContent={content => uploadContent(flow, content, "request")} + message={flow.request}/>
) } @@ -129,7 +135,7 @@ Request = Message(Request) export class Response extends Component { render() { - const { flow, isEdit, updateFlow } = this.props + const { flow, isEdit, updateFlow, uploadContent } = this.props return (
@@ -147,6 +153,8 @@ export class Response extends Component { updateFlow({ response: {content}})} + uploadContent={content => uploadContent(flow, content, "response")} message={flow.response} />
diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx index 0c8cbbd8e..cf197998f 100644 --- a/web/src/js/components/FlowView/ToggleEdit.jsx +++ b/web/src/js/components/FlowView/ToggleEdit.jsx @@ -10,11 +10,11 @@ ToggleEdit.propTypes = { stopEdit: PropTypes.func.isRequired, } -function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) { +function ToggleEdit({ isEdit, startEdit, stopEdit, flow, old_flow }) { return (
{isEdit ? - stopEdit(flow)}> + stopEdit(flow, old_flow)}> : @@ -29,7 +29,8 @@ function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) { export default connect( state => ({ isEdit: !!state.ui.flow.modifiedFlow, - flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]] + flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], + old_flow: state.flows.byId[state.flows.selected[0]] }), { startEdit, diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/common/CodeEditor.jsx index 5b2305a86..ad7af39d0 100644 --- a/web/src/js/components/common/CodeEditor.jsx +++ b/web/src/js/components/common/CodeEditor.jsx @@ -13,10 +13,6 @@ export default class CodeEditor extends Component{ super(props) } - componentWillMount(){ - this.props.onChange(this.props.content) - } - render() { let options = { lineNumbers: true diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index c70ebfa93..e78f63731 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -88,9 +88,31 @@ export function updateEdit(update) { return { type: UPDATE_EDIT, update } } -export function stopEdit(flow) { +export function uploadContent(flow, content, type){ return (dispatch) => { - dispatch(flowsActions.update(flow, flow)).then(() => { + dispatch(flowsActions.updateContent(flow, content, type)).then( () => { + dispatch(flowsActions.updateFlow(flow)) + dispatch({ type: STOP_EDIT }) + }) + } +} + +export function stopEdit(modified_flow, old_flow) { + //make diff of modified_flow and old_flow + return (dispatch) => { + let flow = {...modified_flow} + + if (flow.response.content) { + dispatch(flowsActions.updateContent(flow, flow.response.content, "response")) + flow.response = _.omit(flow.response, "content") + } + if (flow.request.content) { + dispatch(flowsActions.updateContent(flow, flow.request.content, "request")) + flow.request = _.omit(flow.request, "content") + } + + + dispatch(flowsActions.update(flow)).then(() => { dispatch(flowsActions.updateFlow(flow)) dispatch({ type: STOP_EDIT }) }) From f578bf512248c609296d2ff0ea2007a6feac561f Mon Sep 17 00:00:00 2001 From: Clemens Date: Fri, 22 Jul 2016 19:07:53 +0200 Subject: [PATCH 03/15] file upload updates contentview, editable contentloader, diffs on upload --- web/src/js/components/ContentView.jsx | 8 ++-- .../components/ContentView/ContentLoader.jsx | 8 +++- .../components/ContentView/ContentViews.jsx | 4 +- web/src/js/components/FlowView/Messages.jsx | 2 +- web/src/js/components/FlowView/ToggleEdit.jsx | 8 ++-- web/src/js/components/common/CodeEditor.jsx | 31 ++++++-------- web/src/js/ducks/ui/flow.js | 41 +++++++++++-------- web/src/js/utils.js | 14 +++++++ 8 files changed, 67 insertions(+), 49 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 06272627a..7c8ac510b 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -18,7 +18,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, readonly } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, lastFileUpload, uploadContent, onContentChange, content, readonly } = props if (message.contentLength === 0) { return @@ -33,15 +33,14 @@ function ContentView(props) { } const View = ContentViews[contentView] - return (
{View.textView ? ( - + ) : ( - + )}
@@ -73,6 +72,7 @@ export default connect( state => ({ contentView: state.ui.flow.contentView, displayLarge: state.ui.flow.displayLarge, + lastFileUpload: state.ui.flow.lastFileUpload }), { selectView: setContentView, diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index 1a23325cc..503ea7587 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -46,9 +46,13 @@ export default class ContentLoader extends Component { } componentWillReceiveProps(nextProps) { - if (nextProps.message !== this.props.message) { + let reload = nextProps.message !== this.props.message || nextProps.lastFileUpload !== this.props.lastFileUpload + let isUserEdit = !nextProps.readonly && nextProps.message.content + + if (isUserEdit) + this.setState({content: nextProps.message.content}) + else if(reload) this.requestContent(nextProps) - } } componentWillUnmount() { diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 6c7f49009..4ae6044b4 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -60,10 +60,10 @@ ViewAuto.propTypes = { flow: React.PropTypes.object.isRequired, } -export function ViewAuto({ message, flow, readonly }) { +export function ViewAuto({ message, flow, readonly, lastFileUpload }) { const View = ViewAuto.findView(message) if (View.textView) { - return + return } else { return } diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 0f6df8dad..a3beaf849 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -68,7 +68,7 @@ function ResponseLine({ flow, readonly, updateFlow }) { const Message = connect( state => ({ - flow: state.flows.byId[state.flows.selected[0]], + flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], isEdit: !!state.ui.flow.modifiedFlow, }), { diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx index cf197998f..9016348e4 100644 --- a/web/src/js/components/FlowView/ToggleEdit.jsx +++ b/web/src/js/components/FlowView/ToggleEdit.jsx @@ -10,11 +10,11 @@ ToggleEdit.propTypes = { stopEdit: PropTypes.func.isRequired, } -function ToggleEdit({ isEdit, startEdit, stopEdit, flow, old_flow }) { +function ToggleEdit({ isEdit, startEdit, stopEdit, flow, modifiedFlow }) { return (
{isEdit ? - stopEdit(flow, old_flow)}> + stopEdit(flow, modifiedFlow)}> : @@ -29,8 +29,8 @@ function ToggleEdit({ isEdit, startEdit, stopEdit, flow, old_flow }) { export default connect( state => ({ isEdit: !!state.ui.flow.modifiedFlow, - flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], - old_flow: state.flows.byId[state.flows.selected[0]] + modifiedFlow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], + flow: state.flows.byId[state.flows.selected[0]] }), { startEdit, diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/common/CodeEditor.jsx index ad7af39d0..95f1b98b0 100644 --- a/web/src/js/components/common/CodeEditor.jsx +++ b/web/src/js/components/common/CodeEditor.jsx @@ -3,24 +3,19 @@ import { render } from 'react-dom'; import Codemirror from 'react-codemirror'; -export default class CodeEditor extends Component{ - static propTypes = { +CodeEditor.propTypes = { content: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, - } - - constructor(props){ - super(props) - } - - render() { - let options = { - lineNumbers: true - }; - return ( -
e.stopPropagation()}> - -
- ) - } +} + +export default function CodeEditor ( { content, onChange} ){ + + let options = { + lineNumbers: true + }; + return ( +
e.stopPropagation()}> + +
+ ) } diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index e78f63731..268459559 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -1,4 +1,6 @@ import * as flowsActions from '../flows' +import { getDiff } from "../../utils" + import _ from 'lodash' export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW', @@ -6,13 +8,15 @@ export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW', SET_TAB = "UI_FLOWVIEW_SET_TAB", START_EDIT = 'UI_FLOWVIEW_START_EDIT', UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT', - STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT' + STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT', + UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT' const defaultState = { displayLarge: false, modifiedFlow: false, contentView: 'ViewAuto', + lastFileUpload: false, tab: 'request', } @@ -38,6 +42,12 @@ export default function reducer(state = defaultState, action) { modifiedFlow: false } + case UPLOAD_CONTENT: + return { + ... state, + lastFileUpload: new Date() + } + case flowsActions.SELECT: return { ...state, @@ -90,30 +100,25 @@ export function updateEdit(update) { export function uploadContent(flow, content, type){ return (dispatch) => { - dispatch(flowsActions.updateContent(flow, content, type)).then( () => { - dispatch(flowsActions.updateFlow(flow)) - dispatch({ type: STOP_EDIT }) - }) + dispatch(flowsActions.updateContent(flow, content, type)) + dispatch({ type: UPLOAD_CONTENT }) } } -export function stopEdit(modified_flow, old_flow) { - //make diff of modified_flow and old_flow +export function stopEdit(flow, modified_flow) { + let diff = getDiff(flow, modified_flow) return (dispatch) => { - let flow = {...modified_flow} - - if (flow.response.content) { - dispatch(flowsActions.updateContent(flow, flow.response.content, "response")) - flow.response = _.omit(flow.response, "content") + if (diff.response && diff.response.content) { + dispatch(flowsActions.updateContent(flow, diff.response.content, "response")) + delete diff.response.content } - if (flow.request.content) { - dispatch(flowsActions.updateContent(flow, flow.request.content, "request")) - flow.request = _.omit(flow.request, "content") + if (diff.request && diff.request.content) { + dispatch(flowsActions.updateContent(flow, diff.request.content, "request")) + delete diff.request.content } - - dispatch(flowsActions.update(flow)).then(() => { - dispatch(flowsActions.updateFlow(flow)) + dispatch(flowsActions.update(flow, diff)).then(() => { + dispatch(flowsActions.updateFlow(modified_flow)) dispatch({ type: STOP_EDIT }) }) } diff --git a/web/src/js/utils.js b/web/src/js/utils.js index eceda1959..ba07b00a1 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -105,3 +105,17 @@ fetchApi.put = (url, json, options) => fetchApi( ...options } ) + + +export function getDiff(obj1, obj2) { + let result = {...obj2}; + _.forIn(obj1, (value, key) => { + if(_.isEqual(obj2[key], obj1[key])) + result[key] = undefined; + else if(typeof Array.isArray(obj2[key]) && Array.isArray(obj2[key])) + result[key] = {...obj2[key]}; + else if(typeof obj2[key] == 'object' && typeof obj1[key] == 'object') + result[key] = getDiff(obj1[key], obj2[key]); + }); + return result; +} From 0a4bb394da50520c872c0f0b1fcb73dbe6904e34 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 24 Jul 2016 18:20:29 +0800 Subject: [PATCH 04/15] [web] style improvement --- web/src/css/flowdetail.less | 12 +++++++++++- web/src/js/components/FlowView/Headers.jsx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 35857729b..3d9fadb0f 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -64,6 +64,17 @@ } } +.header-table .inline-input { + display: inline-block; + width: 100%; + height: 100%; +} + +.header-colon { + position: absolute; + opacity: 0; +} + .view-options { margin-top: 10px; } @@ -102,7 +113,6 @@ } .header-name { width: 33%; - padding-right: 1em; } .header-value { diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx index 706dd404a..e46b939d9 100644 --- a/web/src/js/components/FlowView/Headers.jsx +++ b/web/src/js/components/FlowView/Headers.jsx @@ -126,7 +126,7 @@ export default class Headers extends Component { onDone={val => this.onChange(i, 0, val)} onRemove={event => this.onRemove(i, 0, event)} onTab={event => this.onTab(i, 0, event)} - />: + />: Date: Mon, 25 Jul 2016 09:05:24 +0200 Subject: [PATCH 05/15] remove lastFileUpload field --- web/src/js/components/ContentView.jsx | 9 ++++----- web/src/js/components/ContentView/ContentViews.jsx | 4 ++-- web/src/js/components/FlowView/Messages.jsx | 5 +++-- web/src/js/ducks/flows.js | 2 +- web/src/js/ducks/ui/flow.js | 14 -------------- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 7c8ac510b..e88d1ce20 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -18,7 +18,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, lastFileUpload, uploadContent, onContentChange, content, readonly } = props + const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, uploadContent, onContentChange, content, readonly } = props if (message.contentLength === 0) { return @@ -36,11 +36,11 @@ function ContentView(props) { return (
{View.textView ? ( - + ) : ( - + )}
@@ -72,11 +72,10 @@ export default connect( state => ({ contentView: state.ui.flow.contentView, displayLarge: state.ui.flow.displayLarge, - lastFileUpload: state.ui.flow.lastFileUpload }), { selectView: setContentView, displayLarge, - updateEdit, + updateEdit } )(ContentView) diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 4ae6044b4..6c7f49009 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -60,10 +60,10 @@ ViewAuto.propTypes = { flow: React.PropTypes.object.isRequired, } -export function ViewAuto({ message, flow, readonly, lastFileUpload }) { +export function ViewAuto({ message, flow, readonly }) { const View = ViewAuto.findView(message) if (View.textView) { - return + return } else { return } diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index a3beaf849..9de25b5b4 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -9,7 +9,8 @@ import ValidateEditor from '../ValueEditor/ValidateEditor' import ValueEditor from '../ValueEditor/ValueEditor' import Headers from './Headers' -import { startEdit, updateEdit, uploadContent } from '../../ducks/ui/flow' +import { startEdit, updateEdit } from '../../ducks/ui/flow' +import * as FlowActions from '../../ducks/flows' import ToggleEdit from './ToggleEdit' function RequestLine({ flow, readonly, updateFlow }) { @@ -73,7 +74,7 @@ const Message = connect( }), { updateFlow: updateEdit, - uploadContent: uploadContent + uploadContent: FlowActions.uploadContent } ) diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index f18e48e6d..b197ff37e 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -112,7 +112,7 @@ export function update(flow, data) { return dispatch => fetchApi.put(`/flows/${flow.id}`, data) } -export function updateContent(flow, file, type) { +export function uploadContent(flow, file, type) { const body = new FormData() if (typeof file !== File) file = new Blob([file], {type: 'plain/text'}) diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index 268459559..62304994f 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -16,7 +16,6 @@ const defaultState = { displayLarge: false, modifiedFlow: false, contentView: 'ViewAuto', - lastFileUpload: false, tab: 'request', } @@ -42,12 +41,6 @@ export default function reducer(state = defaultState, action) { modifiedFlow: false } - case UPLOAD_CONTENT: - return { - ... state, - lastFileUpload: new Date() - } - case flowsActions.SELECT: return { ...state, @@ -98,13 +91,6 @@ export function updateEdit(update) { return { type: UPDATE_EDIT, update } } -export function uploadContent(flow, content, type){ - return (dispatch) => { - dispatch(flowsActions.updateContent(flow, content, type)) - dispatch({ type: UPLOAD_CONTENT }) - } -} - export function stopEdit(flow, modified_flow) { let diff = getDiff(flow, modified_flow) return (dispatch) => { From 61ef7ca91b0cfc072b60e173104e01bd0145a6a1 Mon Sep 17 00:00:00 2001 From: Clemens Date: Mon, 25 Jul 2016 10:56:12 +0200 Subject: [PATCH 06/15] added contentHash --- mitmproxy/web/app.py | 11 ++++++++--- web/src/js/components/ContentView/ContentLoader.jsx | 2 +- web/src/js/ducks/flows.js | 3 +-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mitmproxy/web/app.py b/mitmproxy/web/app.py index eaef887ad..76da8591c 100644 --- a/mitmproxy/web/app.py +++ b/mitmproxy/web/app.py @@ -5,6 +5,8 @@ import json import logging import os.path import re +import hashlib + import six import tornado.websocket @@ -46,6 +48,7 @@ def convert_flow_to_json_dict(flow): "http_version": flow.request.http_version, "headers": tuple(flow.request.headers.items(True)), "contentLength": len(flow.request.content) if flow.request.content is not None else None, + "contentHash": hashlib.sha256(flow.request.raw_content).hexdigest() if flow.request.content is not None else None, "timestamp_start": flow.request.timestamp_start, "timestamp_end": flow.request.timestamp_end, "is_replay": flow.request.is_replay, @@ -57,6 +60,7 @@ def convert_flow_to_json_dict(flow): "reason": flow.response.reason, "headers": tuple(flow.response.headers.items(True)), "contentLength": len(flow.response.content) if flow.response.content is not None else None, + "contentHash": hashlib.sha256(flow.response.raw_content).hexdigest() if flow.response.content is not None else None, "timestamp_start": flow.response.timestamp_start, "timestamp_end": flow.response.timestamp_end, "is_replay": flow.response.is_replay, @@ -295,10 +299,11 @@ class ReplayFlow(RequestHandler): class FlowContent(RequestHandler): def post(self, flow_id, message): - self.flow.backup() - message = getattr(self.flow, message) + flow = self.flow + flow.backup() + message = getattr(flow, message) message.content = self.request.files.values()[0][0].body - self.state.update_flow(self.flow) + self.state.update_flow(flow) def get(self, flow_id, message): message = getattr(self.flow, message) diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index 503ea7587..eff82d057 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -46,7 +46,7 @@ export default class ContentLoader extends Component { } componentWillReceiveProps(nextProps) { - let reload = nextProps.message !== this.props.message || nextProps.lastFileUpload !== this.props.lastFileUpload + let reload = nextProps.message !== this.props.message let isUserEdit = !nextProps.readonly && nextProps.message.content if (isUserEdit) diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index b197ff37e..f96653a90 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -114,8 +114,7 @@ export function update(flow, data) { export function uploadContent(flow, file, type) { const body = new FormData() - if (typeof file !== File) - file = new Blob([file], {type: 'plain/text'}) + file = new Blob([file], {type: 'plain/text'}) body.append('file', file) return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) } From ca4541613c2e40e40dc7bf58535743cc76fc110d Mon Sep 17 00:00:00 2001 From: Clemens Date: Mon, 25 Jul 2016 14:34:00 +0200 Subject: [PATCH 07/15] combine content with header updates --- mitmproxy/web/app.py | 5 +++++ web/src/js/ducks/ui/flow.js | 9 --------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/mitmproxy/web/app.py b/mitmproxy/web/app.py index 76da8591c..848ca4741 100644 --- a/mitmproxy/web/app.py +++ b/mitmproxy/web/app.py @@ -251,11 +251,14 @@ class FlowHandler(RequestHandler): request.port = int(v) elif k == "headers": request.headers.set_state(v) + elif k == "content": + request.text = v else: print("Warning: Unknown update {}.{}: {}".format(a, k, v)) elif a == "response": response = flow.response + for k, v in six.iteritems(b): if k == "msg": response.msg = str(v) @@ -265,6 +268,8 @@ class FlowHandler(RequestHandler): response.http_version = str(v) elif k == "headers": response.headers.set_state(v) + elif k == "content": + response.text = v else: print("Warning: Unknown update {}.{}: {}".format(a, k, v)) else: diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index 62304994f..d77af420f 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -94,15 +94,6 @@ export function updateEdit(update) { export function stopEdit(flow, modified_flow) { let diff = getDiff(flow, modified_flow) return (dispatch) => { - if (diff.response && diff.response.content) { - dispatch(flowsActions.updateContent(flow, diff.response.content, "response")) - delete diff.response.content - } - if (diff.request && diff.request.content) { - dispatch(flowsActions.updateContent(flow, diff.request.content, "request")) - delete diff.request.content - } - dispatch(flowsActions.update(flow, diff)).then(() => { dispatch(flowsActions.updateFlow(modified_flow)) dispatch({ type: STOP_EDIT }) From 1ea094e9dcf187a409dab9ca9a2b3a0b10f243f7 Mon Sep 17 00:00:00 2001 From: Clemens Date: Mon, 25 Jul 2016 15:47:05 +0200 Subject: [PATCH 08/15] remove auto change to raw mode on edit --- web/src/js/components/ContentView.jsx | 2 +- web/src/js/components/ContentView/ContentViews.jsx | 4 ++-- web/src/js/ducks/ui/flow.js | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index e88d1ce20..80dec0f46 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -40,7 +40,7 @@ function ContentView(props) { ) : ( - + )}
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 6c7f49009..b39e545af 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -60,10 +60,10 @@ ViewAuto.propTypes = { flow: React.PropTypes.object.isRequired, } -export function ViewAuto({ message, flow, readonly }) { +export function ViewAuto({ message, flow, readonly, onChange }) { const View = ViewAuto.findView(message) if (View.textView) { - return + return } else { return } diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index d77af420f..100bc771b 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -26,7 +26,6 @@ export default function reducer(state = defaultState, action) { return { ...state, modifiedFlow: action.flow, - contentView: 'ViewRaw' } case UPDATE_EDIT: From 68e437a740a1e3d7356c4f637337aba471ec222f Mon Sep 17 00:00:00 2001 From: Clemens Date: Mon, 25 Jul 2016 17:33:36 +0200 Subject: [PATCH 09/15] rewrite getDiff --- web/src/js/utils.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/src/js/utils.js b/web/src/js/utils.js index ba07b00a1..d3b99bd08 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -109,13 +109,12 @@ fetchApi.put = (url, json, options) => fetchApi( export function getDiff(obj1, obj2) { let result = {...obj2}; - _.forIn(obj1, (value, key) => { - if(_.isEqual(obj2[key], obj1[key])) + for(let key in obj1) { + if(_.isEqual(obj2[key], obj1[key])) result[key] = undefined; - else if(typeof Array.isArray(obj2[key]) && Array.isArray(obj2[key])) - result[key] = {...obj2[key]}; - else if(typeof obj2[key] == 'object' && typeof obj1[key] == 'object') + else if(!(Array.isArray(obj2[key]) && Array.isArray(obj1[key])) && + typeof obj2[key] == 'object' && typeof obj1[key] == 'object') result[key] = getDiff(obj1[key], obj2[key]); - }); + } return result; } From 3254595584e1d711e7ae292ad34753a52f7a0fc1 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 15:12:20 -0700 Subject: [PATCH 10/15] minor fixes --- mitmproxy/web/app.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mitmproxy/web/app.py b/mitmproxy/web/app.py index 848ca4741..8ccc21c52 100644 --- a/mitmproxy/web/app.py +++ b/mitmproxy/web/app.py @@ -47,8 +47,8 @@ def convert_flow_to_json_dict(flow): "path": flow.request.path, "http_version": flow.request.http_version, "headers": tuple(flow.request.headers.items(True)), - "contentLength": len(flow.request.content) if flow.request.content is not None else None, - "contentHash": hashlib.sha256(flow.request.raw_content).hexdigest() if flow.request.content is not None else None, + "contentLength": len(flow.request.raw_content) if flow.request.raw_content is not None else None, + "contentHash": hashlib.sha256(flow.request.raw_content).hexdigest() if flow.request.raw_content is not None else None, "timestamp_start": flow.request.timestamp_start, "timestamp_end": flow.request.timestamp_end, "is_replay": flow.request.is_replay, @@ -59,8 +59,8 @@ def convert_flow_to_json_dict(flow): "status_code": flow.response.status_code, "reason": flow.response.reason, "headers": tuple(flow.response.headers.items(True)), - "contentLength": len(flow.response.content) if flow.response.content is not None else None, - "contentHash": hashlib.sha256(flow.response.raw_content).hexdigest() if flow.response.content is not None else None, + "contentLength": len(flow.response.raw_content) if flow.response.raw_content is not None else None, + "contentHash": hashlib.sha256(flow.response.raw_content).hexdigest() if flow.response.raw_content is not None else None, "timestamp_start": flow.response.timestamp_start, "timestamp_end": flow.response.timestamp_end, "is_replay": flow.response.is_replay, @@ -304,11 +304,10 @@ class ReplayFlow(RequestHandler): class FlowContent(RequestHandler): def post(self, flow_id, message): - flow = self.flow - flow.backup() - message = getattr(flow, message) + self.flow.backup() + message = getattr(self.flow, message) message.content = self.request.files.values()[0][0].body - self.state.update_flow(flow) + self.state.update_flow(self.flow) def get(self, flow_id, message): message = getattr(self.flow, message) From 70dbd1b32d13d30e15c03ee91b0fab7bfdf429b3 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 17:03:50 -0700 Subject: [PATCH 11/15] web: refactor ContentLoader --- web/package.json | 10 +- web/src/js/components/ContentView.jsx | 35 ++----- .../{common => ContentView}/CodeEditor.jsx | 0 .../components/ContentView/ContentLoader.jsx | 97 +++++++++++-------- .../components/ContentView/ContentViews.jsx | 44 +++------ .../ContentView/DownloadContentButton.jsx | 18 ++++ .../ContentView/UploadContentButton.jsx | 28 ++++++ .../components/ContentView/ViewSelector.jsx | 51 +++++++--- web/src/js/utils.js | 1 - 9 files changed, 164 insertions(+), 120 deletions(-) rename web/src/js/components/{common => ContentView}/CodeEditor.jsx (100%) create mode 100644 web/src/js/components/ContentView/DownloadContentButton.jsx create mode 100644 web/src/js/components/ContentView/UploadContentButton.jsx diff --git a/web/package.json b/web/package.json index 81b96adca..302803f24 100644 --- a/web/package.json +++ b/web/package.json @@ -11,15 +11,13 @@ "/src/js" ], "unmockedModulePathPatterns": [ - "react", - "jquery" + "react" ] }, "dependencies": { "bootstrap": "^3.3.6", "classnames": "^2.2.5", "flux": "^2.1.1", - "jquery": "^2.2.3", "lodash": "^4.11.2", "react": "^15.1.0", "react-dom": "^15.1.0", @@ -29,7 +27,7 @@ "redux-logger": "^2.6.1", "redux-thunk": "^2.1.0", "shallowequal": "^0.2.2", - "react-codemirror" : "^0.2.6" + "react-codemirror": "^0.2.6" }, "devDependencies": { "babel-core": "^6.7.7", @@ -55,7 +53,9 @@ "gulp-sourcemaps": "^1.6.0", "gulp-util": "^3.0.7", "jest": "^12.1.1", - "react-addons-test-utils": "^15.1.0", + "react": "^15.2.1", + "react-addons-test-utils": "^15.2.1", + "react-dom": "^15.2.1", "uglifyify": "^3.0.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 80dec0f46..9ec0266b1 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 @@ -35,34 +36,14 @@ function ContentView(props) { const View = ContentViews[contentView] return (
- {View.textView ? ( - - - - ) : ( - - )} + +
  - - - +   - ContentView.fileInput.click()} - title="Upload a file to replace the content." - > - - - ContentView.fileInput = ref} - className="hidden" - type="file" - onChange={e => {if(e.target.files.length > 0) uploadContent(e.target.files[0])}} - /> +
) diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx similarity index 100% rename from web/src/js/components/common/CodeEditor.jsx rename to 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 eff82d057..fb022df65 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 - }) + ) : (
) } -} +}; diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index b39e545af..a1adebea5 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 (
preview @@ -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 ?
{content}
: } +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
{json}
} +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 - } else { - return - } + return } -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 000000000..3f11f9097 --- /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 ( + + + + ) +} diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx new file mode 100644 index 000000000..0652b584a --- /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 ( + fileInput.click()} + title="Upload a file to replace the content."> + + fileInput = ref} + className="hidden" + type="file" + onChange={e => { + if (e.target.files.length > 0) uploadContent(e.target.files[0]) + }} + /> + + + ) +} + diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 9b151a5b1..973d23332 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 ( + + ) +} +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 (
- {views.map(View => ( - - ))} + + auto: {autoViewName} + + {Object.keys(ContentViews).map(name => + name !== "ViewAuto" && + {name.toLowerCase().replace('view', '')} + )} +
) } diff --git a/web/src/js/utils.js b/web/src/js/utils.js index cc17c565f..e44182d0d 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) } From e1587b2bc1b425a162af30d6c271bcd392047b02 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 17:12:39 -0700 Subject: [PATCH 12/15] make empty buttons editable --- web/src/js/components/ContentView.jsx | 13 ++++++------- web/src/js/components/ContentView/ContentLoader.jsx | 6 +++++- web/src/js/components/ContentView/ViewSelector.jsx | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 9ec0266b1..756625097 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -19,17 +19,17 @@ 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, readonly } = props + const { flow, message, contentView, isDisplayLarge, displayLarge, uploadContent, onContentChange, readonly } = props - if (message.contentLength === 0) { + if (message.contentLength === 0 && readonly) { return } - if (message.contentLength === null) { + if (message.contentLength === null && readonly) { return } - if (!displayLarge && ContentView.isContentTooLarge(message)) { + if (!isDisplayLarge && ContentView.isContentTooLarge(message)) { return } @@ -39,7 +39,7 @@ function ContentView(props) {
- +     @@ -52,10 +52,9 @@ function ContentView(props) { export default connect( state => ({ contentView: state.ui.flow.contentView, - displayLarge: state.ui.flow.displayLarge, + isDisplayLarge: state.ui.flow.displayLarge, }), { - selectView: setContentView, displayLarge, updateEdit } diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index fb022df65..697085a9a 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -41,6 +41,10 @@ export default View => class extends React.Component { if (this.state.request) { this.state.request.abort() } + if(props.message.contentLength === 0 || props.message.contentLength === null){ + return this.setState({request: undefined, content: ""}) + } + let requestUrl = MessageUtils.getContentURL(props.flow, props.message) // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. @@ -75,7 +79,7 @@ export default View => class extends React.Component { } render() { - return this.state.content ? ( + return this.state.content !== undefined ? ( ) : (
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 973d23332..89b362314 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -35,7 +35,7 @@ export default function ViewSelector({ message }) { return (
- auto: {autoViewName} + auto: {autoViewName} {Object.keys(ContentViews).map(name => name !== "ViewAuto" && From 67bfc1df147f8a6daa847146625d8887a44ac338 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 17:31:37 -0700 Subject: [PATCH 13/15] fix flow edit --- web/src/css/flowdetail.less | 24 ++++++++++--------- .../components/ContentView/ContentLoader.jsx | 15 ++++++++---- web/src/js/components/FlowView/Headers.jsx | 3 ++- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 3d9fadb0f..d450bca50 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -64,17 +64,6 @@ } } -.header-table .inline-input { - display: inline-block; - width: 100%; - height: 100%; -} - -.header-colon { - position: absolute; - opacity: 0; -} - .view-options { margin-top: 10px; } @@ -117,6 +106,19 @@ .header-value { } + + // This exists so that you can copy + // and paste headers out of mitmweb. + .header-colon { + position: absolute; + opacity: 0; + } + + .inline-input { + display: inline-block; + width: 100%; + height: 100%; + } } .connection-table, .timing-table { diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index 697085a9a..094ce18ba 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -22,12 +22,15 @@ export default View => class extends React.Component { } componentWillMount() { - this.startRequest(this.props) + this.updateContent(this.props) } componentWillReceiveProps(nextProps) { - if (nextProps.message.contentHash !== this.props.message.contentHash) { - this.startRequest(nextProps) + if ( + nextProps.message.content !== this.props.message.content || + nextProps.message.contentHash !== this.props.message.contentHash + ) { + this.updateContent(nextProps) } } @@ -37,13 +40,17 @@ export default View => class extends React.Component { } } - startRequest(props) { + updateContent(props) { if (this.state.request) { this.state.request.abort() } + // We have a few special cases where we do not need to make an HTTP request. if(props.message.contentLength === 0 || props.message.contentLength === null){ return this.setState({request: undefined, content: ""}) } + if(props.message.content !== undefined) { + return this.setState({request: undefined, content: props.message.content}) + } let requestUrl = MessageUtils.getContentURL(props.flow, props.message) diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx index e46b939d9..2e181383a 100644 --- a/web/src/js/components/FlowView/Headers.jsx +++ b/web/src/js/components/FlowView/Headers.jsx @@ -126,7 +126,8 @@ export default class Headers extends Component { onDone={val => this.onChange(i, 0, val)} onRemove={event => this.onRemove(i, 0, event)} onTab={event => this.onTab(i, 0, event)} - />: + /> + : Date: Mon, 25 Jul 2016 17:45:42 -0700 Subject: [PATCH 14/15] simplify stop edit --- .../components/ContentView/ContentLoader.jsx | 6 ++-- .../js/components/ValueEditor/ValueEditor.jsx | 2 +- web/src/js/ducks/ui/flow.js | 33 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index 094ce18ba..ba6702ca8 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -45,12 +45,12 @@ export default View => class extends React.Component { this.state.request.abort() } // We have a few special cases where we do not need to make an HTTP request. - if(props.message.contentLength === 0 || props.message.contentLength === null){ - return this.setState({request: undefined, content: ""}) - } if(props.message.content !== undefined) { return this.setState({request: undefined, content: props.message.content}) } + if(props.message.contentLength === 0 || props.message.contentLength === null){ + return this.setState({request: undefined, content: ""}) + } let requestUrl = MessageUtils.getContentURL(props.flow, props.message) diff --git a/web/src/js/components/ValueEditor/ValueEditor.jsx b/web/src/js/components/ValueEditor/ValueEditor.jsx index dd9c2cde4..852f82c4d 100644 --- a/web/src/js/components/ValueEditor/ValueEditor.jsx +++ b/web/src/js/components/ValueEditor/ValueEditor.jsx @@ -59,7 +59,7 @@ export default class ValueEditor extends Component { return (
this.input = input} - tabIndex={!this.props.readonly && "0"} + tabIndex={this.props.readonly ? undefined : 0} className={className} contentEditable={this.state.editable || undefined} onFocus={this.onFocus} diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index 100bc771b..c94356765 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -8,7 +8,6 @@ export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW', SET_TAB = "UI_FLOWVIEW_SET_TAB", START_EDIT = 'UI_FLOWVIEW_START_EDIT', UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT', - STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT', UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT' @@ -34,12 +33,6 @@ export default function reducer(state = defaultState, action) { modifiedFlow: _.merge({}, state.modifiedFlow, action.update) } - case STOP_EDIT: - return { - ...state, - modifiedFlow: false - } - case flowsActions.SELECT: return { ...state, @@ -47,6 +40,21 @@ export default function reducer(state = defaultState, action) { displayLarge: false, } + case flowsActions.UPDATE: + // There is no explicit "stop edit" event. + // We stop editing when we receive an update for + // the currently edited flow from the server + if (action.item.id === state.modifiedFlow.id) { + return { + ...state, + modifiedFlow: false, + displayLarge: false, + } + } else { + return state + } + + case SET_TAB: return { ...state, @@ -90,12 +98,7 @@ export function updateEdit(update) { return { type: UPDATE_EDIT, update } } -export function stopEdit(flow, modified_flow) { - let diff = getDiff(flow, modified_flow) - return (dispatch) => { - dispatch(flowsActions.update(flow, diff)).then(() => { - dispatch(flowsActions.updateFlow(modified_flow)) - dispatch({ type: STOP_EDIT }) - }) - } +export function stopEdit(flow, modifiedFlow) { + let diff = getDiff(flow, modifiedFlow) + return flowsActions.update(flow, diff) } From 3ebb58f641612a4c512c045187ffe40879720fa7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 25 Jul 2016 17:50:59 -0700 Subject: [PATCH 15/15] add up/download button to ContentTooLarge view --- .../js/components/ContentView/MetaViews.jsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/web/src/js/components/ContentView/MetaViews.jsx b/web/src/js/components/ContentView/MetaViews.jsx index 2d064b546..b926738e5 100644 --- a/web/src/js/components/ContentView/MetaViews.jsx +++ b/web/src/js/components/ContentView/MetaViews.jsx @@ -1,5 +1,7 @@ import React from 'react' import { formatSize } from '../../utils.js' +import UploadContentButton from './UploadContentButton' +import DownloadContentButton from './DownloadContentButton' export function ContentEmpty({ flow, message }) { return ( @@ -17,11 +19,19 @@ export function ContentMissing({ flow, message }) { ) } -export function ContentTooLarge({ message, onClick }) { +export function ContentTooLarge({ message, onClick, uploadContent, flow }) { return ( -
- - {formatSize(message.contentLength)} content size. +
+
+ + + {formatSize(message.contentLength)} content size. +
+
+ +   + +
) }