mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-22 15:37:45 +00:00
web: refactor ContentLoader
This commit is contained in:
parent
79ebcb046e
commit
70dbd1b32d
@ -11,15 +11,13 @@
|
|||||||
"<rootDir>/src/js"
|
"<rootDir>/src/js"
|
||||||
],
|
],
|
||||||
"unmockedModulePathPatterns": [
|
"unmockedModulePathPatterns": [
|
||||||
"react",
|
"react"
|
||||||
"jquery"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"flux": "^2.1.1",
|
"flux": "^2.1.1",
|
||||||
"jquery": "^2.2.3",
|
|
||||||
"lodash": "^4.11.2",
|
"lodash": "^4.11.2",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
@ -29,7 +27,7 @@
|
|||||||
"redux-logger": "^2.6.1",
|
"redux-logger": "^2.6.1",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
"shallowequal": "^0.2.2",
|
"shallowequal": "^0.2.2",
|
||||||
"react-codemirror" : "^0.2.6"
|
"react-codemirror": "^0.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.7.7",
|
"babel-core": "^6.7.7",
|
||||||
@ -55,7 +53,9 @@
|
|||||||
"gulp-sourcemaps": "^1.6.0",
|
"gulp-sourcemaps": "^1.6.0",
|
||||||
"gulp-util": "^3.0.7",
|
"gulp-util": "^3.0.7",
|
||||||
"jest": "^12.1.1",
|
"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",
|
"uglifyify": "^3.0.1",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0",
|
"vinyl-source-stream": "^1.1.0",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { MessageUtils } from '../flow/utils.js'
|
|
||||||
import * as ContentViews from './ContentView/ContentViews'
|
import * as ContentViews from './ContentView/ContentViews'
|
||||||
import * as MetaViews from './ContentView/MetaViews'
|
import * as MetaViews from './ContentView/MetaViews'
|
||||||
import ContentLoader from './ContentView/ContentLoader'
|
|
||||||
import ViewSelector from './ContentView/ViewSelector'
|
import ViewSelector from './ContentView/ViewSelector'
|
||||||
|
import UploadContentButton from './ContentView/UploadContentButton'
|
||||||
|
import DownloadContentButton from './ContentView/DownloadContentButton'
|
||||||
|
|
||||||
import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow'
|
import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow'
|
||||||
|
|
||||||
ContentView.propTypes = {
|
ContentView.propTypes = {
|
||||||
@ -18,7 +19,7 @@ ContentView.propTypes = {
|
|||||||
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
|
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
|
||||||
|
|
||||||
function ContentView(props) {
|
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) {
|
if (message.contentLength === 0) {
|
||||||
return <MetaViews.ContentEmpty {...props}/>
|
return <MetaViews.ContentEmpty {...props}/>
|
||||||
@ -35,34 +36,14 @@ function ContentView(props) {
|
|||||||
const View = ContentViews[contentView]
|
const View = ContentViews[contentView]
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{View.textView ? (
|
<View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/>
|
||||||
<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} />
|
|
||||||
)}
|
|
||||||
<div className="view-options text-center">
|
<div className="view-options text-center">
|
||||||
<ViewSelector onSelectView={selectView} active={View} message={message}/>
|
<ViewSelector onSelectView={selectView} active={View} message={message}/>
|
||||||
|
|
||||||
<a className="btn btn-default btn-xs"
|
<DownloadContentButton flow={flow} message={message}/>
|
||||||
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"
|
<UploadContentButton uploadContent={uploadContent}/>
|
||||||
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])}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,58 +1,34 @@
|
|||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import { MessageUtils } from '../../flow/utils.js'
|
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 = {
|
static propTypes = {
|
||||||
|
...View.propTypes,
|
||||||
|
content: PropTypes.string, // mark as non-required
|
||||||
flow: PropTypes.object.isRequired,
|
flow: PropTypes.object.isRequired,
|
||||||
message: PropTypes.object.isRequired,
|
message: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context)
|
super(props)
|
||||||
this.state = { content: null, request: null }
|
this.state = {
|
||||||
}
|
content: undefined,
|
||||||
|
request: undefined,
|
||||||
requestContent(nextProps) {
|
|
||||||
if (this.state.request) {
|
|
||||||
this.state.request.abort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
componentWillMount() {
|
||||||
this.requestContent(this.props)
|
this.startRequest(this.props)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
let reload = nextProps.message !== this.props.message
|
if (nextProps.message.contentHash !== this.props.message.contentHash) {
|
||||||
let isUserEdit = !nextProps.readonly && nextProps.message.content
|
this.startRequest(nextProps)
|
||||||
|
}
|
||||||
if (isUserEdit)
|
|
||||||
this.setState({content: nextProps.message.content})
|
|
||||||
else if(reload)
|
|
||||||
this.requestContent(nextProps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
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() {
|
render() {
|
||||||
return this.state.content ? (
|
return this.state.content ? (
|
||||||
React.cloneElement(this.props.children, {
|
<View content={this.state.content} {...this.props}/>
|
||||||
content: this.state.content
|
|
||||||
})
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<i className="fa fa-spinner fa-spin"></i>
|
<i className="fa fa-spinner fa-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import ContentLoader from './ContentLoader'
|
import ContentLoader from './ContentLoader'
|
||||||
import { MessageUtils } from '../../flow/utils.js'
|
import { MessageUtils } from '../../flow/utils'
|
||||||
import CodeEditor from '../common/CodeEditor'
|
import CodeEditor from './CodeEditor'
|
||||||
|
|
||||||
|
|
||||||
const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw]
|
const isImage = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i
|
||||||
|
ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg))
|
||||||
ViewImage.regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i
|
|
||||||
ViewImage.matches = msg => ViewImage.regex.test(MessageUtils.getContentType(msg))
|
|
||||||
|
|
||||||
ViewImage.propTypes = {
|
ViewImage.propTypes = {
|
||||||
flow: PropTypes.object.isRequired,
|
flow: PropTypes.object.isRequired,
|
||||||
message: PropTypes.object.isRequired,
|
message: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
function ViewImage({ flow, message }) {
|
||||||
export function ViewImage({ flow, message }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flowview-image">
|
<div className="flowview-image">
|
||||||
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
|
<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 = {
|
ViewRaw.propTypes = {
|
||||||
content: React.PropTypes.string.isRequired,
|
content: React.PropTypes.string.isRequired,
|
||||||
}
|
}
|
||||||
|
function ViewRaw({ content, readonly, onChange }) {
|
||||||
export function ViewRaw({ content, readonly, onChange }) {
|
|
||||||
return readonly ? <pre>{content}</pre> : <CodeEditor content={content} onChange={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 = {
|
ViewJSON.propTypes = {
|
||||||
content: React.PropTypes.string.isRequired,
|
content: React.PropTypes.string.isRequired,
|
||||||
}
|
}
|
||||||
|
function ViewJSON({ content }) {
|
||||||
export function ViewJSON({ content }) {
|
|
||||||
let json = content
|
let json = content
|
||||||
try {
|
try {
|
||||||
json = JSON.stringify(JSON.parse(content), null, 2);
|
json = JSON.stringify(JSON.parse(content), null, 2);
|
||||||
@ -50,23 +43,18 @@ export function ViewJSON({ content }) {
|
|||||||
}
|
}
|
||||||
return <pre>{json}</pre>
|
return <pre>{json}</pre>
|
||||||
}
|
}
|
||||||
|
ViewJSON = ContentLoader(ViewJSON)
|
||||||
|
|
||||||
|
|
||||||
ViewAuto.matches = () => false
|
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 = {
|
ViewAuto.propTypes = {
|
||||||
message: React.PropTypes.object.isRequired,
|
message: React.PropTypes.object.isRequired,
|
||||||
flow: React.PropTypes.object.isRequired,
|
flow: React.PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
function ViewAuto({ message, flow, readonly, onChange }) {
|
||||||
export function ViewAuto({ message, flow, readonly, onChange }) {
|
|
||||||
const View = ViewAuto.findView(message)
|
const View = ViewAuto.findView(message)
|
||||||
if (View.textView) {
|
return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/>
|
||||||
return <ContentLoader message={message} flow={flow}><View readonly={readonly} onChange={onChange} content="" /></ContentLoader>
|
|
||||||
} else {
|
|
||||||
return <View readonly={readonly} message={message} flow={flow} />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
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 React, { PropTypes } from 'react'
|
||||||
import classnames from 'classnames'
|
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 = {
|
ViewSelector.propTypes = {
|
||||||
active: PropTypes.func.isRequired,
|
|
||||||
message: PropTypes.object.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 (
|
return (
|
||||||
<div className="view-selector btn-group btn-group-xs">
|
<div className="view-selector btn-group btn-group-xs">
|
||||||
{views.map(View => (
|
|
||||||
<button
|
<ViewButton name="AutoView">auto: {autoViewName}</ViewButton>
|
||||||
key={View.name}
|
|
||||||
onClick={() => onSelectView(View.name)}
|
{Object.keys(ContentViews).map(name =>
|
||||||
className={classnames('btn btn-default', { active: View === active })}>
|
name !== "ViewAuto" &&
|
||||||
{View === ViewAuto ? (
|
<ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton>
|
||||||
`auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}`
|
)}
|
||||||
) : (
|
|
||||||
View.name.toLowerCase().replace('view', '')
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,6 @@ export const pure = renderFn => class extends React.Component {
|
|||||||
static displayName = renderFn.name
|
static displayName = renderFn.name
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
console.log(!shallowEqual(this.props, nextProps))
|
|
||||||
return !shallowEqual(this.props, nextProps)
|
return !shallowEqual(this.props, nextProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user