mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-01-30 14:58:38 +00:00
Merge branch 'flow_editing_v2'
This commit is contained in:
commit
817b675c52
@ -5,6 +5,8 @@ import json
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
|
||||
import six
|
||||
import tornado.websocket
|
||||
@ -45,7 +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,
|
||||
"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,
|
||||
@ -56,7 +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,
|
||||
"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,
|
||||
@ -248,11 +252,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)
|
||||
@ -262,6 +269,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:
|
||||
|
@ -11,15 +11,13 @@
|
||||
"<rootDir>/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",
|
||||
|
@ -102,11 +102,23 @@
|
||||
}
|
||||
.header-name {
|
||||
width: 33%;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.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 {
|
||||
|
@ -1,12 +1,12 @@
|
||||
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'
|
||||
import CodeEditor from './common/CodeEditor'
|
||||
|
||||
ContentView.propTypes = {
|
||||
// It may seem a bit weird at the first glance:
|
||||
@ -19,61 +19,32 @@ 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, isDisplayLarge, displayLarge, uploadContent, onContentChange, readonly } = props
|
||||
|
||||
if (message.contentLength === 0) {
|
||||
if (message.contentLength === 0 && readonly) {
|
||||
return <MetaViews.ContentEmpty {...props}/>
|
||||
}
|
||||
|
||||
if (message.contentLength === null) {
|
||||
if (message.contentLength === null && readonly) {
|
||||
return <MetaViews.ContentMissing {...props}/>
|
||||
}
|
||||
|
||||
if (!displayLarge && ContentView.isContentTooLarge(message)) {
|
||||
if (!isDisplayLarge && ContentView.isContentTooLarge(message)) {
|
||||
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
|
||||
}
|
||||
|
||||
const View = ContentViews[contentView]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isFlowEditorOpen ? (
|
||||
<ContentLoader flow={flow} message={message}>
|
||||
<CodeEditor content="" onChange={content =>{setModifiedFlowContent(content)}}/>
|
||||
</ContentLoader>
|
||||
): (
|
||||
<div>
|
||||
{View.textView ? (
|
||||
<ContentLoader flow={flow} message={message}>
|
||||
<View content="" />
|
||||
</ContentLoader>
|
||||
) : (
|
||||
<View flow={flow} message={message} />
|
||||
)}
|
||||
<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>
|
||||
|
||||
<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) onContentChange(e.target.files[0])}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/>
|
||||
|
||||
<div className="view-options text-center">
|
||||
<ViewSelector message={message}/>
|
||||
|
||||
<DownloadContentButton flow={flow} message={message}/>
|
||||
|
||||
<UploadContentButton uploadContent={uploadContent}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -81,12 +52,10 @@ function ContentView(props) {
|
||||
export default connect(
|
||||
state => ({
|
||||
contentView: state.ui.flow.contentView,
|
||||
displayLarge: state.ui.flow.displayLarge,
|
||||
isFlowEditorOpen : !!state.ui.flow.modifiedFlow // FIXME
|
||||
isDisplayLarge: state.ui.flow.displayLarge,
|
||||
}),
|
||||
{
|
||||
selectView: setContentView,
|
||||
displayLarge,
|
||||
updateEdit,
|
||||
updateEdit
|
||||
}
|
||||
)(ContentView)
|
||||
|
21
web/src/js/components/ContentView/CodeEditor.jsx
Normal file
21
web/src/js/components/ContentView/CodeEditor.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { render } from 'react-dom';
|
||||
import Codemirror from 'react-codemirror';
|
||||
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default function CodeEditor ( { content, onChange} ){
|
||||
|
||||
let options = {
|
||||
lineNumbers: true
|
||||
};
|
||||
return (
|
||||
<div onKeyDown={e => e.stopPropagation()}>
|
||||
<Codemirror value={content} onChange={onChange} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,53 +1,36 @@
|
||||
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.updateContent(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.message !== this.props.message) {
|
||||
this.requestContent(nextProps)
|
||||
if (
|
||||
nextProps.message.content !== this.props.message.content ||
|
||||
nextProps.message.contentHash !== this.props.message.contentHash
|
||||
) {
|
||||
this.updateContent(nextProps)
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,15 +40,58 @@ export default class ContentLoader extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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.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)
|
||||
|
||||
// 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
|
||||
})
|
||||
return this.state.content !== undefined ? (
|
||||
<View content={this.state.content} {...this.props}/>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<i className="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,19 +1,16 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ContentLoader from './ContentLoader'
|
||||
import { MessageUtils } from '../../flow/utils.js'
|
||||
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"/>
|
||||
@ -21,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 }) {
|
||||
return <pre>{content}</pre>
|
||||
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);
|
||||
@ -49,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 }) {
|
||||
function ViewAuto({ message, flow, readonly, onChange }) {
|
||||
const View = ViewAuto.findView(message)
|
||||
if (View.textView) {
|
||||
return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
|
||||
} else {
|
||||
return <View message={message} flow={flow} />
|
||||
}
|
||||
return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/>
|
||||
}
|
||||
|
||||
export default views
|
||||
export { ViewImage, ViewRaw, ViewAuto, ViewJSON }
|
||||
|
18
web/src/js/components/ContentView/DownloadContentButton.jsx
Normal file
18
web/src/js/components/ContentView/DownloadContentButton.jsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<div className="alert alert-warning">
|
||||
<button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
|
||||
{formatSize(message.contentLength)} content size.
|
||||
<div>
|
||||
<div className="alert alert-warning">
|
||||
|
||||
<button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
|
||||
{formatSize(message.contentLength)} content size.
|
||||
</div>
|
||||
<div className="view-options text-center">
|
||||
<UploadContentButton uploadContent={uploadContent}/>
|
||||
|
||||
<DownloadContentButton flow={flow} message={message}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
28
web/src/js/components/ContentView/UploadContentButton.jsx
Normal file
28
web/src/js/components/ContentView/UploadContentButton.jsx
Normal file
@ -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>
|
||||
|
||||
)
|
||||
}
|
||||
|
@ -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="ViewAuto">auto: {autoViewName}</ViewButton>
|
||||
|
||||
{Object.keys(ContentViews).map(name =>
|
||||
name !== "ViewAuto" &&
|
||||
<ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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)}
|
||||
/>:
|
||||
/>
|
||||
<span className="header-colon">:</span>
|
||||
</td>
|
||||
<td className="header-value">
|
||||
<HeaderEditor
|
||||
|
@ -10,6 +10,7 @@ import ValueEditor from '../ValueEditor/ValueEditor'
|
||||
|
||||
import Headers from './Headers'
|
||||
import { startEdit, updateEdit } from '../../ducks/ui/flow'
|
||||
import * as FlowActions from '../../ducks/flows'
|
||||
import ToggleEdit from './ToggleEdit'
|
||||
|
||||
function RequestLine({ flow, readonly, updateFlow }) {
|
||||
@ -73,12 +74,13 @@ const Message = connect(
|
||||
}),
|
||||
{
|
||||
updateFlow: updateEdit,
|
||||
uploadContent: FlowActions.uploadContent
|
||||
}
|
||||
)
|
||||
|
||||
export class Request extends Component {
|
||||
render() {
|
||||
const { flow, isEdit, updateFlow } = this.props
|
||||
const { flow, isEdit, updateFlow, uploadContent } = this.props
|
||||
|
||||
return (
|
||||
<section className="request">
|
||||
@ -94,7 +96,12 @@ export class Request extends Component {
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
<ContentView flow={flow} message={flow.request}/>
|
||||
<ContentView
|
||||
readonly={!isEdit}
|
||||
flow={flow}
|
||||
onContentChange={content => updateFlow({ request: {content}})}
|
||||
uploadContent={content => uploadContent(flow, content, "request")}
|
||||
message={flow.request}/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -129,7 +136,7 @@ Request = Message(Request)
|
||||
|
||||
export class Response extends Component {
|
||||
render() {
|
||||
const { flow, isEdit, updateFlow } = this.props
|
||||
const { flow, isEdit, updateFlow, uploadContent } = this.props
|
||||
|
||||
return (
|
||||
<section className="response">
|
||||
@ -144,7 +151,13 @@ export class Response extends Component {
|
||||
onChange={headers => updateFlow({ response: { headers } })}
|
||||
/>
|
||||
<hr/>
|
||||
<ContentView flow={flow} message={flow.response}/>
|
||||
<ContentView
|
||||
readonly={!isEdit}
|
||||
flow={flow}
|
||||
onContentChange={content => updateFlow({ response: {content}})}
|
||||
uploadContent={content => uploadContent(flow, content, "response")}
|
||||
message={flow.response}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
@ -10,11 +10,11 @@ ToggleEdit.propTypes = {
|
||||
stopEdit: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) {
|
||||
function ToggleEdit({ isEdit, startEdit, stopEdit, flow, modifiedFlow }) {
|
||||
return (
|
||||
<div className="edit-flow-container">
|
||||
{isEdit ?
|
||||
<a className="edit-flow" onClick={() => stopEdit(flow)}>
|
||||
<a className="edit-flow" onClick={() => stopEdit(flow, modifiedFlow)}>
|
||||
<i className="fa fa-check"/>
|
||||
</a>
|
||||
:
|
||||
@ -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]]
|
||||
modifiedFlow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]],
|
||||
flow: state.flows.byId[state.flows.selected[0]]
|
||||
}),
|
||||
{
|
||||
startEdit,
|
||||
|
@ -59,7 +59,7 @@ export default class ValueEditor extends Component {
|
||||
return (
|
||||
<div
|
||||
ref={input => this.input = input}
|
||||
tabIndex={!this.props.readonly && "0"}
|
||||
tabIndex={this.props.readonly ? undefined : 0}
|
||||
className={className}
|
||||
contentEditable={this.state.editable || undefined}
|
||||
onFocus={this.onFocus}
|
||||
|
@ -1,30 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { render } from 'react-dom';
|
||||
import Codemirror from 'react-codemirror';
|
||||
|
||||
|
||||
export default class CodeEditor extends Component{
|
||||
static propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.props.onChange(this.props.content)
|
||||
}
|
||||
|
||||
render() {
|
||||
let options = {
|
||||
lineNumbers: true
|
||||
};
|
||||
return (
|
||||
<div onKeyDown={e => e.stopPropagation()}>
|
||||
<Codemirror value={this.props.content} onChange={this.props.onChange} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -112,10 +112,9 @@ 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'})
|
||||
file = new Blob([file], {type: 'plain/text'})
|
||||
body.append('file', file)
|
||||
return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} )
|
||||
}
|
||||
|
@ -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,7 +8,7 @@ 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'
|
||||
|
||||
|
||||
const defaultState = {
|
||||
@ -22,7 +24,7 @@ export default function reducer(state = defaultState, action) {
|
||||
case START_EDIT:
|
||||
return {
|
||||
...state,
|
||||
modifiedFlow: action.flow
|
||||
modifiedFlow: action.flow,
|
||||
}
|
||||
|
||||
case UPDATE_EDIT:
|
||||
@ -31,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,
|
||||
@ -44,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,
|
||||
@ -87,11 +98,7 @@ export function updateEdit(update) {
|
||||
return { type: UPDATE_EDIT, update }
|
||||
}
|
||||
|
||||
export function stopEdit(flow) {
|
||||
return (dispatch) => {
|
||||
dispatch(flowsActions.update(flow, flow)).then(() => {
|
||||
dispatch(flowsActions.updateFlow(flow))
|
||||
dispatch({ type: STOP_EDIT })
|
||||
})
|
||||
}
|
||||
export function stopEdit(flow, modifiedFlow) {
|
||||
let diff = getDiff(flow, modifiedFlow)
|
||||
return flowsActions.update(flow, diff)
|
||||
}
|
||||
|
@ -108,11 +108,22 @@ fetchApi.put = (url, json, options) => fetchApi(
|
||||
}
|
||||
)
|
||||
|
||||
export function getDiff(obj1, obj2) {
|
||||
let result = {...obj2};
|
||||
for(let key in obj1) {
|
||||
if(_.isEqual(obj2[key], obj1[key]))
|
||||
result[key] = undefined
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user